\n
\n
\n `;\n ScrollBar.defaultProps = {\n width: 1,\n height: 1,\n };\n ScrollBar.props = {\n width: { type: Number, optional: true },\n height: { type: Number, optional: true },\n direction: String,\n position: Object,\n offset: Number,\n onScroll: Function,\n };\n\n class HorizontalScrollBar extends owl.Component {\n get offset() {\n return this.env.model.getters.getActiveSheetDOMScrollInfo().scrollX;\n }\n get width() {\n return this.env.model.getters.getMainViewportRect().width;\n }\n get isDisplayed() {\n const { xRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());\n return xRatio < 1;\n }\n get position() {\n const { x } = this.env.model.getters.getMainViewportRect();\n return {\n left: `${this.props.leftOffset + x}px`,\n bottom: \"0px\",\n height: `${SCROLLBAR_WIDTH$1}px`,\n right: `0px`,\n };\n }\n onScroll(offset) {\n const { scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n offsetX: offset,\n offsetY: scrollY, // offsetY is the same\n });\n }\n }\n HorizontalScrollBar.components = { ScrollBar };\n HorizontalScrollBar.template = owl.xml /*xml*/ `\n
`;\n HorizontalScrollBar.defaultProps = {\n leftOffset: 0,\n };\n HorizontalScrollBar.props = {\n leftOffset: { type: Number, optional: true },\n };\n\n class VerticalScrollBar extends owl.Component {\n get offset() {\n return this.env.model.getters.getActiveSheetDOMScrollInfo().scrollY;\n }\n get height() {\n return this.env.model.getters.getMainViewportRect().height;\n }\n get isDisplayed() {\n const { yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());\n return yRatio < 1;\n }\n get position() {\n const { y } = this.env.model.getters.getMainViewportRect();\n return {\n top: `${this.props.topOffset + y}px`,\n right: \"0px\",\n width: `${SCROLLBAR_WIDTH$1}px`,\n bottom: `0px`,\n };\n }\n onScroll(offset) {\n const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n offsetX: scrollX,\n offsetY: offset,\n });\n }\n }\n VerticalScrollBar.components = { ScrollBar };\n VerticalScrollBar.template = owl.xml /*xml*/ `\n
`;\n VerticalScrollBar.defaultProps = {\n topOffset: 0,\n };\n VerticalScrollBar.props = {\n topOffset: { type: Number, optional: true },\n };\n\n const registries$1 = {\n ROW: rowMenuRegistry,\n COL: colMenuRegistry,\n CELL: cellMenuRegistry,\n };\n // -----------------------------------------------------------------------------\n // JS\n // -----------------------------------------------------------------------------\n class Grid extends owl.Component {\n constructor() {\n super(...arguments);\n this.HEADER_HEIGHT = HEADER_HEIGHT;\n this.HEADER_WIDTH = HEADER_WIDTH;\n // this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key\n // down itself\n this.keyDownMapping = {\n ENTER: () => {\n const cell = this.env.model.getters.getActiveCell();\n cell.type === CellValueType.empty\n ? this.props.onGridComposerCellFocused()\n : this.props.onComposerContentFocused();\n },\n TAB: () => this.env.model.selection.moveAnchorCell(\"right\", 1),\n \"SHIFT+TAB\": () => this.env.model.selection.moveAnchorCell(\"left\", 1),\n F2: () => {\n const cell = this.env.model.getters.getActiveCell();\n cell.type === CellValueType.empty\n ? this.props.onGridComposerCellFocused()\n : this.props.onComposerContentFocused();\n },\n DELETE: () => {\n this.env.model.dispatch(\"DELETE_CONTENT\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n });\n },\n BACKSPACE: () => {\n this.env.model.dispatch(\"DELETE_CONTENT\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n });\n },\n ESCAPE: () => {\n /** TODO: Clean once we introduce proper focus on sub components. Grid should not have to handle all this logic */\n if (this.env.model.getters.hasOpenedPopover()) {\n this.closeOpenedPopover();\n }\n else if (this.menuState.isOpen) {\n this.closeMenu();\n }\n else {\n this.env.model.dispatch(\"CLEAN_CLIPBOARD_HIGHLIGHT\");\n }\n },\n \"CTRL+A\": () => this.env.model.selection.loopSelection(),\n \"CTRL+Z\": () => this.env.model.dispatch(\"REQUEST_UNDO\"),\n \"CTRL+Y\": () => this.env.model.dispatch(\"REQUEST_REDO\"),\n \"CTRL+B\": () => this.env.model.dispatch(\"SET_FORMATTING\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n style: { bold: !this.env.model.getters.getCurrentStyle().bold },\n }),\n \"CTRL+I\": () => this.env.model.dispatch(\"SET_FORMATTING\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n style: { italic: !this.env.model.getters.getCurrentStyle().italic },\n }),\n \"CTRL+U\": () => this.env.model.dispatch(\"SET_FORMATTING\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n style: { underline: !this.env.model.getters.getCurrentStyle().underline },\n }),\n \"ALT+=\": () => {\n var _a;\n const sheetId = this.env.model.getters.getActiveSheetId();\n const mainSelectedZone = this.env.model.getters.getSelectedZone();\n const { anchor } = this.env.model.getters.getSelection();\n const sums = this.env.model.getters.getAutomaticSums(sheetId, mainSelectedZone, anchor.cell);\n if (this.env.model.getters.isSingleCellOrMerge(sheetId, mainSelectedZone) ||\n (this.env.model.getters.isEmpty(sheetId, mainSelectedZone) && sums.length <= 1)) {\n const zone = (_a = sums[0]) === null || _a === void 0 ? void 0 : _a.zone;\n const zoneXc = zone ? this.env.model.getters.zoneToXC(sheetId, sums[0].zone) : \"\";\n const formula = `=SUM(${zoneXc})`;\n this.props.onGridComposerCellFocused(formula, { start: 5, end: 5 + zoneXc.length });\n }\n else {\n this.env.model.dispatch(\"SUM_SELECTION\");\n }\n },\n \"CTRL+HOME\": () => {\n const sheetId = this.env.model.getters.getActiveSheetId();\n const { col, row } = this.env.model.getters.getNextVisibleCellPosition({\n sheetId,\n col: 0,\n row: 0,\n });\n this.env.model.selection.selectCell(col, row);\n },\n \"CTRL+END\": () => {\n const sheetId = this.env.model.getters.getActiveSheetId();\n const col = this.env.model.getters.findVisibleHeader(sheetId, \"COL\", range(0, this.env.model.getters.getNumberCols(sheetId)).reverse());\n const row = this.env.model.getters.findVisibleHeader(sheetId, \"ROW\", range(0, this.env.model.getters.getNumberRows(sheetId)).reverse());\n this.env.model.selection.selectCell(col, row);\n },\n \"SHIFT+ \": () => {\n const sheetId = this.env.model.getters.getActiveSheetId();\n const newZone = {\n ...this.env.model.getters.getSelectedZone(),\n left: 0,\n right: this.env.model.getters.getNumberCols(sheetId) - 1,\n };\n const position = this.env.model.getters.getActivePosition();\n this.env.model.selection.selectZone({ cell: position, zone: newZone });\n },\n \"CTRL+ \": () => {\n const sheetId = this.env.model.getters.getActiveSheetId();\n const newZone = {\n ...this.env.model.getters.getSelectedZone(),\n top: 0,\n bottom: this.env.model.getters.getNumberRows(sheetId) - 1,\n };\n const position = this.env.model.getters.getActivePosition();\n this.env.model.selection.selectZone({ cell: position, zone: newZone });\n },\n \"CTRL+SHIFT+ \": () => {\n this.env.model.selection.selectAll();\n },\n \"SHIFT+PAGEDOWN\": () => {\n this.env.model.dispatch(\"ACTIVATE_NEXT_SHEET\");\n },\n \"SHIFT+PAGEUP\": () => {\n this.env.model.dispatch(\"ACTIVATE_PREVIOUS_SHEET\");\n },\n PAGEDOWN: () => this.env.model.dispatch(\"SHIFT_VIEWPORT_DOWN\"),\n PAGEUP: () => this.env.model.dispatch(\"SHIFT_VIEWPORT_UP\"),\n \"CTRL+K\": () => INSERT_LINK(this.env),\n };\n }\n setup() {\n this.menuState = owl.useState({\n isOpen: false,\n position: null,\n menuItems: [],\n });\n this.gridRef = owl.useRef(\"grid\");\n this.hiddenInput = owl.useRef(\"hiddenInput\");\n this.canvasPosition = useAbsoluteBoundingRect(this.gridRef);\n this.hoveredCell = owl.useState({ col: undefined, row: undefined });\n owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });\n owl.useExternalListener(document.body, \"cut\", this.copy.bind(this, true));\n owl.useExternalListener(document.body, \"copy\", this.copy.bind(this, false));\n owl.useExternalListener(document.body, \"paste\", this.paste);\n owl.onMounted(() => this.focus());\n this.props.exposeFocus(() => this.focus());\n useGridDrawing(\"canvas\", this.env.model, () => this.env.model.getters.getSheetViewDimensionWithHeaders());\n owl.useEffect(() => this.focus(), () => [this.env.model.getters.getActiveSheetId()]);\n this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {\n this.moveCanvas(deltaX, deltaY);\n this.hoveredCell.col = undefined;\n this.hoveredCell.row = undefined;\n });\n }\n onCellHovered({ col, row }) {\n this.hoveredCell.col = col;\n this.hoveredCell.row = row;\n }\n get gridOverlayDimensions() {\n return `\n top: ${HEADER_HEIGHT}px;\n left: ${HEADER_WIDTH}px;\n height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH$1}px);\n width: calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH$1}px);\n `;\n }\n onClosePopover() {\n if (this.env.model.getters.hasOpenedPopover()) {\n this.closeOpenedPopover();\n }\n this.focus();\n }\n focus() {\n if (!this.env.model.getters.getSelectedFigureId() &&\n this.env.model.getters.getEditionMode() === \"inactive\") {\n this.hiddenInput.el.focus();\n }\n }\n get gridEl() {\n if (!this.gridRef.el) {\n throw new Error(\"Grid el is not defined.\");\n }\n return this.gridRef.el;\n }\n getAutofillPosition() {\n const zone = this.env.model.getters.getSelectedZone();\n const rect = this.env.model.getters.getVisibleRect(zone);\n return {\n left: rect.x + rect.width - AUTOFILL_EDGE_LENGTH / 2,\n top: rect.y + rect.height - AUTOFILL_EDGE_LENGTH / 2,\n };\n }\n isAutoFillActive() {\n const zone = this.env.model.getters.getSelectedZone();\n const rect = this.env.model.getters.getVisibleRect({\n left: zone.right,\n right: zone.right,\n top: zone.bottom,\n bottom: zone.bottom,\n });\n return !(rect.width === 0 || rect.height === 0);\n }\n onGridResized({ height, width }) {\n this.env.model.dispatch(\"RESIZE_SHEETVIEW\", {\n width: width,\n height: height,\n gridOffsetX: HEADER_WIDTH,\n gridOffsetY: HEADER_HEIGHT,\n });\n }\n moveCanvas(deltaX, deltaY) {\n const { scrollX, scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n offsetX: scrollX + deltaX,\n offsetY: scrollY + deltaY,\n });\n }\n getClientPositionKey(client) {\n var _a, _b, _c;\n return `${client.id}-${(_a = client.position) === null || _a === void 0 ? void 0 : _a.sheetId}-${(_b = client.position) === null || _b === void 0 ? void 0 : _b.col}-${(_c = client.position) === null || _c === void 0 ? void 0 : _c.row}`;\n }\n isCellHovered(col, row) {\n return this.hoveredCell.col === col && this.hoveredCell.row === row;\n }\n getGridRect() {\n return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() };\n }\n // ---------------------------------------------------------------------------\n // Zone selection with mouse\n // ---------------------------------------------------------------------------\n onCellClicked(col, row, { ctrlKey, shiftKey }) {\n if (ctrlKey) {\n this.env.model.dispatch(\"PREPARE_SELECTION_INPUT_EXPANSION\");\n }\n if (this.env.model.getters.hasOpenedPopover()) {\n this.closeOpenedPopover();\n }\n if (this.env.model.getters.getEditionMode() === \"editing\") {\n this.env.model.dispatch(\"STOP_EDITION\");\n }\n if (shiftKey) {\n this.env.model.selection.setAnchorCorner(col, row);\n }\n else if (ctrlKey) {\n this.env.model.selection.addCellToSelection(col, row);\n }\n else {\n this.env.model.selection.selectCell(col, row);\n }\n let prevCol = col;\n let prevRow = row;\n const onMouseMove = (col, row) => {\n if ((col !== prevCol && col != -1) || (row !== prevRow && row != -1)) {\n prevCol = col === -1 ? prevCol : col;\n prevRow = row === -1 ? prevRow : row;\n this.env.model.selection.setAnchorCorner(prevCol, prevRow);\n }\n };\n const onMouseUp = () => {\n this.env.model.dispatch(\"STOP_SELECTION_INPUT\");\n if (this.env.model.getters.isPaintingFormat()) {\n this.env.model.dispatch(\"PASTE\", {\n target: this.env.model.getters.getSelectedZones(),\n });\n }\n };\n dragAndDropBeyondTheViewport(this.env, onMouseMove, onMouseUp);\n }\n onCellDoubleClicked(col, row) {\n const sheetId = this.env.model.getters.getActiveSheetId();\n ({ col, row } = this.env.model.getters.getMainCellPosition({ sheetId, col, row }));\n const cell = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });\n if (cell.type === CellValueType.empty) {\n this.props.onGridComposerCellFocused();\n }\n else {\n this.props.onComposerContentFocused();\n }\n }\n closeOpenedPopover() {\n this.env.model.dispatch(\"CLOSE_CELL_POPOVER\");\n }\n // ---------------------------------------------------------------------------\n // Keyboard interactions\n // ---------------------------------------------------------------------------\n processArrows(ev) {\n ev.preventDefault();\n ev.stopPropagation();\n if (this.env.model.getters.hasOpenedPopover()) {\n this.closeOpenedPopover();\n }\n updateSelectionWithArrowKeys(ev, this.env.model.selection);\n if (this.env.model.getters.isPaintingFormat()) {\n this.env.model.dispatch(\"PASTE\", {\n target: this.env.model.getters.getSelectedZones(),\n });\n }\n }\n onKeydown(ev) {\n if (ev.key.startsWith(\"Arrow\")) {\n this.processArrows(ev);\n return;\n }\n let keyDownString = \"\";\n if (ev.ctrlKey)\n keyDownString += \"CTRL+\";\n if (ev.metaKey)\n keyDownString += \"CTRL+\";\n if (ev.altKey)\n keyDownString += \"ALT+\";\n if (ev.shiftKey)\n keyDownString += \"SHIFT+\";\n keyDownString += ev.key.toUpperCase();\n let handler = this.keyDownMapping[keyDownString];\n if (handler) {\n ev.preventDefault();\n ev.stopPropagation();\n handler();\n return;\n }\n }\n onInput(ev) {\n // the user meant to paste in the sheet, not open the composer with the pasted content\n if (!ev.isComposing && ev.inputType === \"insertFromPaste\") {\n return;\n }\n if (ev.data) {\n // if the user types a character on the grid, it means he wants to start composing the selected cell with that\n // character\n ev.preventDefault();\n ev.stopPropagation();\n this.props.onGridComposerCellFocused(ev.data);\n }\n }\n // ---------------------------------------------------------------------------\n // Context Menu\n // ---------------------------------------------------------------------------\n onInputContextMenu(ev) {\n ev.preventDefault();\n const lastZone = this.env.model.getters.getSelectedZone();\n const { left: col, top: row } = lastZone;\n let type = \"CELL\";\n this.env.model.dispatch(\"STOP_EDITION\");\n if (this.env.model.getters.getActiveCols().has(col)) {\n type = \"COL\";\n }\n else if (this.env.model.getters.getActiveRows().has(row)) {\n type = \"ROW\";\n }\n const { x, y, width, height } = this.env.model.getters.getVisibleRect(lastZone);\n this.toggleContextMenu(type, x + width, y + height);\n }\n onCellRightClicked(col, row, { x, y }) {\n const zones = this.env.model.getters.getSelectedZones();\n const lastZone = zones[zones.length - 1];\n let type = \"CELL\";\n if (!isInside(col, row, lastZone)) {\n this.env.model.selection.getBackToDefault();\n this.env.model.selection.selectCell(col, row);\n }\n else {\n if (this.env.model.getters.getActiveCols().has(col)) {\n type = \"COL\";\n }\n else if (this.env.model.getters.getActiveRows().has(row)) {\n type = \"ROW\";\n }\n }\n this.toggleContextMenu(type, x, y);\n }\n toggleContextMenu(type, x, y) {\n if (this.env.model.getters.hasOpenedPopover()) {\n this.closeOpenedPopover();\n }\n this.menuState.isOpen = true;\n this.menuState.position = { x, y };\n this.menuState.menuItems = registries$1[type]\n .getAll()\n .filter((item) => !item.isVisible || item.isVisible(this.env));\n }\n copy(cut, ev) {\n if (!this.gridEl.contains(document.activeElement)) {\n return;\n }\n const clipboardData = ev.clipboardData;\n if (!clipboardData) {\n this.displayWarningCopyPasteNotSupported();\n return;\n }\n /* If we are currently editing a cell, let the default behavior */\n if (this.env.model.getters.getEditionMode() !== \"inactive\") {\n return;\n }\n if (cut) {\n interactiveCut(this.env);\n }\n else {\n this.env.model.dispatch(\"COPY\");\n }\n const content = this.env.model.getters.getClipboardContent();\n for (const type in content) {\n clipboardData.setData(type, content[type]);\n }\n ev.preventDefault();\n }\n paste(ev) {\n if (!this.gridEl.contains(document.activeElement)) {\n return;\n }\n const clipboardData = ev.clipboardData;\n if (!clipboardData) {\n this.displayWarningCopyPasteNotSupported();\n return;\n }\n if (clipboardData.types.indexOf(ClipboardMIMEType.PlainText) > -1) {\n const content = clipboardData.getData(ClipboardMIMEType.PlainText);\n const target = this.env.model.getters.getSelectedZones();\n const clipboardString = this.env.model.getters.getClipboardTextContent();\n if (clipboardString === content) {\n // the paste actually comes from o-spreadsheet itself\n interactivePaste(this.env, target);\n }\n else {\n interactivePasteFromOS(this.env, target, content);\n }\n }\n }\n displayWarningCopyPasteNotSupported() {\n this.env.raiseError(_lt(\"Copy/Paste is not supported in this browser.\"));\n }\n closeMenu() {\n this.menuState.isOpen = false;\n this.focus();\n }\n }\n Grid.template = \"o-spreadsheet-Grid\";\n Grid.components = {\n GridComposer,\n GridOverlay,\n GridPopover,\n HeadersOverlay,\n Menu,\n Autofill,\n ClientTag,\n Highlight,\n Popover,\n VerticalScrollBar,\n HorizontalScrollBar,\n FilterIconsOverlay,\n };\n Grid.props = {\n sidePanelIsOpen: Boolean,\n exposeFocus: Function,\n focusComposer: String,\n onComposerContentFocused: Function,\n onGridComposerCellFocused: Function,\n };\n\n /**\n * Represent a raw XML string\n */\n class XMLString {\n /**\n * @param xmlString should be a well formed, properly escaped XML string\n */\n constructor(xmlString) {\n this.xmlString = xmlString;\n }\n toString() {\n return this.xmlString;\n }\n }\n const XLSX_CHART_TYPES = [\n \"areaChart\",\n \"area3DChart\",\n \"lineChart\",\n \"line3DChart\",\n \"stockChart\",\n \"radarChart\",\n \"scatterChart\",\n \"pieChart\",\n \"pie3DChart\",\n \"doughnutChart\",\n \"barChart\",\n \"bar3DChart\",\n \"ofPieChart\",\n \"surfaceChart\",\n \"surface3DChart\",\n \"bubbleChart\",\n ];\n\n /** In XLSX color format (no #) */\n const AUTO_COLOR = \"000000\";\n const XLSX_ICONSET_MAP = {\n arrow: \"3Arrows\",\n smiley: \"3Symbols\",\n dot: \"3TrafficLights1\",\n };\n const NAMESPACE = {\n styleSheet: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n sst: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n Relationships: \"http://schemas.openxmlformats.org/package/2006/relationships\",\n Types: \"http://schemas.openxmlformats.org/package/2006/content-types\",\n worksheet: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n workbook: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n drawing: \"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing\",\n table: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n revision: \"http://schemas.microsoft.com/office/spreadsheetml/2014/revision\",\n revision3: \"http://schemas.microsoft.com/office/spreadsheetml/2016/revision3\",\n markupCompatibility: \"http://schemas.openxmlformats.org/markup-compatibility/2006\",\n };\n const DRAWING_NS_A = \"http://schemas.openxmlformats.org/drawingml/2006/main\";\n const DRAWING_NS_C = \"http://schemas.openxmlformats.org/drawingml/2006/chart\";\n const CONTENT_TYPES = {\n workbook: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\",\n sheet: \"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\",\n sharedStrings: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml\",\n styles: \"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\",\n drawing: \"application/vnd.openxmlformats-officedocument.drawing+xml\",\n chart: \"application/vnd.openxmlformats-officedocument.drawingml.chart+xml\",\n themes: \"application/vnd.openxmlformats-officedocument.theme+xml\",\n table: \"application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml\",\n pivot: \"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml\",\n externalLink: \"application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml\",\n };\n const XLSX_RELATION_TYPE = {\n document: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\",\n sheet: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\",\n sharedStrings: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\",\n styles: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\",\n drawing: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing\",\n chart: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart\",\n theme: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme\",\n table: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/table\",\n hyperlink: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\",\n };\n const RELATIONSHIP_NSR = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships\";\n const HEIGHT_FACTOR = 0.75; // 100px => 75 u\n const WIDTH_FACTOR = 0.1317; // 100px => 13.17 u\n /** unit : maximum number of characters a column can hold at the standard font size. What. */\n const EXCEL_DEFAULT_COL_WIDTH = 8.43;\n /** unit : points */\n const EXCEL_DEFAULT_ROW_HEIGHT = 12.75;\n const EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS = 30;\n const EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS = 100;\n const FIRST_NUMFMT_ID = 164;\n const FORCE_DEFAULT_ARGS_FUNCTIONS = {\n FLOOR: [{ type: \"NUMBER\", value: 1 }],\n CEILING: [{ type: \"NUMBER\", value: 1 }],\n ROUND: [{ type: \"NUMBER\", value: 0 }],\n ROUNDUP: [{ type: \"NUMBER\", value: 0 }],\n ROUNDDOWN: [{ type: \"NUMBER\", value: 0 }],\n };\n /**\n * This list contains all \"future\" functions that are not compatible with older versions of Excel\n * For more information, see https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/5d1b6d44-6fc1-4ecd-8fef-0b27406cc2bf\n */\n const NON_RETROCOMPATIBLE_FUNCTIONS = [\n \"ACOT\",\n \"ACOTH\",\n \"AGGREGATE\",\n \"ARABIC\",\n \"BASE\",\n \"BETA.DIST\",\n \"BETA.INV\",\n \"BINOM.DIST\",\n \"BINOM.DIST.RANGE\",\n \"BINOM.INV\",\n \"BITAND\",\n \"BITLSHIFT\",\n \"BITOR\",\n \"BITRSHIFT\",\n \"BITXOR\",\n \"CEILING.MATH\",\n \"CEILING.PRECISE\",\n \"CHISQ.DIST\",\n \"CHISQ.DIST.RT\",\n \"CHISQ.INV\",\n \"CHISQ.INV.RT\",\n \"CHISQ.TEST\",\n \"COMBINA\",\n \"CONCAT\",\n \"CONFIDENCE.NORM\",\n \"CONFIDENCE.T\",\n \"COT\",\n \"COTH\",\n \"COVARIANCE.P\",\n \"COVARIANCE.S\",\n \"CSC\",\n \"CSCH\",\n \"DAYS\",\n \"DECIMAL\",\n \"ERF.PRECISE\",\n \"ERFC.PRECISE\",\n \"EXPON.DIST\",\n \"F.DIST\",\n \"F.DIST.RT\",\n \"F.INV\",\n \"F.INV.RT\",\n \"F.TEST\",\n \"FILTERXML\",\n \"FLOOR.MATH\",\n \"FLOOR.PRECISE\",\n \"FORECAST.ETS\",\n \"FORECAST.ETS.CONFINT\",\n \"FORECAST.ETS.SEASONALITY\",\n \"FORECAST.ETS.STAT\",\n \"FORECAST.LINEAR\",\n \"FORMULATEXT\",\n \"GAMMA\",\n \"GAMMA.DIST\",\n \"GAMMA.INV\",\n \"GAMMALN.PRECISE\",\n \"GAUSS\",\n \"HYPGEOM.DIST\",\n \"IFNA\",\n \"IFS\",\n \"IMCOSH\",\n \"IMCOT\",\n \"IMCSC\",\n \"IMCSCH\",\n \"IMSEC\",\n \"IMSECH\",\n \"IMSINH\",\n \"IMTAN\",\n \"ISFORMULA\",\n \"ISOWEEKNUM\",\n \"LOGNORM.DIST\",\n \"LOGNORM.INV\",\n \"MAXIFS\",\n \"MINIFS\",\n \"MODE.MULT\",\n \"MODE.SNGL\",\n \"MUNIT\",\n \"NEGBINOM.DIST\",\n \"NORM.DIST\",\n \"NORM.INV\",\n \"NORM.S.DIST\",\n \"NORM.S.INV\",\n \"NUMBERVALUE\",\n \"PDURATION\",\n \"PERCENTILE.EXC\",\n \"PERCENTILE.INC\",\n \"PERCENTRANK.EXC\",\n \"PERCENTRANK.INC\",\n \"PERMUTATIONA\",\n \"PHI\",\n \"POISSON.DIST\",\n \"QUARTILE.EXC\",\n \"QUARTILE.INC\",\n \"QUERYSTRING\",\n \"RANK.AVG\",\n \"RANK.EQ\",\n \"RRI\",\n \"SEC\",\n \"SECH\",\n \"SHEET\",\n \"SHEETS\",\n \"SKEW.P\",\n \"STDEV.P\",\n \"STDEV.S\",\n \"SWITCH\",\n \"T.DIST\",\n \"T.DIST.2T\",\n \"T.DIST.RT\",\n \"T.INV\",\n \"T.INV.2T\",\n \"T.TEST\",\n \"TEXTJOIN\",\n \"UNICHAR\",\n \"UNICODE\",\n \"VAR.P\",\n \"VAR.S\",\n \"WEBSERVICE\",\n \"WEIBULL.DIST\",\n \"XOR\",\n \"Z.TEST\",\n ];\n const CONTENT_TYPES_FILE = \"[Content_Types].xml\";\n\n /**\n * Map of the different types of conversions warnings and their name in error messages\n */\n var WarningTypes;\n (function (WarningTypes) {\n WarningTypes[\"DiagonalBorderNotSupported\"] = \"Diagonal Borders\";\n WarningTypes[\"BorderStyleNotSupported\"] = \"Border style\";\n WarningTypes[\"FillStyleNotSupported\"] = \"Fill Style\";\n WarningTypes[\"FontNotSupported\"] = \"Font\";\n WarningTypes[\"HorizontalAlignmentNotSupported\"] = \"Horizontal Alignment\";\n WarningTypes[\"VerticalAlignmentNotSupported\"] = \"Vertical Alignments\";\n WarningTypes[\"MultipleRulesCfNotSupported\"] = \"Multiple rules conditional formats\";\n WarningTypes[\"CfTypeNotSupported\"] = \"Conditional format type\";\n WarningTypes[\"CfFormatBorderNotSupported\"] = \"Borders in conditional formats\";\n WarningTypes[\"CfFormatAlignmentNotSupported\"] = \"Alignment in conditional formats\";\n WarningTypes[\"CfFormatNumFmtNotSupported\"] = \"Num formats in conditional formats\";\n WarningTypes[\"CfIconSetEmptyIconNotSupported\"] = \"IconSets with empty icons\";\n WarningTypes[\"BadlyFormattedHyperlink\"] = \"Badly formatted hyperlink\";\n WarningTypes[\"NumFmtIdNotSupported\"] = \"Number format\";\n })(WarningTypes || (WarningTypes = {}));\n class XLSXImportWarningManager {\n constructor() {\n this._parsingWarnings = new Set();\n this._conversionWarnings = new Set();\n }\n addParsingWarning(warning) {\n this._parsingWarnings.add(warning);\n }\n addConversionWarning(warning) {\n this._conversionWarnings.add(warning);\n }\n get warnings() {\n return [...this._parsingWarnings, ...this._conversionWarnings];\n }\n /**\n * Add a warning \"... is not supported\" to the manager.\n *\n * @param type the type of the warning to add\n * @param name optional, name of the element that was not supported\n * @param supported optional, list of the supported elements\n */\n generateNotSupportedWarning(type, name, supported) {\n let warning = `${type} ${name ? '\"' + name + '\" is' : \"are\"} not yet supported. `;\n if (supported) {\n warning += `Only ${supported.join(\", \")} are currently supported.`;\n }\n if (!this._conversionWarnings.has(warning)) {\n this._conversionWarnings.add(warning);\n }\n }\n }\n\n const SUPPORTED_BORDER_STYLES = [\"thin\"];\n const SUPPORTED_HORIZONTAL_ALIGNMENTS = [\"general\", \"left\", \"center\", \"right\"];\n const SUPPORTED_FONTS = [\"Arial\"];\n const SUPPORTED_FILL_PATTERNS = [\"solid\"];\n const SUPPORTED_CF_TYPES = [\n \"expression\",\n \"cellIs\",\n \"colorScale\",\n \"iconSet\",\n \"containsText\",\n \"notContainsText\",\n \"beginsWith\",\n \"endsWith\",\n \"containsBlanks\",\n \"notContainsBlanks\",\n ];\n /** Map between cell type in XLSX file and human readable cell type */\n const CELL_TYPE_CONVERSION_MAP = {\n b: \"boolean\",\n d: \"date\",\n e: \"error\",\n inlineStr: \"inlineStr\",\n n: \"number\",\n s: \"sharedString\",\n str: \"str\",\n };\n /** Conversion map Border Style in XLSX <=> Border style in o_spreadsheet*/\n const BORDER_STYLE_CONVERSION_MAP = {\n dashDot: \"thin\",\n dashDotDot: \"thin\",\n dashed: \"thin\",\n dotted: \"thin\",\n double: \"thin\",\n hair: \"thin\",\n medium: \"thin\",\n mediumDashDot: \"thin\",\n mediumDashDotDot: \"thin\",\n mediumDashed: \"thin\",\n none: undefined,\n slantDashDot: \"thin\",\n thick: \"thin\",\n thin: \"thin\",\n };\n /** Conversion map Horizontal Alignment in XLSX <=> Horizontal Alignment in o_spreadsheet*/\n const H_ALIGNMENT_CONVERSION_MAP = {\n general: undefined,\n left: \"left\",\n center: \"center\",\n right: \"right\",\n fill: \"left\",\n justify: \"left\",\n centerContinuous: \"center\",\n distributed: \"center\",\n };\n /** Convert the \"CellIs\" cf operator.\n * We have all the operators that the xlsx have, but ours begin with a uppercase character */\n function convertCFCellIsOperator(xlsxCfOperator) {\n return (xlsxCfOperator.slice(0, 1).toUpperCase() +\n xlsxCfOperator.slice(1));\n }\n /** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */\n const CF_TYPE_CONVERSION_MAP = {\n aboveAverage: undefined,\n expression: undefined,\n cellIs: undefined,\n colorScale: undefined,\n dataBar: undefined,\n iconSet: undefined,\n top10: undefined,\n uniqueValues: undefined,\n duplicateValues: undefined,\n containsText: \"ContainsText\",\n notContainsText: \"NotContains\",\n beginsWith: \"BeginsWith\",\n endsWith: \"EndsWith\",\n containsBlanks: \"IsEmpty\",\n notContainsBlanks: \"IsNotEmpty\",\n containsErrors: undefined,\n notContainsErrors: undefined,\n timePeriod: undefined,\n };\n /** Conversion map CF thresholds types in XLSX <=> Cf thresholds types in o_spreadsheet */\n const CF_THRESHOLD_CONVERSION_MAP = {\n num: \"number\",\n percent: \"percentage\",\n max: \"value\",\n min: \"value\",\n percentile: \"percentile\",\n formula: \"formula\",\n };\n /**\n * Conversion map between Excels IconSets and our own IconSets. The string is the key of the iconset in the ICON_SETS constant.\n *\n * NoIcons is undefined instead of an empty string because we don't support it and need to mange it separately.\n */\n const ICON_SET_CONVERSION_MAP = {\n NoIcons: undefined,\n \"3Arrows\": \"arrows\",\n \"3ArrowsGray\": \"arrows\",\n \"3Symbols\": \"smiley\",\n \"3Symbols2\": \"smiley\",\n \"3Signs\": \"dots\",\n \"3Flags\": \"dots\",\n \"3TrafficLights1\": \"dots\",\n \"3TrafficLights2\": \"dots\",\n \"4Arrows\": \"arrows\",\n \"4ArrowsGray\": \"arrows\",\n \"4RedToBlack\": \"dots\",\n \"4Rating\": \"smiley\",\n \"4TrafficLights\": \"dots\",\n \"5Arrows\": \"arrows\",\n \"5ArrowsGray\": \"arrows\",\n \"5Rating\": \"smiley\",\n \"5Quarters\": \"dots\",\n \"3Stars\": \"smiley\",\n \"3Triangles\": \"arrows\",\n \"5Boxes\": \"dots\",\n };\n /** Map between legend position in XLSX file and human readable position */\n const DRAWING_LEGEND_POSITION_CONVERSION_MAP = {\n b: \"bottom\",\n t: \"top\",\n l: \"left\",\n r: \"right\",\n tr: \"right\",\n };\n /** Conversion map chart types in XLSX <=> Cf chart types o_spreadsheet (undefined for unsupported chart types)*/\n const CHART_TYPE_CONVERSION_MAP = {\n areaChart: undefined,\n area3DChart: undefined,\n lineChart: \"line\",\n line3DChart: undefined,\n stockChart: undefined,\n radarChart: undefined,\n scatterChart: undefined,\n pieChart: \"pie\",\n pie3DChart: undefined,\n doughnutChart: \"pie\",\n barChart: \"bar\",\n bar3DChart: undefined,\n ofPieChart: undefined,\n surfaceChart: undefined,\n surface3DChart: undefined,\n bubbleChart: undefined,\n };\n /** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/\n const SUBTOTAL_FUNCTION_CONVERSION_MAP = {\n \"1\": \"AVERAGE\",\n \"2\": \"COUNT\",\n \"3\": \"COUNTA\",\n \"4\": \"MAX\",\n \"5\": \"MIN\",\n \"6\": \"PRODUCT\",\n \"7\": \"STDEV\",\n \"8\": \"STDEVP\",\n \"9\": \"SUM\",\n \"10\": \"VAR\",\n \"11\": \"VARP\",\n \"101\": \"AVERAGE\",\n \"102\": \"COUNT\",\n \"103\": \"COUNTA\",\n \"104\": \"MAX\",\n \"105\": \"MIN\",\n \"106\": \"PRODUCT\",\n \"107\": \"STDEV\",\n \"108\": \"STDEVP\",\n \"109\": \"SUM\",\n \"110\": \"VAR\",\n \"111\": \"VARP\",\n };\n /** Mapping between Excel format indexes (see XLSX_FORMAT_MAP) and some supported formats */\n const XLSX_FORMATS_CONVERSION_MAP = {\n 0: \"\",\n 1: \"0\",\n 2: \"0.00\",\n 3: \"#,#00\",\n 4: \"#,##0.00\",\n 9: \"0%\",\n 10: \"0.00%\",\n 11: undefined,\n 12: undefined,\n 13: undefined,\n 14: \"m/d/yyyy\",\n 15: \"m/d/yyyy\",\n 16: \"m/d/yyyy\",\n 17: \"m/d/yyyy\",\n 18: \"hh:mm:ss a\",\n 19: \"hh:mm:ss a\",\n 20: \"hhhh:mm:ss\",\n 21: \"hhhh:mm:ss\",\n 22: \"m/d/yy h:mm\",\n 37: undefined,\n 38: undefined,\n 39: undefined,\n 40: undefined,\n 45: \"hhhh:mm:ss\",\n 46: \"hhhh:mm:ss\",\n 47: \"hhhh:mm:ss\",\n 48: undefined,\n 49: undefined,\n };\n /**\n * Mapping format index to format defined by default\n *\n * OpenXML $18.8.30\n * */\n const XLSX_FORMAT_MAP = {\n \"0\": 1,\n \"0.00\": 2,\n \"#,#00\": 3,\n \"#,##0.00\": 4,\n \"0%\": 9,\n \"0.00%\": 10,\n \"0.00E+00\": 11,\n \"# ?/?\": 12,\n \"# ??/??\": 13,\n \"mm-dd-yy\": 14,\n \"d-mm-yy\": 15,\n \"mm-yy\": 16,\n \"mmm-yy\": 17,\n \"h:mm AM/PM\": 18,\n \"h:mm:ss AM/PM\": 19,\n \"h:mm\": 20,\n \"h:mm:ss\": 21,\n \"m/d/yy h:mm\": 22,\n \"#,##0 ;(#,##0)\": 37,\n \"#,##0 ;[Red](#,##0)\": 38,\n \"#,##0.00;(#,##0.00)\": 39,\n \"#,##0.00;[Red](#,##0.00)\": 40,\n \"mm:ss\": 45,\n \"[h]:mm:ss\": 46,\n \"mmss.0\": 47,\n \"##0.0E+0\": 48,\n \"@\": 49,\n \"hh:mm:ss a\": 19, // TODO: discuss: this format is not recognized by excel for example (doesn't follow their guidelines I guess)\n };\n /** OpenXML $18.8.27 */\n const XLSX_INDEXED_COLORS = {\n 0: \"000000\",\n 1: \"FFFFFF\",\n 2: \"FF0000\",\n 3: \"00FF00\",\n 4: \"0000FF\",\n 5: \"FFFF00\",\n 6: \"FF00FF\",\n 7: \"00FFFF\",\n 8: \"000000\",\n 9: \"FFFFFF\",\n 10: \"FF0000\",\n 11: \"00FF00\",\n 12: \"0000FF\",\n 13: \"FFFF00\",\n 14: \"FF00FF\",\n 15: \"00FFFF\",\n 16: \"800000\",\n 17: \"008000\",\n 18: \"000080\",\n 19: \"808000\",\n 20: \"800080\",\n 21: \"008080\",\n 22: \"C0C0C0\",\n 23: \"808080\",\n 24: \"9999FF\",\n 25: \"993366\",\n 26: \"FFFFCC\",\n 27: \"CCFFFF\",\n 28: \"660066\",\n 29: \"FF8080\",\n 30: \"0066CC\",\n 31: \"CCCCFF\",\n 32: \"000080\",\n 33: \"FF00FF\",\n 34: \"FFFF00\",\n 35: \"00FFFF\",\n 36: \"800080\",\n 37: \"800000\",\n 38: \"008080\",\n 39: \"0000FF\",\n 40: \"00CCFF\",\n 41: \"CCFFFF\",\n 42: \"CCFFCC\",\n 43: \"FFFF99\",\n 44: \"99CCFF\",\n 45: \"FF99CC\",\n 46: \"CC99FF\",\n 47: \"FFCC99\",\n 48: \"3366FF\",\n 49: \"33CCCC\",\n 50: \"99CC00\",\n 51: \"FFCC00\",\n 52: \"FF9900\",\n 53: \"FF6600\",\n 54: \"666699\",\n 55: \"969696\",\n 56: \"003366\",\n 57: \"339966\",\n 58: \"003300\",\n 59: \"333300\",\n 60: \"993300\",\n 61: \"993366\",\n 62: \"333399\",\n 63: \"333333\",\n 64: \"000000\",\n 65: \"FFFFFF\", // system background\n };\n\n /**\n * Most of the functions could stay private, but are exported for testing purposes\n */\n /**\n *\n * Extract the color referenced inside of an XML element and return it as an hex string #RRGGBBAA (or #RRGGBB\n * if alpha = FF)\n *\n * The color is an attribute of the element that can be :\n * - rgb : an rgb string\n * - theme : a reference to a theme element\n * - auto : automatic coloring. Return const AUTO_COLOR in constants.ts.\n * - indexed : a legacy indexing scheme for colors. The only value that should be present in a xlsx is\n * 64 = System Foreground, that we can replace with AUTO_COLOR.\n */\n function convertColor(xlsxColor) {\n if (!xlsxColor) {\n return undefined;\n }\n let rgb;\n if (xlsxColor.rgb) {\n rgb = xlsxColor.rgb;\n }\n else if (xlsxColor.auto) {\n rgb = AUTO_COLOR;\n }\n else if (xlsxColor.indexed) {\n rgb = XLSX_INDEXED_COLORS[xlsxColor.indexed];\n }\n else {\n return undefined;\n }\n rgb = xlsxColorToHEXA(rgb);\n if (xlsxColor.tint) {\n rgb = applyTint(rgb, xlsxColor.tint);\n }\n rgb = rgb.toUpperCase();\n // Remove unnecessary alpha\n if (rgb.length === 9 && rgb.endsWith(\"FF\")) {\n rgb = rgb.slice(0, 7);\n }\n return rgb;\n }\n /**\n * Convert a hex color AARRGGBB (or RRGGBB)(representation inside XLSX Xmls) to a standard js color\n * representation #RRGGBBAA\n */\n function xlsxColorToHEXA(color) {\n if (color.length === 6)\n return \"#\" + color + \"FF\";\n return \"#\" + color.slice(2) + color.slice(0, 2);\n }\n /**\n * Apply tint to a color (see OpenXml spec \u00a718.3.1.15);\n */\n function applyTint(color, tint) {\n const rgba = colorToRGBA(color);\n const hsla = rgbaToHSLA(rgba);\n if (tint < 0) {\n hsla.l = hsla.l * (1 + tint);\n }\n if (tint > 0) {\n hsla.l = hsla.l * (1 - tint) + (100 - 100 * (1 - tint));\n }\n return rgbaToHex(hslaToRGBA(hsla));\n }\n /**\n * Convert a hex + alpha color string to an integer representation. Also remove the alpha.\n *\n * eg. #FF0000FF => 4278190335\n */\n function hexaToInt(hex) {\n if (hex.length === 9) {\n hex = hex.slice(0, 7);\n }\n return parseInt(hex.replace(\"#\", \"\"), 16);\n }\n\n /**\n * Get the relative path between two files\n *\n * Eg.:\n * from \"folder1/file1.txt\" to \"folder2/file2.txt\" => \"../folder2/file2.txt\"\n */\n function getRelativePath(from, to) {\n const fromPathParts = from.split(\"/\");\n const toPathParts = to.split(\"/\");\n let relPath = \"\";\n let startIndex = 0;\n for (let i = 0; i < fromPathParts.length - 1; i++) {\n if (fromPathParts[i] === toPathParts[i]) {\n startIndex++;\n }\n else {\n relPath += \"../\";\n }\n }\n relPath += toPathParts.slice(startIndex).join(\"/\");\n return relPath;\n }\n /**\n * Convert an array of element into an object where the objects keys were the elements position in the array.\n * Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.\n *\n * eg. : [\"a\", \"b\"] => {0:\"a\", 1:\"b\"}\n */\n function arrayToObject(array, indexOffset = 0) {\n const obj = {};\n for (let i = 0; i < array.length; i++) {\n if (array[i]) {\n obj[i + indexOffset] = array[i];\n }\n }\n return obj;\n }\n /**\n * Convert an object whose keys are numbers to an array were the element index was their key in the object.\n *\n * eg. : {0:\"a\", 2:\"b\"} => [\"a\", undefined, \"b\"]\n */\n function objectToArray(obj) {\n const arr = [];\n for (let key of Object.keys(obj).map(Number)) {\n arr[key] = obj[key];\n }\n return arr;\n }\n /**\n * In xlsx we can have string with unicode characters with the format _x00fa_.\n * Replace with characters understandable by JS\n */\n function fixXlsxUnicode(str) {\n return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {\n return String.fromCharCode(parseInt(code, 16));\n });\n }\n\n function convertBorders(data, warningManager) {\n const borderArray = data.borders.map((border) => {\n addBorderWarnings(border, warningManager);\n const b = {\n top: convertBorderDescr$1(border.top, warningManager),\n bottom: convertBorderDescr$1(border.bottom, warningManager),\n left: convertBorderDescr$1(border.left, warningManager),\n right: convertBorderDescr$1(border.right, warningManager),\n };\n Object.keys(b).forEach((key) => b[key] === undefined && delete b[key]);\n return b;\n });\n return arrayToObject(borderArray, 1);\n }\n function convertBorderDescr$1(borderDescr, warningManager) {\n if (!borderDescr)\n return undefined;\n addBorderDescrWarnings(borderDescr, warningManager);\n const style = BORDER_STYLE_CONVERSION_MAP[borderDescr.style];\n return style ? [style, convertColor(borderDescr.color)] : undefined;\n }\n function convertStyles(data, warningManager) {\n const stylesArray = data.styles.map((style) => {\n return convertStyle({\n fontStyle: data.fonts[style.fontId],\n fillStyle: data.fills[style.fillId],\n alignment: style.alignment,\n }, warningManager);\n });\n return arrayToObject(stylesArray, 1);\n }\n function convertStyle(styleStruct, warningManager) {\n var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;\n addStyleWarnings(styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.fontStyle, styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.fillStyle, warningManager);\n addHorizontalAlignmentWarnings((_a = styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.alignment) === null || _a === void 0 ? void 0 : _a.horizontal, warningManager);\n addVerticalAlignmentWarnings((_b = styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.alignment) === null || _b === void 0 ? void 0 : _b.vertical, warningManager);\n return {\n bold: (_c = styleStruct.fontStyle) === null || _c === void 0 ? void 0 : _c.bold,\n italic: (_d = styleStruct.fontStyle) === null || _d === void 0 ? void 0 : _d.italic,\n strikethrough: (_e = styleStruct.fontStyle) === null || _e === void 0 ? void 0 : _e.strike,\n underline: (_f = styleStruct.fontStyle) === null || _f === void 0 ? void 0 : _f.underline,\n align: ((_g = styleStruct.alignment) === null || _g === void 0 ? void 0 : _g.horizontal)\n ? H_ALIGNMENT_CONVERSION_MAP[styleStruct.alignment.horizontal]\n : undefined,\n // In xlsx fills, bgColor is the color of the fill, and fgColor is the color of the pattern above the background, except in solid fills\n fillColor: ((_h = styleStruct.fillStyle) === null || _h === void 0 ? void 0 : _h.patternType) === \"solid\"\n ? convertColor((_j = styleStruct.fillStyle) === null || _j === void 0 ? void 0 : _j.fgColor)\n : convertColor((_k = styleStruct.fillStyle) === null || _k === void 0 ? void 0 : _k.bgColor),\n textColor: convertColor((_l = styleStruct.fontStyle) === null || _l === void 0 ? void 0 : _l.color),\n fontSize: ((_m = styleStruct.fontStyle) === null || _m === void 0 ? void 0 : _m.size)\n ? getClosestFontSize(styleStruct.fontStyle.size)\n : undefined,\n };\n }\n function convertFormats(data, warningManager) {\n const formats = [];\n for (let style of data.styles) {\n const format = convertXlsxFormat(style.numFmtId, data.numFmts, warningManager);\n if (format) {\n formats[style.numFmtId] = format;\n }\n }\n return arrayToObject(formats, 1);\n }\n /**\n * Convert excel format to o_spreadsheet format\n *\n * Excel format are defined in openXML \u00a718.8.31\n */\n function convertXlsxFormat(numFmtId, formats, warningManager) {\n var _a, _b, _c;\n if (numFmtId === 0) {\n return undefined;\n }\n // Format is either defined in the imported data, or the formatId is defined in openXML \u00a718.8.30\n let format = XLSX_FORMATS_CONVERSION_MAP[numFmtId] || ((_a = formats.find((f) => f.id === numFmtId)) === null || _a === void 0 ? void 0 : _a.format);\n if (format) {\n try {\n let convertedFormat = format.replace(/(.*?);.*/, \"$1\"); // only take first part of multi-part format\n convertedFormat = convertedFormat.replace(/\\[(.*)-[A-Z0-9]{3}\\]/g, \"[$1]\"); // remove currency and locale/date system/number system info (ECMA \u00a718.8.31)\n convertedFormat = convertedFormat.replace(/\\[\\$\\]/g, \"\"); // remove empty bocks\n // Quotes in format escape sequences of characters. ATM we only support [$...] blocks to escape characters, and only one of them per format\n const numberOfQuotes = ((_b = convertedFormat.match(/\"/g)) === null || _b === void 0 ? void 0 : _b.length) || 0;\n const numberOfOpenBrackets = ((_c = convertedFormat.match(/\\[/g)) === null || _c === void 0 ? void 0 : _c.length) || 0;\n if (numberOfQuotes / 2 + numberOfOpenBrackets > 1) {\n throw new Error(\"Multiple escaped blocks in format\");\n }\n convertedFormat = convertedFormat.replace(/\"(.*)\"/g, \"[$$$1]\"); // replace '\"...\"' by '[$...]'\n convertedFormat = convertedFormat.replace(/_.{1}/g, \"\"); // _ == ignore with of next char for align purposes. Not supported ATM\n convertedFormat = convertedFormat.replace(/\\*.{1}/g, \"\"); // * == repeat next character enough to fill the line. Not supported ATM\n convertedFormat = convertedFormat.replace(/\\\\ /g, \" \"); // unescape spaces\n formatValue(0, convertedFormat);\n return convertedFormat;\n }\n catch (e) { }\n }\n warningManager.generateNotSupportedWarning(WarningTypes.NumFmtIdNotSupported, format || `nmFmtId ${numFmtId}`);\n return undefined;\n }\n /**\n * We currently only support only a set of font sizes, we cannot define new font sizes.\n * This function adapts an arbitrary font size to the closest supported font size.\n */\n function getClosestFontSize(fontSize) {\n const supportedSizes = Object.keys(fontSizeMap).map(Number);\n const closest = supportedSizes.reduce((prev, curr) => Math.abs(curr - fontSize) < Math.abs(prev - fontSize) ? curr : prev);\n return closest;\n }\n // ---------------------------------------------------------------------------\n // Warnings\n // ---------------------------------------------------------------------------\n function addStyleWarnings(font, fill, warningManager) {\n if (font && font.name && !SUPPORTED_FONTS.includes(font.name)) {\n warningManager.generateNotSupportedWarning(WarningTypes.FontNotSupported, font.name, SUPPORTED_FONTS);\n }\n if (fill && fill.patternType && !SUPPORTED_FILL_PATTERNS.includes(fill.patternType)) {\n warningManager.generateNotSupportedWarning(WarningTypes.FillStyleNotSupported, fill.patternType, SUPPORTED_FILL_PATTERNS);\n }\n }\n function addBorderDescrWarnings(borderDescr, warningManager) {\n if (!SUPPORTED_BORDER_STYLES.includes(borderDescr.style)) {\n warningManager.generateNotSupportedWarning(WarningTypes.BorderStyleNotSupported, borderDescr.style, SUPPORTED_BORDER_STYLES);\n }\n }\n function addBorderWarnings(border, warningManager) {\n if (border.diagonal) {\n warningManager.generateNotSupportedWarning(WarningTypes.DiagonalBorderNotSupported);\n }\n }\n function addHorizontalAlignmentWarnings(alignment, warningManager) {\n if (alignment && !SUPPORTED_HORIZONTAL_ALIGNMENTS.includes(alignment)) {\n warningManager.generateNotSupportedWarning(WarningTypes.HorizontalAlignmentNotSupported, alignment, SUPPORTED_HORIZONTAL_ALIGNMENTS);\n }\n }\n function addVerticalAlignmentWarnings(alignment, warningManager) {\n if (alignment) {\n warningManager.generateNotSupportedWarning(WarningTypes.VerticalAlignmentNotSupported);\n }\n }\n\n function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {\n const cfs = [];\n let cfId = 1;\n for (let cf of xlsxCfs) {\n if (cf.cfRules.length === 0)\n continue;\n addCfConversionWarnings(cf, dxfs, warningManager);\n const rule = cf.cfRules[0];\n let operator;\n const values = [];\n if (rule.dxfId === undefined && !(rule.type === \"colorScale\" || rule.type === \"iconSet\"))\n continue;\n switch (rule.type) {\n case \"aboveAverage\":\n case \"containsErrors\":\n case \"notContainsErrors\":\n case \"dataBar\":\n case \"duplicateValues\":\n case \"expression\":\n case \"top10\":\n case \"uniqueValues\":\n case \"timePeriod\":\n // Not supported\n continue;\n case \"colorScale\":\n const colorScale = convertColorScale(cfId++, cf);\n if (colorScale) {\n cfs.push(colorScale);\n }\n continue;\n case \"iconSet\":\n const iconSet = convertIconSet(cfId++, cf, warningManager);\n if (iconSet) {\n cfs.push(iconSet);\n }\n continue;\n case \"containsText\":\n case \"notContainsText\":\n case \"beginsWith\":\n case \"endsWith\":\n if (!rule.text)\n continue;\n operator = CF_TYPE_CONVERSION_MAP[rule.type];\n values.push(rule.text);\n break;\n case \"containsBlanks\":\n case \"notContainsBlanks\":\n operator = CF_TYPE_CONVERSION_MAP[rule.type];\n break;\n case \"cellIs\":\n if (!rule.operator || !rule.formula || rule.formula.length === 0)\n continue;\n operator = convertCFCellIsOperator(rule.operator);\n values.push(rule.formula[0]);\n if (rule.formula.length === 2) {\n values.push(rule.formula[1]);\n }\n break;\n }\n if (operator && rule.dxfId !== undefined) {\n cfs.push({\n id: (cfId++).toString(),\n ranges: cf.sqref,\n stopIfTrue: rule.stopIfTrue,\n rule: {\n type: \"CellIsRule\",\n operator: operator,\n values: values,\n style: convertStyle({ fontStyle: dxfs[rule.dxfId].font, fillStyle: dxfs[rule.dxfId].fill }, warningManager),\n },\n });\n }\n }\n return cfs;\n }\n function convertColorScale(id, xlsxCf) {\n const scale = xlsxCf.cfRules[0].colorScale;\n if (!scale ||\n scale.cfvos.length !== scale.colors.length ||\n scale.cfvos.length < 2 ||\n scale.cfvos.length > 3) {\n return undefined;\n }\n const thresholds = [];\n for (let i = 0; i < scale.cfvos.length; i++) {\n thresholds.push({\n color: hexaToInt(convertColor(scale.colors[i]) || \"#FFFFFF\"),\n type: CF_THRESHOLD_CONVERSION_MAP[scale.cfvos[i].type],\n value: scale.cfvos[i].value,\n });\n }\n const minimum = thresholds[0];\n const maximum = thresholds.length === 2 ? thresholds[1] : thresholds[2];\n const midpoint = thresholds.length === 3 ? thresholds[1] : undefined;\n return {\n id: id.toString(),\n stopIfTrue: xlsxCf.cfRules[0].stopIfTrue,\n ranges: xlsxCf.sqref,\n rule: { type: \"ColorScaleRule\", minimum, midpoint, maximum },\n };\n }\n /**\n * Convert Icons Sets.\n *\n * In the Xlsx extension of OpenXml, the IconSets can either be simply an IconSet, or a list of Icons\n * (ie. their respective IconSet and their id in this set).\n *\n * In the case of a list of icons :\n * - The order of the icons is lower => middle => upper\n * - The their ids are : 0 : bad, 1 : neutral, 2 : good\n */\n function convertIconSet(id, xlsxCf, warningManager) {\n const xlsxIconSet = xlsxCf.cfRules[0].iconSet;\n if (!xlsxIconSet)\n return undefined;\n let cfVos = xlsxIconSet.cfvos;\n let cfIcons = xlsxIconSet.cfIcons;\n if (cfVos.length < 3 || (cfIcons && cfIcons.length < 3)) {\n return undefined;\n }\n // We don't support icon sets with more than 3 icons, so take the extrema and the middle.\n if (cfVos.length > 3) {\n cfVos = [cfVos[0], cfVos[Math.floor(cfVos.length / 2)], cfVos[cfVos.length - 1]];\n }\n if (cfIcons && cfIcons.length > 3) {\n cfIcons = [cfIcons[0], cfIcons[Math.floor(cfIcons.length / 2)], cfIcons[cfIcons.length - 1]];\n }\n // In xlsx, the thresholds are NOT in the first cfVo, but on the second and third\n const thresholds = [];\n for (let i = 1; i <= 2; i++) {\n const type = CF_THRESHOLD_CONVERSION_MAP[cfVos[i].type];\n if (type === \"value\") {\n return undefined;\n }\n thresholds.push({\n value: cfVos[i].value || \"\",\n operator: cfVos[i].gte ? \"ge\" : \"gt\",\n type: type,\n });\n }\n let icons = {\n lower: cfIcons\n ? convertIcons(cfIcons[0].iconSet, cfIcons[0].iconId)\n : convertIcons(xlsxIconSet.iconSet, 0),\n middle: cfIcons\n ? convertIcons(cfIcons[1].iconSet, cfIcons[1].iconId)\n : convertIcons(xlsxIconSet.iconSet, 1),\n upper: cfIcons\n ? convertIcons(cfIcons[2].iconSet, cfIcons[2].iconId)\n : convertIcons(xlsxIconSet.iconSet, 2),\n };\n if (xlsxIconSet.reverse) {\n icons = { upper: icons.lower, middle: icons.middle, lower: icons.upper };\n }\n // We don't support empty icons in an IconSet, put a dot icon instead\n for (let key of Object.keys(icons)) {\n if (!icons[key]) {\n warningManager.generateNotSupportedWarning(WarningTypes.CfIconSetEmptyIconNotSupported);\n switch (key) {\n case \"upper\":\n icons[key] = ICON_SETS.dots.good;\n break;\n case \"middle\":\n icons[key] = ICON_SETS.dots.neutral;\n break;\n case \"lower\":\n icons[key] = ICON_SETS.dots.bad;\n break;\n }\n }\n }\n return {\n id: id.toString(),\n stopIfTrue: xlsxCf.cfRules[0].stopIfTrue,\n ranges: xlsxCf.sqref,\n rule: {\n type: \"IconSetRule\",\n icons: icons,\n upperInflectionPoint: thresholds[1],\n lowerInflectionPoint: thresholds[0],\n },\n };\n }\n /**\n * Convert an icon from a XLSX.\n *\n * The indexes are : 0 : bad, 1 : neutral, 2 : good\n */\n function convertIcons(xlsxIconSet, index) {\n const iconSet = ICON_SET_CONVERSION_MAP[xlsxIconSet];\n if (!iconSet)\n return \"\";\n return index === 0\n ? ICON_SETS[iconSet].bad\n : index === 1\n ? ICON_SETS[iconSet].neutral\n : ICON_SETS[iconSet].good;\n }\n // ---------------------------------------------------------------------------\n // Warnings\n // ---------------------------------------------------------------------------\n function addCfConversionWarnings(cf, dxfs, warningManager) {\n if (cf.cfRules.length > 1) {\n warningManager.generateNotSupportedWarning(WarningTypes.MultipleRulesCfNotSupported);\n }\n if (!SUPPORTED_CF_TYPES.includes(cf.cfRules[0].type)) {\n warningManager.generateNotSupportedWarning(WarningTypes.CfTypeNotSupported, cf.cfRules[0].type);\n }\n if (cf.cfRules[0].dxfId) {\n const dxf = dxfs[cf.cfRules[0].dxfId];\n if (dxf.border) {\n warningManager.generateNotSupportedWarning(WarningTypes.CfFormatBorderNotSupported);\n }\n if (dxf.alignment) {\n warningManager.generateNotSupportedWarning(WarningTypes.CfFormatAlignmentNotSupported);\n }\n if (dxf.numFmt) {\n warningManager.generateNotSupportedWarning(WarningTypes.CfFormatNumFmtNotSupported);\n }\n }\n }\n\n // -------------------------------------\n // CF HELPERS\n // -------------------------------------\n /**\n * Convert the conditional formatting o-spreadsheet operator to\n * the corresponding excel operator.\n * */\n function convertOperator(operator) {\n switch (operator) {\n case \"IsNotEmpty\":\n return \"notContainsBlanks\";\n case \"IsEmpty\":\n return \"containsBlanks\";\n case \"NotContains\":\n return \"notContainsBlanks\";\n default:\n return operator.charAt(0).toLowerCase() + operator.slice(1);\n }\n }\n // -------------------------------------\n // WORKSHEET HELPERS\n // -------------------------------------\n function getCellType(value) {\n switch (typeof value) {\n case \"boolean\":\n return \"b\";\n case \"string\":\n return \"str\";\n case \"number\":\n return \"n\";\n }\n }\n /**\n * For some reason, Excel will only take the devicePixelRatio (i.e. interface scale on Windows desktop)\n * into account for the height.\n */\n function convertHeightToExcel(height) {\n return Math.round(HEIGHT_FACTOR * height * window.devicePixelRatio * 100) / 100;\n }\n function convertWidthToExcel(width) {\n return Math.round(WIDTH_FACTOR * width * 100) / 100;\n }\n function convertHeightFromExcel(height) {\n if (!height)\n return height;\n return Math.round((height / HEIGHT_FACTOR) * 100) / 100;\n }\n function convertWidthFromExcel(width) {\n if (!width)\n return width;\n return Math.round((width / WIDTH_FACTOR) * 100) / 100;\n }\n function convertBorderDescr(descr) {\n if (!descr) {\n return undefined;\n }\n return {\n style: descr[0],\n color: { rgb: descr[1] },\n };\n }\n function extractStyle(cell, data) {\n let style = {};\n if (cell.style) {\n style = data.styles[cell.style];\n }\n const format = cell.format ? data.formats[cell.format] : undefined;\n const exportedBorder = {};\n if (cell.border) {\n const border = data.borders[cell.border];\n exportedBorder.left = convertBorderDescr(border.left);\n exportedBorder.right = convertBorderDescr(border.right);\n exportedBorder.bottom = convertBorderDescr(border.bottom);\n exportedBorder.top = convertBorderDescr(border.top);\n }\n const styles = {\n font: {\n size: (style === null || style === void 0 ? void 0 : style.fontSize) || DEFAULT_FONT_SIZE,\n color: { rgb: (style === null || style === void 0 ? void 0 : style.textColor) ? style.textColor : \"000000\" },\n family: 2,\n name: \"Arial\",\n },\n fill: (style === null || style === void 0 ? void 0 : style.fillColor)\n ? {\n fgColor: { rgb: style.fillColor },\n }\n : { reservedAttribute: \"none\" },\n numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,\n border: exportedBorder || {},\n alignment: {\n vertical: \"center\",\n horizontal: style.align,\n },\n };\n styles.font[\"strike\"] = !!(style === null || style === void 0 ? void 0 : style.strikethrough) || undefined;\n styles.font[\"underline\"] = !!(style === null || style === void 0 ? void 0 : style.underline) || undefined;\n styles.font[\"bold\"] = !!(style === null || style === void 0 ? void 0 : style.bold) || undefined;\n styles.font[\"italic\"] = !!(style === null || style === void 0 ? void 0 : style.italic) || undefined;\n return styles;\n }\n function normalizeStyle(construct, styles) {\n const { id: fontId } = pushElement(styles[\"font\"], construct.fonts);\n const { id: fillId } = pushElement(styles[\"fill\"], construct.fills);\n const { id: borderId } = pushElement(styles[\"border\"], construct.borders);\n // Normalize this\n const numFmtId = convertFormat(styles[\"numFmt\"], construct.numFmts);\n const style = {\n fontId,\n fillId,\n borderId,\n numFmtId,\n alignment: {\n vertical: styles.alignment.vertical,\n horizontal: styles.alignment.horizontal,\n },\n };\n const { id } = pushElement(style, construct.styles);\n return id;\n }\n function convertFormat(format, numFmtStructure) {\n if (!format) {\n return 0;\n }\n let formatId = XLSX_FORMAT_MAP[format.format];\n if (!formatId) {\n const { id } = pushElement(format, numFmtStructure);\n formatId = id + FIRST_NUMFMT_ID;\n }\n return formatId;\n }\n /**\n * Add a relation to the given file and return its id.\n */\n function addRelsToFile(relsFiles, path, rel) {\n let relsFile = relsFiles.find((file) => file.path === path);\n // the id is a one-based int casted as string\n let id;\n if (!relsFile) {\n id = \"rId1\";\n relsFiles.push({ path, rels: [{ ...rel, id }] });\n }\n else {\n id = `rId${(relsFile.rels.length + 1).toString()}`;\n relsFile.rels.push({\n ...rel,\n id,\n });\n }\n return id;\n }\n function pushElement(property, propertyList) {\n for (let [key, value] of Object.entries(propertyList)) {\n if (JSON.stringify(value) === JSON.stringify(property)) {\n return { id: parseInt(key, 10), list: propertyList };\n }\n }\n let elemId = propertyList.findIndex((elem) => JSON.stringify(elem) === JSON.stringify(property));\n if (elemId === -1) {\n propertyList.push(property);\n elemId = propertyList.length - 1;\n }\n return {\n id: elemId,\n list: propertyList,\n };\n }\n const chartIds = [];\n /**\n * Convert a chart o-spreadsheet id to a xlsx id which\n * are unsigned integers (starting from 1).\n */\n function convertChartId(chartId) {\n const xlsxId = chartIds.findIndex((id) => id === chartId);\n if (xlsxId === -1) {\n chartIds.push(chartId);\n return chartIds.length;\n }\n return xlsxId + 1;\n }\n /**\n * Convert a value expressed in dot to EMU.\n * EMU = English Metrical Unit\n * There are 914400 EMU per inch.\n *\n * /!\\ A value expressed in EMU cannot be fractional.\n * See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement\n */\n function convertDotValueToEMU(value) {\n const DPI = 96;\n return Math.round((value * 914400) / DPI);\n }\n function getRangeSize(reference, defaultSheetIndex, data) {\n let xc = reference;\n let sheetName = undefined;\n ({ xc, sheetName } = splitReference(reference));\n let rangeSheetIndex;\n if (sheetName) {\n const index = data.sheets.findIndex((sheet) => sheet.name === sheetName);\n if (index < 0) {\n throw new Error(\"Unable to find a sheet with the name \" + sheetName);\n }\n rangeSheetIndex = index;\n }\n else {\n rangeSheetIndex = Number(defaultSheetIndex);\n }\n const zone = toUnboundedZone(xc);\n if (zone.right === undefined) {\n zone.right = data.sheets[rangeSheetIndex].colNumber;\n }\n if (zone.bottom === undefined) {\n zone.bottom = data.sheets[rangeSheetIndex].rowNumber;\n }\n return (zone.right - zone.left + 1) * (zone.bottom - zone.top + 1);\n }\n function convertEMUToDotValue(value) {\n const DPI = 96;\n return Math.round((value * DPI) / 914400);\n }\n /**\n * Get the position of the start of a column in Excel (in px).\n */\n function getColPosition(colIndex, sheetData) {\n var _a;\n let position = 0;\n for (let i = 0; i < colIndex; i++) {\n const colAtIndex = sheetData.cols.find((col) => i >= col.min && i <= col.max);\n if (colAtIndex === null || colAtIndex === void 0 ? void 0 : colAtIndex.width) {\n position += colAtIndex.width;\n }\n else if ((_a = sheetData.sheetFormat) === null || _a === void 0 ? void 0 : _a.defaultColWidth) {\n position += sheetData.sheetFormat.defaultColWidth;\n }\n else {\n position += EXCEL_DEFAULT_COL_WIDTH;\n }\n }\n return position / WIDTH_FACTOR;\n }\n /**\n * Get the position of the start of a row in Excel (in px).\n */\n function getRowPosition(rowIndex, sheetData) {\n var _a;\n let position = 0;\n for (let i = 0; i < rowIndex; i++) {\n const rowAtIndex = sheetData.rows[i];\n if (rowAtIndex === null || rowAtIndex === void 0 ? void 0 : rowAtIndex.height) {\n position += rowAtIndex.height;\n }\n else if ((_a = sheetData.sheetFormat) === null || _a === void 0 ? void 0 : _a.defaultRowHeight) {\n position += sheetData.sheetFormat.defaultRowHeight;\n }\n else {\n position += EXCEL_DEFAULT_ROW_HEIGHT;\n }\n }\n return position / HEIGHT_FACTOR;\n }\n\n function convertFigures(sheetData) {\n let id = 1;\n return sheetData.figures\n .map((figure) => convertFigure(figure, (id++).toString(), sheetData))\n .filter(isDefined$1);\n }\n function convertFigure(figure, id, sheetData) {\n const x1 = getColPosition(figure.anchors[0].col, sheetData) +\n convertEMUToDotValue(figure.anchors[0].colOffset);\n const x2 = getColPosition(figure.anchors[1].col, sheetData) +\n convertEMUToDotValue(figure.anchors[1].colOffset);\n const y1 = getRowPosition(figure.anchors[0].row, sheetData) +\n convertEMUToDotValue(figure.anchors[0].rowOffset);\n const y2 = getRowPosition(figure.anchors[1].row, sheetData) +\n convertEMUToDotValue(figure.anchors[1].rowOffset);\n const width = x2 - x1;\n const height = y2 - y1;\n const chartData = convertChartData(figure.data);\n if (!chartData)\n return undefined;\n return {\n id: id,\n x: x1,\n y: y1,\n width: width,\n height: height,\n tag: \"chart\",\n data: convertChartData(figure.data),\n };\n }\n function convertChartData(chartData) {\n var _a;\n const labelRange = (_a = chartData.dataSets[0].label) === null || _a === void 0 ? void 0 : _a.replace(/\\$/g, \"\");\n let dataSets = chartData.dataSets.map((data) => data.range.replace(/\\$/g, \"\"));\n // For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset\n if (chartData.type === \"pie\") {\n dataSets.reverse();\n }\n return {\n dataSets,\n dataSetsHaveTitle: false,\n labelRange,\n title: chartData.title || \"\",\n type: chartData.type,\n background: convertColor({ rgb: chartData.backgroundColor }) || \"#FFFFFF\",\n verticalAxisPosition: chartData.verticalAxisPosition,\n legendPosition: chartData.legendPosition,\n stacked: chartData.stacked || false,\n aggregated: false,\n labelsAsText: false,\n };\n }\n\n /**\n * Match external reference (ex. '[1]Sheet 3'!$B$4)\n *\n * First match group is the external reference id\n * Second match group is the sheet id\n * Third match group is the reference of the cell\n */\n const externalReferenceRegex = new RegExp(/'?\\[([0-9]*)\\](.*)'?!(\\$?[a-zA-Z]*\\$?[0-9]*)/g);\n const subtotalRegex = new RegExp(/SUBTOTAL\\(([0-9]*),/g);\n const cellRegex = new RegExp(cellReference.source, \"ig\");\n function convertFormulasContent(sheet, data) {\n const sfMap = getSharedFormulasMap(sheet);\n for (let cell of sheet.rows.map((row) => row.cells).flat()) {\n if (cell === null || cell === void 0 ? void 0 : cell.formula) {\n cell.formula.content =\n cell.formula.sharedIndex !== undefined && !cell.formula.content\n ? \"=\" + adaptFormula(cell.xc, sfMap[cell.formula.sharedIndex])\n : \"=\" + cell.formula.content;\n cell.formula.content = convertFormula(cell.formula.content, data);\n }\n }\n }\n function getSharedFormulasMap(sheet) {\n const formulas = {};\n for (let row of sheet.rows) {\n for (let cell of row.cells) {\n if (cell.formula && cell.formula.sharedIndex !== undefined && cell.formula.content) {\n formulas[cell.formula.sharedIndex] = { refCellXc: cell.xc, formula: cell.formula.content };\n }\n }\n }\n return formulas;\n }\n /**\n * Convert an XLSX formula into something we can evaluate.\n * - remove _xlfn. flags before function names\n * - convert the SUBTOTAL(index, formula) function to the function given by its index\n * - change #REF! into #REF\n * - convert external references into their value\n */\n function convertFormula(formula, data) {\n formula = formula.replace(\"_xlfn.\", \"\");\n formula = formula.replace(/#REF!/g, \"#REF\");\n // SUBOTOTAL function, eg. =SUBTOTAL(3, {formula})\n formula = formula.replace(subtotalRegex, (match, functionId) => {\n const convertedFunction = SUBTOTAL_FUNCTION_CONVERSION_MAP[functionId];\n return convertedFunction ? convertedFunction + \"(\" : match;\n });\n // External references, eg. ='[1]Sheet 3'!$B$4\n formula = formula.replace(externalReferenceRegex, (match, externalRefId, sheetName, cellRef) => {\n var _a;\n externalRefId = Number(externalRefId) - 1;\n cellRef = cellRef.replace(/\\$/g, \"\");\n const sheetIndex = data.externalBooks[externalRefId].sheetNames.findIndex((name) => name === sheetName);\n if (sheetIndex === -1) {\n return match;\n }\n const externalDataset = (_a = data.externalBooks[externalRefId].datasets.find((dataset) => dataset.sheetId === sheetIndex)) === null || _a === void 0 ? void 0 : _a.data;\n if (!externalDataset) {\n return match;\n }\n const datasetValue = externalDataset && externalDataset[cellRef];\n const convertedValue = Number(datasetValue) ? datasetValue : `\"${datasetValue}\"`;\n return convertedValue || match;\n });\n return formula;\n }\n /**\n * Transform a shared formula for the given target.\n *\n * This will compute the offset between the original cell of the shared formula and the target cell,\n * then apply this offset to all the ranges in the formula (taking fixed references into account)\n */\n function adaptFormula(targetCell, sf) {\n const refPosition = toCartesian(sf.refCellXc);\n let newFormula = sf.formula.slice();\n let match;\n do {\n match = cellRegex.exec(newFormula);\n if (match) {\n const formulaPosition = toCartesian(match[0].replace(\"$\", \"\"));\n const targetPosition = toCartesian(targetCell);\n const rangePart = {\n colFixed: match[0].startsWith(\"$\"),\n rowFixed: match[0].includes(\"$\", 1),\n };\n const offset = {\n col: targetPosition.col - refPosition.col,\n row: targetPosition.row - refPosition.row,\n };\n const offsettedPosition = {\n col: rangePart.colFixed ? formulaPosition.col : formulaPosition.col + offset.col,\n row: rangePart.rowFixed ? formulaPosition.row : formulaPosition.row + offset.row,\n };\n newFormula =\n newFormula.slice(0, match.index) +\n toXC(offsettedPosition.col, offsettedPosition.row, rangePart) +\n newFormula.slice(match.index + match[0].length);\n }\n } while (match);\n return newFormula;\n }\n\n function convertSheets(data, warningManager) {\n return data.sheets.map((sheet) => {\n convertFormulasContent(sheet, data);\n const sheetDims = getSheetDims(sheet);\n const sheetOptions = sheet.sheetViews[0];\n return {\n id: sheet.sheetName,\n areGridLinesVisible: sheetOptions ? sheetOptions.showGridLines : true,\n name: sheet.sheetName,\n colNumber: sheetDims[0],\n rowNumber: sheetDims[1],\n cells: convertCells(sheet, data, sheetDims, warningManager),\n merges: sheet.merges,\n cols: convertCols(sheet, sheetDims[0]),\n rows: convertRows(sheet, sheetDims[1]),\n conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),\n figures: convertFigures(sheet),\n isVisible: sheet.isVisible,\n panes: sheetOptions\n ? { xSplit: sheetOptions.pane.xSplit, ySplit: sheetOptions.pane.ySplit }\n : { xSplit: 0, ySplit: 0 },\n filterTables: [],\n };\n });\n }\n function convertCols(sheet, numberOfCols) {\n var _a;\n const cols = {};\n // Excel begins indexes at 1\n for (let i = 1; i < numberOfCols + 1; i++) {\n const col = sheet.cols.find((col) => col.min <= i && i <= col.max);\n let colSize;\n if (col && col.width)\n colSize = col.width;\n else if ((_a = sheet.sheetFormat) === null || _a === void 0 ? void 0 : _a.defaultColWidth)\n colSize = sheet.sheetFormat.defaultColWidth;\n else\n colSize = EXCEL_DEFAULT_COL_WIDTH;\n cols[i - 1] = { size: convertWidthFromExcel(colSize), isHidden: col === null || col === void 0 ? void 0 : col.hidden };\n }\n return cols;\n }\n function convertRows(sheet, numberOfRows) {\n var _a;\n const rows = {};\n // Excel begins indexes at 1\n for (let i = 1; i < numberOfRows + 1; i++) {\n const row = sheet.rows.find((row) => row.index === i);\n let rowSize;\n if (row && row.height)\n rowSize = row.height;\n else if ((_a = sheet.sheetFormat) === null || _a === void 0 ? void 0 : _a.defaultRowHeight)\n rowSize = sheet.sheetFormat.defaultRowHeight;\n else\n rowSize = EXCEL_DEFAULT_ROW_HEIGHT;\n rows[i - 1] = { size: convertHeightFromExcel(rowSize), isHidden: row === null || row === void 0 ? void 0 : row.hidden };\n }\n return rows;\n }\n /** Remove newlines (\\n) in shared strings, We do not support them */\n function convertSharedStrings(xlsxSharedStrings) {\n return xlsxSharedStrings.map((str) => str.replace(/\\n/g, \"\"));\n }\n function convertCells(sheet, data, sheetDims, warningManager) {\n const cells = {};\n const sharedStrings = convertSharedStrings(data.sharedStrings);\n const hyperlinkMap = sheet.hyperlinks.reduce((map, link) => {\n map[link.xc] = link;\n return map;\n }, {});\n for (let row of sheet.rows) {\n for (let cell of row.cells) {\n cells[cell.xc] = {\n content: getCellValue(cell, hyperlinkMap, sharedStrings, warningManager),\n // + 1 : our indexes for normalized values begin at 1 and not 0\n style: cell.styleIndex ? cell.styleIndex + 1 : undefined,\n border: cell.styleIndex ? data.styles[cell.styleIndex].borderId + 1 : undefined,\n format: cell.styleIndex ? data.styles[cell.styleIndex].numFmtId + 1 : undefined,\n };\n }\n }\n // Apply row style\n for (let row of sheet.rows.filter((row) => row.styleIndex)) {\n for (let colIndex = 1; colIndex <= sheetDims[0]; colIndex++) {\n const xc = toXC(colIndex - 1, row.index - 1); // Excel indexes start at 1\n let cell = cells[xc];\n if (!cell) {\n cell = {};\n cells[xc] = cell;\n }\n cell.style = cell.style ? cell.style : row.styleIndex + 1;\n cell.border = cell.border ? cell.border : data.styles[row.styleIndex].borderId + 1;\n cell.format = cell.format ? cell.format : data.styles[row.styleIndex].numFmtId + 1;\n }\n }\n // Apply col style\n for (let col of sheet.cols.filter((col) => col.styleIndex)) {\n for (let colIndex = col.min; colIndex <= Math.min(col.max, sheetDims[0]); colIndex++) {\n for (let rowIndex = 1; rowIndex <= sheetDims[1]; rowIndex++) {\n const xc = toXC(colIndex - 1, rowIndex - 1); // Excel indexes start at 1\n let cell = cells[xc];\n if (!cell) {\n cell = {};\n cells[xc] = cell;\n }\n cell.style = cell.style ? cell.style : col.styleIndex + 1;\n cell.border = cell.border ? cell.border : data.styles[col.styleIndex].borderId + 1;\n cell.format = cell.format ? cell.format : data.styles[col.styleIndex].numFmtId + 1;\n }\n }\n }\n return cells;\n }\n function getCellValue(cell, hyperLinksMap, sharedStrings, warningManager) {\n let cellValue;\n switch (cell.type) {\n case \"sharedString\":\n const ssIndex = parseInt(cell.value, 10);\n cellValue = sharedStrings[ssIndex];\n break;\n case \"boolean\":\n cellValue = Number(cell.value) ? \"TRUE\" : \"FALSE\";\n break;\n case \"date\": // I'm not sure where this is used rather than a number with a format\n case \"error\": // I don't think Excel really uses this\n case \"inlineStr\":\n case \"number\":\n case \"str\":\n cellValue = cell.value;\n break;\n }\n if (cellValue && hyperLinksMap[cell.xc]) {\n cellValue = convertHyperlink(hyperLinksMap[cell.xc], cellValue, warningManager);\n }\n if (cell.formula) {\n cellValue = cell.formula.content;\n }\n return cellValue;\n }\n function convertHyperlink(link, cellValue, warningManager) {\n const label = link.display || cellValue;\n if (!link.relTarget && !link.location) {\n warningManager.generateNotSupportedWarning(WarningTypes.BadlyFormattedHyperlink);\n }\n const url = link.relTarget\n ? link.relTarget\n : buildSheetLink(splitReference(link.location).sheetName);\n return markdownLink(label, url);\n }\n function getSheetDims(sheet) {\n const dims = [0, 0];\n for (let row of sheet.rows) {\n dims[0] = Math.max(dims[0], ...row.cells.map((cell) => toCartesian(cell.xc).col));\n dims[1] = Math.max(dims[1], row.index);\n }\n dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);\n dims[1] = Math.max(dims[1], EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS);\n return dims;\n }\n\n const TABLE_HEADER_STYLE = {\n fillColor: \"#000000\",\n textColor: \"#ffffff\",\n bold: true,\n };\n const TABLE_HIGHLIGHTED_CELL_STYLE = {\n bold: true,\n };\n const TABLE_BORDER_STYLE = [\"thin\", \"#000000FF\"];\n /**\n * Convert the imported XLSX tables.\n *\n * We will create a FilterTable if the imported table have filters, then apply a style in all the cells of the table\n * and convert the table-specific formula references into standard references.\n *\n * Change the converted data in-place.\n */\n function convertTables(convertedData, xlsxData) {\n for (const xlsxSheet of xlsxData.sheets) {\n for (const table of xlsxSheet.tables) {\n const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName);\n if (!sheet || !table.autoFilter)\n continue;\n if (!sheet.filterTables)\n sheet.filterTables = [];\n sheet.filterTables.push({ range: table.ref });\n }\n }\n applyTableStyle(convertedData, xlsxData);\n convertTableFormulaReferences(convertedData.sheets, xlsxData.sheets);\n }\n /**\n * Apply a style to all the cells that are in a table, and add the created styles in the converted data.\n *\n * In XLSXs, the style of the cells of a table are not directly in the sheet, but rather deduced from the style of\n * the table that is defined in the table's XML file. The style of the table is a string referencing a standard style\n * defined in the OpenXML specifications. As there are 80+ different styles, we won't implement every one of them but\n * we will just define a style that will be used for all the imported tables.\n */\n function applyTableStyle(convertedData, xlsxData) {\n var _a, _b, _c, _d;\n const styles = objectToArray(convertedData.styles);\n const borders = objectToArray(convertedData.borders);\n for (let xlsxSheet of xlsxData.sheets) {\n for (let table of xlsxSheet.tables) {\n const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName);\n if (!sheet)\n continue;\n const tableZone = toZone(table.ref);\n // Table style\n for (let i = 0; i < table.headerRowCount; i++) {\n applyStyleToZone(TABLE_HEADER_STYLE, { ...tableZone, bottom: tableZone.top + i }, sheet.cells, styles);\n }\n for (let i = 0; i < table.totalsRowCount; i++) {\n applyStyleToZone(TABLE_HIGHLIGHTED_CELL_STYLE, { ...tableZone, top: tableZone.bottom - i }, sheet.cells, styles);\n }\n if ((_a = table.style) === null || _a === void 0 ? void 0 : _a.showFirstColumn) {\n applyStyleToZone(TABLE_HIGHLIGHTED_CELL_STYLE, { ...tableZone, right: tableZone.left }, sheet.cells, styles);\n }\n if ((_b = table.style) === null || _b === void 0 ? void 0 : _b.showLastColumn) {\n applyStyleToZone(TABLE_HIGHLIGHTED_CELL_STYLE, { ...tableZone, left: tableZone.right }, sheet.cells, styles);\n }\n // Table borders\n // Borders at : table outline + col(/row) if showColumnStripes(/showRowStripes) + border above totalRow\n for (let col = tableZone.left; col <= tableZone.right; col++) {\n for (let row = tableZone.top; row <= tableZone.bottom; row++) {\n const xc = toXC(col, row);\n const cell = sheet.cells[xc];\n const border = {\n left: col === tableZone.left || ((_c = table.style) === null || _c === void 0 ? void 0 : _c.showColumnStripes)\n ? TABLE_BORDER_STYLE\n : undefined,\n right: col === tableZone.right ? TABLE_BORDER_STYLE : undefined,\n top: row === tableZone.top ||\n ((_d = table.style) === null || _d === void 0 ? void 0 : _d.showRowStripes) ||\n row > tableZone.bottom - table.totalsRowCount\n ? TABLE_BORDER_STYLE\n : undefined,\n bottom: row === tableZone.bottom ? TABLE_BORDER_STYLE : undefined,\n };\n const newBorder = (cell === null || cell === void 0 ? void 0 : cell.border) ? { ...borders[cell.border], ...border } : border;\n let borderIndex = borders.findIndex((border) => deepEquals(border, newBorder));\n if (borderIndex === -1) {\n borderIndex = borders.length;\n borders.push(newBorder);\n }\n if (cell) {\n cell.border = borderIndex;\n }\n else {\n sheet.cells[xc] = { border: borderIndex };\n }\n }\n }\n }\n }\n convertedData.styles = arrayToObject(styles);\n convertedData.borders = arrayToObject(borders);\n }\n /**\n * Apply a style to all the cells in the zone. The applied style WILL NOT overwrite values in existing style of the cell.\n *\n * If a style that was not in the styles array was applied, push it into the style array.\n */\n function applyStyleToZone(appliedStyle, zone, cells, styles) {\n for (let col = zone.left; col <= zone.right; col++) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n const xc = toXC(col, row);\n const cell = cells[xc];\n const newStyle = (cell === null || cell === void 0 ? void 0 : cell.style) ? { ...styles[cell.style], ...appliedStyle } : appliedStyle;\n let styleIndex = styles.findIndex((style) => deepEquals(style, newStyle));\n if (styleIndex === -1) {\n styleIndex = styles.length;\n styles.push(newStyle);\n }\n if (cell) {\n cell.style = styleIndex;\n }\n else {\n cells[xc] = { style: styleIndex };\n }\n }\n }\n }\n /**\n * In all the sheets, replace the table-only references in the formula cells with standard references.\n */\n function convertTableFormulaReferences(convertedSheets, xlsxSheets) {\n for (let sheet of convertedSheets) {\n const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;\n for (let table of tables) {\n const tabRef = table.name + \"[\";\n for (let position of positions(toZone(table.ref))) {\n const xc = toXC(position.col, position.row);\n const cell = sheet.cells[xc];\n if (cell && cell.content && cell.content.startsWith(\"=\")) {\n let refIndex;\n while ((refIndex = cell.content.indexOf(tabRef)) !== -1) {\n let reference = cell.content.slice(refIndex + tabRef.length);\n // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]\n let endIndex = reference.indexOf(\"]\");\n if (reference.startsWith(`[`)) {\n endIndex = reference.indexOf(\"]\", endIndex + 1);\n endIndex = reference.indexOf(\"]\", endIndex + 1);\n }\n reference = reference.slice(0, endIndex);\n const convertedRef = convertTableReference(reference, table, xc);\n cell.content =\n cell.content.slice(0, refIndex) +\n convertedRef +\n cell.content.slice(tabRef.length + refIndex + endIndex + 1);\n }\n }\n }\n }\n }\n }\n /**\n * Convert table-specific references in formulas into standard references.\n *\n * A reference in a table can have the form (only the part between brackets should be given to this function):\n * - tableName[colName] : reference to the whole column \"colName\"\n * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName\n *\n * The available keywords are :\n * - #All : all the column (including totals)\n * - #Data : only the column data (no headers/totals)\n * - #Headers : only the header of the column\n * - #Totals : only the totals of the column\n * - #This Row : only the element in the same row as the cell\n */\n function convertTableReference(expr, table, cellXc) {\n const refElements = expr.split(\",\");\n const tableZone = toZone(table.ref);\n const refZone = { ...tableZone };\n let isReferencedZoneValid = true;\n // Single column reference\n if (refElements.length === 1) {\n const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);\n refZone.left = refZone.right = colRelativeIndex + tableZone.left;\n if (table.headerRowCount) {\n refZone.top += table.headerRowCount;\n }\n if (table.totalsRowCount) {\n refZone.bottom -= 1;\n }\n }\n // Other references\n else {\n switch (refElements[0].slice(1, refElements[0].length - 1)) {\n case \"#All\":\n refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;\n refZone.bottom = tableZone.bottom;\n break;\n case \"#Data\":\n refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;\n refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;\n break;\n case \"#This Row\":\n refZone.top = refZone.bottom = toCartesian(cellXc).row;\n break;\n case \"#Headers\":\n refZone.top = refZone.bottom = tableZone.top;\n if (!table.headerRowCount) {\n isReferencedZoneValid = false;\n }\n break;\n case \"#Totals\":\n refZone.top = refZone.bottom = tableZone.bottom;\n if (!table.totalsRowCount) {\n isReferencedZoneValid = false;\n }\n break;\n }\n const colRef = refElements[1].slice(1, refElements[1].length - 1);\n const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);\n refZone.left = refZone.right = colRelativeIndex + tableZone.left;\n }\n if (!isReferencedZoneValid) {\n return INCORRECT_RANGE_STRING;\n }\n return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);\n }\n\n // -------------------------------------\n // XML HELPERS\n // -------------------------------------\n function createXMLFile(doc, path, contentType) {\n return {\n content: new XMLSerializer().serializeToString(doc),\n path,\n contentType,\n };\n }\n function xmlEscape(str) {\n return String(str)\n .replace(/\\&/g, \"&\")\n .replace(/\\/g, \">\")\n .replace(/\\\"/g, \""\")\n .replace(/\\'/g, \"'\");\n }\n function formatAttributes(attrs) {\n return new XMLString(attrs.map(([key, val]) => `${key}=\"${xmlEscape(val)}\"`).join(\" \"));\n }\n function parseXML(xmlString, mimeType = \"text/xml\") {\n const document = new DOMParser().parseFromString(xmlString.toString(), mimeType);\n const parserError = document.querySelector(\"parsererror\");\n if (parserError) {\n const errorString = parserError.innerHTML;\n const lineNumber = parseInt(errorString.split(\":\")[0], 10);\n const xmlStringArray = xmlString.toString().trim().split(\"\\n\");\n const xmlPreview = xmlStringArray\n .slice(Math.max(lineNumber - 3, 0), Math.min(lineNumber + 2, xmlStringArray.length))\n .join(\"\\n\");\n throw new Error(`XML string could not be parsed: ${errorString}\\n${xmlPreview}`);\n }\n return document;\n }\n function getDefaultXLSXStructure() {\n return {\n relsFiles: [],\n sharedStrings: [],\n // default Values that will always be part of the style sheet\n styles: [\n {\n fontId: 0,\n fillId: 0,\n numFmtId: 0,\n borderId: 0,\n alignment: { vertical: \"center\" },\n },\n ],\n fonts: [\n {\n size: DEFAULT_FONT_SIZE,\n family: 2,\n color: { rgb: \"000000\" },\n name: \"Calibri\",\n },\n ],\n fills: [{ reservedAttribute: \"none\" }, { reservedAttribute: \"gray125\" }],\n borders: [{}],\n numFmts: [],\n dxfs: [],\n };\n }\n function createOverride(partName, contentType) {\n return escapeXml /*xml*/ `\n
\n `;\n }\n function joinXmlNodes(xmlNodes) {\n return new XMLString(xmlNodes.join(\"\\n\"));\n }\n /**\n * Escape interpolated values except if the value is already\n * a properly escaped XML string.\n *\n * ```\n * escapeXml`
${\"This will be escaped\"} `\n * ```\n */\n function escapeXml(strings, ...expressions) {\n let str = [strings[0]];\n for (let i = 0; i < expressions.length; i++) {\n const value = expressions[i] instanceof XMLString ? expressions[i] : xmlEscape(expressions[i]);\n str.push(value + strings[i + 1]);\n }\n return new XMLString(concat(str));\n }\n /**\n * Removes the namespace of all the xml tags in the string.\n *\n * Eg. : \"ns:test a\" => \"test a\"\n */\n function removeNamespaces(query) {\n return query.replace(/[a-z0-9]+:(?=[a-z0-9]+)/gi, \"\");\n }\n /**\n * Escape the namespace's colons of all the xml tags in the string.\n *\n * Eg. : \"ns:test a\" => \"ns\\\\:test a\"\n */\n function escapeNamespaces(query) {\n return query.replace(/([a-z0-9]+):(?=[a-z0-9]+)/gi, \"$1\\\\:\");\n }\n /**\n * Return true if the querySelector ignores the namespaces when searching for a tag in the DOM.\n *\n * Should return true if it's running on a browser, and false if it's running on jest (jsdom).\n */\n function areNamespaceIgnoredByQuerySelector() {\n const doc = new DOMParser().parseFromString(\"
\", \"text/xml\");\n return doc.querySelector(\"test\") !== null;\n }\n\n class AttributeValue {\n constructor(value) {\n this.value = value;\n }\n asString() {\n return fixXlsxUnicode(String(this.value));\n }\n asBool() {\n return Boolean(Number(this.value));\n }\n asNum() {\n return Number(this.value);\n }\n }\n class XlsxBaseExtractor {\n constructor(rootFile, xlsxStructure, warningManager) {\n // The xml file we are currently parsing. We should have one Extractor class by XLSXImportFile, but\n // the XLSXImportFile contains both the main .xml file, and the .rels file\n this.currentFile = undefined;\n this.rootFile = rootFile;\n this.currentFile = rootFile.file.fileName;\n this.xlsxFileStructure = xlsxStructure;\n this.warningManager = warningManager;\n this.areNamespaceIgnored = areNamespaceIgnoredByQuerySelector();\n this.relationships = {};\n if (rootFile.rels) {\n this.extractRelationships(rootFile.rels).map((rel) => {\n this.relationships[rel.id] = rel;\n });\n }\n }\n /**\n * Extract all the relationships inside a .xml.rels file\n */\n extractRelationships(relFile) {\n return this.mapOnElements({ parent: relFile.xml, query: \"Relationship\" }, (relationshipElement) => {\n return {\n id: this.extractAttr(relationshipElement, \"Id\", { required: true }).asString(),\n target: this.extractAttr(relationshipElement, \"Target\", { required: true }).asString(),\n type: this.extractAttr(relationshipElement, \"Type\", { required: true }).asString(),\n };\n });\n }\n /**\n * Get the list of all the XLSX files in the XLSX file structure\n */\n getListOfFiles() {\n const files = Object.values(this.xlsxFileStructure).flat().filter(isDefined$1);\n return files;\n }\n /**\n * Return an array containing the return value of the given function applied to all the XML elements\n * found using the MapOnElementArgs.\n *\n * The arguments contains :\n * - query : a QuerySelector string to find the elements to apply the function to\n * - parent : an XML element or XML document in which to find the queried elements\n * - children : if true, the function is applied on the direct children of the queried element\n *\n * This method will also handle the errors thrown in the argument function.\n */\n mapOnElements(args, fct) {\n var _a;\n const ret = [];\n const oldWorkingDocument = this.currentFile;\n let elements;\n if (args.children) {\n const children = (_a = this.querySelector(args.parent, args.query)) === null || _a === void 0 ? void 0 : _a.children;\n elements = children ? children : [];\n }\n else {\n elements = this.querySelectorAll(args.parent, args.query);\n }\n if (elements) {\n for (let element of elements) {\n try {\n ret.push(fct(element));\n }\n catch (e) {\n this.catchErrorOnElement(e, element);\n }\n }\n }\n this.currentFile = oldWorkingDocument;\n return ret;\n }\n /**\n * Log an error caught when parsing an element in the warningManager.\n */\n catchErrorOnElement(error, onElement) {\n const errorMsg = onElement\n ? `Error when parsing an element <${onElement.tagName}> of file ${this.currentFile}, skip this element. \\n${error.stack}`\n : `Error when parsing file ${this.currentFile}.`;\n this.warningManager.addParsingWarning([errorMsg, error.message].join(\"\\n\"));\n }\n /**\n * Extract an attribute from an Element.\n *\n * If the attribute is required but was not found, will add a warning in the warningManager if it was given a default\n * value, and throw an error if no default value was given.\n *\n * Can only return undefined value for non-required attributes without default value.\n */\n extractAttr(e, attName, optionalArgs) {\n const attribute = e.attributes[attName];\n if (!attribute)\n this.handleMissingValue(e, `attribute \"${attName}\"`, optionalArgs);\n const value = (attribute === null || attribute === void 0 ? void 0 : attribute.value) ? attribute.value : optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default;\n return (value === undefined ? undefined : new AttributeValue(value));\n }\n /**\n * Extract the text content of an Element.\n *\n * If the text content is required but was not found, will add a warning in the warningManager if it was given a default\n * value, and throw an error if no default value was given.\n *\n * Can only return undefined value for non-required text content without default value.\n */\n extractTextContent(element, optionalArgs) {\n var _a;\n if ((optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default) !== undefined && typeof optionalArgs.default !== \"string\") {\n throw new Error(\"extractTextContent default value should be a string\");\n }\n const shouldPreserveSpaces = ((_a = element === null || element === void 0 ? void 0 : element.attributes[\"xml:space\"]) === null || _a === void 0 ? void 0 : _a.value) === \"preserve\";\n let textContent = element === null || element === void 0 ? void 0 : element.textContent;\n if (!element || textContent === null) {\n this.handleMissingValue(element, `text content`, optionalArgs);\n }\n if (textContent) {\n textContent = shouldPreserveSpaces ? textContent : textContent.trim();\n }\n return (textContent ? fixXlsxUnicode(textContent) : optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default);\n }\n /**\n * Extract an attribute of a child of the given element.\n *\n * The reference of a child can be a string (tag of the child) or an number (index in the list of children of the element)\n *\n * If the attribute is required but either the attribute or the referenced child element was not found, it will\n * will add a warning in the warningManager if it was given a default value, and throw an error if no default value was given.\n *\n * Can only return undefined value for non-required attributes without default value.\n */\n extractChildAttr(e, childRef, attName, optionalArgs) {\n var _a;\n let child;\n if (typeof childRef === \"number\") {\n child = e.children[childRef];\n }\n else {\n child = this.querySelector(e, childRef);\n }\n if (!child) {\n this.handleMissingValue(e, typeof childRef === \"number\" ? `child at index ${childRef}` : `child <${childRef}>`, optionalArgs);\n }\n const value = child\n ? (_a = this.extractAttr(child, attName, optionalArgs)) === null || _a === void 0 ? void 0 : _a.asString()\n : optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default;\n return (value !== undefined ? new AttributeValue(value) : undefined);\n }\n /**\n * Extract the text content of a child of the given element.\n *\n * If the text content is required but either the text content or the referenced child element was not found, it will\n * will add a warning in the warningManager if it was given a default value, and throw an error if no default value was given.\n *\n * Can only return undefined value for non-required text content without default value.\n */\n extractChildTextContent(e, childRef, optionalArgs) {\n if ((optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default) !== undefined && typeof optionalArgs.default !== \"string\") {\n throw new Error(\"extractTextContent default value should be a string\");\n }\n let child = this.querySelector(e, childRef);\n if (!child) {\n this.handleMissingValue(e, `child <${childRef}>`, optionalArgs);\n }\n return (child ? this.extractTextContent(child, optionalArgs) : optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default);\n }\n /**\n * Should be called if a extractAttr/extractTextContent doesn't find the element it needs to extract.\n *\n * If the extractable was required, this function will add a warning in the warningManager if there was a default value,\n * and throw an error if no default value was given.\n */\n handleMissingValue(parentElement, missingElementName, optionalArgs) {\n if (optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.required) {\n if (optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default) {\n this.warningManager.addParsingWarning(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, replacing it by the default value ${optionalArgs.default}`);\n }\n else {\n throw new Error(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, and no default value was set`);\n }\n }\n }\n /**\n * Extract a color, extracting it from the theme if needed.\n *\n * Will throw an error if the element references a theme, but no theme was provided or the theme it doesn't contain the color.\n */\n extractColor(colorElement, theme, defaultColor) {\n var _a, _b, _c, _d, _e;\n if (!colorElement) {\n return defaultColor ? { rgb: defaultColor } : undefined;\n }\n const themeIndex = (_a = this.extractAttr(colorElement, \"theme\")) === null || _a === void 0 ? void 0 : _a.asString();\n let rgb;\n if (themeIndex !== undefined) {\n if (!theme || !theme.clrScheme) {\n throw new Error(\"Color referencing a theme but no theme was provided\");\n }\n rgb = this.getThemeColor(themeIndex, theme.clrScheme);\n }\n else {\n rgb = (_b = this.extractAttr(colorElement, \"rgb\")) === null || _b === void 0 ? void 0 : _b.asString();\n }\n const color = {\n rgb,\n auto: (_c = this.extractAttr(colorElement, \"auto\")) === null || _c === void 0 ? void 0 : _c.asBool(),\n indexed: (_d = this.extractAttr(colorElement, \"indexed\")) === null || _d === void 0 ? void 0 : _d.asNum(),\n tint: (_e = this.extractAttr(colorElement, \"tint\")) === null || _e === void 0 ? void 0 : _e.asNum(),\n };\n return color;\n }\n /**\n * Returns the xlsx file targeted by a relationship.\n */\n getTargetXmlFile(relationship) {\n if (!relationship)\n throw new Error(\"Undefined target file\");\n let target = relationship.target;\n target = target.replace(\"../\", \"\");\n target = target.replace(\"./\", \"\");\n // Use \"endsWith\" because targets are relative paths, and we know the files by their absolute path.\n const f = this.getListOfFiles().find((f) => f.file.fileName.endsWith(target));\n if (!f || !f.file)\n throw new Error(\"Cannot find target file\");\n return f;\n }\n /**\n * Wrapper of querySelector, but we'll remove the namespaces from the query if areNamespacesIgnored is true.\n *\n * Why we need to do this :\n * - For an XML \"
\"\n * - on Jest(jsdom) : xml.querySelector(\"test\") == null, xml.querySelector(\"t\\\\:test\") ==
\n * - on Browser : xml.querySelector(\"test\") ==
, xml.querySelector(\"t\\\\:test\") == null\n */\n querySelector(element, query) {\n query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);\n return element.querySelector(query);\n }\n /**\n * Wrapper of querySelectorAll, but we'll remove the namespaces from the query if areNamespacesIgnored is true.\n *\n * Why we need to do this :\n * - For an XML \"
\"\n * - on Jest(jsdom) : xml.querySelectorAll(\"test\") == [], xml.querySelectorAll(\"t\\\\:test\") == [
]\n * - on Browser : xml.querySelectorAll(\"test\") == [
], xml.querySelectorAll(\"t\\\\:test\") == []\n */\n querySelectorAll(element, query) {\n query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);\n return element.querySelectorAll(query);\n }\n /**\n * Get a color from its id in the Theme's colorScheme.\n *\n * Note that Excel don't use the colors from the theme but from its own internal theme, so the displayed\n * colors will be different in the import than in excel.\n * .\n */\n getThemeColor(colorId, clrScheme) {\n switch (colorId) {\n case \"0\": // 0 : sysColor window text\n return \"FFFFFF\";\n case \"1\": // 1 : sysColor window background\n return \"000000\";\n // Don't ask me why these 2 are inverted, I cannot find any documentation for it but everyone does it\n case \"2\":\n return clrScheme[\"3\"].value;\n case \"3\":\n return clrScheme[\"2\"].value;\n default:\n return clrScheme[colorId].value;\n }\n }\n }\n\n /**\n * XLSX Extractor class that can be used for either sharedString XML files or theme XML files.\n *\n * Since they both are quite simple, it make sense to make a single class to manage them all, to avoid unnecessary file\n * cluttering.\n */\n class XlsxMiscExtractor extends XlsxBaseExtractor {\n getTheme() {\n const clrScheme = this.mapOnElements({ query: \"a:clrScheme\", parent: this.rootFile.file.xml, children: true }, (element) => {\n return {\n name: element.tagName,\n value: this.extractChildAttr(element, 0, \"val\", {\n required: true,\n default: AUTO_COLOR,\n }).asString(),\n lastClr: this.extractChildAttr(element, 0, \"lastClr\", {\n default: AUTO_COLOR,\n }).asString(),\n };\n });\n return { clrScheme };\n }\n /**\n * Get the array of shared strings of the XLSX.\n *\n * Worth noting that running a prettier on the xml can mess up some strings, since there is an option in the\n * xmls to keep the spacing and not trim the string.\n */\n getSharedStrings() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"si\" }, (ssElement) => {\n // Shared string can either be a simple text, or a rich text (text with formatting, possibly in multiple parts)\n if (ssElement.children[0].tagName === \"t\") {\n return this.extractTextContent(ssElement) || \"\";\n }\n // We don't support rich text formatting, we'll only extract the text\n else {\n return this.mapOnElements({ parent: ssElement, query: \"t\" }, (textElement) => {\n return this.extractTextContent(textElement) || \"\";\n }).join(\"\");\n }\n });\n }\n }\n\n class XlsxCfExtractor extends XlsxBaseExtractor {\n constructor(sheetFile, xlsxStructure, warningManager, theme) {\n super(sheetFile, xlsxStructure, warningManager);\n this.theme = theme;\n }\n extractConditionalFormattings() {\n const cfs = this.mapOnElements({ parent: this.rootFile.file.xml, query: \"worksheet > conditionalFormatting\" }, (cfElement) => {\n var _a;\n return {\n // sqref = ranges on which the cf applies, separated by spaces\n sqref: this.extractAttr(cfElement, \"sqref\", { required: true }).asString().split(\" \"),\n pivot: (_a = this.extractAttr(cfElement, \"pivot\")) === null || _a === void 0 ? void 0 : _a.asBool(),\n cfRules: this.extractCFRules(cfElement, this.theme),\n };\n });\n // XLSX extension to OpenXml\n cfs.push(...this.mapOnElements({ parent: this.rootFile.file.xml, query: \"extLst x14:conditionalFormatting\" }, (cfElement) => {\n var _a;\n return {\n sqref: this.extractChildTextContent(cfElement, \"xm:sqref\", { required: true }).split(\" \"),\n pivot: (_a = this.extractAttr(cfElement, \"xm:pivot\")) === null || _a === void 0 ? void 0 : _a.asBool(),\n cfRules: this.extractCFRules(cfElement, this.theme),\n };\n }));\n return cfs;\n }\n extractCFRules(cfElement, theme) {\n return this.mapOnElements({ parent: cfElement, query: \"cfRule, x14:cfRule\" }, (cfRuleElement) => {\n var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;\n const cfType = this.extractAttr(cfRuleElement, \"type\", {\n required: true,\n }).asString();\n if (cfType === \"dataBar\") {\n // Databars are an extension to OpenXml and have a different format (XLSX \u00a72.6.30). Do'nt bother\n // extracting them as we don't support them.\n throw new Error(\"Databars conditional formats are not supported.\");\n }\n return {\n type: cfType,\n priority: this.extractAttr(cfRuleElement, \"priority\", { required: true }).asNum(),\n colorScale: this.extractCfColorScale(cfRuleElement, theme),\n formula: this.extractCfFormula(cfRuleElement),\n iconSet: this.extractCfIconSet(cfRuleElement),\n dxfId: (_a = this.extractAttr(cfRuleElement, \"dxfId\")) === null || _a === void 0 ? void 0 : _a.asNum(),\n stopIfTrue: (_b = this.extractAttr(cfRuleElement, \"stopIfTrue\")) === null || _b === void 0 ? void 0 : _b.asBool(),\n aboveAverage: (_c = this.extractAttr(cfRuleElement, \"aboveAverage\")) === null || _c === void 0 ? void 0 : _c.asBool(),\n percent: (_d = this.extractAttr(cfRuleElement, \"percent\")) === null || _d === void 0 ? void 0 : _d.asBool(),\n bottom: (_e = this.extractAttr(cfRuleElement, \"bottom\")) === null || _e === void 0 ? void 0 : _e.asBool(),\n operator: (_f = this.extractAttr(cfRuleElement, \"operator\")) === null || _f === void 0 ? void 0 : _f.asString(),\n text: (_g = this.extractAttr(cfRuleElement, \"text\")) === null || _g === void 0 ? void 0 : _g.asString(),\n timePeriod: (_h = this.extractAttr(cfRuleElement, \"timePeriod\")) === null || _h === void 0 ? void 0 : _h.asString(),\n rank: (_j = this.extractAttr(cfRuleElement, \"rank\")) === null || _j === void 0 ? void 0 : _j.asNum(),\n stdDev: (_k = this.extractAttr(cfRuleElement, \"stdDev\")) === null || _k === void 0 ? void 0 : _k.asNum(),\n equalAverage: (_l = this.extractAttr(cfRuleElement, \"equalAverage\")) === null || _l === void 0 ? void 0 : _l.asBool(),\n };\n });\n }\n extractCfFormula(cfRulesElement) {\n return this.mapOnElements({ parent: cfRulesElement, query: \"formula\" }, (cfFormulaElements) => {\n return this.extractTextContent(cfFormulaElements, { required: true });\n });\n }\n extractCfColorScale(cfRulesElement, theme) {\n const colorScaleElement = this.querySelector(cfRulesElement, \"colorScale\");\n if (!colorScaleElement)\n return undefined;\n return {\n colors: this.mapOnElements({ parent: colorScaleElement, query: \"color\" }, (colorElement) => {\n return this.extractColor(colorElement, theme, \"ffffff\");\n }),\n cfvos: this.extractCFVos(colorScaleElement),\n };\n }\n extractCfIconSet(cfRulesElement) {\n var _a, _b;\n const iconSetElement = this.querySelector(cfRulesElement, \"iconSet, x14:iconSet\");\n if (!iconSetElement)\n return undefined;\n return {\n iconSet: this.extractAttr(iconSetElement, \"iconSet\", {\n default: \"3TrafficLights1\",\n }).asString(),\n showValue: this.extractAttr(iconSetElement, \"showValue\", { default: true }).asBool(),\n percent: this.extractAttr(iconSetElement, \"percent\", { default: true }).asBool(),\n reverse: (_a = this.extractAttr(iconSetElement, \"reverse\")) === null || _a === void 0 ? void 0 : _a.asBool(),\n custom: (_b = this.extractAttr(iconSetElement, \"custom\")) === null || _b === void 0 ? void 0 : _b.asBool(),\n cfvos: this.extractCFVos(iconSetElement),\n cfIcons: this.extractCfIcons(iconSetElement),\n };\n }\n extractCfIcons(iconSetElement) {\n const icons = this.mapOnElements({ parent: iconSetElement, query: \"cfIcon, x14:cfIcon\" }, (cfIconElement) => {\n return {\n iconSet: this.extractAttr(cfIconElement, \"iconSet\", {\n required: true,\n }).asString(),\n iconId: this.extractAttr(cfIconElement, \"iconId\", { required: true }).asNum(),\n };\n });\n return icons.length === 0 ? undefined : icons;\n }\n extractCFVos(parent) {\n return this.mapOnElements({ parent, query: \"cfvo, x14:cfvo\" }, (cfVoElement) => {\n var _a, _b;\n return {\n type: this.extractAttr(cfVoElement, \"type\", {\n required: true,\n }).asString(),\n gte: (_a = this.extractAttr(cfVoElement, \"gte\", { default: true })) === null || _a === void 0 ? void 0 : _a.asBool(),\n value: cfVoElement.attributes[\"val\"]\n ? (_b = this.extractAttr(cfVoElement, \"val\")) === null || _b === void 0 ? void 0 : _b.asString()\n : this.extractChildTextContent(cfVoElement, \"f, xm:f\"),\n };\n });\n }\n }\n\n class XlsxChartExtractor extends XlsxBaseExtractor {\n extractChart() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"c:chartSpace\" }, (rootChartElement) => {\n const chartType = this.getChartType(rootChartElement);\n if (!CHART_TYPE_CONVERSION_MAP[chartType]) {\n throw new Error(`Unsupported chart type ${chartType}`);\n }\n // Title can be separated into multiple xml elements (for styling and such), we only import the text\n const chartTitle = this.mapOnElements({ parent: rootChartElement, query: \"c:title a:t\" }, (textElement) => {\n return textElement.textContent || \"\";\n }).join(\"\");\n const barChartGrouping = this.extractChildAttr(rootChartElement, \"c:grouping\", \"val\", {\n default: \"clustered\",\n }).asString();\n return {\n title: chartTitle,\n type: CHART_TYPE_CONVERSION_MAP[chartType],\n dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`)),\n backgroundColor: this.extractChildAttr(rootChartElement, \"c:chartSpace > c:spPr a:srgbClr\", \"val\", {\n default: \"ffffff\",\n }).asString(),\n verticalAxisPosition: this.extractChildAttr(rootChartElement, \"c:valAx > c:axPos\", \"val\", {\n default: \"l\",\n }).asString() === \"r\"\n ? \"right\"\n : \"left\",\n legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(rootChartElement, \"c:legendPos\", \"val\", {\n default: \"b\",\n }).asString()],\n stacked: barChartGrouping === \"stacked\",\n fontColor: \"000000\",\n };\n })[0];\n }\n extractChartDatasets(chartElement) {\n return this.mapOnElements({ parent: chartElement, query: \"c:ser\" }, (chartDataElement) => {\n return {\n label: this.extractChildTextContent(chartDataElement, \"c:cat c:f\"),\n range: this.extractChildTextContent(chartDataElement, \"c:val c:f\", { required: true }),\n };\n });\n }\n /**\n * The chart type in the XML isn't explicitly defined, but there is an XML element that define the\n * chart, and this element tag name tells us which type of chart it is. We just need to find this XML element.\n */\n getChartType(chartElement) {\n const plotAreaElement = this.querySelector(chartElement, \"c:plotArea\");\n if (!plotAreaElement) {\n throw new Error(\"Missing plot area in the chart definition.\");\n }\n for (let child of plotAreaElement.children) {\n const tag = removeNamespaces(child.tagName);\n if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {\n return tag;\n }\n }\n throw new Error(\"Unknown chart type\");\n }\n }\n\n class XlsxFigureExtractor extends XlsxBaseExtractor {\n extractFigures() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"xdr:wsDr\", children: true }, (figureElement) => {\n const anchorType = removeNamespaces(figureElement.tagName);\n if (anchorType !== \"twoCellAnchor\") {\n throw new Error(\"Only twoCellAnchor are supported for xlsx drawings.\");\n }\n const chartElement = this.querySelector(figureElement, \"c:chart\");\n if (!chartElement) {\n throw new Error(\"Only chart figures are currently supported.\");\n }\n return {\n anchors: [\n this.extractFigureAnchor(\"xdr:from\", figureElement),\n this.extractFigureAnchor(\"xdr:to\", figureElement),\n ],\n data: this.extractChart(chartElement),\n };\n });\n }\n extractFigureAnchor(anchorTag, figureElement) {\n const anchor = this.querySelector(figureElement, anchorTag);\n if (!anchor) {\n throw new Error(`Missing anchor element ${anchorTag}`);\n }\n return {\n col: Number(this.extractChildTextContent(anchor, \"xdr:col\", { required: true })),\n colOffset: Number(this.extractChildTextContent(anchor, \"xdr:colOff\", { required: true })),\n row: Number(this.extractChildTextContent(anchor, \"xdr:row\", { required: true })),\n rowOffset: Number(this.extractChildTextContent(anchor, \"xdr:rowOff\", { required: true })),\n };\n }\n extractChart(chartElement) {\n const chartId = this.extractAttr(chartElement, \"r:id\", { required: true }).asString();\n const chartFile = this.getTargetXmlFile(this.relationships[chartId]);\n const chartDefinition = new XlsxChartExtractor(chartFile, this.xlsxFileStructure, this.warningManager).extractChart();\n if (!chartDefinition) {\n throw new Error(\"Unable to extract chart definition\");\n }\n return chartDefinition;\n }\n }\n\n /**\n * We don't really support pivot tables, we'll just extract them as Tables.\n */\n class XlsxPivotExtractor extends XlsxBaseExtractor {\n getPivotTable() {\n return this.mapOnElements(\n // Use :root instead of \"pivotTableDefinition\" because others pivotTableDefinition elements are present inside the root\n // pivotTableDefinition elements.\n { query: \":root\", parent: this.rootFile.file.xml }, (pivotElement) => {\n return {\n displayName: this.extractAttr(pivotElement, \"name\", { required: true }).asString(),\n id: this.extractAttr(pivotElement, \"name\", { required: true }).asString(),\n ref: this.extractChildAttr(pivotElement, \"location\", \"ref\", {\n required: true,\n }).asString(),\n headerRowCount: this.extractChildAttr(pivotElement, \"location\", \"firstDataRow\", {\n default: 0,\n }).asNum(),\n totalsRowCount: 1,\n cols: [],\n style: {\n showFirstColumn: true,\n showRowStripes: true,\n },\n };\n })[0];\n }\n }\n\n class XlsxTableExtractor extends XlsxBaseExtractor {\n getTable() {\n return this.mapOnElements({ query: \"table\", parent: this.rootFile.file.xml }, (tableElement) => {\n var _a;\n return {\n displayName: this.extractAttr(tableElement, \"displayName\", {\n required: true,\n }).asString(),\n name: (_a = this.extractAttr(tableElement, \"name\")) === null || _a === void 0 ? void 0 : _a.asString(),\n id: this.extractAttr(tableElement, \"id\", { required: true }).asString(),\n ref: this.extractAttr(tableElement, \"ref\", { required: true }).asString(),\n headerRowCount: this.extractAttr(tableElement, \"headerRowCount\", {\n default: 1,\n }).asNum(),\n totalsRowCount: this.extractAttr(tableElement, \"totalsRowCount\", {\n default: 0,\n }).asNum(),\n cols: this.extractTableCols(tableElement),\n style: this.extractTableStyleInfo(tableElement),\n autoFilter: this.extractTableAutoFilter(tableElement),\n };\n })[0];\n }\n extractTableCols(tableElement) {\n return this.mapOnElements({ query: \"tableColumn\", parent: tableElement }, (tableColElement) => {\n return {\n id: this.extractAttr(tableColElement, \"id\", { required: true }).asString(),\n name: this.extractAttr(tableColElement, \"name\", { required: true }).asString(),\n colFormula: this.extractChildTextContent(tableColElement, \"calculatedColumnFormula\"),\n };\n });\n }\n extractTableStyleInfo(tableElement) {\n return this.mapOnElements({ query: \"tableStyleInfo\", parent: tableElement }, (tableStyleElement) => {\n var _a, _b, _c, _d, _e;\n return {\n name: (_a = this.extractAttr(tableStyleElement, \"name\")) === null || _a === void 0 ? void 0 : _a.asString(),\n showFirstColumn: (_b = this.extractAttr(tableStyleElement, \"showFirstColumn\")) === null || _b === void 0 ? void 0 : _b.asBool(),\n showLastColumn: (_c = this.extractAttr(tableStyleElement, \"showLastColumn\")) === null || _c === void 0 ? void 0 : _c.asBool(),\n showRowStripes: (_d = this.extractAttr(tableStyleElement, \"showRowStripes\")) === null || _d === void 0 ? void 0 : _d.asBool(),\n showColumnStripes: (_e = this.extractAttr(tableStyleElement, \"showColumnStripes\")) === null || _e === void 0 ? void 0 : _e.asBool(),\n };\n })[0];\n }\n extractTableAutoFilter(tableElement) {\n return this.mapOnElements({ query: \"autoFilter\", parent: tableElement }, (autoFilterElement) => {\n return {\n columns: this.extractFilterColumns(autoFilterElement),\n zone: this.extractAttr(autoFilterElement, \"ref\", { required: true }).asString(),\n };\n })[0];\n }\n extractFilterColumns(autoFilterElement) {\n return this.mapOnElements({ query: \"tableColumn\", parent: autoFilterElement }, (filterColumnElement) => {\n return {\n colId: this.extractAttr(autoFilterElement, \"colId\", { required: true }).asNum(),\n hiddenButton: this.extractAttr(autoFilterElement, \"hiddenButton\", {\n default: false,\n }).asBool(),\n filters: this.extractSimpleFilter(filterColumnElement),\n };\n });\n }\n extractSimpleFilter(filterColumnElement) {\n return this.mapOnElements({ query: \"filter\", parent: filterColumnElement }, (filterColumnElement) => {\n return {\n val: this.extractAttr(filterColumnElement, \"val\", { required: true }).asString(),\n };\n });\n }\n }\n\n class XlsxSheetExtractor extends XlsxBaseExtractor {\n constructor(sheetFile, xlsxStructure, warningManager, theme) {\n super(sheetFile, xlsxStructure, warningManager);\n this.theme = theme;\n }\n getSheet() {\n return this.mapOnElements({ query: \"worksheet\", parent: this.rootFile.file.xml }, (sheetElement) => {\n const sheetWorkbookInfo = this.getSheetWorkbookInfo();\n return {\n sheetName: this.extractSheetName(),\n sheetViews: this.extractSheetViews(sheetElement),\n sheetFormat: this.extractSheetFormat(sheetElement),\n cols: this.extractCols(sheetElement),\n rows: this.extractRows(sheetElement),\n sharedFormulas: this.extractSharedFormulas(sheetElement),\n merges: this.extractMerges(sheetElement),\n cfs: this.extractConditionalFormats(),\n figures: this.extractFigures(sheetElement),\n hyperlinks: this.extractHyperLinks(sheetElement),\n tables: [...this.extractTables(sheetElement), ...this.extractPivotTables()],\n isVisible: sheetWorkbookInfo.state === \"visible\" ? true : false,\n };\n })[0];\n }\n extractSheetViews(worksheet) {\n return this.mapOnElements({ parent: worksheet, query: \"sheetView\" }, (sheetViewElement) => {\n const paneElement = this.querySelector(sheetViewElement, \"pane\");\n return {\n tabSelected: this.extractAttr(sheetViewElement, \"tabSelected\", {\n default: false,\n }).asBool(),\n showFormulas: this.extractAttr(sheetViewElement, \"showFormulas\", {\n default: false,\n }).asBool(),\n showGridLines: this.extractAttr(sheetViewElement, \"showGridLines\", {\n default: true,\n }).asBool(),\n showRowColHeaders: this.extractAttr(sheetViewElement, \"showRowColHeaders\", {\n default: true,\n }).asBool(),\n pane: {\n xSplit: paneElement\n ? this.extractAttr(paneElement, \"xSplit\", { default: 0 }).asNum()\n : 0,\n ySplit: paneElement\n ? this.extractAttr(paneElement, \"ySplit\", { default: 0 }).asNum()\n : 0,\n },\n };\n });\n }\n extractSheetName() {\n const relativePath = getRelativePath(this.xlsxFileStructure.workbook.file.fileName, this.rootFile.file.fileName);\n const workbookRels = this.extractRelationships(this.xlsxFileStructure.workbook.rels);\n const relId = workbookRels.find((rel) => rel.target === relativePath).id;\n // Having a namespace in the attributes names mess with the querySelector, and the behavior is not the same\n // for every XML parser. So we'll search manually instead of using a querySelector to search for an attribute value.\n for (let sheetElement of this.querySelectorAll(this.xlsxFileStructure.workbook.file.xml, \"sheet\")) {\n if (sheetElement.attributes[\"r:id\"].value === relId) {\n return sheetElement.attributes[\"name\"].value;\n }\n }\n throw new Error(\"Missing sheet name\");\n }\n getSheetWorkbookInfo() {\n const relativePath = getRelativePath(this.xlsxFileStructure.workbook.file.fileName, this.rootFile.file.fileName);\n const workbookRels = this.extractRelationships(this.xlsxFileStructure.workbook.rels);\n const relId = workbookRels.find((rel) => rel.target === relativePath).id;\n const workbookSheets = this.mapOnElements({ parent: this.xlsxFileStructure.workbook.file.xml, query: \"sheet\" }, (sheetElement) => {\n return {\n relationshipId: this.extractAttr(sheetElement, \"r:id\", { required: true }).asString(),\n sheetId: this.extractAttr(sheetElement, \"sheetId\", { required: true }).asString(),\n sheetName: this.extractAttr(sheetElement, \"name\", { required: true }).asString(),\n state: this.extractAttr(sheetElement, \"state\", {\n default: \"visible\",\n }).asString(),\n };\n });\n const info = workbookSheets.find((info) => info.relationshipId === relId);\n if (!info) {\n throw new Error(\"Cannot find corresponding workbook sheet\");\n }\n return info;\n }\n extractConditionalFormats() {\n return new XlsxCfExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractConditionalFormattings();\n }\n extractFigures(worksheet) {\n const figures = this.mapOnElements({ parent: worksheet, query: \"drawing\" }, (drawingElement) => {\n var _a;\n const drawingId = (_a = this.extractAttr(drawingElement, \"r:id\", { required: true })) === null || _a === void 0 ? void 0 : _a.asString();\n const drawingFile = this.getTargetXmlFile(this.relationships[drawingId]);\n const figures = new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();\n return figures;\n })[0];\n return figures || [];\n }\n extractTables(worksheet) {\n return this.mapOnElements({ query: \"tablePart\", parent: worksheet }, (tablePartElement) => {\n var _a;\n const tableId = (_a = this.extractAttr(tablePartElement, \"r:id\", { required: true })) === null || _a === void 0 ? void 0 : _a.asString();\n const tableFile = this.getTargetXmlFile(this.relationships[tableId]);\n const tableExtractor = new XlsxTableExtractor(tableFile, this.xlsxFileStructure, this.warningManager);\n return tableExtractor.getTable();\n });\n }\n extractPivotTables() {\n try {\n return Object.values(this.relationships)\n .filter((relationship) => relationship.type.endsWith(\"pivotTable\"))\n .map((pivotRelationship) => {\n const pivotFile = this.getTargetXmlFile(pivotRelationship);\n const pivot = new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();\n return pivot;\n });\n }\n catch (e) {\n this.catchErrorOnElement(e);\n return [];\n }\n }\n extractMerges(worksheet) {\n return this.mapOnElements({ parent: worksheet, query: \"mergeCell\" }, (mergeElement) => {\n return this.extractAttr(mergeElement, \"ref\", { required: true }).asString();\n });\n }\n extractSheetFormat(worksheet) {\n const formatElement = this.querySelector(worksheet, \"sheetFormatPr\");\n if (!formatElement)\n return undefined;\n return {\n defaultColWidth: this.extractAttr(formatElement, \"defaultColWidth\", {\n default: EXCEL_DEFAULT_COL_WIDTH.toString(),\n }).asNum(),\n defaultRowHeight: this.extractAttr(formatElement, \"defaultRowHeight\", {\n default: EXCEL_DEFAULT_ROW_HEIGHT.toString(),\n }).asNum(),\n };\n }\n extractCols(worksheet) {\n return this.mapOnElements({ parent: worksheet, query: \"cols col\" }, (colElement) => {\n var _a, _b, _c, _d, _e, _f, _g;\n return {\n width: (_a = this.extractAttr(colElement, \"width\")) === null || _a === void 0 ? void 0 : _a.asNum(),\n customWidth: (_b = this.extractAttr(colElement, \"customWidth\")) === null || _b === void 0 ? void 0 : _b.asBool(),\n bestFit: (_c = this.extractAttr(colElement, \"bestFit\")) === null || _c === void 0 ? void 0 : _c.asBool(),\n hidden: (_d = this.extractAttr(colElement, \"hidden\")) === null || _d === void 0 ? void 0 : _d.asBool(),\n min: (_e = this.extractAttr(colElement, \"min\", { required: true })) === null || _e === void 0 ? void 0 : _e.asNum(),\n max: (_f = this.extractAttr(colElement, \"max\", { required: true })) === null || _f === void 0 ? void 0 : _f.asNum(),\n styleIndex: (_g = this.extractAttr(colElement, \"style\")) === null || _g === void 0 ? void 0 : _g.asNum(),\n };\n });\n }\n extractRows(worksheet) {\n return this.mapOnElements({ parent: worksheet, query: \"sheetData row\" }, (rowElement) => {\n var _a, _b, _c, _d, _e;\n return {\n index: (_a = this.extractAttr(rowElement, \"r\", { required: true })) === null || _a === void 0 ? void 0 : _a.asNum(),\n cells: this.extractCells(rowElement),\n height: (_b = this.extractAttr(rowElement, \"ht\")) === null || _b === void 0 ? void 0 : _b.asNum(),\n customHeight: (_c = this.extractAttr(rowElement, \"customHeight\")) === null || _c === void 0 ? void 0 : _c.asBool(),\n hidden: (_d = this.extractAttr(rowElement, \"hidden\")) === null || _d === void 0 ? void 0 : _d.asBool(),\n styleIndex: (_e = this.extractAttr(rowElement, \"s\")) === null || _e === void 0 ? void 0 : _e.asNum(),\n };\n });\n }\n extractCells(row) {\n return this.mapOnElements({ parent: row, query: \"c\" }, (cellElement) => {\n var _a, _b, _c;\n return {\n xc: (_a = this.extractAttr(cellElement, \"r\", { required: true })) === null || _a === void 0 ? void 0 : _a.asString(),\n styleIndex: (_b = this.extractAttr(cellElement, \"s\")) === null || _b === void 0 ? void 0 : _b.asNum(),\n type: CELL_TYPE_CONVERSION_MAP[(_c = this.extractAttr(cellElement, \"t\", { default: \"n\" })) === null || _c === void 0 ? void 0 : _c.asString()],\n value: this.extractChildTextContent(cellElement, \"v\"),\n formula: this.extractCellFormula(cellElement),\n };\n });\n }\n extractCellFormula(cellElement) {\n var _a, _b;\n const formulaElement = this.querySelector(cellElement, \"f\");\n if (!formulaElement)\n return undefined;\n return {\n content: this.extractTextContent(formulaElement),\n sharedIndex: (_a = this.extractAttr(formulaElement, \"si\")) === null || _a === void 0 ? void 0 : _a.asNum(),\n ref: (_b = this.extractAttr(formulaElement, \"ref\")) === null || _b === void 0 ? void 0 : _b.asString(),\n };\n }\n extractHyperLinks(worksheet) {\n return this.mapOnElements({ parent: worksheet, query: \"hyperlink\" }, (linkElement) => {\n var _a, _b, _c, _d;\n const relId = (_a = this.extractAttr(linkElement, \"r:id\")) === null || _a === void 0 ? void 0 : _a.asString();\n return {\n xc: (_b = this.extractAttr(linkElement, \"ref\", { required: true })) === null || _b === void 0 ? void 0 : _b.asString(),\n location: (_c = this.extractAttr(linkElement, \"location\")) === null || _c === void 0 ? void 0 : _c.asString(),\n display: (_d = this.extractAttr(linkElement, \"display\")) === null || _d === void 0 ? void 0 : _d.asString(),\n relTarget: relId ? this.relationships[relId].target : undefined,\n };\n });\n }\n extractSharedFormulas(worksheet) {\n const sfElements = this.querySelectorAll(worksheet, `f[si][ref]`);\n const sfMap = {};\n for (let sfElement of sfElements) {\n const index = this.extractAttr(sfElement, \"si\", { required: true }).asNum();\n const formula = this.extractTextContent(sfElement, { required: true });\n sfMap[index] = formula;\n }\n const sfs = [];\n for (let i = 0; i < Object.keys(sfMap).length; i++) {\n if (!sfMap[i]) {\n this.warningManager.addParsingWarning(`Missing shared formula ${i}, replacing it by empty formula`);\n sfs.push(\"\");\n }\n else {\n sfs.push(sfMap[i]);\n }\n }\n return sfs;\n }\n }\n\n class XlsxStyleExtractor extends XlsxBaseExtractor {\n constructor(xlsxStructure, warningManager, theme) {\n super(xlsxStructure.styles, xlsxStructure, warningManager);\n this.theme = theme;\n }\n getNumFormats() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"numFmt\" }, (numFmtElement) => {\n return this.extractNumFormats(numFmtElement);\n });\n }\n extractNumFormats(numFmtElement) {\n return {\n id: this.extractAttr(numFmtElement, \"numFmtId\", {\n required: true,\n }).asNum(),\n format: this.extractAttr(numFmtElement, \"formatCode\", {\n required: true,\n default: \"\",\n }).asString(),\n };\n }\n getFonts() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"font\" }, (font) => {\n return this.extractFont(font);\n });\n }\n extractFont(fontElement) {\n var _a, _b, _c, _d;\n const name = this.extractChildAttr(fontElement, \"name\", \"val\", {\n default: \"Arial\",\n }).asString();\n const size = this.extractChildAttr(fontElement, \"sz\", \"val\", {\n default: DEFAULT_FONT_SIZE.toString(),\n }).asNum();\n const color = this.extractColor(this.querySelector(fontElement, `color`), this.theme);\n // The behavior for these is kinda strange. The text is italic if there is either a \"italic\" tag with no \"val\"\n // attribute, or a tag with a \"val\" attribute = \"1\" (boolean).\n const italicElement = this.querySelector(fontElement, `i`) || undefined;\n const italic = italicElement && ((_a = italicElement.attributes[\"val\"]) === null || _a === void 0 ? void 0 : _a.value) !== \"0\";\n const boldElement = this.querySelector(fontElement, `b`) || undefined;\n const bold = boldElement && ((_b = boldElement.attributes[\"val\"]) === null || _b === void 0 ? void 0 : _b.value) !== \"0\";\n const strikeElement = this.querySelector(fontElement, `strike`) || undefined;\n const strike = strikeElement && ((_c = strikeElement.attributes[\"val\"]) === null || _c === void 0 ? void 0 : _c.value) !== \"0\";\n const underlineElement = this.querySelector(fontElement, `u`) || undefined;\n const underline = underlineElement && ((_d = underlineElement.attributes[\"val\"]) === null || _d === void 0 ? void 0 : _d.value) !== \"none\";\n return { name, size, color, italic, bold, underline, strike };\n }\n getFills() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"fill\" }, (fillElement) => {\n return this.extractFill(fillElement);\n });\n }\n extractFill(fillElement) {\n var _a;\n // Fills are either patterns of gradients\n const fillChild = fillElement.children[0];\n if (fillChild.tagName === \"patternFill\") {\n return {\n patternType: (_a = fillChild.attributes[\"patternType\"]) === null || _a === void 0 ? void 0 : _a.value,\n bgColor: this.extractColor(this.querySelector(fillChild, \"bgColor\"), this.theme),\n fgColor: this.extractColor(this.querySelector(fillChild, \"fgColor\"), this.theme),\n };\n }\n else {\n // We don't support gradients. Take the second gradient color as fill color\n return {\n patternType: \"solid\",\n fgColor: this.extractColor(this.querySelectorAll(fillChild, \"color\")[1], this.theme),\n };\n }\n }\n getBorders() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"border\" }, (borderElement) => {\n return this.extractBorder(borderElement);\n });\n }\n extractBorder(borderElement) {\n var _a, _b;\n const border = {\n left: this.extractSingleBorder(borderElement, \"left\", this.theme),\n right: this.extractSingleBorder(borderElement, \"right\", this.theme),\n top: this.extractSingleBorder(borderElement, \"top\", this.theme),\n bottom: this.extractSingleBorder(borderElement, \"bottom\", this.theme),\n diagonal: this.extractSingleBorder(borderElement, \"diagonal\", this.theme),\n };\n if (border.diagonal) {\n border.diagonalUp = (_a = this.extractAttr(borderElement, \"diagonalUp\")) === null || _a === void 0 ? void 0 : _a.asBool();\n border.diagonalDown = (_b = this.extractAttr(borderElement, \"diagonalDown\")) === null || _b === void 0 ? void 0 : _b.asBool();\n }\n return border;\n }\n extractSingleBorder(borderElement, direction, theme) {\n const directionElement = this.querySelector(borderElement, direction);\n if (!directionElement || !directionElement.attributes[\"style\"])\n return undefined;\n return {\n style: this.extractAttr(directionElement, \"style\", {\n required: true,\n default: \"thin\",\n }).asString(),\n color: this.extractColor(directionElement.children[0], theme, \"000000\"),\n };\n }\n extractAlignment(alignmentElement) {\n var _a, _b, _c, _d, _e, _f, _g;\n return {\n horizontal: this.extractAttr(alignmentElement, \"horizontal\", {\n default: \"general\",\n }).asString(),\n vertical: this.extractAttr(alignmentElement, \"vertical\", {\n default: \"center\",\n }).asString(),\n textRotation: (_a = this.extractAttr(alignmentElement, \"textRotation\")) === null || _a === void 0 ? void 0 : _a.asNum(),\n wrapText: (_b = this.extractAttr(alignmentElement, \"wrapText\")) === null || _b === void 0 ? void 0 : _b.asBool(),\n indent: (_c = this.extractAttr(alignmentElement, \"indent\")) === null || _c === void 0 ? void 0 : _c.asNum(),\n relativeIndent: (_d = this.extractAttr(alignmentElement, \"relativeIndent\")) === null || _d === void 0 ? void 0 : _d.asNum(),\n justifyLastLine: (_e = this.extractAttr(alignmentElement, \"justifyLastLine\")) === null || _e === void 0 ? void 0 : _e.asBool(),\n shrinkToFit: (_f = this.extractAttr(alignmentElement, \"shrinkToFit\")) === null || _f === void 0 ? void 0 : _f.asBool(),\n readingOrder: (_g = this.extractAttr(alignmentElement, \"readingOrder\")) === null || _g === void 0 ? void 0 : _g.asNum(),\n };\n }\n getDxfs() {\n return this.mapOnElements({ query: \"dxf\", parent: this.rootFile.file.xml }, (dxfElement) => {\n const fontElement = this.querySelector(dxfElement, \"font\");\n const fillElement = this.querySelector(dxfElement, \"fill\");\n const borderElement = this.querySelector(dxfElement, \"border\");\n const numFmtElement = this.querySelector(dxfElement, \"numFmt\");\n const alignmentElement = this.querySelector(dxfElement, \"alignment\");\n return {\n font: fontElement ? this.extractFont(fontElement) : undefined,\n fill: fillElement ? this.extractFill(fillElement) : undefined,\n numFmt: numFmtElement ? this.extractNumFormats(numFmtElement) : undefined,\n alignment: alignmentElement ? this.extractAlignment(alignmentElement) : undefined,\n border: borderElement ? this.extractBorder(borderElement) : undefined,\n };\n });\n }\n getStyles() {\n return this.mapOnElements({ query: \"cellXfs xf\", parent: this.rootFile.file.xml }, (styleElement) => {\n const alignmentElement = this.querySelector(styleElement, \"alignment\");\n return {\n fontId: this.extractAttr(styleElement, \"fontId\", {\n required: true,\n default: 0,\n }).asNum(),\n fillId: this.extractAttr(styleElement, \"fillId\", {\n required: true,\n default: 0,\n }).asNum(),\n borderId: this.extractAttr(styleElement, \"borderId\", {\n required: true,\n default: 0,\n }).asNum(),\n numFmtId: this.extractAttr(styleElement, \"numFmtId\", {\n required: true,\n default: 0,\n }).asNum(),\n alignment: alignmentElement ? this.extractAlignment(alignmentElement) : undefined,\n };\n });\n }\n }\n\n class XlsxExternalBookExtractor extends XlsxBaseExtractor {\n getExternalBook() {\n return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"externalBook\" }, (bookElement) => {\n return {\n rId: this.extractAttr(bookElement, \"r:id\", { required: true }).asString(),\n sheetNames: this.mapOnElements({ parent: bookElement, query: \"sheetName\" }, (sheetNameElement) => {\n return this.extractAttr(sheetNameElement, \"val\", { required: true }).asString();\n }),\n datasets: this.extractExternalSheetData(bookElement),\n };\n })[0];\n }\n extractExternalSheetData(externalBookElement) {\n return this.mapOnElements({ parent: externalBookElement, query: \"sheetData\" }, (sheetDataElement) => {\n const cellsData = this.mapOnElements({ parent: sheetDataElement, query: \"cell\" }, (cellElement) => {\n return {\n xc: this.extractAttr(cellElement, \"r\", { required: true }).asString(),\n value: this.extractChildTextContent(cellElement, \"v\", { required: true }),\n };\n });\n const dataMap = {};\n for (let cell of cellsData) {\n dataMap[cell.xc] = cell.value;\n }\n return {\n sheetId: this.extractAttr(sheetDataElement, \"sheetId\", { required: true }).asNum(),\n data: dataMap,\n };\n });\n }\n }\n\n /**\n * Return all the xmls converted to XLSXImportFile corresponding to the given content type.\n */\n function getXLSXFilesOfType(contentType, xmls) {\n const paths = getPathsOfContent(contentType, xmls);\n return getXlsxFile(paths, xmls);\n }\n /**\n * From an array of file path, return the equivalents XLSXFiles. An XLSX File is composed of an XML,\n * and optionally of a relationships XML.\n */\n function getXlsxFile(files, xmls) {\n const ret = [];\n for (let file of files) {\n const rels = getRelationFile(file, xmls);\n ret.push({\n file: { fileName: file, xml: xmls[file] },\n rels: rels ? { fileName: rels, xml: xmls[rels] } : undefined,\n });\n }\n return ret;\n }\n /**\n * Return all the path of the files in a XLSX directory that have content of the given type.\n */\n function getPathsOfContent(contentType, xmls) {\n const xml = xmls[CONTENT_TYPES_FILE];\n const sheetItems = xml.querySelectorAll(`Override[ContentType=\"${contentType}\"]`);\n const paths = [];\n for (let item of sheetItems) {\n const file = item === null || item === void 0 ? void 0 : item.attributes[\"PartName\"].value;\n paths.push(file.substring(1)); // Remove the heading \"/\"\n }\n return paths;\n }\n /**\n * Get the corresponding relationship file for a given xml file in a XLSX directory.\n */\n function getRelationFile(file, xmls) {\n if (file === CONTENT_TYPES_FILE) {\n return \"_rels/.rels\";\n }\n let relsFile = \"\";\n const pathParts = file.split(\"/\");\n for (let i = 0; i < pathParts.length - 1; i++) {\n relsFile += pathParts[i] + \"/\";\n }\n relsFile += \"_rels/\";\n relsFile += pathParts[pathParts.length - 1] + \".rels\";\n if (!xmls[relsFile]) {\n relsFile = undefined;\n }\n return relsFile;\n }\n\n const EXCEL_IMPORT_VERSION = 12;\n class XlsxReader {\n constructor(files) {\n this.warningManager = new XLSXImportWarningManager();\n this.xmls = {};\n for (let key of Object.keys(files)) {\n // Random files can be in xlsx (like a bin file for printer settings)\n if (key.endsWith(\".xml\") || key.endsWith(\".rels\")) {\n this.xmls[key] = parseXML(new XMLString(files[key]));\n }\n }\n }\n convertXlsx() {\n const xlsxData = this.getXlsxData();\n const convertedData = this.convertImportedData(xlsxData);\n return convertedData;\n }\n // ---------------------------------------------------------------------------\n // Parsing XMLs\n // ---------------------------------------------------------------------------\n getXlsxData() {\n const xlsxFileStructure = this.buildXlsxFileStructure();\n const theme = xlsxFileStructure.theme\n ? new XlsxMiscExtractor(xlsxFileStructure.theme, xlsxFileStructure, this.warningManager).getTheme()\n : undefined;\n const sharedStrings = xlsxFileStructure.sharedStrings\n ? new XlsxMiscExtractor(xlsxFileStructure.sharedStrings, xlsxFileStructure, this.warningManager).getSharedStrings()\n : [];\n // Sort sheets by file name : the sheets will always be named sheet1.xml, sheet2.xml, ... in order\n const sheets = xlsxFileStructure.sheets\n .sort((a, b) => a.file.fileName.localeCompare(b.file.fileName, undefined, { numeric: true }))\n .map((sheetFile) => {\n return new XlsxSheetExtractor(sheetFile, xlsxFileStructure, this.warningManager, theme).getSheet();\n });\n const externalBooks = xlsxFileStructure.externalLinks.map((externalLinkFile) => {\n return new XlsxExternalBookExtractor(externalLinkFile, xlsxFileStructure, this.warningManager).getExternalBook();\n });\n const styleExtractor = new XlsxStyleExtractor(xlsxFileStructure, this.warningManager, theme);\n return {\n fonts: styleExtractor.getFonts(),\n fills: styleExtractor.getFills(),\n borders: styleExtractor.getBorders(),\n dxfs: styleExtractor.getDxfs(),\n numFmts: styleExtractor.getNumFormats(),\n styles: styleExtractor.getStyles(),\n sheets: sheets,\n sharedStrings,\n externalBooks,\n };\n }\n buildXlsxFileStructure() {\n const xlsxFileStructure = {\n sheets: getXLSXFilesOfType(CONTENT_TYPES.sheet, this.xmls),\n workbook: getXLSXFilesOfType(CONTENT_TYPES.workbook, this.xmls)[0],\n styles: getXLSXFilesOfType(CONTENT_TYPES.styles, this.xmls)[0],\n sharedStrings: getXLSXFilesOfType(CONTENT_TYPES.sharedStrings, this.xmls)[0],\n theme: getXLSXFilesOfType(CONTENT_TYPES.themes, this.xmls)[0],\n charts: getXLSXFilesOfType(CONTENT_TYPES.chart, this.xmls),\n figures: getXLSXFilesOfType(CONTENT_TYPES.drawing, this.xmls),\n tables: getXLSXFilesOfType(CONTENT_TYPES.table, this.xmls),\n pivots: getXLSXFilesOfType(CONTENT_TYPES.pivot, this.xmls),\n externalLinks: getXLSXFilesOfType(CONTENT_TYPES.externalLink, this.xmls),\n };\n if (!xlsxFileStructure.workbook.rels) {\n throw Error(_lt(\"Cannot find workbook relations file\"));\n }\n return xlsxFileStructure;\n }\n // ---------------------------------------------------------------------------\n // Conversion\n // ---------------------------------------------------------------------------\n convertImportedData(data) {\n const convertedData = {\n version: EXCEL_IMPORT_VERSION,\n sheets: convertSheets(data, this.warningManager),\n styles: convertStyles(data, this.warningManager),\n formats: convertFormats(data, this.warningManager),\n borders: convertBorders(data, this.warningManager),\n entities: {},\n revisionId: DEFAULT_REVISION_ID,\n };\n convertTables(convertedData, data);\n // Remove falsy attributes in styles. Not mandatory, but make objects more readable when debugging\n Object.keys(data.styles).map((key) => {\n data.styles[key] = removeFalsyAttributes(data.styles[key]);\n });\n return convertedData;\n }\n }\n\n /**\n * parses a formula (as a string) into the same formula,\n * but with the references to other cells extracted\n *\n * =sum(a3:b1) + c3 --> =sum(|0|) + |1|\n *\n * @param formula\n */\n function normalizeV9(formula) {\n const tokens = rangeTokenize(formula);\n let dependencies = [];\n let noRefFormula = \"\".concat(...tokens.map((token) => {\n if (token.type === \"REFERENCE\" && cellReference.test(token.value)) {\n const value = token.value.trim();\n if (!dependencies.includes(value)) {\n dependencies.push(value);\n }\n return `${FORMULA_REF_IDENTIFIER}${dependencies.indexOf(value)}${FORMULA_REF_IDENTIFIER}`;\n }\n else {\n return token.value;\n }\n }));\n return { text: noRefFormula, dependencies };\n }\n\n /**\n * This is the current state version number. It should be incremented each time\n * a breaking change is made in the way the state is handled, and an upgrade\n * function should be defined\n */\n const CURRENT_VERSION = 12;\n const INITIAL_SHEET_ID = \"Sheet1\";\n /**\n * This function tries to load anything that could look like a valid\n * workbookData object. It applies any migrations, if needed, and return a\n * current, complete workbookData object.\n *\n * It also ensures that there is at least one sheet.\n */\n function load(data, verboseImport) {\n if (!data) {\n return createEmptyWorkbookData();\n }\n if (data[\"[Content_Types].xml\"]) {\n const reader = new XlsxReader(data);\n data = reader.convertXlsx();\n if (verboseImport) {\n for (let parsingError of reader.warningManager.warnings.sort()) {\n console.warn(parsingError);\n }\n }\n }\n data = JSON.parse(JSON.stringify(data));\n // apply migrations, if needed\n if (\"version\" in data) {\n if (data.version < CURRENT_VERSION) {\n data = migrate(data);\n }\n }\n data = repairData(data);\n return data;\n }\n function migrate(data) {\n const index = MIGRATIONS.findIndex((m) => m.from === data.version);\n for (let i = index; i < MIGRATIONS.length; i++) {\n data = MIGRATIONS[i].applyMigration(data);\n }\n return data;\n }\n const MIGRATIONS = [\n {\n description: \"add the `activeSheet` field on data\",\n from: 1,\n to: 2,\n applyMigration(data) {\n if (data.sheets && data.sheets[0]) {\n data.activeSheet = data.sheets[0].name;\n }\n return data;\n },\n },\n {\n description: \"add an id field in each sheet\",\n from: 2,\n to: 3,\n applyMigration(data) {\n if (data.sheets && data.sheets.length) {\n for (let sheet of data.sheets) {\n sheet.id = sheet.id || sheet.name;\n }\n }\n return data;\n },\n },\n {\n description: \"activeSheet is now an id, not the name of a sheet\",\n from: 3,\n to: 4,\n applyMigration(data) {\n if (data.sheets && data.activeSheet) {\n const activeSheet = data.sheets.find((s) => s.name === data.activeSheet);\n data.activeSheet = activeSheet.id;\n }\n return data;\n },\n },\n {\n description: \"add figures object in each sheets\",\n from: 4,\n to: 5,\n applyMigration(data) {\n for (let sheet of data.sheets || []) {\n sheet.figures = sheet.figures || [];\n }\n return data;\n },\n },\n {\n description: \"normalize the content of the cell if it is a formula to avoid parsing all the formula that vary only by the cells they use\",\n from: 5,\n to: 6,\n applyMigration(data) {\n for (let sheet of data.sheets || []) {\n for (let xc in sheet.cells || []) {\n const cell = sheet.cells[xc];\n if (cell.content && cell.content.startsWith(\"=\")) {\n cell.formula = normalizeV9(cell.content);\n }\n }\n }\n return data;\n },\n },\n {\n description: \"transform chart data structure\",\n from: 6,\n to: 7,\n applyMigration(data) {\n for (let sheet of data.sheets || []) {\n for (let f in sheet.figures || []) {\n const { dataSets, ...newData } = sheet.figures[f].data;\n const newDataSets = [];\n for (let ds of dataSets) {\n if (ds.labelCell) {\n const dataRange = toZone(ds.dataRange);\n const newRange = ds.labelCell + \":\" + toXC(dataRange.right, dataRange.bottom);\n newDataSets.push(newRange);\n }\n else {\n newDataSets.push(ds.dataRange);\n }\n }\n newData.dataSetsHaveTitle = Boolean(dataSets[0].labelCell);\n newData.dataSets = newDataSets;\n sheet.figures[f].data = newData;\n }\n }\n return data;\n },\n },\n {\n description: \"remove single quotes in sheet names\",\n from: 7,\n to: 8,\n applyMigration(data) {\n var _a;\n const namesTaken = [];\n const globalForbiddenInExcel = new RegExp(FORBIDDEN_IN_EXCEL_REGEX, \"g\");\n for (let sheet of data.sheets || []) {\n if (!sheet.name) {\n continue;\n }\n const oldName = sheet.name;\n const escapedName = oldName.replace(globalForbiddenInExcel, \"_\");\n let i = 1;\n let newName = escapedName;\n while (namesTaken.includes(newName)) {\n newName = `${escapedName}${i}`;\n i++;\n }\n sheet.name = newName;\n namesTaken.push(newName);\n const replaceName = (str) => {\n if (str === undefined) {\n return str;\n }\n // replaceAll is only available in next Typescript version\n let newString = str.replace(oldName, newName);\n let currentString = str;\n while (currentString !== newString) {\n currentString = newString;\n newString = currentString.replace(oldName, newName);\n }\n return currentString;\n };\n //cells\n for (let xc in sheet.cells) {\n const cell = sheet.cells[xc];\n if (cell.formula) {\n cell.formula.dependencies = cell.formula.dependencies.map(replaceName);\n }\n }\n //charts\n for (let figure of sheet.figures || []) {\n if (figure.type === \"chart\") {\n const dataSets = figure.data.dataSets.map(replaceName);\n const labelRange = replaceName(figure.data.labelRange);\n figure.data = { ...figure.data, dataSets, labelRange };\n }\n }\n //ConditionalFormats\n for (let cf of sheet.conditionalFormats || []) {\n cf.ranges = cf.ranges.map(replaceName);\n for (const thresholdName of [\n \"minimum\",\n \"maximum\",\n \"midpoint\",\n \"upperInflectionPoint\",\n \"lowerInflectionPoint\",\n ]) {\n if (((_a = cf.rule[thresholdName]) === null || _a === void 0 ? void 0 : _a.type) === \"formula\") {\n cf.rule[thresholdName].value = replaceName(cf.rule[thresholdName].value);\n }\n }\n }\n }\n return data;\n },\n },\n {\n description: \"transform chart data structure with design attributes\",\n from: 8,\n to: 9,\n applyMigration(data) {\n for (const sheet of data.sheets || []) {\n for (const chart of sheet.figures || []) {\n chart.data.background = BACKGROUND_CHART_COLOR;\n chart.data.verticalAxisPosition = \"left\";\n chart.data.legendPosition = \"top\";\n chart.data.stacked = false;\n }\n }\n return data;\n },\n },\n {\n description: \"de-normalize formula to reduce exported json size (~30%)\",\n from: 9,\n to: 10,\n applyMigration(data) {\n for (let sheet of data.sheets || []) {\n for (let xc in sheet.cells || []) {\n const cell = sheet.cells[xc];\n if (cell.formula) {\n let { text, dependencies } = cell.formula;\n for (let [index, d] of Object.entries(dependencies)) {\n const stringPosition = `\\\\${FORMULA_REF_IDENTIFIER}${index}\\\\${FORMULA_REF_IDENTIFIER}`;\n text = text.replace(new RegExp(stringPosition, \"g\"), d);\n }\n cell.content = text;\n delete cell.formula;\n }\n }\n }\n return data;\n },\n },\n {\n description: \"normalize the formats of the cells\",\n from: 10,\n to: 11,\n applyMigration(data) {\n const formats = {};\n for (let sheet of data.sheets || []) {\n for (let xc in sheet.cells || []) {\n const cell = sheet.cells[xc];\n if (cell.format) {\n cell.format = getItemId(cell.format, formats);\n }\n }\n }\n data.formats = formats;\n return data;\n },\n },\n {\n description: \"Add isVisible to sheets\",\n from: 11,\n to: 12,\n applyMigration(data) {\n for (let sheet of data.sheets || []) {\n sheet.isVisible = true;\n }\n return data;\n },\n },\n ];\n /**\n * This function is used to repair faulty data independently of the migration.\n */\n function repairData(data) {\n data = forceUnicityOfFigure(data);\n data = setDefaults(data);\n return data;\n }\n /**\n * Force the unicity of figure ids accross sheets\n */\n function forceUnicityOfFigure(data) {\n if (data.uniqueFigureIds) {\n return data;\n }\n const figureIds = new Set();\n const uuidGenerator = new UuidGenerator();\n for (const sheet of data.sheets || []) {\n for (const figure of sheet.figures || []) {\n if (figureIds.has(figure.id)) {\n figure.id += uuidGenerator.uuidv4();\n }\n figureIds.add(figure.id);\n }\n }\n data.uniqueFigureIds = true;\n return data;\n }\n /**\n * sanity check: try to fix missing fields/corrupted state by providing\n * sensible default values\n */\n function setDefaults(data) {\n data = Object.assign(createEmptyWorkbookData(), data, { version: CURRENT_VERSION });\n data.sheets = data.sheets\n ? data.sheets.map((s, i) => Object.assign(createEmptySheet(`Sheet${i + 1}`, `Sheet${i + 1}`), s))\n : [];\n if (data.sheets.length === 0) {\n data.sheets.push(createEmptySheet(INITIAL_SHEET_ID, \"Sheet1\"));\n }\n return data;\n }\n /**\n * The goal of this function is to repair corrupted/wrong initial messages caused by\n * a bug.\n * The bug should obviously be fixed, but it's too late for existing spreadsheet.\n */\n function repairInitialMessages(data, initialMessages) {\n initialMessages = fixTranslatedSheetIds(data, initialMessages);\n initialMessages = dropCommands(initialMessages, \"SORT_CELLS\");\n initialMessages = dropCommands(initialMessages, \"SET_DECIMAL\");\n initialMessages = fixChartDefinitions(data, initialMessages);\n return initialMessages;\n }\n /**\n * When the workbook data is originally empty, a new one is generated on-the-fly.\n * A bug caused the sheet id to be non-deterministic. The sheet id was propagated in\n * commands.\n * This function repairs initial commands with a wrong sheetId.\n */\n function fixTranslatedSheetIds(data, initialMessages) {\n // the fix is only needed when the workbook is generated on-the-fly\n if (Object.keys(data).length !== 0) {\n return initialMessages;\n }\n const sheetIds = [];\n const messages = [];\n const fixSheetId = (cmd) => {\n if (cmd.type === \"CREATE_SHEET\") {\n sheetIds.push(cmd.sheetId);\n }\n else if (\"sheetId\" in cmd && !sheetIds.includes(cmd.sheetId)) {\n return { ...cmd, sheetId: INITIAL_SHEET_ID };\n }\n return cmd;\n };\n for (const message of initialMessages) {\n if (message.type === \"REMOTE_REVISION\") {\n messages.push({\n ...message,\n commands: message.commands.map(fixSheetId),\n });\n }\n else {\n messages.push(message);\n }\n }\n return messages;\n }\n function dropCommands(initialMessages, commandType) {\n const messages = [];\n for (const message of initialMessages) {\n if (message.type === \"REMOTE_REVISION\") {\n messages.push({\n ...message,\n commands: message.commands.filter((command) => command.type !== commandType),\n });\n }\n else {\n messages.push(message);\n }\n }\n return messages;\n }\n function fixChartDefinitions(data, initialMessages) {\n var _a;\n const messages = [];\n const map = {};\n for (const sheet of data.sheets || []) {\n (_a = sheet.figures) === null || _a === void 0 ? void 0 : _a.forEach((figure) => {\n if (figure.tag === \"chart\") {\n // chart definition\n map[figure.id] = figure.data;\n }\n });\n }\n for (const message of initialMessages) {\n if (message.type === \"REMOTE_REVISION\") {\n const commands = [];\n for (const cmd of message.commands) {\n let command = cmd;\n switch (cmd.type) {\n case \"CREATE_CHART\":\n map[cmd.id] = cmd.definition;\n break;\n case \"UPDATE_CHART\":\n if (!map[cmd.id]) {\n /** the chart does not exist on the map, it might have been created after a duplicate sheet.\n * We don't have access to the definition, so we skip the command.\n */\n console.log(`Fix chart definition: chart with id ${cmd.id} not found.`);\n continue;\n }\n const definition = map[cmd.id];\n const newDefinition = { ...definition, ...cmd.definition };\n command = { ...cmd, definition: newDefinition };\n map[cmd.id] = newDefinition;\n break;\n }\n commands.push(command);\n }\n messages.push({\n ...message,\n commands,\n });\n }\n else {\n messages.push(message);\n }\n }\n return messages;\n }\n // -----------------------------------------------------------------------------\n // Helpers\n // -----------------------------------------------------------------------------\n function createEmptySheet(sheetId, name) {\n return {\n id: sheetId,\n name,\n colNumber: 26,\n rowNumber: 100,\n cells: {},\n cols: {},\n rows: {},\n merges: [],\n conditionalFormats: [],\n figures: [],\n filterTables: [],\n isVisible: true,\n };\n }\n function createEmptyWorkbookData(sheetName = \"Sheet1\") {\n const data = {\n version: CURRENT_VERSION,\n sheets: [createEmptySheet(INITIAL_SHEET_ID, sheetName)],\n entities: {},\n styles: {},\n formats: {},\n borders: {},\n revisionId: DEFAULT_REVISION_ID,\n uniqueFigureIds: true,\n };\n return data;\n }\n function createEmptyExcelSheet(sheetId, name) {\n return {\n ...createEmptySheet(sheetId, name),\n charts: [],\n };\n }\n function createEmptyExcelWorkbookData() {\n return {\n ...createEmptyWorkbookData(),\n sheets: [createEmptyExcelSheet(INITIAL_SHEET_ID, \"Sheet1\")],\n };\n }\n\n /**\n * Core plugins handle spreadsheet data.\n * They are responsible to import, export and maintain the spreadsheet\n * persisted state.\n * They should not be concerned about UI parts or transient state.\n */\n class CorePlugin extends BasePlugin {\n constructor({ getters, stateObserver, range, dispatch, uuidGenerator }) {\n super(stateObserver, dispatch);\n this.range = range;\n range.addRangeProvider(this.adaptRanges.bind(this));\n this.getters = getters;\n this.uuidGenerator = uuidGenerator;\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) { }\n export(data) { }\n /**\n * This method can be implemented in any plugin, to loop over the plugin's data structure and adapt the plugin's ranges.\n * To adapt them, the implementation of the function must have a perfect knowledge of the data structure, thus\n * implementing the loops over it makes sense in the plugin itself.\n * When calling the method applyChange, the range will be adapted if necessary, then a copy will be returned along with\n * the type of change that occurred.\n *\n * @param applyChange a function that, when called, will adapt the range according to the change on the grid\n * @param sheetId an optional sheetId to adapt either range of that sheet specifically, or ranges pointing to that sheet\n */\n adaptRanges(applyChange, sheetId) { }\n /**\n * Implement this method to clean unused external resources, such as images\n * stored on a server which have been deleted.\n */\n garbageCollectExternalResources() { }\n }\n\n /**\n * Formatting plugin.\n *\n * This plugin manages all things related to a cell look:\n * - borders\n */\n class BordersPlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.borders = {};\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n handle(cmd) {\n switch (cmd.type) {\n case \"ADD_MERGE\":\n for (const zone of cmd.target) {\n this.addBordersToMerge(cmd.sheetId, zone);\n }\n break;\n case \"DUPLICATE_SHEET\":\n const borders = this.borders[cmd.sheetId];\n if (borders) {\n // borders is a sparse 2D array.\n // map and slice preserve empty values and do not set `undefined` instead\n const bordersCopy = borders\n .slice()\n .map((col) => col === null || col === void 0 ? void 0 : col.slice().map((border) => ({ ...border })));\n this.history.update(\"borders\", cmd.sheetIdTo, bordersCopy);\n }\n break;\n case \"DELETE_SHEET\":\n const allBorders = { ...this.borders };\n delete allBorders[cmd.sheetId];\n this.history.update(\"borders\", allBorders);\n break;\n case \"SET_BORDER\":\n this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);\n break;\n case \"SET_FORMATTING\":\n if (cmd.border) {\n const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));\n this.setBorders(cmd.sheetId, target, cmd.border);\n }\n break;\n case \"CLEAR_FORMATTING\":\n this.clearBorders(cmd.sheetId, cmd.target);\n break;\n case \"REMOVE_COLUMNS_ROWS\":\n for (let el of cmd.elements) {\n if (cmd.dimension === \"COL\") {\n this.shiftBordersHorizontally(cmd.sheetId, el + 1, -1);\n }\n else {\n this.shiftBordersVertically(cmd.sheetId, el + 1, -1);\n }\n }\n break;\n case \"ADD_COLUMNS_ROWS\":\n if (cmd.dimension === \"COL\") {\n this.handleAddColumns(cmd);\n }\n else {\n this.handleAddRows(cmd);\n }\n break;\n }\n }\n /**\n * Move borders according to the inserted columns.\n * Ensure borders continuity.\n */\n handleAddColumns(cmd) {\n // The new columns have already been inserted in the sheet at this point.\n let colLeftOfInsertion;\n let colRightOfInsertion;\n if (cmd.position === \"before\") {\n this.shiftBordersHorizontally(cmd.sheetId, cmd.base, cmd.quantity, {\n moveFirstLeftBorder: true,\n });\n colLeftOfInsertion = cmd.base - 1;\n colRightOfInsertion = cmd.base + cmd.quantity;\n }\n else {\n this.shiftBordersHorizontally(cmd.sheetId, cmd.base + 1, cmd.quantity, {\n moveFirstLeftBorder: false,\n });\n colLeftOfInsertion = cmd.base;\n colRightOfInsertion = cmd.base + cmd.quantity + 1;\n }\n this.ensureColumnBorderContinuity(cmd.sheetId, colLeftOfInsertion, colRightOfInsertion);\n }\n /**\n * Move borders according to the inserted rows.\n * Ensure borders continuity.\n */\n handleAddRows(cmd) {\n // The new rows have already been inserted at this point.\n let rowAboveInsertion;\n let rowBelowInsertion;\n if (cmd.position === \"before\") {\n this.shiftBordersVertically(cmd.sheetId, cmd.base, cmd.quantity, {\n moveFirstTopBorder: true,\n });\n rowAboveInsertion = cmd.base - 1;\n rowBelowInsertion = cmd.base + cmd.quantity;\n }\n else {\n this.shiftBordersVertically(cmd.sheetId, cmd.base + 1, cmd.quantity, {\n moveFirstTopBorder: false,\n });\n rowAboveInsertion = cmd.base;\n rowBelowInsertion = cmd.base + cmd.quantity + 1;\n }\n this.ensureRowBorderContinuity(cmd.sheetId, rowAboveInsertion, rowBelowInsertion);\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getCellBorder({ sheetId, col, row }) {\n var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;\n const border = {\n top: (_c = (_b = (_a = this.borders[sheetId]) === null || _a === void 0 ? void 0 : _a[col]) === null || _b === void 0 ? void 0 : _b[row]) === null || _c === void 0 ? void 0 : _c.horizontal,\n bottom: (_f = (_e = (_d = this.borders[sheetId]) === null || _d === void 0 ? void 0 : _d[col]) === null || _e === void 0 ? void 0 : _e[row + 1]) === null || _f === void 0 ? void 0 : _f.horizontal,\n left: (_j = (_h = (_g = this.borders[sheetId]) === null || _g === void 0 ? void 0 : _g[col]) === null || _h === void 0 ? void 0 : _h[row]) === null || _j === void 0 ? void 0 : _j.vertical,\n right: (_m = (_l = (_k = this.borders[sheetId]) === null || _k === void 0 ? void 0 : _k[col + 1]) === null || _l === void 0 ? void 0 : _l[row]) === null || _m === void 0 ? void 0 : _m.vertical,\n };\n if (!border.bottom && !border.left && !border.right && !border.top) {\n return null;\n }\n return border;\n }\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n /**\n * Ensure border continuity between two columns.\n * If the two columns have the same borders (at each row respectively),\n * the same borders are applied to each cell in between.\n */\n ensureColumnBorderContinuity(sheetId, leftColumn, rightColumn) {\n const targetCols = range(leftColumn + 1, rightColumn);\n for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {\n const leftBorder = this.getCellBorder({ sheetId, col: leftColumn, row });\n const rightBorder = this.getCellBorder({ sheetId, col: rightColumn, row });\n if (leftBorder && rightBorder) {\n const commonSides = this.getCommonSides(leftBorder, rightBorder);\n for (let col of targetCols) {\n this.addBorder(sheetId, col, row, commonSides);\n }\n }\n }\n }\n /**\n * Ensure border continuity between two rows.\n * If the two rows have the same borders (at each column respectively),\n * the same borders are applied to each cell in between.\n */\n ensureRowBorderContinuity(sheetId, topRow, bottomRow) {\n const targetRows = range(topRow + 1, bottomRow);\n for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {\n const aboveBorder = this.getCellBorder({ sheetId, col, row: topRow });\n const belowBorder = this.getCellBorder({ sheetId, col, row: bottomRow });\n if (aboveBorder && belowBorder) {\n const commonSides = this.getCommonSides(aboveBorder, belowBorder);\n for (let row of targetRows) {\n this.addBorder(sheetId, col, row, commonSides);\n }\n }\n }\n }\n /**\n * From two borders, return a new border with sides defined in both borders.\n * i.e. the intersection of two borders.\n */\n getCommonSides(border1, border2) {\n const commonBorder = {};\n for (let side of [\"top\", \"bottom\", \"left\", \"right\"]) {\n if (border1[side] && border1[side] === border2[side]) {\n commonBorder[side] = border1[side];\n }\n }\n return commonBorder;\n }\n /**\n * Get all the columns which contains at least a border\n */\n getColumnsWithBorders(sheetId) {\n const sheetBorders = this.borders[sheetId];\n if (!sheetBorders)\n return [];\n return Object.keys(sheetBorders).map((index) => parseInt(index, 10));\n }\n /**\n * Get the range of all the rows in the sheet\n */\n getRowsRange(sheetId) {\n const sheetBorders = this.borders[sheetId];\n if (!sheetBorders)\n return [];\n return range(0, this.getters.getNumberRows(sheetId) + 1);\n }\n /**\n * Move borders of a sheet horizontally.\n * @param sheetId\n * @param start starting column (included)\n * @param delta how much borders will be moved (negative if moved to the left)\n */\n shiftBordersHorizontally(sheetId, start, delta, { moveFirstLeftBorder } = {}) {\n const borders = this.borders[sheetId];\n if (!borders)\n return;\n if (delta < 0) {\n this.moveBordersOfColumn(sheetId, start, delta, \"vertical\", {\n destructive: false,\n });\n }\n this.getColumnsWithBorders(sheetId)\n .filter((col) => col >= start)\n .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up\n .forEach((col) => {\n if ((col === start && moveFirstLeftBorder) || col !== start) {\n this.moveBordersOfColumn(sheetId, col, delta, \"vertical\");\n }\n this.moveBordersOfColumn(sheetId, col, delta, \"horizontal\");\n });\n }\n /**\n * Move borders of a sheet vertically.\n * @param sheetId\n * @param start starting row (included)\n * @param delta how much borders will be moved (negative if moved to the above)\n */\n shiftBordersVertically(sheetId, start, delta, { moveFirstTopBorder } = {}) {\n const borders = this.borders[sheetId];\n if (!borders)\n return;\n if (delta < 0) {\n this.moveBordersOfRow(sheetId, start, delta, \"horizontal\", {\n destructive: false,\n });\n }\n this.getRowsRange(sheetId)\n .filter((row) => row >= start)\n .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up\n .forEach((row) => {\n if ((row === start && moveFirstTopBorder) || row !== start) {\n this.moveBordersOfRow(sheetId, row, delta, \"horizontal\");\n }\n this.moveBordersOfRow(sheetId, row, delta, \"vertical\");\n });\n }\n /**\n * Moves the borders (left if `vertical` or top if `horizontal` depending on\n * `borderDirection`) of all cells in an entire row `delta` rows to the right\n * (`delta` > 0) or to the left (`delta` < 0).\n * Note that as the left of a cell is the right of the cell-1, if the left is\n * moved the right is also moved. However, if `horizontal`, the bottom border\n * is not moved.\n * It does it by replacing the target border by the moved border. If the\n * argument `destructive` is given false, the target border is preserved if\n * the moved border is empty\n */\n moveBordersOfRow(sheetId, row, delta, borderDirection, { destructive } = { destructive: true }) {\n const borders = this.borders[sheetId];\n if (!borders)\n return;\n this.getColumnsWithBorders(sheetId).forEach((col) => {\n var _a, _b, _c, _d;\n const targetBorder = (_b = (_a = borders[col]) === null || _a === void 0 ? void 0 : _a[row + delta]) === null || _b === void 0 ? void 0 : _b[borderDirection];\n const movedBorder = (_d = (_c = borders[col]) === null || _c === void 0 ? void 0 : _c[row]) === null || _d === void 0 ? void 0 : _d[borderDirection];\n this.history.update(\"borders\", sheetId, col, row + delta, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);\n this.history.update(\"borders\", sheetId, col, row, borderDirection, undefined);\n });\n }\n /**\n * Moves the borders (left if `vertical` or top if `horizontal` depending on\n * `borderDirection`) of all cells in an entire column `delta` columns below\n * (`delta` > 0) or above (`delta` < 0).\n * Note that as the top of a cell is the bottom of the cell-1, if the top is\n * moved the bottom is also moved. However, if `vertical`, the right border\n * is not moved.\n * It does it by replacing the target border by the moved border. If the\n * argument `destructive` is given false, the target border is preserved if\n * the moved border is empty\n */\n moveBordersOfColumn(sheetId, col, delta, borderDirection, { destructive } = { destructive: true }) {\n const borders = this.borders[sheetId];\n if (!borders)\n return;\n this.getRowsRange(sheetId).forEach((row) => {\n var _a, _b, _c, _d;\n const targetBorder = (_b = (_a = borders[col + delta]) === null || _a === void 0 ? void 0 : _a[row]) === null || _b === void 0 ? void 0 : _b[borderDirection];\n const movedBorder = (_d = (_c = borders[col]) === null || _c === void 0 ? void 0 : _c[row]) === null || _d === void 0 ? void 0 : _d[borderDirection];\n this.history.update(\"borders\", sheetId, col + delta, row, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);\n this.history.update(\"borders\", sheetId, col, row, borderDirection, undefined);\n });\n }\n /**\n * Set the borders of a cell.\n * It overrides the current border if override == true.\n */\n setBorder(sheetId, col, row, border, override = true) {\n var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;\n if (override || !((_d = (_c = (_b = (_a = this.borders) === null || _a === void 0 ? void 0 : _a[sheetId]) === null || _b === void 0 ? void 0 : _b[col]) === null || _c === void 0 ? void 0 : _c[row]) === null || _d === void 0 ? void 0 : _d.vertical)) {\n this.history.update(\"borders\", sheetId, col, row, \"vertical\", border === null || border === void 0 ? void 0 : border.left);\n }\n if (override || !((_h = (_g = (_f = (_e = this.borders) === null || _e === void 0 ? void 0 : _e[sheetId]) === null || _f === void 0 ? void 0 : _f[col]) === null || _g === void 0 ? void 0 : _g[row]) === null || _h === void 0 ? void 0 : _h.horizontal)) {\n this.history.update(\"borders\", sheetId, col, row, \"horizontal\", border === null || border === void 0 ? void 0 : border.top);\n }\n if (override || !((_m = (_l = (_k = (_j = this.borders) === null || _j === void 0 ? void 0 : _j[sheetId]) === null || _k === void 0 ? void 0 : _k[col + 1]) === null || _l === void 0 ? void 0 : _l[row]) === null || _m === void 0 ? void 0 : _m.vertical)) {\n this.history.update(\"borders\", sheetId, col + 1, row, \"vertical\", border === null || border === void 0 ? void 0 : border.right);\n }\n if (override || !((_r = (_q = (_p = (_o = this.borders) === null || _o === void 0 ? void 0 : _o[sheetId]) === null || _p === void 0 ? void 0 : _p[col]) === null || _q === void 0 ? void 0 : _q[row + 1]) === null || _r === void 0 ? void 0 : _r.horizontal)) {\n this.history.update(\"borders\", sheetId, col, row + 1, \"horizontal\", border === null || border === void 0 ? void 0 : border.bottom);\n }\n }\n /**\n * Remove the borders of a zone\n */\n clearBorders(sheetId, zones) {\n for (let zone of zones) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n this.history.update(\"borders\", sheetId, zone.right + 1, row, \"vertical\", undefined);\n for (let col = zone.left; col <= zone.right; col++) {\n this.history.update(\"borders\", sheetId, col, row, undefined);\n }\n }\n for (let col = zone.left; col <= zone.right; col++) {\n this.history.update(\"borders\", sheetId, col, zone.bottom + 1, \"horizontal\", undefined);\n }\n }\n }\n /**\n * Add a border to the existing one to a cell\n */\n addBorder(sheetId, col, row, border) {\n this.setBorder(sheetId, col, row, {\n ...this.getCellBorder({ sheetId, col, row }),\n ...border,\n });\n }\n /**\n * Set the borders of a zone by computing the borders to add from the given\n * command\n */\n setBorders(sheetId, zones, command) {\n if (command === \"clear\") {\n return this.clearBorders(sheetId, zones);\n }\n for (let zone of zones) {\n if (command === \"h\" || command === \"hv\" || command === \"all\") {\n for (let row = zone.top + 1; row <= zone.bottom; row++) {\n for (let col = zone.left; col <= zone.right; col++) {\n this.addBorder(sheetId, col, row, { top: DEFAULT_BORDER_DESC });\n }\n }\n }\n if (command === \"v\" || command === \"hv\" || command === \"all\") {\n for (let row = zone.top; row <= zone.bottom; row++) {\n for (let col = zone.left + 1; col <= zone.right; col++) {\n this.addBorder(sheetId, col, row, { left: DEFAULT_BORDER_DESC });\n }\n }\n }\n if (command === \"left\" || command === \"all\" || command === \"external\") {\n for (let row = zone.top; row <= zone.bottom; row++) {\n this.addBorder(sheetId, zone.left, row, { left: DEFAULT_BORDER_DESC });\n }\n }\n if (command === \"right\" || command === \"all\" || command === \"external\") {\n for (let row = zone.top; row <= zone.bottom; row++) {\n this.addBorder(sheetId, zone.right + 1, row, { left: DEFAULT_BORDER_DESC });\n }\n }\n if (command === \"top\" || command === \"all\" || command === \"external\") {\n for (let col = zone.left; col <= zone.right; col++) {\n this.addBorder(sheetId, col, zone.top, { top: DEFAULT_BORDER_DESC });\n }\n }\n if (command === \"bottom\" || command === \"all\" || command === \"external\") {\n for (let col = zone.left; col <= zone.right; col++) {\n this.addBorder(sheetId, col, zone.bottom + 1, { top: DEFAULT_BORDER_DESC });\n }\n }\n }\n }\n /**\n * Compute the borders to add to the given zone merged.\n */\n addBordersToMerge(sheetId, zone) {\n const { left, right, top, bottom } = zone;\n const bordersTopLeft = this.getCellBorder({ sheetId, col: left, row: top });\n const bordersBottomRight = this.getCellBorder({ sheetId, col: right, row: bottom });\n this.clearBorders(sheetId, [zone]);\n if (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.top) {\n this.setBorders(sheetId, [{ ...zone, bottom: top }], \"top\");\n }\n if (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.left) {\n this.setBorders(sheetId, [{ ...zone, right: left }], \"left\");\n }\n if ((bordersBottomRight === null || bordersBottomRight === void 0 ? void 0 : bordersBottomRight.bottom) || (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.bottom)) {\n this.setBorders(sheetId, [{ ...zone, top: bottom }], \"bottom\");\n }\n if ((bordersBottomRight === null || bordersBottomRight === void 0 ? void 0 : bordersBottomRight.right) || (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.right)) {\n this.setBorders(sheetId, [{ ...zone, left: right }], \"right\");\n }\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) {\n // Borders\n if (data.borders) {\n for (let sheet of data.sheets) {\n for (let [xc, cell] of Object.entries(sheet.cells)) {\n if (cell === null || cell === void 0 ? void 0 : cell.border) {\n const border = data.borders[cell.border];\n const { col, row } = toCartesian(xc);\n this.setBorder(sheet.id, col, row, border, false);\n }\n }\n }\n }\n // Merges\n for (let sheetData of data.sheets) {\n if (sheetData.merges) {\n for (let merge of sheetData.merges) {\n this.addBordersToMerge(sheetData.id, toZone(merge));\n }\n }\n }\n }\n export(data) {\n // Borders\n let borderId = 0;\n const borders = {};\n /**\n * Get the id of the given border. If the border does not exist, it creates\n * one.\n */\n function getBorderId(border) {\n for (let [key, value] of Object.entries(borders)) {\n if (stringify(value) === stringify(border)) {\n return parseInt(key, 10);\n }\n }\n borders[++borderId] = border;\n return borderId;\n }\n for (let sheet of data.sheets) {\n for (let col = 0; col < sheet.colNumber; col++) {\n for (let row = 0; row < sheet.rowNumber; row++) {\n const border = this.getCellBorder({ sheetId: sheet.id, col, row });\n if (border) {\n const xc = toXC(col, row);\n const cell = sheet.cells[xc];\n const borderId = getBorderId(border);\n if (cell) {\n cell.border = borderId;\n }\n else {\n sheet.cells[xc] = { border: borderId };\n }\n }\n }\n }\n }\n data.borders = borders;\n }\n exportForExcel(data) {\n this.export(data);\n }\n }\n BordersPlugin.getters = [\"getCellBorder\"];\n\n const nbspRegexp = new RegExp(String.fromCharCode(160), \"g\");\n /**\n * Core Plugin\n *\n * This is the most fundamental of all plugins. It defines how to interact with\n * cell and sheet content.\n */\n class CellPlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.nextId = 1;\n this.cells = {};\n }\n adaptRanges(applyChange, sheetId) {\n for (const sheet of Object.keys(this.cells)) {\n for (const cell of Object.values(this.cells[sheet] || {})) {\n if (cell.isFormula) {\n for (const range of cell.dependencies) {\n if (!sheetId || range.sheetId === sheetId) {\n const change = applyChange(range);\n if (change.changeType !== \"NONE\") {\n this.history.update(\"cells\", sheet, cell.id, \"dependencies\", cell.dependencies.indexOf(range), change.range);\n }\n }\n }\n }\n }\n }\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"UPDATE_CELL\":\n case \"CLEAR_CELL\":\n return this.checkCellOutOfSheet(cmd.sheetId, cmd.col, cmd.row);\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"SET_FORMATTING\":\n if (\"style\" in cmd) {\n this.setStyle(cmd.sheetId, cmd.target, cmd.style);\n }\n if (\"format\" in cmd && cmd.format !== undefined) {\n this.setFormatter(cmd.sheetId, cmd.target, cmd.format);\n }\n break;\n case \"CLEAR_FORMATTING\":\n this.clearFormatting(cmd.sheetId, cmd.target);\n break;\n case \"ADD_COLUMNS_ROWS\":\n if (cmd.dimension === \"COL\") {\n this.handleAddColumnsRows(cmd, this.copyColumnStyle.bind(this));\n }\n else {\n this.handleAddColumnsRows(cmd, this.copyRowStyle.bind(this));\n }\n break;\n case \"UPDATE_CELL\":\n this.updateCell(cmd.sheetId, cmd.col, cmd.row, cmd);\n break;\n case \"CLEAR_CELL\":\n this.dispatch(\"UPDATE_CELL\", {\n sheetId: cmd.sheetId,\n col: cmd.col,\n row: cmd.row,\n content: \"\",\n style: null,\n format: \"\",\n });\n break;\n }\n }\n /**\n * Set a format to all the cells in a zone\n */\n setFormatter(sheetId, zones, format) {\n for (let zone of zones) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n for (let col = zone.left; col <= zone.right; col++) {\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n format,\n });\n }\n }\n }\n }\n /**\n * Clear the styles and format of zones\n */\n clearFormatting(sheetId, zones) {\n for (let zone of zones) {\n for (let col = zone.left; col <= zone.right; col++) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n // commandHelpers.updateCell(sheetId, col, row, { style: undefined});\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n style: null,\n format: \"\",\n });\n }\n }\n }\n }\n /**\n * Copy the style of the reference column/row to the new columns/rows.\n */\n handleAddColumnsRows(cmd, fn) {\n // The new elements have already been inserted in the sheet at this point.\n let insertedElements;\n let styleReference;\n if (cmd.position === \"before\") {\n insertedElements = range(cmd.base, cmd.base + cmd.quantity);\n styleReference = cmd.base + cmd.quantity;\n }\n else {\n insertedElements = range(cmd.base + 1, cmd.base + cmd.quantity + 1);\n styleReference = cmd.base;\n }\n fn(cmd.sheetId, styleReference, insertedElements);\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) {\n for (let sheet of data.sheets) {\n // cells\n for (let xc in sheet.cells) {\n const cellData = sheet.cells[xc];\n const { col, row } = toCartesian(xc);\n if ((cellData === null || cellData === void 0 ? void 0 : cellData.content) || (cellData === null || cellData === void 0 ? void 0 : cellData.format) || (cellData === null || cellData === void 0 ? void 0 : cellData.style)) {\n const cell = this.importCell(sheet.id, cellData, data.styles, data.formats);\n this.history.update(\"cells\", sheet.id, cell.id, cell);\n this.dispatch(\"UPDATE_CELL_POSITION\", {\n cellId: cell.id,\n col,\n row,\n sheetId: sheet.id,\n });\n }\n }\n }\n }\n export(data) {\n const styles = {};\n const formats = {};\n for (let _sheet of data.sheets) {\n const cells = {};\n const positions = Object.keys(this.cells[_sheet.id] || {})\n .map((cellId) => this.getters.getCellPosition(cellId))\n .sort((a, b) => (a.col === b.col ? a.row - b.row : a.col - b.col));\n for (const position of positions) {\n const cell = this.getters.getCell(position);\n const xc = toXC(position.col, position.row);\n cells[xc] = {\n style: cell.style ? getItemId(cell.style, styles) : undefined,\n format: cell.format ? getItemId(cell.format, formats) : undefined,\n content: cell.content || undefined,\n };\n }\n _sheet.cells = cells;\n }\n data.styles = styles;\n data.formats = formats;\n }\n importCell(sheetId, cellData, normalizedStyles, normalizedFormats) {\n const style = (cellData.style && normalizedStyles[cellData.style]) || undefined;\n const format = (cellData.format && normalizedFormats[cellData.format]) || undefined;\n const cellId = this.getNextUid();\n return this.createCell(cellId, (cellData === null || cellData === void 0 ? void 0 : cellData.content) || \"\", format, style, sheetId);\n }\n exportForExcel(data) {\n this.export(data);\n }\n // ---------------------------------------------------------------------------\n // GETTERS\n // ---------------------------------------------------------------------------\n getCells(sheetId) {\n return this.cells[sheetId] || {};\n }\n /**\n * get a cell by ID. Used in evaluation when evaluating an async cell, we need to be able to find it back after\n * starting an async evaluation even if it has been moved or re-allocated\n */\n getCellById(cellId) {\n // this must be as fast as possible\n for (const sheetId in this.cells) {\n const sheet = this.cells[sheetId];\n const cell = sheet[cellId];\n if (cell) {\n return cell;\n }\n }\n return undefined;\n }\n /*\n * Reconstructs the original formula string based on a normalized form and its dependencies\n */\n buildFormulaContent(sheetId, cell, dependencies) {\n const ranges = dependencies || [...cell.dependencies];\n return concat(cell.compiledFormula.tokens.map((token) => {\n if (token.type === \"REFERENCE\") {\n const range = ranges.shift();\n return this.getters.getRangeString(range, sheetId);\n }\n return token.value;\n }));\n }\n getFormulaCellContent(sheetId, cell) {\n return this.buildFormulaContent(sheetId, cell);\n }\n getCellStyle(position) {\n var _a;\n return ((_a = this.getters.getCell(position)) === null || _a === void 0 ? void 0 : _a.style) || {};\n }\n /**\n * Converts a zone to a XC coordinate system\n *\n * The conversion also treats merges as one single cell\n *\n * Examples:\n * {top:0,left:0,right:0,bottom:0} ==> A1\n * {top:0,left:0,right:1,bottom:1} ==> A1:B2\n *\n * if A1:B2 is a merge:\n * {top:0,left:0,right:1,bottom:1} ==> A1\n * {top:1,left:0,right:1,bottom:2} ==> A1:B3\n *\n * if A1:B2 and A4:B5 are merges:\n * {top:1,left:0,right:1,bottom:3} ==> A1:A5\n */\n zoneToXC(sheetId, zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {\n zone = this.getters.expandZone(sheetId, zone);\n const topLeft = toXC(zone.left, zone.top, fixedParts[0]);\n const botRight = toXC(zone.right, zone.bottom, fixedParts.length > 1 ? fixedParts[1] : fixedParts[0]);\n const cellTopLeft = this.getters.getMainCellPosition({\n sheetId,\n col: zone.left,\n row: zone.top,\n });\n const cellBotRight = this.getters.getMainCellPosition({\n sheetId,\n col: zone.right,\n row: zone.bottom,\n });\n const sameCell = cellTopLeft.col === cellBotRight.col && cellTopLeft.row === cellBotRight.row;\n if (topLeft != botRight && !sameCell) {\n return topLeft + \":\" + botRight;\n }\n return topLeft;\n }\n setStyle(sheetId, target, style) {\n for (let zone of target) {\n for (let col = zone.left; col <= zone.right; col++) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n const cell = this.getters.getCell({ sheetId, col, row });\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n style: style ? { ...cell === null || cell === void 0 ? void 0 : cell.style, ...style } : undefined,\n });\n }\n }\n }\n }\n /**\n * Copy the style of one column to other columns.\n */\n copyColumnStyle(sheetId, refColumn, targetCols) {\n for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {\n const format = this.getFormat(sheetId, refColumn, row);\n if (format.style || format.format) {\n for (let col of targetCols) {\n this.dispatch(\"UPDATE_CELL\", { sheetId, col, row, ...format });\n }\n }\n }\n }\n /**\n * Copy the style of one row to other rows.\n */\n copyRowStyle(sheetId, refRow, targetRows) {\n for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {\n const format = this.getFormat(sheetId, col, refRow);\n if (format.style || format.format) {\n for (let row of targetRows) {\n this.dispatch(\"UPDATE_CELL\", { sheetId, col, row, ...format });\n }\n }\n }\n }\n /**\n * gets the currently used style/border of a cell based on it's coordinates\n */\n getFormat(sheetId, col, row) {\n const format = {};\n const position = this.getters.getMainCellPosition({ sheetId, col, row });\n const cell = this.getters.getCell(position);\n if (cell) {\n if (cell.style) {\n format[\"style\"] = cell.style;\n }\n if (cell.format) {\n format[\"format\"] = cell.format;\n }\n }\n return format;\n }\n getNextUid() {\n const id = this.nextId.toString();\n this.history.update(\"nextId\", this.nextId + 1);\n return id;\n }\n updateCell(sheetId, col, row, after) {\n var _a;\n const before = this.getters.getCell({ sheetId, col, row });\n const hasContent = \"content\" in after || \"formula\" in after;\n // Compute the new cell properties\n const afterContent = hasContent\n ? ((_a = after.content) === null || _a === void 0 ? void 0 : _a.replace(nbspRegexp, \"\")) || \"\"\n : (before === null || before === void 0 ? void 0 : before.content) || \"\";\n let style;\n if (after.style !== undefined) {\n style = after.style || undefined;\n }\n else {\n style = before ? before.style : undefined;\n }\n let format = (\"format\" in after ? after.format : before && before.format) || detectFormat(afterContent);\n /* Read the following IF as:\n * we need to remove the cell if it is completely empty, but we can know if it completely empty if:\n * - the command says the new content is empty and has no border/format/style\n * - the command has no content property, in this case\n * - either there wasn't a cell at this place and the command says border/format/style is empty\n * - or there was a cell at this place, but it's an empty cell and the command says border/format/style is empty\n * */\n if (((hasContent && !afterContent && !after.formula) ||\n (!hasContent && (!before || before.content === \"\"))) &&\n !style &&\n !format) {\n if (before) {\n this.history.update(\"cells\", sheetId, before.id, undefined);\n this.dispatch(\"UPDATE_CELL_POSITION\", {\n cellId: undefined,\n col,\n row,\n sheetId,\n });\n }\n return;\n }\n const cellId = (before === null || before === void 0 ? void 0 : before.id) || this.getNextUid();\n const cell = this.createCell(cellId, afterContent, format, style, sheetId);\n this.history.update(\"cells\", sheetId, cell.id, cell);\n this.dispatch(\"UPDATE_CELL_POSITION\", { cellId: cell.id, col, row, sheetId });\n }\n createCell(id, content, format, style, sheetId) {\n if (!content.startsWith(\"=\")) {\n return this.createLiteralCell(id, content, format, style);\n }\n try {\n return this.createFormulaCell(id, content, format, style, sheetId);\n }\n catch (error) {\n return this.createErrorFormula(id, content, format, style, error);\n }\n }\n createLiteralCell(id, content, format, style) {\n return {\n id,\n content,\n style,\n format,\n isFormula: false,\n };\n }\n createFormulaCell(id, content, format, style, sheetId) {\n const compiledFormula = compile(content);\n if (compiledFormula.dependencies.length) {\n return this.createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId);\n }\n return {\n id,\n content,\n style,\n format,\n isFormula: true,\n compiledFormula,\n dependencies: [],\n };\n }\n /**\n * Create a new formula cell with the content\n * being a computed property to rebuild the dependencies XC.\n */\n createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId) {\n const dependencies = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));\n const buildFormulaContent = this.buildFormulaContent.bind(this);\n // Only for formulas with dependencies because\n // **the closure is expensive memory-wise**\n return {\n id,\n get content() {\n return buildFormulaContent(sheetId, {\n dependencies: this.dependencies,\n compiledFormula: this.compiledFormula,\n });\n },\n style,\n format,\n isFormula: true,\n compiledFormula,\n dependencies,\n };\n }\n createErrorFormula(id, content, format, style, error) {\n return {\n id,\n content,\n style,\n format,\n isFormula: true,\n compiledFormula: {\n dependencies: [],\n tokens: tokenize(content),\n execute: function () {\n throw error;\n },\n },\n dependencies: [],\n };\n }\n checkCellOutOfSheet(sheetId, col, row) {\n const sheet = this.getters.tryGetSheet(sheetId);\n if (!sheet)\n return 27 /* CommandResult.InvalidSheetId */;\n const sheetZone = this.getters.getSheetZone(sheetId);\n return isInside(col, row, sheetZone) ? 0 /* CommandResult.Success */ : 18 /* CommandResult.TargetOutOfSheet */;\n }\n }\n CellPlugin.getters = [\n \"zoneToXC\",\n \"getCells\",\n \"getFormulaCellContent\",\n \"getCellStyle\",\n \"buildFormulaContent\",\n \"getCellById\",\n ];\n\n class ChartPlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.charts = {};\n this.createChart = chartFactory(this.getters);\n this.validateChartDefinition = (cmd) => validateChartDefinition(this, cmd.definition);\n }\n adaptRanges(applyChange) {\n for (const [chartId, chart] of Object.entries(this.charts)) {\n this.history.update(\"charts\", chartId, chart === null || chart === void 0 ? void 0 : chart.updateRanges(applyChange));\n }\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"CREATE_CHART\":\n return this.checkValidations(cmd, this.chainValidations(this.validateChartDefinition, this.checkChartDuplicate));\n case \"UPDATE_CHART\":\n return this.checkValidations(cmd, this.chainValidations(this.validateChartDefinition, this.checkChartExists));\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handle(cmd) {\n var _a;\n switch (cmd.type) {\n case \"CREATE_CHART\":\n this.addFigure(cmd.id, cmd.sheetId, cmd.position, cmd.size);\n this.addChart(cmd.id, cmd.definition);\n break;\n case \"UPDATE_CHART\": {\n this.addChart(cmd.id, cmd.definition);\n break;\n }\n case \"DUPLICATE_SHEET\": {\n const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId);\n for (const fig of sheetFiguresFrom) {\n if (fig.tag === \"chart\") {\n const figureIdBase = fig.id.split(FIGURE_ID_SPLITTER).pop();\n const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;\n const chart = (_a = this.charts[fig.id]) === null || _a === void 0 ? void 0 : _a.copyForSheetId(cmd.sheetIdTo);\n if (chart) {\n this.dispatch(\"CREATE_CHART\", {\n id: duplicatedFigureId,\n position: { x: fig.x, y: fig.y },\n size: { width: fig.width, height: fig.height },\n definition: chart.getDefinition(),\n sheetId: cmd.sheetIdTo,\n });\n }\n }\n }\n break;\n }\n case \"DELETE_FIGURE\":\n this.history.update(\"charts\", cmd.id, undefined);\n break;\n case \"DELETE_SHEET\":\n for (let id of this.getChartIds(cmd.sheetId)) {\n this.history.update(\"charts\", id, undefined);\n }\n break;\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getContextCreationChart(figureId) {\n var _a;\n return (_a = this.charts[figureId]) === null || _a === void 0 ? void 0 : _a.getContextCreation();\n }\n getChart(figureId) {\n return this.charts[figureId];\n }\n getChartType(figureId) {\n var _a;\n const type = (_a = this.charts[figureId]) === null || _a === void 0 ? void 0 : _a.type;\n if (!type) {\n throw new Error(\"Chart not defined.\");\n }\n return type;\n }\n isChartDefined(figureId) {\n return figureId in this.charts && this.charts !== undefined;\n }\n getChartIds(sheetId) {\n return Object.entries(this.charts)\n .filter(([, chart]) => (chart === null || chart === void 0 ? void 0 : chart.sheetId) === sheetId)\n .map(([id]) => id);\n }\n getChartDefinition(figureId) {\n var _a;\n const definition = (_a = this.charts[figureId]) === null || _a === void 0 ? void 0 : _a.getDefinition();\n if (!definition) {\n throw new Error(`There is no chart with the given figureId: ${figureId}`);\n }\n return definition;\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) {\n for (let sheet of data.sheets) {\n if (sheet.figures) {\n for (let figure of sheet.figures) {\n // TODO:\n // figure data should be external IMO => chart should be in sheet.chart\n // instead of in figure.data\n if (figure.tag === \"chart\") {\n this.charts[figure.id] = this.createChart(figure.id, figure.data, sheet.id);\n }\n }\n }\n }\n }\n export(data) {\n var _a;\n if (data.sheets) {\n for (let sheet of data.sheets) {\n // TODO This code is false, if two plugins want ot insert figures on the sheet, it will crash !\n const sheetFigures = this.getters.getFigures(sheet.id);\n const figures = [];\n for (let sheetFigure of sheetFigures) {\n const figure = sheetFigure;\n if (figure && figure.tag === \"chart\") {\n const data = (_a = this.charts[figure.id]) === null || _a === void 0 ? void 0 : _a.getDefinition();\n if (data) {\n figure.data = data;\n figures.push(figure);\n }\n }\n else {\n figures.push(figure);\n }\n }\n sheet.figures = figures;\n }\n }\n }\n exportForExcel(data) {\n var _a;\n for (let sheet of data.sheets) {\n const sheetFigures = this.getters.getFigures(sheet.id);\n const figures = [];\n for (let figure of sheetFigures) {\n if (figure && figure.tag === \"chart\") {\n const figureData = (_a = this.charts[figure.id]) === null || _a === void 0 ? void 0 : _a.getDefinitionForExcel();\n if (figureData) {\n figures.push({\n ...figure,\n data: figureData,\n });\n }\n }\n }\n sheet.charts = figures;\n }\n }\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n /**\n * Add a figure with tag chart with the given id at the given position\n */\n addFigure(id, sheetId, position = { x: 0, y: 0 }, size = {\n width: DEFAULT_FIGURE_WIDTH,\n height: DEFAULT_FIGURE_HEIGHT,\n }) {\n if (this.getters.getFigure(sheetId, id)) {\n return;\n }\n const figure = {\n id,\n x: position.x,\n y: position.y,\n width: size.width,\n height: size.height,\n tag: \"chart\",\n };\n this.dispatch(\"CREATE_FIGURE\", { sheetId, figure });\n }\n /**\n * Add a chart in the local state. If a chart already exists, this chart is\n * replaced\n */\n addChart(id, definition) {\n const sheetId = this.getters.getFigureSheetId(id);\n if (sheetId) {\n this.history.update(\"charts\", id, this.createChart(id, definition, sheetId));\n }\n }\n checkChartDuplicate(cmd) {\n return this.getters.getFigureSheetId(cmd.id)\n ? 84 /* CommandResult.DuplicatedChartId */\n : 0 /* CommandResult.Success */;\n }\n checkChartExists(cmd) {\n return this.getters.getFigureSheetId(cmd.id)\n ? 0 /* CommandResult.Success */\n : 85 /* CommandResult.ChartDoesNotExist */;\n }\n }\n ChartPlugin.getters = [\n \"isChartDefined\",\n \"getChartDefinition\",\n \"getChartType\",\n \"getChartIds\",\n \"getChart\",\n \"getContextCreationChart\",\n ];\n\n // -----------------------------------------------------------------------------\n // Constants\n // -----------------------------------------------------------------------------\n function stringToNumber(value) {\n return value === \"\" ? NaN : Number(value);\n }\n class ConditionalFormatPlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.cfRules = {};\n }\n loopThroughRangesOfSheet(sheetId, applyChange) {\n for (const rule of this.cfRules[sheetId]) {\n for (const range of rule.ranges) {\n const change = applyChange(range);\n switch (change.changeType) {\n case \"REMOVE\":\n let copy = rule.ranges.slice();\n copy.splice(rule.ranges.indexOf(range), 1);\n if (copy.length >= 1) {\n this.history.update(\"cfRules\", sheetId, this.cfRules[sheetId].indexOf(rule), \"ranges\", copy);\n }\n else {\n this.removeConditionalFormatting(rule.id, sheetId);\n }\n break;\n case \"RESIZE\":\n case \"MOVE\":\n case \"CHANGE\":\n this.history.update(\"cfRules\", sheetId, this.cfRules[sheetId].indexOf(rule), \"ranges\", rule.ranges.indexOf(range), change.range);\n break;\n }\n }\n }\n }\n adaptRanges(applyChange, sheetId) {\n if (sheetId) {\n this.loopThroughRangesOfSheet(sheetId, applyChange);\n }\n else {\n for (const sheetId of Object.keys(this.cfRules)) {\n this.loopThroughRangesOfSheet(sheetId, applyChange);\n }\n }\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"ADD_CONDITIONAL_FORMAT\":\n return this.checkValidations(cmd, this.checkCFRule, this.checkEmptyRange);\n case \"MOVE_CONDITIONAL_FORMAT\":\n return this.checkValidReordering(cmd.cfId, cmd.direction, cmd.sheetId);\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"CREATE_SHEET\":\n this.cfRules[cmd.sheetId] = [];\n break;\n case \"DUPLICATE_SHEET\":\n this.history.update(\"cfRules\", cmd.sheetIdTo, []);\n for (const cf of this.getConditionalFormats(cmd.sheetId)) {\n this.addConditionalFormatting(cf, cmd.sheetIdTo);\n }\n break;\n case \"DELETE_SHEET\":\n const cfRules = Object.assign({}, this.cfRules);\n delete cfRules[cmd.sheetId];\n this.history.update(\"cfRules\", cfRules);\n break;\n case \"ADD_CONDITIONAL_FORMAT\":\n const cf = {\n ...cmd.cf,\n ranges: cmd.ranges.map((rangeData) => this.getters.getRangeString(this.getters.getRangeFromRangeData(rangeData), cmd.sheetId)),\n };\n this.addConditionalFormatting(cf, cmd.sheetId);\n break;\n case \"REMOVE_CONDITIONAL_FORMAT\":\n this.removeConditionalFormatting(cmd.id, cmd.sheetId);\n break;\n case \"MOVE_CONDITIONAL_FORMAT\":\n this.reorderConditionalFormatting(cmd.cfId, cmd.direction, cmd.sheetId);\n break;\n }\n }\n import(data) {\n for (let sheet of data.sheets) {\n this.cfRules[sheet.id] = sheet.conditionalFormats.map((rule) => this.mapToConditionalFormatInternal(sheet.id, rule));\n }\n }\n export(data) {\n if (data.sheets) {\n for (let sheet of data.sheets) {\n if (this.cfRules[sheet.id]) {\n sheet.conditionalFormats = this.cfRules[sheet.id].map((rule) => this.mapToConditionalFormat(sheet.id, rule));\n }\n }\n }\n }\n exportForExcel(data) {\n this.export(data);\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n /**\n * Returns all the conditional format rules defined for the current sheet to display the user\n */\n getConditionalFormats(sheetId) {\n var _a;\n return ((_a = this.cfRules[sheetId]) === null || _a === void 0 ? void 0 : _a.map((cf) => this.mapToConditionalFormat(sheetId, cf))) || [];\n }\n getRulesSelection(sheetId, selection) {\n const ruleIds = new Set();\n selection.forEach((zone) => {\n const zoneRuleId = this.getRulesByZone(sheetId, zone);\n zoneRuleId.forEach((ruleId) => {\n ruleIds.add(ruleId);\n });\n });\n return Array.from(ruleIds);\n }\n getRulesByZone(sheetId, zone) {\n const ruleIds = new Set();\n for (let row = zone.top; row <= zone.bottom; row++) {\n for (let col = zone.left; col <= zone.right; col++) {\n const cellRules = this.getRulesByCell(sheetId, col, row);\n cellRules.forEach((rule) => {\n ruleIds.add(rule.id);\n });\n }\n }\n return ruleIds;\n }\n getRulesByCell(sheetId, cellCol, cellRow) {\n const rules = [];\n for (let cf of this.cfRules[sheetId]) {\n for (let range of cf.ranges) {\n if (isInside(cellCol, cellRow, range.zone)) {\n rules.push(cf);\n }\n }\n }\n return new Set(rules.map((rule) => {\n return this.mapToConditionalFormat(sheetId, rule);\n }));\n }\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n mapToConditionalFormat(sheetId, cf) {\n return {\n ...cf,\n ranges: cf.ranges.map((range) => {\n return this.getters.getRangeString(range, sheetId);\n }),\n };\n }\n mapToConditionalFormatInternal(sheet, cf) {\n const conditionalFormat = {\n ...cf,\n ranges: cf.ranges.map((range) => {\n return this.getters.getRangeFromSheetXC(sheet, range);\n }),\n };\n return conditionalFormat;\n }\n /**\n * Add or replace a conditional format rule\n */\n addConditionalFormatting(cf, sheet) {\n const currentCF = this.cfRules[sheet].slice();\n const replaceIndex = currentCF.findIndex((c) => c.id === cf.id);\n const newCF = this.mapToConditionalFormatInternal(sheet, cf);\n if (replaceIndex > -1) {\n currentCF.splice(replaceIndex, 1, newCF);\n }\n else {\n currentCF.push(newCF);\n }\n this.history.update(\"cfRules\", sheet, currentCF);\n }\n checkValidReordering(cfId, direction, sheetId) {\n if (!this.cfRules[sheetId])\n return 27 /* CommandResult.InvalidSheetId */;\n const ruleIndex = this.cfRules[sheetId].findIndex((cf) => cf.id === cfId);\n if (ruleIndex === -1)\n return 71 /* CommandResult.InvalidConditionalFormatId */;\n const cfIndex2 = direction === \"up\" ? ruleIndex - 1 : ruleIndex + 1;\n if (cfIndex2 < 0 || cfIndex2 >= this.cfRules[sheetId].length) {\n return 71 /* CommandResult.InvalidConditionalFormatId */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkEmptyRange(cmd) {\n return cmd.ranges.length ? 0 /* CommandResult.Success */ : 24 /* CommandResult.EmptyRange */;\n }\n checkCFRule(cmd) {\n const rule = cmd.cf.rule;\n switch (rule.type) {\n case \"CellIsRule\":\n return this.checkValidations(rule, this.checkOperatorArgsNumber(2, [\"Between\", \"NotBetween\"]), this.checkOperatorArgsNumber(1, [\n \"BeginsWith\",\n \"ContainsText\",\n \"EndsWith\",\n \"GreaterThan\",\n \"GreaterThanOrEqual\",\n \"LessThan\",\n \"LessThanOrEqual\",\n \"NotContains\",\n ]), this.checkOperatorArgsNumber(0, [\"IsEmpty\", \"IsNotEmpty\"]));\n case \"ColorScaleRule\": {\n return this.checkValidations(rule, this.chainValidations(this.checkThresholds(this.checkFormulaCompilation)), this.chainValidations(this.checkThresholds(this.checkNaN), this.batchValidations(this.checkMinBiggerThanMax, this.checkMinBiggerThanMid, this.checkMidBiggerThanMax\n // Those three validations can be factorized further\n )));\n }\n case \"IconSetRule\": {\n return this.checkValidations(rule, this.chainValidations(this.checkInflectionPoints(this.checkNaN), this.checkLowerBiggerThanUpper), this.chainValidations(this.checkInflectionPoints(this.checkFormulaCompilation)));\n }\n }\n return 0 /* CommandResult.Success */;\n }\n checkOperatorArgsNumber(expectedNumber, operators) {\n if (expectedNumber > 2) {\n throw new Error(\"Checking more than 2 arguments is currently not supported. Add the appropriate CommandResult if you want to.\");\n }\n return (rule) => {\n if (operators.includes(rule.operator)) {\n const errors = [];\n const isEmpty = (value) => value === undefined || value === \"\";\n if (expectedNumber >= 1 && isEmpty(rule.values[0])) {\n errors.push(51 /* CommandResult.FirstArgMissing */);\n }\n if (expectedNumber >= 2 && isEmpty(rule.values[1])) {\n errors.push(52 /* CommandResult.SecondArgMissing */);\n }\n return errors.length ? errors : 0 /* CommandResult.Success */;\n }\n return 0 /* CommandResult.Success */;\n };\n }\n checkNaN(threshold, thresholdName) {\n if ([\"number\", \"percentage\", \"percentile\"].includes(threshold.type) &&\n (threshold.value === \"\" || isNaN(threshold.value))) {\n switch (thresholdName) {\n case \"min\":\n return 53 /* CommandResult.MinNaN */;\n case \"max\":\n return 55 /* CommandResult.MaxNaN */;\n case \"mid\":\n return 54 /* CommandResult.MidNaN */;\n case \"upperInflectionPoint\":\n return 56 /* CommandResult.ValueUpperInflectionNaN */;\n case \"lowerInflectionPoint\":\n return 57 /* CommandResult.ValueLowerInflectionNaN */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n checkFormulaCompilation(threshold, thresholdName) {\n if (threshold.type !== \"formula\")\n return 0 /* CommandResult.Success */;\n try {\n compile(threshold.value || \"\");\n }\n catch (error) {\n switch (thresholdName) {\n case \"min\":\n return 58 /* CommandResult.MinInvalidFormula */;\n case \"max\":\n return 60 /* CommandResult.MaxInvalidFormula */;\n case \"mid\":\n return 59 /* CommandResult.MidInvalidFormula */;\n case \"upperInflectionPoint\":\n return 61 /* CommandResult.ValueUpperInvalidFormula */;\n case \"lowerInflectionPoint\":\n return 62 /* CommandResult.ValueLowerInvalidFormula */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n checkThresholds(check) {\n return this.batchValidations((rule) => check(rule.minimum, \"min\"), (rule) => check(rule.maximum, \"max\"), (rule) => (rule.midpoint ? check(rule.midpoint, \"mid\") : 0 /* CommandResult.Success */));\n }\n checkInflectionPoints(check) {\n return this.batchValidations((rule) => check(rule.lowerInflectionPoint, \"lowerInflectionPoint\"), (rule) => check(rule.upperInflectionPoint, \"upperInflectionPoint\"));\n }\n checkLowerBiggerThanUpper(rule) {\n const minValue = rule.lowerInflectionPoint.value;\n const maxValue = rule.upperInflectionPoint.value;\n if ([\"number\", \"percentage\", \"percentile\"].includes(rule.lowerInflectionPoint.type) &&\n rule.lowerInflectionPoint.type === rule.upperInflectionPoint.type &&\n Number(minValue) > Number(maxValue)) {\n return 48 /* CommandResult.LowerBiggerThanUpper */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkMinBiggerThanMax(rule) {\n const minValue = rule.minimum.value;\n const maxValue = rule.maximum.value;\n if ([\"number\", \"percentage\", \"percentile\"].includes(rule.minimum.type) &&\n rule.minimum.type === rule.maximum.type &&\n stringToNumber(minValue) >= stringToNumber(maxValue)) {\n return 47 /* CommandResult.MinBiggerThanMax */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkMidBiggerThanMax(rule) {\n var _a;\n const midValue = (_a = rule.midpoint) === null || _a === void 0 ? void 0 : _a.value;\n const maxValue = rule.maximum.value;\n if (rule.midpoint &&\n [\"number\", \"percentage\", \"percentile\"].includes(rule.midpoint.type) &&\n rule.midpoint.type === rule.maximum.type &&\n stringToNumber(midValue) >= stringToNumber(maxValue)) {\n return 49 /* CommandResult.MidBiggerThanMax */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkMinBiggerThanMid(rule) {\n var _a;\n const minValue = rule.minimum.value;\n const midValue = (_a = rule.midpoint) === null || _a === void 0 ? void 0 : _a.value;\n if (rule.midpoint &&\n [\"number\", \"percentage\", \"percentile\"].includes(rule.midpoint.type) &&\n rule.minimum.type === rule.midpoint.type &&\n stringToNumber(minValue) >= stringToNumber(midValue)) {\n return 50 /* CommandResult.MinBiggerThanMid */;\n }\n return 0 /* CommandResult.Success */;\n }\n removeConditionalFormatting(id, sheet) {\n const cfIndex = this.cfRules[sheet].findIndex((s) => s.id === id);\n if (cfIndex !== -1) {\n const currentCF = this.cfRules[sheet].slice();\n currentCF.splice(cfIndex, 1);\n this.history.update(\"cfRules\", sheet, currentCF);\n }\n }\n reorderConditionalFormatting(cfId, direction, sheetId) {\n const cfIndex1 = this.cfRules[sheetId].findIndex((s) => s.id === cfId);\n const cfIndex2 = direction === \"up\" ? cfIndex1 - 1 : cfIndex1 + 1;\n if (cfIndex2 < 0 || cfIndex2 >= this.cfRules[sheetId].length)\n return;\n if (cfIndex1 !== -1 && cfIndex2 !== -1) {\n const currentCF = [...this.cfRules[sheetId]];\n const tmp = currentCF[cfIndex1];\n currentCF[cfIndex1] = currentCF[cfIndex2];\n currentCF[cfIndex2] = tmp;\n this.history.update(\"cfRules\", sheetId, currentCF);\n }\n }\n }\n ConditionalFormatPlugin.getters = [\"getConditionalFormats\", \"getRulesSelection\", \"getRulesByCell\"];\n\n class FigurePlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.figures = {};\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"CREATE_FIGURE\":\n return this.checkFigureDuplicate(cmd.figure.id);\n case \"UPDATE_FIGURE\":\n case \"DELETE_FIGURE\":\n return this.checkFigureExists(cmd.sheetId, cmd.id);\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"CREATE_SHEET\":\n this.figures[cmd.sheetId] = {};\n break;\n case \"DELETE_SHEET\":\n this.deleteSheet(cmd.sheetId);\n break;\n case \"CREATE_FIGURE\":\n this.addFigure(cmd.figure, cmd.sheetId);\n break;\n case \"UPDATE_FIGURE\":\n const { type, sheetId, ...update } = cmd;\n const figure = update;\n this.updateFigure(sheetId, figure);\n break;\n case \"DELETE_FIGURE\":\n this.removeFigure(cmd.id, cmd.sheetId);\n break;\n }\n }\n updateFigure(sheetId, figure) {\n if (!(\"id\" in figure)) {\n return;\n }\n for (const [key, value] of Object.entries(figure)) {\n switch (key) {\n case \"x\":\n case \"y\":\n if (value !== undefined) {\n this.history.update(\"figures\", sheetId, figure.id, key, Math.max(value, 0));\n }\n break;\n case \"width\":\n case \"height\":\n if (value !== undefined) {\n this.history.update(\"figures\", sheetId, figure.id, key, value);\n }\n break;\n }\n }\n }\n addFigure(figure, sheetId) {\n this.history.update(\"figures\", sheetId, figure.id, figure);\n }\n deleteSheet(sheetId) {\n this.history.update(\"figures\", sheetId, undefined);\n }\n removeFigure(id, sheetId) {\n this.history.update(\"figures\", sheetId, id, undefined);\n }\n checkFigureExists(sheetId, figureId) {\n var _a;\n if (((_a = this.figures[sheetId]) === null || _a === void 0 ? void 0 : _a[figureId]) === undefined) {\n return 70 /* CommandResult.FigureDoesNotExist */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkFigureDuplicate(figureId) {\n if (Object.values(this.figures).find((sheet) => sheet === null || sheet === void 0 ? void 0 : sheet[figureId])) {\n return 82 /* CommandResult.DuplicatedFigureId */;\n }\n return 0 /* CommandResult.Success */;\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getFigures(sheetId) {\n return Object.values(this.figures[sheetId] || {}).filter(isDefined$1);\n }\n getFigure(sheetId, figureId) {\n var _a;\n return (_a = this.figures[sheetId]) === null || _a === void 0 ? void 0 : _a[figureId];\n }\n getFigureSheetId(figureId) {\n return Object.keys(this.figures).find((sheetId) => { var _a; return ((_a = this.figures[sheetId]) === null || _a === void 0 ? void 0 : _a[figureId]) !== undefined; });\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) {\n for (let sheet of data.sheets) {\n const figures = {};\n sheet.figures.forEach((figure) => {\n figures[figure.id] = figure;\n });\n this.figures[sheet.id] = figures;\n }\n }\n export(data) {\n for (const sheet of data.sheets) {\n for (const figure of this.getFigures(sheet.id)) {\n const data = undefined;\n sheet.figures.push({ ...figure, data });\n }\n }\n }\n exportForExcel(data) {\n this.export(data);\n }\n }\n FigurePlugin.getters = [\"getFigures\", \"getFigure\", \"getFigureSheetId\"];\n\n class FilterTable {\n constructor(zone) {\n this.filters = [];\n this.zone = zone;\n const uuid = new UuidGenerator();\n this.id = uuid.uuidv4();\n for (const i of range(zone.left, zone.right + 1)) {\n const filterZone = { ...this.zone, left: i, right: i };\n this.filters.push(new Filter(uuid.uuidv4(), filterZone));\n }\n }\n /** Get zone of the table without the headers */\n get contentZone() {\n if (this.zone.bottom === this.zone.top) {\n return undefined;\n }\n return { ...this.zone, top: this.zone.top + 1 };\n }\n getFilterId(col) {\n var _a;\n return (_a = this.filters.find((filter) => filter.col === col)) === null || _a === void 0 ? void 0 : _a.id;\n }\n clone() {\n return new FilterTable(this.zone);\n }\n }\n class Filter {\n constructor(id, zone) {\n if (zone.left !== zone.right) {\n throw new Error(\"Can only define a filter on a single column\");\n }\n this.id = id;\n this.zoneWithHeaders = zone;\n }\n get col() {\n return this.zoneWithHeaders.left;\n }\n /** Filtered zone, ie. zone of the filter without the header */\n get filteredZone() {\n const zone = this.zoneWithHeaders;\n if (zone.bottom === zone.top) {\n return undefined;\n }\n return { ...zone, top: zone.top + 1 };\n }\n }\n\n class FiltersPlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.tables = {};\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"CREATE_FILTER_TABLE\":\n if (!areZonesContinuous(...cmd.target)) {\n return 81 /* CommandResult.NonContinuousTargets */;\n }\n const zone = union(...cmd.target);\n const checkFilterOverlap = () => {\n if (this.getFilterTables(cmd.sheetId).some((filter) => overlap(filter.zone, zone))) {\n return 78 /* CommandResult.FilterOverlap */;\n }\n return 0 /* CommandResult.Success */;\n };\n const checkMergeInFilter = () => {\n const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, zone);\n for (let merge of mergesInTarget) {\n if (overlap(zone, merge)) {\n return 80 /* CommandResult.MergeInFilter */;\n }\n }\n return 0 /* CommandResult.Success */;\n };\n return this.checkValidations(cmd, checkFilterOverlap, checkMergeInFilter);\n case \"ADD_MERGE\":\n for (let merge of cmd.target) {\n for (let filterTable of this.getFilterTables(cmd.sheetId)) {\n if (overlap(filterTable.zone, merge)) {\n return 80 /* CommandResult.MergeInFilter */;\n }\n }\n }\n break;\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"CREATE_SHEET\":\n this.history.update(\"tables\", cmd.sheetId, {});\n break;\n case \"DELETE_SHEET\":\n const filterTables = { ...this.tables };\n delete filterTables[cmd.sheetId];\n this.history.update(\"tables\", filterTables);\n break;\n case \"DUPLICATE_SHEET\":\n this.history.update(\"tables\", cmd.sheetIdTo, deepCopy(this.tables[cmd.sheetId]));\n break;\n case \"ADD_COLUMNS_ROWS\":\n this.onAddColumnsRows(cmd);\n break;\n case \"REMOVE_COLUMNS_ROWS\":\n this.onDeleteColumnsRows(cmd);\n break;\n case \"CREATE_FILTER_TABLE\": {\n const zone = union(...cmd.target);\n const newFilterTable = this.createFilterTable(zone);\n this.history.update(\"tables\", cmd.sheetId, newFilterTable.id, newFilterTable);\n break;\n }\n case \"REMOVE_FILTER_TABLE\": {\n const tables = {};\n for (const filterTable of this.getFilterTables(cmd.sheetId)) {\n if (cmd.target.every((zone) => !intersection(zone, filterTable.zone))) {\n tables[filterTable.id] = filterTable;\n }\n }\n this.history.update(\"tables\", cmd.sheetId, tables);\n break;\n }\n case \"UPDATE_CELL\": {\n const sheetId = cmd.sheetId;\n for (let table of this.getFilterTables(sheetId)) {\n if (this.canUpdateCellCmdExtendTable(cmd, table)) {\n this.extendTableDown(sheetId, table);\n }\n }\n break;\n }\n }\n }\n getFilters(sheetId) {\n return this.getFilterTables(sheetId)\n .map((filterTable) => filterTable.filters)\n .flat();\n }\n getFilterTables(sheetId) {\n return this.tables[sheetId] ? Object.values(this.tables[sheetId]).filter(isDefined$1) : [];\n }\n getFilter(position) {\n var _a;\n return (_a = this.getFilterTable(position)) === null || _a === void 0 ? void 0 : _a.filters.find((filter) => filter.col === position.col);\n }\n getFilterId(position) {\n var _a;\n return (_a = this.getFilter(position)) === null || _a === void 0 ? void 0 : _a.id;\n }\n getFilterTable({ sheetId, col, row }) {\n return this.getFilterTables(sheetId).find((filterTable) => isInside(col, row, filterTable.zone));\n }\n /** Get the filter tables that are fully inside the given zone */\n getFilterTablesInZone(sheetId, zone) {\n return this.getFilterTables(sheetId).filter((filterTable) => isZoneInside(filterTable.zone, zone));\n }\n doesZonesContainFilter(sheetId, zones) {\n for (const zone of zones) {\n for (const filterTable of this.getFilterTables(sheetId)) {\n if (intersection(zone, filterTable.zone)) {\n return true;\n }\n }\n }\n return false;\n }\n onAddColumnsRows(cmd) {\n for (const filterTable of this.getFilterTables(cmd.sheetId)) {\n const zone = expandZoneOnInsertion(filterTable.zone, cmd.dimension === \"COL\" ? \"left\" : \"top\", cmd.base, cmd.position, cmd.quantity);\n const filters = [];\n for (const filter of filterTable.filters) {\n const filterZone = expandZoneOnInsertion(filter.zoneWithHeaders, cmd.dimension === \"COL\" ? \"left\" : \"top\", cmd.base, cmd.position, cmd.quantity);\n filters.push(new Filter(filter.id, filterZone));\n }\n // Add filters for new columns\n if (filters.length < zoneToDimension(zone).width) {\n for (let col = zone.left; col <= zone.right; col++) {\n if (!filters.find((filter) => filter.col === col)) {\n filters.push(new Filter(this.uuidGenerator.uuidv4(), { ...zone, left: col, right: col }));\n }\n }\n filters.sort((f1, f2) => f1.col - f2.col);\n }\n this.history.update(\"tables\", cmd.sheetId, filterTable.id, \"zone\", zone);\n this.history.update(\"tables\", cmd.sheetId, filterTable.id, \"filters\", filters);\n }\n }\n onDeleteColumnsRows(cmd) {\n for (const table of this.getFilterTables(cmd.sheetId)) {\n const zone = reduceZoneOnDeletion(table.zone, cmd.dimension === \"COL\" ? \"left\" : \"top\", cmd.elements);\n if (!zone) {\n const tables = { ...this.tables[cmd.sheetId] };\n delete tables[table.id];\n this.history.update(\"tables\", cmd.sheetId, tables);\n }\n else {\n if (zoneToXc(zone) !== zoneToXc(table.zone)) {\n const filters = [];\n for (const filter of table.filters) {\n const newFilterZone = reduceZoneOnDeletion(filter.zoneWithHeaders, cmd.dimension === \"COL\" ? \"left\" : \"top\", cmd.elements);\n if (newFilterZone) {\n filters.push(new Filter(filter.id, newFilterZone));\n }\n }\n this.history.update(\"tables\", cmd.sheetId, table.id, \"zone\", zone);\n this.history.update(\"tables\", cmd.sheetId, table.id, \"filters\", filters);\n }\n }\n }\n }\n createFilterTable(zone) {\n return new FilterTable(zone);\n }\n /** Extend a table down one row */\n extendTableDown(sheetId, table) {\n const newZone = { ...table.zone, bottom: table.zone.bottom + 1 };\n this.history.update(\"tables\", sheetId, table.id, \"zone\", newZone);\n for (let filterIndex = 0; filterIndex < table.filters.length; filterIndex++) {\n const filter = table.filters[filterIndex];\n const newFilterZone = {\n ...filter.zoneWithHeaders,\n bottom: filter.zoneWithHeaders.bottom + 1,\n };\n this.history.update(\"tables\", sheetId, table.id, \"filters\", filterIndex, \"zoneWithHeaders\", newFilterZone);\n }\n return;\n }\n /**\n * Check if an UpdateCell command should cause the given table to be extended by one row.\n *\n * The table should be extended if all of these conditions are true:\n * 1) The updated cell is right below the table\n * 2) The command adds a content to the cell\n * 3) No cell right below the table had any content before the command\n * 4) Extending the table down would not overlap with another filter\n * 5) Extending the table down would not overlap with a merge\n *\n */\n canUpdateCellCmdExtendTable({ content: newCellContent, sheetId, col, row }, table) {\n var _a;\n if (!newCellContent) {\n return;\n }\n const zone = table.zone;\n if (!(zone.bottom + 1 === row && col >= zone.left && col <= zone.right)) {\n return false;\n }\n for (const col of range(zone.left, zone.right + 1)) {\n const position = { sheetId, col, row };\n // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content\n const cellContent = (_a = this.getters.getCell(position)) === null || _a === void 0 ? void 0 : _a.content;\n if (cellContent) {\n return false;\n }\n if (this.getters.getFilter(position)) {\n return false;\n }\n if (this.getters.isInMerge(position)) {\n return false;\n }\n }\n return true;\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) {\n for (const sheet of data.sheets) {\n for (const filterTableData of sheet.filterTables || []) {\n const table = this.createFilterTable(toZone(filterTableData.range));\n this.history.update(\"tables\", sheet.id, table.id, table);\n }\n }\n }\n export(data) {\n for (const sheet of data.sheets) {\n for (const filterTable of this.getFilterTables(sheet.id)) {\n sheet.filterTables.push({\n range: zoneToXc(filterTable.zone),\n });\n }\n }\n }\n exportForExcel(data) {\n this.export(data);\n }\n }\n FiltersPlugin.getters = [\n \"doesZonesContainFilter\",\n \"getFilter\",\n \"getFilters\",\n \"getFilterTable\",\n \"getFilterTables\",\n \"getFilterTablesInZone\",\n \"getFilterId\",\n ];\n\n class HeaderSizePlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.sizes = {};\n }\n handle(cmd) {\n var _a, _b, _c;\n switch (cmd.type) {\n case \"CREATE_SHEET\": {\n const computedSizes = this.computeSheetSizes(cmd.sheetId);\n const sizes = {\n COL: computedSizes.COL.map((size) => ({\n manualSize: undefined,\n computedSize: lazy(size),\n })),\n ROW: computedSizes.ROW.map((size) => ({\n manualSize: undefined,\n computedSize: lazy(size),\n })),\n };\n this.history.update(\"sizes\", cmd.sheetId, sizes);\n break;\n }\n case \"DUPLICATE_SHEET\":\n // make sure the values are computed in case the original sheet is deleted\n for (const row of this.sizes[cmd.sheetId].ROW) {\n row.computedSize();\n }\n for (const col of this.sizes[cmd.sheetId].COL) {\n col.computedSize();\n }\n this.history.update(\"sizes\", cmd.sheetIdTo, deepCopy(this.sizes[cmd.sheetId]));\n break;\n case \"DELETE_SHEET\":\n const sizes = { ...this.sizes };\n delete sizes[cmd.sheetId];\n this.history.update(\"sizes\", sizes);\n break;\n case \"REMOVE_COLUMNS_ROWS\": {\n let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]];\n for (let headerIndex of [...cmd.elements].sort((a, b) => b - a)) {\n sizes.splice(headerIndex, 1);\n }\n const min = Math.min(...cmd.elements);\n sizes = sizes.map((size, row) => {\n if (cmd.dimension === \"ROW\" && row >= min) {\n // invalidate sizes\n return {\n manualSize: size.manualSize,\n computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),\n };\n }\n return size;\n });\n this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, sizes);\n break;\n }\n case \"ADD_COLUMNS_ROWS\": {\n let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]];\n const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);\n const baseSize = sizes[cmd.base];\n sizes.splice(addIndex, 0, ...Array(cmd.quantity).fill(baseSize));\n sizes = sizes.map((size, row) => {\n if (cmd.dimension === \"ROW\" && row > cmd.base + cmd.quantity) {\n // invalidate sizes\n return {\n manualSize: size.manualSize,\n computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),\n };\n }\n return size;\n });\n this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, sizes);\n break;\n }\n case \"RESIZE_COLUMNS_ROWS\":\n for (let el of cmd.elements) {\n if (cmd.dimension === \"ROW\") {\n const height = this.getRowTallestCellSize(cmd.sheetId, el);\n const size = height;\n this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, el, {\n manualSize: cmd.size || undefined,\n computedSize: lazy(size),\n });\n }\n else {\n this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, el, {\n manualSize: cmd.size || undefined,\n computedSize: lazy(cmd.size || DEFAULT_CELL_WIDTH),\n });\n }\n }\n break;\n case \"UPDATE_CELL\":\n if (!((_c = (_b = (_a = this.sizes[cmd.sheetId]) === null || _a === void 0 ? void 0 : _a[\"ROW\"]) === null || _b === void 0 ? void 0 : _b[cmd.row]) === null || _c === void 0 ? void 0 : _c.manualSize)) {\n const { sheetId, row } = cmd;\n this.history.update(\"sizes\", sheetId, \"ROW\", row, \"computedSize\", lazy(() => this.getRowTallestCellSize(sheetId, row)));\n }\n break;\n case \"ADD_MERGE\":\n case \"REMOVE_MERGE\":\n for (let target of cmd.target) {\n for (let row of range(target.top, target.bottom + 1)) {\n const rowHeight = this.getRowTallestCellSize(cmd.sheetId, row);\n if (rowHeight !== this.getRowSize(cmd.sheetId, row)) {\n this.history.update(\"sizes\", cmd.sheetId, \"ROW\", row, \"computedSize\", lazy(rowHeight));\n }\n }\n }\n break;\n }\n return;\n }\n getColSize(sheetId, index) {\n return this.getHeaderSize(sheetId, \"COL\", index);\n }\n getRowSize(sheetId, index) {\n return this.getHeaderSize(sheetId, \"ROW\", index);\n }\n getHeaderSize(sheetId, dimension, index) {\n var _a, _b, _c, _d;\n return Math.round(((_b = (_a = this.sizes[sheetId]) === null || _a === void 0 ? void 0 : _a[dimension][index]) === null || _b === void 0 ? void 0 : _b.manualSize) ||\n ((_d = (_c = this.sizes[sheetId]) === null || _c === void 0 ? void 0 : _c[dimension][index]) === null || _d === void 0 ? void 0 : _d.computedSize()) ||\n this.getDefaultHeaderSize(dimension));\n }\n computeSheetSizes(sheetId) {\n var _a, _b;\n const sizes = { COL: [], ROW: [] };\n for (let col of range(0, this.getters.getNumberCols(sheetId))) {\n sizes.COL.push(this.getHeaderSize(sheetId, \"COL\", col));\n }\n for (let row of range(0, this.getters.getNumberRows(sheetId))) {\n let rowSize = (_b = (_a = this.sizes[sheetId]) === null || _a === void 0 ? void 0 : _a[\"ROW\"]) === null || _b === void 0 ? void 0 : _b[row].manualSize;\n if (!rowSize) {\n const height = this.getRowTallestCellSize(sheetId, row);\n rowSize = height;\n }\n sizes.ROW.push(rowSize);\n }\n return sizes;\n }\n getDefaultHeaderSize(dimension) {\n return dimension === \"COL\" ? DEFAULT_CELL_WIDTH : DEFAULT_CELL_HEIGHT;\n }\n /**\n * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row\n * merge, or the height of the cell computed based on its font size.\n */\n getCellHeight(position) {\n const merge = this.getters.getMerge(position);\n if (merge && merge.bottom !== merge.top) {\n return DEFAULT_CELL_HEIGHT;\n }\n const cell = this.getters.getCell(position);\n return getDefaultCellHeight(cell === null || cell === void 0 ? void 0 : cell.style);\n }\n /**\n * Get the tallest cell of a row and its size.\n *\n * The tallest cell of the row correspond to the cell with the biggest font size,\n * and that is not part of a multi-line merge.\n */\n getRowTallestCellSize(sheetId, row) {\n const cellIds = this.getters.getRowCells(sheetId, row);\n let maxHeight = 0;\n for (let i = 0; i < cellIds.length; i++) {\n const cell = this.getters.getCellById(cellIds[i]);\n if (!cell)\n continue;\n const position = this.getters.getCellPosition(cell.id);\n const cellHeight = this.getCellHeight(position);\n if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {\n maxHeight = cellHeight;\n }\n }\n if (maxHeight <= DEFAULT_CELL_HEIGHT) {\n return DEFAULT_CELL_HEIGHT;\n }\n return maxHeight;\n }\n import(data) {\n for (let sheet of data.sheets) {\n const manualSizes = { COL: [], ROW: [] };\n for (let [rowIndex, row] of Object.entries(sheet.rows)) {\n if (row.size) {\n manualSizes[\"ROW\"][rowIndex] = row.size;\n }\n }\n for (let [colIndex, col] of Object.entries(sheet.cols)) {\n if (col.size) {\n manualSizes[\"COL\"][colIndex] = col.size;\n }\n }\n const computedSizes = this.computeSheetSizes(sheet.id);\n this.sizes[sheet.id] = {\n COL: computedSizes.COL.map((size, i) => ({\n manualSize: manualSizes.COL[i],\n computedSize: lazy(size),\n })),\n ROW: computedSizes.ROW.map((size, i) => ({\n manualSize: manualSizes.ROW[i],\n computedSize: lazy(size),\n })),\n };\n }\n return;\n }\n exportForExcel(data) {\n this.exportData(data, true);\n }\n export(data) {\n this.exportData(data);\n }\n /**\n * Export the header sizes\n *\n * @param exportDefaults : if true, export column/row sizes even if they have the default size\n */\n exportData(data, exportDefaults = false) {\n var _a, _b;\n for (let sheet of data.sheets) {\n // Export row sizes\n if (sheet.rows === undefined) {\n sheet.rows = {};\n }\n for (let row of range(0, this.getters.getNumberRows(sheet.id))) {\n if (exportDefaults || ((_a = this.sizes[sheet.id][\"ROW\"][row]) === null || _a === void 0 ? void 0 : _a.manualSize)) {\n sheet.rows[row] = { ...sheet.rows[row], size: this.getRowSize(sheet.id, row) };\n }\n }\n // Export col sizes\n if (sheet.cols === undefined) {\n sheet.cols = {};\n }\n for (let col of range(0, this.getters.getNumberCols(sheet.id))) {\n if (exportDefaults || ((_b = this.sizes[sheet.id][\"COL\"][col]) === null || _b === void 0 ? void 0 : _b.manualSize)) {\n sheet.cols[col] = { ...sheet.cols[col], size: this.getColSize(sheet.id, col) };\n }\n }\n }\n }\n }\n HeaderSizePlugin.getters = [\"getRowSize\", \"getColSize\"];\n\n class HeaderVisibilityPlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.hiddenHeaders = {};\n }\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"HIDE_COLUMNS_ROWS\": {\n if (!this.hiddenHeaders[cmd.sheetId]) {\n return 27 /* CommandResult.InvalidSheetId */;\n }\n const hiddenGroup = cmd.dimension === \"COL\"\n ? this.getHiddenColsGroups(cmd.sheetId)\n : this.getHiddenRowsGroups(cmd.sheetId);\n const elements = cmd.dimension === \"COL\"\n ? this.getters.getNumberCols(cmd.sheetId)\n : this.getters.getNumberRows(cmd.sheetId);\n return (hiddenGroup || []).flat().concat(cmd.elements).length < elements\n ? 0 /* CommandResult.Success */\n : 66 /* CommandResult.TooManyHiddenElements */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"CREATE_SHEET\":\n const hiddenHeaders = {\n COL: Array(this.getters.getNumberCols(cmd.sheetId)).fill(false),\n ROW: Array(this.getters.getNumberRows(cmd.sheetId)).fill(false),\n };\n this.history.update(\"hiddenHeaders\", cmd.sheetId, hiddenHeaders);\n break;\n case \"DUPLICATE_SHEET\":\n this.history.update(\"hiddenHeaders\", cmd.sheetIdTo, deepCopy(this.hiddenHeaders[cmd.sheetId]));\n break;\n case \"DELETE_SHEET\":\n this.history.update(\"hiddenHeaders\", cmd.sheetId, undefined);\n break;\n case \"REMOVE_COLUMNS_ROWS\": {\n const hiddenHeaders = [...this.hiddenHeaders[cmd.sheetId][cmd.dimension]];\n for (let el of [...cmd.elements].sort((a, b) => b - a)) {\n hiddenHeaders.splice(el, 1);\n }\n this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, hiddenHeaders);\n break;\n }\n case \"ADD_COLUMNS_ROWS\": {\n const hiddenHeaders = [...this.hiddenHeaders[cmd.sheetId][cmd.dimension]];\n const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);\n hiddenHeaders.splice(addIndex, 0, ...Array(cmd.quantity).fill(false));\n this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, hiddenHeaders);\n break;\n }\n case \"HIDE_COLUMNS_ROWS\":\n for (let el of cmd.elements) {\n this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, el, true);\n }\n break;\n case \"UNHIDE_COLUMNS_ROWS\":\n for (let el of cmd.elements) {\n this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, el, false);\n }\n break;\n }\n return;\n }\n isRowHiddenByUser(sheetId, index) {\n return this.hiddenHeaders[sheetId].ROW[index];\n }\n isColHiddenByUser(sheetId, index) {\n return this.hiddenHeaders[sheetId].COL[index];\n }\n getHiddenColsGroups(sheetId) {\n const consecutiveIndexes = [[]];\n const hiddenCols = this.hiddenHeaders[sheetId].COL;\n for (let col = 0; col < hiddenCols.length; col++) {\n const isColHidden = hiddenCols[col];\n if (isColHidden) {\n consecutiveIndexes[consecutiveIndexes.length - 1].push(col);\n }\n else {\n if (consecutiveIndexes[consecutiveIndexes.length - 1].length !== 0) {\n consecutiveIndexes.push([]);\n }\n }\n }\n if (consecutiveIndexes[consecutiveIndexes.length - 1].length === 0) {\n consecutiveIndexes.pop();\n }\n return consecutiveIndexes;\n }\n getHiddenRowsGroups(sheetId) {\n const consecutiveIndexes = [[]];\n const hiddenCols = this.hiddenHeaders[sheetId].ROW;\n for (let row = 0; row < hiddenCols.length; row++) {\n const isRowHidden = hiddenCols[row];\n if (isRowHidden) {\n consecutiveIndexes[consecutiveIndexes.length - 1].push(row);\n }\n else {\n if (consecutiveIndexes[consecutiveIndexes.length - 1].length !== 0) {\n consecutiveIndexes.push([]);\n }\n }\n }\n if (consecutiveIndexes[consecutiveIndexes.length - 1].length === 0) {\n consecutiveIndexes.pop();\n }\n return consecutiveIndexes;\n }\n import(data) {\n var _a, _b;\n for (let sheet of data.sheets) {\n this.hiddenHeaders[sheet.id] = { COL: [], ROW: [] };\n for (let row = 0; row < sheet.rowNumber; row++) {\n this.hiddenHeaders[sheet.id].ROW[row] = Boolean((_a = sheet.rows[row]) === null || _a === void 0 ? void 0 : _a.isHidden);\n }\n for (let col = 0; col < sheet.colNumber; col++) {\n this.hiddenHeaders[sheet.id].COL[col] = Boolean((_b = sheet.cols[col]) === null || _b === void 0 ? void 0 : _b.isHidden);\n }\n }\n return;\n }\n exportForExcel(data) {\n this.exportData(data, true);\n }\n export(data) {\n this.exportData(data);\n }\n exportData(data, exportDefaults = false) {\n for (let sheet of data.sheets) {\n if (sheet.rows === undefined) {\n sheet.rows = {};\n }\n for (let row = 0; row < this.getters.getNumberRows(sheet.id); row++) {\n if (exportDefaults || this.hiddenHeaders[sheet.id][\"ROW\"][row]) {\n if (sheet.rows[row] === undefined) {\n sheet.rows[row] = {};\n }\n sheet.rows[row].isHidden = this.hiddenHeaders[sheet.id][\"ROW\"][row];\n }\n }\n if (sheet.cols === undefined) {\n sheet.cols = {};\n }\n for (let col = 0; col < this.getters.getNumberCols(sheet.id); col++) {\n if (exportDefaults || this.hiddenHeaders[sheet.id][\"COL\"][col]) {\n if (sheet.cols[col] === undefined) {\n sheet.cols[col] = {};\n }\n sheet.cols[col].isHidden = this.hiddenHeaders[sheet.id][\"COL\"][col];\n }\n }\n }\n }\n }\n HeaderVisibilityPlugin.getters = [\n \"getHiddenColsGroups\",\n \"getHiddenRowsGroups\",\n \"isRowHiddenByUser\",\n \"isColHiddenByUser\",\n ];\n\n class ImagePlugin extends CorePlugin {\n constructor(config) {\n super(config);\n this.images = {};\n /**\n * paths of images synced with the file store server.\n */\n this.syncedImages = new Set();\n this.fileStore = config.external.fileStore;\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"CREATE_IMAGE\":\n if (this.getters.getFigure(cmd.sheetId, cmd.figureId)) {\n return 28 /* CommandResult.InvalidFigureId */;\n }\n return 0 /* CommandResult.Success */;\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"CREATE_IMAGE\":\n this.addFigure(cmd.figureId, cmd.sheetId, cmd.position, cmd.size);\n this.history.update(\"images\", cmd.sheetId, cmd.figureId, cmd.definition);\n this.syncedImages.add(cmd.definition.path);\n break;\n case \"DUPLICATE_SHEET\": {\n const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId);\n for (const fig of sheetFiguresFrom) {\n if (fig.tag === \"image\") {\n const figureIdBase = fig.id.split(FIGURE_ID_SPLITTER).pop();\n const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;\n const image = this.getImage(fig.id);\n if (image) {\n const size = { width: fig.width, height: fig.height };\n this.dispatch(\"CREATE_IMAGE\", {\n sheetId: cmd.sheetIdTo,\n figureId: duplicatedFigureId,\n position: { x: fig.x, y: fig.y },\n size,\n definition: deepCopy(image),\n });\n }\n }\n }\n break;\n }\n case \"DELETE_FIGURE\":\n this.history.update(\"images\", cmd.sheetId, cmd.id, undefined);\n break;\n case \"DELETE_SHEET\":\n this.history.update(\"images\", cmd.sheetId, undefined);\n break;\n }\n }\n /**\n * Delete unused images from the file store\n */\n garbageCollectExternalResources() {\n var _a;\n const images = new Set(this.getAllImages().map((image) => image.path));\n for (const path of this.syncedImages) {\n if (!images.has(path)) {\n (_a = this.fileStore) === null || _a === void 0 ? void 0 : _a.delete(path);\n }\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getImage(figureId) {\n for (const sheet of Object.values(this.images)) {\n if (sheet && sheet[figureId]) {\n return sheet[figureId];\n }\n }\n throw new Error(`There is no image with the given figureId: ${figureId}`);\n }\n getImagePath(figureId) {\n return this.getImage(figureId).path;\n }\n getImageSize(figureId) {\n return this.getImage(figureId).size;\n }\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n addFigure(id, sheetId, position, size) {\n const figure = {\n id,\n x: position.x,\n y: position.y,\n width: size.width,\n height: size.height,\n tag: \"image\",\n };\n this.dispatch(\"CREATE_FIGURE\", { sheetId, figure });\n }\n import(data) {\n for (const sheet of data.sheets) {\n const images = (sheet.figures || []).filter((figure) => figure.tag === \"image\");\n for (const image of images) {\n this.history.update(\"images\", sheet.id, image.id, image.data);\n this.syncedImages.add(image.data.path);\n }\n }\n }\n export(data) {\n var _a;\n for (const sheet of data.sheets) {\n const images = sheet.figures.filter((figure) => figure.tag === \"image\");\n for (const image of images) {\n image.data = (_a = this.images[sheet.id]) === null || _a === void 0 ? void 0 : _a[image.id];\n }\n }\n }\n getAllImages() {\n const images = [];\n for (const sheetId in this.images) {\n images.push(...Object.values(this.images[sheetId] || {}).filter(isDefined$1));\n }\n return images;\n }\n }\n ImagePlugin.getters = [\"getImage\", \"getImagePath\", \"getImageSize\"];\n\n class MergePlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.nextId = 1;\n this.merges = {};\n this.mergeCellMap = {};\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n const force = \"force\" in cmd ? !!cmd.force : false;\n switch (cmd.type) {\n case \"ADD_MERGE\":\n if (force) {\n return this.checkValidations(cmd, this.checkFrozenPanes);\n }\n return this.checkValidations(cmd, this.checkDestructiveMerge, this.checkOverlap, this.checkFrozenPanes);\n case \"UPDATE_CELL\":\n return this.checkMergedContentUpdate(cmd);\n case \"REMOVE_MERGE\":\n return this.checkMergeExists(cmd);\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"CREATE_SHEET\":\n this.history.update(\"merges\", cmd.sheetId, {});\n this.history.update(\"mergeCellMap\", cmd.sheetId, {});\n break;\n case \"DELETE_SHEET\":\n this.history.update(\"merges\", cmd.sheetId, {});\n this.history.update(\"mergeCellMap\", cmd.sheetId, {});\n break;\n case \"DUPLICATE_SHEET\":\n const merges = this.merges[cmd.sheetId];\n if (!merges)\n break;\n for (const range of Object.values(merges).filter(isDefined$1)) {\n this.addMerge(cmd.sheetIdTo, range.zone);\n }\n break;\n case \"ADD_MERGE\":\n for (const zone of cmd.target) {\n this.addMerge(cmd.sheetId, zone);\n }\n break;\n case \"REMOVE_MERGE\":\n for (const zone of cmd.target) {\n this.removeMerge(cmd.sheetId, zone);\n }\n break;\n }\n }\n adaptRanges(applyChange, sheetId) {\n const sheetIds = sheetId ? [sheetId] : Object.keys(this.merges);\n for (const sheetId of sheetIds) {\n this.applyRangeChangeOnSheet(sheetId, applyChange);\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getMerges(sheetId) {\n return Object.keys(this.merges[sheetId] || {})\n .map((mergeId) => this.getMergeById(sheetId, parseInt(mergeId, 10)))\n .filter(isDefined$1);\n }\n getMerge({ sheetId, col, row }) {\n var _a;\n const sheetMap = this.mergeCellMap[sheetId];\n const mergeId = sheetMap ? col in sheetMap && ((_a = sheetMap[col]) === null || _a === void 0 ? void 0 : _a[row]) : undefined;\n return mergeId ? this.getMergeById(sheetId, mergeId) : undefined;\n }\n getMergesInZone(sheetId, zone) {\n var _a;\n const sheetMap = this.mergeCellMap[sheetId];\n if (!sheetMap)\n return [];\n const mergeIds = new Set();\n for (const { col, row } of positions(zone)) {\n const mergeId = (_a = sheetMap[col]) === null || _a === void 0 ? void 0 : _a[row];\n if (mergeId) {\n mergeIds.add(mergeId);\n }\n }\n return Array.from(mergeIds)\n .map((mergeId) => this.getMergeById(sheetId, mergeId))\n .filter(isDefined$1);\n }\n /**\n * Return true if the zone intersects an existing merge:\n * if they have at least a common cell\n */\n doesIntersectMerge(sheetId, zone) {\n return positions(zone).some(({ col, row }) => this.getMerge({ sheetId, col, row }) !== undefined);\n }\n /**\n * Returns true if two columns have at least one merge in common\n */\n doesColumnsHaveCommonMerges(sheetId, colA, colB) {\n const sheet = this.getters.getSheet(sheetId);\n for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {\n if (this.isInSameMerge(sheet.id, colA, row, colB, row)) {\n return true;\n }\n }\n return false;\n }\n /**\n * Returns true if two rows have at least one merge in common\n */\n doesRowsHaveCommonMerges(sheetId, rowA, rowB) {\n const sheet = this.getters.getSheet(sheetId);\n for (let col = 0; col <= this.getters.getNumberCols(sheetId); col++) {\n if (this.isInSameMerge(sheet.id, col, rowA, col, rowB)) {\n return true;\n }\n }\n return false;\n }\n /**\n * Add all necessary merge to the current selection to make it valid\n */\n expandZone(sheetId, zone) {\n let { left, right, top, bottom } = zone;\n let result = { left, right, top, bottom };\n for (let id in this.merges[sheetId]) {\n const merge = this.getMergeById(sheetId, parseInt(id));\n if (merge && overlap(merge, result)) {\n result = union(merge, result);\n }\n }\n return isEqual(result, zone) ? result : this.expandZone(sheetId, result);\n }\n isInSameMerge(sheetId, colA, rowA, colB, rowB) {\n const mergeA = this.getMerge({ sheetId, col: colA, row: rowA });\n const mergeB = this.getMerge({ sheetId, col: colB, row: rowB });\n if (!mergeA || !mergeB) {\n return false;\n }\n return isEqual(mergeA, mergeB);\n }\n isInMerge({ sheetId, col, row }) {\n var _a;\n const sheetMap = this.mergeCellMap[sheetId];\n return sheetMap ? col in sheetMap && Boolean((_a = sheetMap[col]) === null || _a === void 0 ? void 0 : _a[row]) : false;\n }\n getMainCellPosition(position) {\n if (!this.isInMerge(position)) {\n return position;\n }\n const mergeTopLeftPos = this.getMerge(position).topLeft;\n return { sheetId: position.sheetId, col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };\n }\n getBottomLeftCell(position) {\n if (!this.isInMerge(position)) {\n return position;\n }\n const { bottom, left } = this.getMerge(position);\n return { sheetId: position.sheetId, col: left, row: bottom };\n }\n isMergeHidden(sheetId, merge) {\n const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);\n const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);\n for (let group of hiddenColsGroups) {\n if (merge.left >= group[0] && merge.right <= group[group.length - 1]) {\n return true;\n }\n }\n for (let group of hiddenRowsGroups) {\n if (merge.top >= group[0] && merge.bottom <= group[group.length - 1]) {\n return true;\n }\n }\n return false;\n }\n /**\n * Check if the zone represents a single cell or a single merge.\n */\n isSingleCellOrMerge(sheetId, zone) {\n const merge = this.getMerge({ sheetId, col: zone.left, row: zone.top });\n if (merge) {\n return isEqual(zone, merge);\n }\n const { width, height } = zoneToDimension(zone);\n return width === 1 && height === 1;\n }\n // ---------------------------------------------------------------------------\n // Merges\n // ---------------------------------------------------------------------------\n /**\n * Return true if the current selection requires losing state if it is merged.\n * This happens when there is some textual content in other cells than the\n * top left.\n */\n isMergeDestructive(sheetId, zone) {\n let { left, right, top, bottom } = zone;\n right = clip(right, 0, this.getters.getNumberCols(sheetId) - 1);\n bottom = clip(bottom, 0, this.getters.getNumberRows(sheetId) - 1);\n for (let row = top; row <= bottom; row++) {\n for (let col = left; col <= right; col++) {\n if (col !== left || row !== top) {\n const cell = this.getters.getCell({ sheetId, col, row });\n if (cell && cell.content !== \"\") {\n return true;\n }\n }\n }\n }\n return false;\n }\n getMergeById(sheetId, mergeId) {\n var _a;\n const range = (_a = this.merges[sheetId]) === null || _a === void 0 ? void 0 : _a[mergeId];\n return range !== undefined ? rangeToMerge(mergeId, range) : undefined;\n }\n checkDestructiveMerge({ sheetId, target }) {\n const sheet = this.getters.tryGetSheet(sheetId);\n if (!sheet)\n return 0 /* CommandResult.Success */;\n const isDestructive = target.some((zone) => this.isMergeDestructive(sheetId, zone));\n return isDestructive ? 3 /* CommandResult.MergeIsDestructive */ : 0 /* CommandResult.Success */;\n }\n checkOverlap({ target }) {\n for (const zone of target) {\n for (const zone2 of target) {\n if (zone !== zone2 && overlap(zone, zone2)) {\n return 65 /* CommandResult.MergeOverlap */;\n }\n }\n }\n return 0 /* CommandResult.Success */;\n }\n checkFrozenPanes({ sheetId, target }) {\n const sheet = this.getters.tryGetSheet(sheetId);\n if (!sheet)\n return 0 /* CommandResult.Success */;\n const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n for (const zone of target) {\n if ((zone.left < xSplit && zone.right >= xSplit) ||\n (zone.top < ySplit && zone.bottom >= ySplit)) {\n return 75 /* CommandResult.FrozenPaneOverlap */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n /**\n * The content of a merged cell should always be empty.\n * Except for the top-left cell.\n */\n checkMergedContentUpdate(cmd) {\n const { col, row, content } = cmd;\n if (content === undefined) {\n return 0 /* CommandResult.Success */;\n }\n const { col: mainCol, row: mainRow } = this.getMainCellPosition(cmd);\n if (mainCol === col && mainRow === row) {\n return 0 /* CommandResult.Success */;\n }\n return 4 /* CommandResult.CellIsMerged */;\n }\n checkMergeExists(cmd) {\n const { sheetId, target } = cmd;\n for (const zone of target) {\n const { left, top } = zone;\n const merge = this.getMerge({ sheetId, col: left, row: top });\n if (merge === undefined || !isEqual(zone, merge)) {\n return 5 /* CommandResult.InvalidTarget */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n /**\n * Merge the current selection. Note that:\n * - it assumes that we have a valid selection (no intersection with other\n * merges)\n * - it does nothing if the merge is trivial: A1:A1\n */\n addMerge(sheetId, zone) {\n let { left, right, top, bottom } = zone;\n right = clip(right, 0, this.getters.getNumberCols(sheetId) - 1);\n bottom = clip(bottom, 0, this.getters.getNumberRows(sheetId) - 1);\n const tl = toXC(left, top);\n const br = toXC(right, bottom);\n if (tl === br) {\n return;\n }\n const topLeft = this.getters.getCell({ sheetId, col: left, row: top });\n let id = this.nextId++;\n this.history.update(\"merges\", sheetId, id, this.getters.getRangeFromSheetXC(sheetId, zoneToXc({ left, top, right, bottom })));\n let previousMerges = new Set();\n for (let row = top; row <= bottom; row++) {\n for (let col = left; col <= right; col++) {\n if (col !== left || row !== top) {\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n style: topLeft ? topLeft.style : null,\n content: \"\",\n });\n }\n const merge = this.getMerge({ sheetId, col, row });\n if (merge) {\n previousMerges.add(merge.id);\n }\n this.history.update(\"mergeCellMap\", sheetId, col, row, id);\n }\n }\n for (let mergeId of previousMerges) {\n const { top, bottom, left, right } = this.getMergeById(sheetId, mergeId);\n for (let row = top; row <= bottom; row++) {\n for (let col = left; col <= right; col++) {\n const position = { sheetId, col, row };\n const merge = this.getMerge(position);\n if (!merge || merge.id !== id) {\n this.history.update(\"mergeCellMap\", sheetId, col, row, undefined);\n this.dispatch(\"CLEAR_CELL\", position);\n }\n }\n }\n this.history.update(\"merges\", sheetId, mergeId, undefined);\n }\n }\n removeMerge(sheetId, zone) {\n const { left, top, bottom, right } = zone;\n const merge = this.getMerge({ sheetId, col: left, row: top });\n if (merge === undefined || !isEqual(zone, merge)) {\n return;\n }\n this.history.update(\"merges\", sheetId, merge.id, undefined);\n for (let r = top; r <= bottom; r++) {\n for (let c = left; c <= right; c++) {\n this.history.update(\"mergeCellMap\", sheetId, c, r, undefined);\n }\n }\n }\n /**\n * Apply a range change on merges of a particular sheet.\n */\n applyRangeChangeOnSheet(sheetId, applyChange) {\n const merges = Object.entries(this.merges[sheetId] || {});\n for (const [mergeId, range] of merges) {\n if (range) {\n const currentZone = range.zone;\n const result = applyChange(range);\n switch (result.changeType) {\n case \"NONE\":\n break;\n case \"REMOVE\":\n this.removeMerge(sheetId, currentZone);\n break;\n default:\n const { width, height } = zoneToDimension(result.range.zone);\n if (width === 1 && height === 1) {\n this.removeMerge(sheetId, currentZone);\n }\n else {\n this.history.update(\"merges\", sheetId, parseInt(mergeId, 10), result.range);\n }\n break;\n }\n }\n }\n this.history.update(\"mergeCellMap\", sheetId, {});\n for (const merge of this.getMerges(sheetId)) {\n for (const { col, row } of positions(merge)) {\n this.history.update(\"mergeCellMap\", sheetId, col, row, merge.id);\n }\n }\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) {\n const sheets = data.sheets || [];\n for (let sheetData of sheets) {\n this.history.update(\"merges\", sheetData.id, {});\n this.history.update(\"mergeCellMap\", sheetData.id, {});\n if (sheetData.merges) {\n this.importMerges(sheetData.id, sheetData.merges);\n }\n }\n }\n importMerges(sheetId, merges) {\n for (let merge of merges) {\n this.addMerge(sheetId, toZone(merge));\n }\n }\n export(data) {\n for (let sheetData of data.sheets) {\n const merges = this.merges[sheetData.id];\n if (merges) {\n sheetData.merges.push(...exportMerges(merges));\n }\n }\n }\n exportForExcel(data) {\n this.export(data);\n }\n }\n MergePlugin.getters = [\n \"isInMerge\",\n \"isInSameMerge\",\n \"isMergeHidden\",\n \"getMainCellPosition\",\n \"getBottomLeftCell\",\n \"expandZone\",\n \"doesIntersectMerge\",\n \"doesColumnsHaveCommonMerges\",\n \"doesRowsHaveCommonMerges\",\n \"getMerges\",\n \"getMerge\",\n \"getMergesInZone\",\n \"isSingleCellOrMerge\",\n ];\n function exportMerges(merges) {\n return Object.entries(merges)\n .map(([mergeId, range]) => (range ? rangeToMerge(parseInt(mergeId, 10), range) : undefined))\n .filter(isDefined$1)\n .map((merge) => toXC(merge.left, merge.top) + \":\" + toXC(merge.right, merge.bottom));\n }\n function rangeToMerge(mergeId, range) {\n return {\n ...range.zone,\n topLeft: { col: range.zone.left, row: range.zone.top },\n id: mergeId,\n };\n }\n\n class RangeAdapter {\n constructor(getters) {\n this.providers = [];\n this.getters = getters;\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n if (cmd.type === \"MOVE_RANGES\") {\n return cmd.target.length === 1 ? 0 /* CommandResult.Success */ : 26 /* CommandResult.InvalidZones */;\n }\n return 0 /* CommandResult.Success */;\n }\n beforeHandle(command) { }\n handle(cmd) {\n switch (cmd.type) {\n case \"REMOVE_COLUMNS_ROWS\": {\n let start = cmd.dimension === \"COL\" ? \"left\" : \"top\";\n let end = cmd.dimension === \"COL\" ? \"right\" : \"bottom\";\n let dimension = cmd.dimension === \"COL\" ? \"columns\" : \"rows\";\n const elements = [...cmd.elements];\n elements.sort((a, b) => b - a);\n const groups = groupConsecutive(elements);\n this.executeOnAllRanges((range) => {\n if (range.sheetId !== cmd.sheetId) {\n return { changeType: \"NONE\" };\n }\n let newRange = range;\n let changeType = \"NONE\";\n for (let group of groups) {\n const min = Math.min(...group);\n const max = Math.max(...group);\n if (range.zone[start] <= min && min <= range.zone[end]) {\n const toRemove = Math.min(range.zone[end], max) - min + 1;\n changeType = \"RESIZE\";\n newRange = this.createAdaptedRange(newRange, dimension, changeType, -toRemove);\n }\n else if (range.zone[start] >= min && range.zone[end] <= max) {\n changeType = \"REMOVE\";\n newRange = range.clone({ ...this.getInvalidRange() });\n }\n else if (range.zone[start] <= max && range.zone[end] >= max) {\n const toRemove = max - range.zone[start] + 1;\n changeType = \"RESIZE\";\n newRange = this.createAdaptedRange(newRange, dimension, changeType, -toRemove);\n newRange = this.createAdaptedRange(newRange, dimension, \"MOVE\", -(range.zone[start] - min));\n }\n else if (min < range.zone[start]) {\n changeType = \"MOVE\";\n newRange = this.createAdaptedRange(newRange, dimension, changeType, -(max - min + 1));\n }\n }\n if (changeType !== \"NONE\") {\n return { changeType, range: newRange };\n }\n return { changeType: \"NONE\" };\n }, cmd.sheetId);\n break;\n }\n case \"ADD_COLUMNS_ROWS\": {\n let start = cmd.dimension === \"COL\" ? \"left\" : \"top\";\n let end = cmd.dimension === \"COL\" ? \"right\" : \"bottom\";\n let dimension = cmd.dimension === \"COL\" ? \"columns\" : \"rows\";\n this.executeOnAllRanges((range) => {\n if (range.sheetId !== cmd.sheetId) {\n return { changeType: \"NONE\" };\n }\n if (cmd.position === \"after\") {\n if (range.zone[start] <= cmd.base && cmd.base < range.zone[end]) {\n return {\n changeType: \"RESIZE\",\n range: this.createAdaptedRange(range, dimension, \"RESIZE\", cmd.quantity),\n };\n }\n if (cmd.base < range.zone[start]) {\n return {\n changeType: \"MOVE\",\n range: this.createAdaptedRange(range, dimension, \"MOVE\", cmd.quantity),\n };\n }\n }\n else {\n if (range.zone[start] < cmd.base && cmd.base <= range.zone[end]) {\n return {\n changeType: \"RESIZE\",\n range: this.createAdaptedRange(range, dimension, \"RESIZE\", cmd.quantity),\n };\n }\n if (cmd.base <= range.zone[start]) {\n return {\n changeType: \"MOVE\",\n range: this.createAdaptedRange(range, dimension, \"MOVE\", cmd.quantity),\n };\n }\n }\n return { changeType: \"NONE\" };\n }, cmd.sheetId);\n break;\n }\n case \"DELETE_SHEET\": {\n this.executeOnAllRanges((range) => {\n if (range.sheetId !== cmd.sheetId) {\n return { changeType: \"NONE\" };\n }\n const invalidSheetName = this.getters.getSheetName(cmd.sheetId);\n range = range.clone({\n ...this.getInvalidRange(),\n invalidSheetName,\n });\n return { changeType: \"REMOVE\", range };\n }, cmd.sheetId);\n break;\n }\n case \"RENAME_SHEET\": {\n this.executeOnAllRanges((range) => {\n if (range.sheetId === cmd.sheetId) {\n return { changeType: \"CHANGE\", range };\n }\n if (cmd.name && range.invalidSheetName === cmd.name) {\n const invalidSheetName = undefined;\n const sheetId = cmd.sheetId;\n const newRange = range.clone({ sheetId, invalidSheetName });\n return { changeType: \"CHANGE\", range: newRange };\n }\n return { changeType: \"NONE\" };\n });\n break;\n }\n case \"MOVE_RANGES\": {\n const originZone = cmd.target[0];\n this.executeOnAllRanges((range) => {\n if (range.sheetId !== cmd.sheetId || !isZoneInside(range.zone, originZone)) {\n return { changeType: \"NONE\" };\n }\n const targetSheetId = cmd.targetSheetId;\n const offsetX = cmd.col - originZone.left;\n const offsetY = cmd.row - originZone.top;\n const adaptedRange = this.createAdaptedRange(range, \"both\", \"MOVE\", [offsetX, offsetY]);\n const prefixSheet = cmd.sheetId === targetSheetId ? adaptedRange.prefixSheet : true;\n return {\n changeType: \"MOVE\",\n range: adaptedRange.clone({ sheetId: targetSheetId, prefixSheet }),\n };\n });\n break;\n }\n }\n }\n finalize() { }\n /**\n * Return a modified adapting function that verifies that after adapting a range, the range is still valid.\n * Any range that gets adapted by the function adaptRange in parameter does so\n * without caring if the start and end of the range in both row and column\n * direction can be incorrect. This function ensure that an incorrect range gets removed.\n */\n verifyRangeRemoved(adaptRange) {\n return (range) => {\n const result = adaptRange(range);\n if (result.changeType !== \"NONE\" && !isZoneValid(result.range.zone)) {\n return { range: result.range, changeType: \"REMOVE\" };\n }\n return result;\n };\n }\n createAdaptedRange(range, dimension, operation, by) {\n const zone = createAdaptedZone(range.unboundedZone, dimension, operation, by);\n const adaptedRange = range.clone({ zone });\n return adaptedRange;\n }\n executeOnAllRanges(adaptRange, sheetId) {\n const func = this.verifyRangeRemoved(adaptRange);\n for (const provider of this.providers) {\n provider(func, sheetId);\n }\n }\n /**\n * Stores the functions bound to each plugin to be able to iterate over all ranges of the application,\n * without knowing any details of the internal data structure of the plugins and without storing ranges\n * in the range adapter.\n *\n * @param provider a function bound to a plugin that will loop over its internal data structure to find\n * all ranges\n */\n addRangeProvider(provider) {\n this.providers.push(provider);\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n createAdaptedRanges(ranges, offsetX, offsetY, sheetId) {\n const rangesImpl = ranges.map((range) => RangeImpl.fromRange(range, this.getters));\n return rangesImpl.map((range) => {\n if (!isZoneValid(range.zone)) {\n return range;\n }\n const copySheetId = range.prefixSheet ? range.sheetId : sheetId;\n const unboundZone = {\n ...range.unboundedZone,\n // Don't shift left if the range is a full row without header\n left: range.isFullRow && !range.unboundedZone.hasHeader\n ? range.unboundedZone.left\n : range.unboundedZone.left + (range.parts[0].colFixed ? 0 : offsetX),\n // Don't shift right if the range is a full row\n right: range.isFullRow\n ? range.unboundedZone.right\n : range.unboundedZone.right +\n ((range.parts[1] || range.parts[0]).colFixed ? 0 : offsetX),\n // Don't shift up if the range is a column row without header\n top: range.isFullCol && !range.unboundedZone.hasHeader\n ? range.unboundedZone.top\n : range.unboundedZone.top + (range.parts[0].rowFixed ? 0 : offsetY),\n // Don't shift down if the range is a full column\n bottom: range.isFullCol\n ? range.unboundedZone.bottom\n : range.unboundedZone.bottom +\n ((range.parts[1] || range.parts[0]).rowFixed ? 0 : offsetY),\n };\n return range.clone({ sheetId: copySheetId, zone: unboundZone }).orderZone();\n });\n }\n /**\n * Creates a range from a XC reference that can contain a sheet reference\n * @param defaultSheetId the sheet to default to if the sheetXC parameter does not contain a sheet reference (usually the active sheet Id)\n * @param sheetXC the string description of a range, in the form SheetName!XC:XC\n */\n getRangeFromSheetXC(defaultSheetId, sheetXC) {\n if (!rangeReference.test(sheetXC)) {\n return new RangeImpl({\n sheetId: \"\",\n zone: { left: -1, top: -1, right: -1, bottom: -1 },\n parts: [],\n invalidXc: sheetXC,\n prefixSheet: false,\n }, this.getters.getSheetSize);\n }\n let sheetName;\n let xc = sheetXC;\n let prefixSheet = false;\n if (sheetXC.includes(\"!\")) {\n ({ xc, sheetName } = splitReference(sheetXC));\n if (sheetName) {\n prefixSheet = true;\n }\n }\n const zone = toUnboundedZone(xc);\n const parts = RangeImpl.getRangeParts(xc, zone);\n const invalidSheetName = sheetName && !this.getters.getSheetIdByName(sheetName) ? sheetName : undefined;\n const sheetId = this.getters.getSheetIdByName(sheetName) || defaultSheetId;\n const rangeInterface = { prefixSheet, zone, sheetId, invalidSheetName, parts };\n return new RangeImpl(rangeInterface, this.getters.getSheetSize).orderZone();\n }\n /**\n * Same as `getRangeString` but add all necessary merge to the range to make it a valid selection\n */\n getSelectionRangeString(range, forSheetId) {\n const rangeImpl = RangeImpl.fromRange(range, this.getters);\n const expandedZone = this.getters.expandZone(rangeImpl.sheetId, rangeImpl.zone);\n const expandedRange = rangeImpl.clone({\n zone: {\n ...expandedZone,\n bottom: rangeImpl.isFullCol ? undefined : expandedZone.bottom,\n right: rangeImpl.isFullRow ? undefined : expandedZone.right,\n },\n });\n return this.getRangeString(expandedRange, forSheetId);\n }\n /**\n * Gets the string that represents the range as it is at the moment of the call.\n * The string will be prefixed with the sheet name if the call specified a sheet id in `forSheetId`\n * different than the sheet on which the range has been created.\n *\n * @param range the range (received from getRangeFromXC or getRangeFromZone)\n * @param forSheetId the id of the sheet where the range string is supposed to be used.\n */\n getRangeString(range, forSheetId) {\n if (!range) {\n return INCORRECT_RANGE_STRING;\n }\n if (range.invalidXc) {\n return range.invalidXc;\n }\n if (range.zone.bottom - range.zone.top < 0 || range.zone.right - range.zone.left < 0) {\n return INCORRECT_RANGE_STRING;\n }\n if (range.zone.left < 0 || range.zone.top < 0) {\n return INCORRECT_RANGE_STRING;\n }\n const rangeImpl = RangeImpl.fromRange(range, this.getters);\n let prefixSheet = rangeImpl.sheetId !== forSheetId || rangeImpl.invalidSheetName || rangeImpl.prefixSheet;\n let sheetName = \"\";\n if (prefixSheet) {\n if (rangeImpl.invalidSheetName) {\n sheetName = rangeImpl.invalidSheetName;\n }\n else {\n sheetName = getComposerSheetName(this.getters.getSheetName(rangeImpl.sheetId));\n }\n }\n if (prefixSheet && !sheetName) {\n return INCORRECT_RANGE_STRING;\n }\n let rangeString = this.getRangePartString(rangeImpl, 0);\n if (rangeImpl.parts && rangeImpl.parts.length === 2) {\n // this if converts A2:A2 into A2 except if any part of the original range had fixed row or column (with $)\n if (rangeImpl.zone.top !== rangeImpl.zone.bottom ||\n rangeImpl.zone.left !== rangeImpl.zone.right ||\n rangeImpl.parts[0].rowFixed ||\n rangeImpl.parts[0].colFixed ||\n rangeImpl.parts[1].rowFixed ||\n rangeImpl.parts[1].colFixed) {\n rangeString += \":\";\n rangeString += this.getRangePartString(rangeImpl, 1);\n }\n }\n return `${prefixSheet ? sheetName + \"!\" : \"\"}${rangeString}`;\n }\n getRangeDataFromXc(sheetId, xc) {\n return this.getters.getRangeFromSheetXC(sheetId, xc).rangeData;\n }\n getRangeDataFromZone(sheetId, zone) {\n return { _sheetId: sheetId, _zone: zone };\n }\n getRangeFromRangeData(data) {\n const rangeInterface = {\n prefixSheet: false,\n zone: data._zone,\n sheetId: data._sheetId,\n invalidSheetName: undefined,\n parts: [\n { colFixed: false, rowFixed: false },\n { colFixed: false, rowFixed: false },\n ],\n };\n return new RangeImpl(rangeInterface, this.getters.getSheetSize);\n }\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n /**\n * Get a Xc string that represent a part of a range\n */\n getRangePartString(range, part) {\n const colFixed = range.parts && range.parts[part].colFixed ? \"$\" : \"\";\n const col = part === 0 ? numberToLetters(range.zone.left) : numberToLetters(range.zone.right);\n const rowFixed = range.parts && range.parts[part].rowFixed ? \"$\" : \"\";\n const row = part === 0 ? String(range.zone.top + 1) : String(range.zone.bottom + 1);\n let str = \"\";\n if (range.isFullCol) {\n if (part === 0 && range.unboundedZone.hasHeader) {\n str = colFixed + col + rowFixed + row;\n }\n else {\n str = colFixed + col;\n }\n }\n else if (range.isFullRow) {\n if (part === 0 && range.unboundedZone.hasHeader) {\n str = colFixed + col + rowFixed + row;\n }\n else {\n str = rowFixed + row;\n }\n }\n else {\n str = colFixed + col + rowFixed + row;\n }\n return str;\n }\n getInvalidRange() {\n return {\n parts: [],\n prefixSheet: false,\n zone: { left: -1, top: -1, right: -1, bottom: -1 },\n sheetId: \"\",\n invalidXc: INCORRECT_RANGE_STRING,\n };\n }\n }\n RangeAdapter.getters = [\n \"getRangeString\",\n \"getSelectionRangeString\",\n \"getRangeFromSheetXC\",\n \"createAdaptedRanges\",\n \"getRangeDataFromXc\",\n \"getRangeDataFromZone\",\n \"getRangeFromRangeData\",\n ];\n\n class SheetPlugin extends CorePlugin {\n constructor() {\n super(...arguments);\n this.sheetIdsMapName = {};\n this.orderedSheetIds = [];\n this.sheets = {};\n this.cellPosition = {};\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n const genericChecks = this.chainValidations(this.checkSheetExists, this.checkZones)(cmd);\n if (genericChecks !== 0 /* CommandResult.Success */) {\n return genericChecks;\n }\n switch (cmd.type) {\n case \"HIDE_SHEET\": {\n if (this.getVisibleSheetIds().length === 1) {\n return 9 /* CommandResult.NotEnoughSheets */;\n }\n return 0 /* CommandResult.Success */;\n }\n case \"CREATE_SHEET\": {\n return this.checkValidations(cmd, this.checkSheetName, this.checkSheetPosition);\n }\n case \"MOVE_SHEET\":\n const currentIndex = this.orderedSheetIds.indexOf(cmd.sheetId);\n if (cmd.direction === \"left\") {\n const leftSheets = this.orderedSheetIds\n .slice(0, currentIndex)\n .map((id) => !this.isSheetVisible(id));\n return leftSheets.every((isHidden) => isHidden)\n ? 14 /* CommandResult.WrongSheetMove */\n : 0 /* CommandResult.Success */;\n }\n else {\n const rightSheets = this.orderedSheetIds\n .slice(currentIndex + 1)\n .map((id) => !this.isSheetVisible(id));\n return rightSheets.every((isHidden) => isHidden)\n ? 14 /* CommandResult.WrongSheetMove */\n : 0 /* CommandResult.Success */;\n }\n case \"RENAME_SHEET\":\n return this.isRenameAllowed(cmd);\n case \"DELETE_SHEET\":\n return this.orderedSheetIds.length > 1\n ? 0 /* CommandResult.Success */\n : 9 /* CommandResult.NotEnoughSheets */;\n case \"REMOVE_COLUMNS_ROWS\": {\n const length = cmd.dimension === \"COL\"\n ? this.getNumberCols(cmd.sheetId)\n : this.getNumberRows(cmd.sheetId);\n return length > cmd.elements.length\n ? 0 /* CommandResult.Success */\n : 8 /* CommandResult.NotEnoughElements */;\n }\n case \"FREEZE_ROWS\": {\n return this.checkValidations(cmd, this.checkRowFreezeQuantity, this.checkRowFreezeOverlapMerge);\n }\n case \"FREEZE_COLUMNS\": {\n return this.checkValidations(cmd, this.checkColFreezeQuantity, this.checkColFreezeOverlapMerge);\n }\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"SET_GRID_LINES_VISIBILITY\":\n this.setGridLinesVisibility(cmd.sheetId, cmd.areGridLinesVisible);\n break;\n case \"DELETE_CONTENT\":\n this.clearZones(cmd.sheetId, cmd.target);\n break;\n case \"CREATE_SHEET\":\n const sheet = this.createSheet(cmd.sheetId, cmd.name || this.getNextSheetName(), cmd.cols || 26, cmd.rows || 100, cmd.position);\n this.history.update(\"sheetIdsMapName\", sheet.name, sheet.id);\n break;\n case \"MOVE_SHEET\":\n this.moveSheet(cmd.sheetId, cmd.direction);\n break;\n case \"RENAME_SHEET\":\n this.renameSheet(this.sheets[cmd.sheetId], cmd.name);\n break;\n case \"HIDE_SHEET\":\n this.hideSheet(cmd.sheetId);\n break;\n case \"SHOW_SHEET\":\n this.showSheet(cmd.sheetId);\n break;\n case \"DUPLICATE_SHEET\":\n this.duplicateSheet(cmd.sheetId, cmd.sheetIdTo);\n break;\n case \"DELETE_SHEET\":\n this.deleteSheet(this.sheets[cmd.sheetId]);\n break;\n case \"REMOVE_COLUMNS_ROWS\":\n if (cmd.dimension === \"COL\") {\n this.removeColumns(this.sheets[cmd.sheetId], [...cmd.elements]);\n }\n else {\n this.removeRows(this.sheets[cmd.sheetId], [...cmd.elements]);\n }\n break;\n case \"ADD_COLUMNS_ROWS\":\n if (cmd.dimension === \"COL\") {\n this.addColumns(this.sheets[cmd.sheetId], cmd.base, cmd.position, cmd.quantity);\n }\n else {\n this.addRows(this.sheets[cmd.sheetId], cmd.base, cmd.position, cmd.quantity);\n }\n break;\n case \"UPDATE_CELL_POSITION\":\n this.updateCellPosition(cmd);\n break;\n case \"FREEZE_COLUMNS\":\n this.setPaneDivisions(cmd.sheetId, cmd.quantity, \"COL\");\n break;\n case \"FREEZE_ROWS\":\n this.setPaneDivisions(cmd.sheetId, cmd.quantity, \"ROW\");\n break;\n case \"UNFREEZE_ROWS\":\n this.setPaneDivisions(cmd.sheetId, 0, \"ROW\");\n break;\n case \"UNFREEZE_COLUMNS\":\n this.setPaneDivisions(cmd.sheetId, 0, \"COL\");\n break;\n case \"UNFREEZE_COLUMNS_ROWS\":\n this.setPaneDivisions(cmd.sheetId, 0, \"COL\");\n this.setPaneDivisions(cmd.sheetId, 0, \"ROW\");\n }\n }\n // ---------------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------------\n import(data) {\n var _a, _b;\n // we need to fill the sheetIds mapping first, because otherwise formulas\n // that depends on a sheet not already imported will not be able to be\n // compiled\n for (let sheet of data.sheets) {\n this.sheetIdsMapName[sheet.name] = sheet.id;\n }\n for (let sheetData of data.sheets) {\n const name = sheetData.name || _t(\"Sheet\") + (Object.keys(this.sheets).length + 1);\n const { colNumber, rowNumber } = this.getImportedSheetSize(sheetData);\n const sheet = {\n id: sheetData.id,\n name: name,\n numberOfCols: colNumber,\n rows: createDefaultRows(rowNumber),\n areGridLinesVisible: sheetData.areGridLinesVisible === undefined ? true : sheetData.areGridLinesVisible,\n isVisible: sheetData.isVisible,\n panes: {\n xSplit: ((_a = sheetData.panes) === null || _a === void 0 ? void 0 : _a.xSplit) || 0,\n ySplit: ((_b = sheetData.panes) === null || _b === void 0 ? void 0 : _b.ySplit) || 0,\n },\n };\n this.orderedSheetIds.push(sheet.id);\n this.sheets[sheet.id] = sheet;\n }\n }\n exportSheets(data) {\n data.sheets = this.orderedSheetIds.filter(isDefined$1).map((id) => {\n const sheet = this.sheets[id];\n const sheetData = {\n id: sheet.id,\n name: sheet.name,\n colNumber: sheet.numberOfCols,\n rowNumber: this.getters.getNumberRows(sheet.id),\n rows: {},\n cols: {},\n merges: [],\n cells: {},\n conditionalFormats: [],\n figures: [],\n filterTables: [],\n areGridLinesVisible: sheet.areGridLinesVisible === undefined ? true : sheet.areGridLinesVisible,\n isVisible: sheet.isVisible,\n };\n if (sheet.panes.xSplit || sheet.panes.ySplit) {\n sheetData.panes = sheet.panes;\n }\n return sheetData;\n });\n }\n export(data) {\n this.exportSheets(data);\n }\n exportForExcel(data) {\n this.exportSheets(data);\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getGridLinesVisibility(sheetId) {\n return this.getSheet(sheetId).areGridLinesVisible;\n }\n tryGetSheet(sheetId) {\n return this.sheets[sheetId];\n }\n getSheet(sheetId) {\n const sheet = this.sheets[sheetId];\n if (!sheet) {\n throw new Error(`Sheet ${sheetId} not found.`);\n }\n return sheet;\n }\n isSheetVisible(sheetId) {\n return this.getSheet(sheetId).isVisible;\n }\n /**\n * Return the sheet name. Throw if the sheet is not found.\n */\n getSheetName(sheetId) {\n return this.getSheet(sheetId).name;\n }\n /**\n * Return the sheet name or undefined if the sheet doesn't exist.\n */\n tryGetSheetName(sheetId) {\n var _a;\n return (_a = this.tryGetSheet(sheetId)) === null || _a === void 0 ? void 0 : _a.name;\n }\n getSheetIdByName(name) {\n if (name) {\n const unquotedName = getUnquotedSheetName(name);\n for (const key in this.sheetIdsMapName) {\n if (key.toUpperCase() === unquotedName.toUpperCase()) {\n return this.sheetIdsMapName[key];\n }\n }\n }\n return undefined;\n }\n getSheetIds() {\n return this.orderedSheetIds;\n }\n getVisibleSheetIds() {\n return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));\n }\n getEvaluationSheets() {\n return this.sheets;\n }\n doesHeaderExist(sheetId, dimension, index) {\n return dimension === \"COL\"\n ? index >= 0 && index < this.getNumberCols(sheetId)\n : index >= 0 && index < this.getNumberRows(sheetId);\n }\n getRow(sheetId, index) {\n const row = this.getSheet(sheetId).rows[index];\n if (!row) {\n throw new Error(`Row ${row} not found.`);\n }\n return row;\n }\n getCell({ sheetId, col, row }) {\n var _a;\n const sheet = this.tryGetSheet(sheetId);\n const cellId = (_a = sheet === null || sheet === void 0 ? void 0 : sheet.rows[row]) === null || _a === void 0 ? void 0 : _a.cells[col];\n if (cellId === undefined) {\n return undefined;\n }\n return this.getters.getCellById(cellId);\n }\n getColsZone(sheetId, start, end) {\n return {\n top: 0,\n bottom: this.getNumberRows(sheetId) - 1,\n left: start,\n right: end,\n };\n }\n getRowCells(sheetId, row) {\n var _a;\n return Object.values((_a = this.getSheet(sheetId).rows[row]) === null || _a === void 0 ? void 0 : _a.cells).filter(isDefined$1);\n }\n getRowsZone(sheetId, start, end) {\n return {\n top: start,\n bottom: end,\n left: 0,\n right: this.getSheet(sheetId).numberOfCols - 1,\n };\n }\n getCellPosition(cellId) {\n const cell = this.cellPosition[cellId];\n if (!cell) {\n throw new Error(`asking for a cell position that doesn't exist, cell id: ${cellId}`);\n }\n return cell;\n }\n getNumberCols(sheetId) {\n return this.getSheet(sheetId).numberOfCols;\n }\n getNumberRows(sheetId) {\n return this.getSheet(sheetId).rows.length;\n }\n getNumberHeaders(sheetId, dimension) {\n return dimension === \"COL\" ? this.getNumberCols(sheetId) : this.getNumberRows(sheetId);\n }\n getNextSheetName(baseName = \"Sheet\") {\n let i = 1;\n const names = this.orderedSheetIds.map(this.getSheetName.bind(this));\n let name = `${baseName}${i}`;\n while (names.includes(name)) {\n name = `${baseName}${i}`;\n i++;\n }\n return name;\n }\n getSheetSize(sheetId) {\n return {\n height: this.getNumberRows(sheetId),\n width: this.getNumberCols(sheetId),\n };\n }\n getSheetZone(sheetId) {\n return {\n top: 0,\n left: 0,\n bottom: this.getNumberRows(sheetId) - 1,\n right: this.getNumberCols(sheetId) - 1,\n };\n }\n getPaneDivisions(sheetId) {\n return this.getSheet(sheetId).panes;\n }\n setPaneDivisions(sheetId, base, dimension) {\n const panes = { ...this.getPaneDivisions(sheetId) };\n if (dimension === \"COL\") {\n panes.xSplit = base;\n }\n else if (dimension === \"ROW\") {\n panes.ySplit = base;\n }\n this.history.update(\"sheets\", sheetId, \"panes\", panes);\n }\n // ---------------------------------------------------------------------------\n // Row/Col manipulation\n // ---------------------------------------------------------------------------\n /**\n * Check if a zone only contains empty cells\n */\n isEmpty(sheetId, zone) {\n return positions(zone)\n .map(({ col, row }) => this.getCell({ sheetId, col, row }))\n .every((cell) => !cell || cell.content === \"\");\n }\n updateCellPosition(cmd) {\n const { sheetId, cellId, col, row } = cmd;\n if (cellId) {\n this.setNewPosition(cellId, sheetId, col, row);\n }\n else {\n this.clearPosition(sheetId, col, row);\n }\n }\n /**\n * Set the cell at a new position and clear its previous position.\n */\n setNewPosition(cellId, sheetId, col, row) {\n const currentPosition = this.cellPosition[cellId];\n if (currentPosition) {\n this.clearPosition(sheetId, currentPosition.col, currentPosition.row);\n }\n this.history.update(\"cellPosition\", cellId, {\n row: row,\n col: col,\n sheetId: sheetId,\n });\n this.history.update(\"sheets\", sheetId, \"rows\", row, \"cells\", col, cellId);\n }\n /**\n * Remove the cell at the given position (if there's one)\n */\n clearPosition(sheetId, col, row) {\n var _a;\n const cellId = (_a = this.sheets[sheetId]) === null || _a === void 0 ? void 0 : _a.rows[row].cells[col];\n if (cellId) {\n this.history.update(\"cellPosition\", cellId, undefined);\n this.history.update(\"sheets\", sheetId, \"rows\", row, \"cells\", col, undefined);\n }\n }\n setGridLinesVisibility(sheetId, areGridLinesVisible) {\n this.history.update(\"sheets\", sheetId, \"areGridLinesVisible\", areGridLinesVisible);\n }\n clearZones(sheetId, zones) {\n for (let zone of zones) {\n for (let col = zone.left; col <= zone.right; col++) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n const cell = this.sheets[sheetId].rows[row].cells[col];\n if (cell) {\n this.dispatch(\"UPDATE_CELL\", {\n sheetId: sheetId,\n content: \"\",\n col,\n row,\n });\n }\n }\n }\n }\n }\n createSheet(id, name, colNumber, rowNumber, position) {\n const sheet = {\n id,\n name,\n numberOfCols: colNumber,\n rows: createDefaultRows(rowNumber),\n areGridLinesVisible: true,\n isVisible: true,\n panes: {\n xSplit: 0,\n ySplit: 0,\n },\n };\n const orderedSheetIds = this.orderedSheetIds.slice();\n orderedSheetIds.splice(position, 0, sheet.id);\n const sheets = this.sheets;\n this.history.update(\"orderedSheetIds\", orderedSheetIds);\n this.history.update(\"sheets\", Object.assign({}, sheets, { [sheet.id]: sheet }));\n return sheet;\n }\n moveSheet(sheetId, direction) {\n const orderedSheetIds = this.orderedSheetIds.slice();\n const currentIndex = orderedSheetIds.findIndex((id) => id === sheetId);\n const sheet = orderedSheetIds.splice(currentIndex, 1);\n let index = direction === \"left\"\n ? this.findIndexOfPreviousVisibleSheet(currentIndex - 1, orderedSheetIds)\n : this.findIndexOfNextVisibleSheet(currentIndex + 1, orderedSheetIds);\n if (index === undefined) {\n index = orderedSheetIds.length;\n }\n orderedSheetIds.splice(index, 0, sheet[0]);\n this.history.update(\"orderedSheetIds\", orderedSheetIds);\n }\n findIndexOfPreviousVisibleSheet(current, orderedSheetIds) {\n while (current >= 0 && !this.isSheetVisible(orderedSheetIds[current])) {\n current--;\n }\n if (current === -1) {\n throw new Error(\"There is no previous visible sheet\");\n }\n return current;\n }\n findIndexOfNextVisibleSheet(current, orderedSheetIds) {\n while (current < orderedSheetIds.length && !this.isSheetVisible(orderedSheetIds[current])) {\n current++;\n }\n if (current === orderedSheetIds.length - 1 &&\n !this.isSheetVisible(orderedSheetIds[current - 1])) {\n return undefined;\n }\n return current;\n }\n checkSheetName(cmd) {\n const { orderedSheetIds, sheets } = this;\n const name = cmd.name && cmd.name.trim().toLowerCase();\n if (orderedSheetIds.find((id) => { var _a; return ((_a = sheets[id]) === null || _a === void 0 ? void 0 : _a.name.toLowerCase()) === name; })) {\n return 11 /* CommandResult.DuplicatedSheetName */;\n }\n if (FORBIDDEN_IN_EXCEL_REGEX.test(name)) {\n return 13 /* CommandResult.ForbiddenCharactersInSheetName */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkSheetPosition(cmd) {\n const { orderedSheetIds } = this;\n if (cmd.position > orderedSheetIds.length || cmd.position < 0) {\n return 15 /* CommandResult.WrongSheetPosition */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkRowFreezeQuantity(cmd) {\n return cmd.quantity >= 1 && cmd.quantity < this.getNumberRows(cmd.sheetId)\n ? 0 /* CommandResult.Success */\n : 74 /* CommandResult.InvalidFreezeQuantity */;\n }\n checkColFreezeQuantity(cmd) {\n return cmd.quantity >= 1 && cmd.quantity < this.getNumberCols(cmd.sheetId)\n ? 0 /* CommandResult.Success */\n : 74 /* CommandResult.InvalidFreezeQuantity */;\n }\n checkRowFreezeOverlapMerge(cmd) {\n const merges = this.getters.getMerges(cmd.sheetId);\n for (let merge of merges) {\n if (merge.top < cmd.quantity && cmd.quantity <= merge.bottom) {\n return 65 /* CommandResult.MergeOverlap */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n checkColFreezeOverlapMerge(cmd) {\n const merges = this.getters.getMerges(cmd.sheetId);\n for (let merge of merges) {\n if (merge.left < cmd.quantity && cmd.quantity <= merge.right) {\n return 65 /* CommandResult.MergeOverlap */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n isRenameAllowed(cmd) {\n const name = cmd.name && cmd.name.trim().toLowerCase();\n if (!name) {\n return 10 /* CommandResult.MissingSheetName */;\n }\n return this.checkSheetName(cmd);\n }\n renameSheet(sheet, name) {\n const oldName = sheet.name;\n this.history.update(\"sheets\", sheet.id, \"name\", name.trim());\n const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);\n sheetIdsMapName[name] = sheet.id;\n delete sheetIdsMapName[oldName];\n this.history.update(\"sheetIdsMapName\", sheetIdsMapName);\n }\n hideSheet(sheetId) {\n this.history.update(\"sheets\", sheetId, \"isVisible\", false);\n }\n showSheet(sheetId) {\n this.history.update(\"sheets\", sheetId, \"isVisible\", true);\n }\n duplicateSheet(fromId, toId) {\n const sheet = this.getSheet(fromId);\n const toName = this.getDuplicateSheetName(sheet.name);\n const newSheet = JSON.parse(JSON.stringify(sheet));\n newSheet.id = toId;\n newSheet.name = toName;\n for (let col = 0; col <= newSheet.numberOfCols; col++) {\n for (let row = 0; row <= newSheet.rows.length; row++) {\n if (newSheet.rows[row]) {\n newSheet.rows[row].cells[col] = undefined;\n }\n }\n }\n const orderedSheetIds = this.orderedSheetIds.slice();\n const currentIndex = orderedSheetIds.indexOf(fromId);\n orderedSheetIds.splice(currentIndex + 1, 0, newSheet.id);\n this.history.update(\"orderedSheetIds\", orderedSheetIds);\n this.history.update(\"sheets\", Object.assign({}, this.sheets, { [newSheet.id]: newSheet }));\n for (const cell of Object.values(this.getters.getCells(fromId))) {\n const { col, row } = this.getCellPosition(cell.id);\n this.dispatch(\"UPDATE_CELL\", {\n sheetId: newSheet.id,\n col,\n row,\n content: cell.content,\n format: cell.format,\n style: cell.style,\n });\n }\n const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);\n sheetIdsMapName[newSheet.name] = newSheet.id;\n this.history.update(\"sheetIdsMapName\", sheetIdsMapName);\n }\n getDuplicateSheetName(sheetName) {\n let i = 1;\n const names = this.orderedSheetIds.map(this.getSheetName.bind(this));\n const baseName = _lt(\"Copy of %s\", sheetName);\n let name = baseName.toString();\n while (names.includes(name)) {\n name = `${baseName} (${i})`;\n i++;\n }\n return name;\n }\n deleteSheet(sheet) {\n const name = sheet.name;\n const sheets = Object.assign({}, this.sheets);\n delete sheets[sheet.id];\n this.history.update(\"sheets\", sheets);\n const orderedSheetIds = this.orderedSheetIds.slice();\n const currentIndex = orderedSheetIds.indexOf(sheet.id);\n orderedSheetIds.splice(currentIndex, 1);\n this.history.update(\"orderedSheetIds\", orderedSheetIds);\n const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);\n delete sheetIdsMapName[name];\n this.history.update(\"sheetIdsMapName\", sheetIdsMapName);\n }\n /**\n * Delete column. This requires a lot of handling:\n * - Update all the formulas in all sheets\n * - Move the cells\n * - Update the cols/rows (size, number, (cells), ...)\n * - Reevaluate the cells\n *\n * @param sheet ID of the sheet on which deletion should be applied\n * @param columns Columns to delete\n */\n removeColumns(sheet, columns) {\n // This is necessary because we have to delete elements in correct order:\n // begin with the end.\n columns.sort((a, b) => b - a);\n for (let column of columns) {\n // Move the cells.\n this.moveCellOnColumnsDeletion(sheet, column);\n }\n const numberOfCols = this.sheets[sheet.id].numberOfCols;\n this.history.update(\"sheets\", sheet.id, \"numberOfCols\", numberOfCols - columns.length);\n const count = columns.filter((col) => col < sheet.panes.xSplit).length;\n if (count) {\n this.setPaneDivisions(sheet.id, sheet.panes.xSplit - count, \"COL\");\n }\n }\n /**\n * Delete row. This requires a lot of handling:\n * - Update the merges\n * - Update all the formulas in all sheets\n * - Move the cells\n * - Update the cols/rows (size, number, (cells), ...)\n * - Reevaluate the cells\n *\n * @param sheet ID of the sheet on which deletion should be applied\n * @param rows Rows to delete\n */\n removeRows(sheet, rows) {\n // This is necessary because we have to delete elements in correct order:\n // begin with the end.\n rows.sort((a, b) => b - a);\n for (let group of groupConsecutive(rows)) {\n // Move the cells.\n this.moveCellOnRowsDeletion(sheet, group[group.length - 1], group[0]);\n // Effectively delete the element and recompute the left-right/top-bottom.\n group.map((row) => this.updateRowsStructureOnDeletion(row, sheet));\n }\n const count = rows.filter((row) => row < sheet.panes.ySplit).length;\n if (count) {\n this.setPaneDivisions(sheet.id, sheet.panes.ySplit - count, \"ROW\");\n }\n }\n addColumns(sheet, column, position, quantity) {\n const index = position === \"before\" ? column : column + 1;\n // Move the cells.\n this.moveCellsOnAddition(sheet, index, quantity, \"columns\");\n const numberOfCols = this.sheets[sheet.id].numberOfCols;\n this.history.update(\"sheets\", sheet.id, \"numberOfCols\", numberOfCols + quantity);\n if (index < sheet.panes.xSplit) {\n this.setPaneDivisions(sheet.id, sheet.panes.xSplit + quantity, \"COL\");\n }\n }\n addRows(sheet, row, position, quantity) {\n const index = position === \"before\" ? row : row + 1;\n this.addEmptyRows(sheet, quantity);\n // Move the cells.\n this.moveCellsOnAddition(sheet, index, quantity, \"rows\");\n // Recompute the left-right/top-bottom.\n this.updateRowsStructureOnAddition(sheet, row, quantity);\n if (index < sheet.panes.ySplit) {\n this.setPaneDivisions(sheet.id, sheet.panes.ySplit + quantity, \"ROW\");\n }\n }\n moveCellOnColumnsDeletion(sheet, deletedColumn) {\n for (let [index, row] of Object.entries(sheet.rows)) {\n const rowIndex = parseInt(index, 10);\n for (let i in row.cells) {\n const colIndex = parseInt(i, 10);\n const cellId = row.cells[i];\n if (cellId) {\n if (colIndex === deletedColumn) {\n this.dispatch(\"CLEAR_CELL\", {\n sheetId: sheet.id,\n col: colIndex,\n row: rowIndex,\n });\n }\n if (colIndex > deletedColumn) {\n this.dispatch(\"UPDATE_CELL_POSITION\", {\n sheetId: sheet.id,\n cellId: cellId,\n col: colIndex - 1,\n row: rowIndex,\n });\n }\n }\n }\n }\n }\n /**\n * Move the cells after a column or rows insertion\n */\n moveCellsOnAddition(sheet, addedElement, quantity, dimension) {\n const commands = [];\n for (const [index, row] of Object.entries(sheet.rows)) {\n const rowIndex = parseInt(index, 10);\n if (dimension !== \"rows\" || rowIndex >= addedElement) {\n for (let i in row.cells) {\n const colIndex = parseInt(i, 10);\n const cellId = row.cells[i];\n if (cellId) {\n if (dimension === \"rows\" || colIndex >= addedElement) {\n commands.push({\n type: \"UPDATE_CELL_POSITION\",\n sheetId: sheet.id,\n cellId: cellId,\n col: colIndex + (dimension === \"columns\" ? quantity : 0),\n row: rowIndex + (dimension === \"rows\" ? quantity : 0),\n });\n }\n }\n }\n }\n }\n for (let cmd of commands.reverse()) {\n this.dispatch(cmd.type, cmd);\n }\n }\n /**\n * Move all the cells that are from the row under `deleteToRow` up to `deleteFromRow`\n *\n * b.e.\n * move vertically with delete from 3 and delete to 5 will first clear all the cells from lines 3 to 5,\n * then take all the row starting at index 6 and add them back at index 3\n *\n */\n moveCellOnRowsDeletion(sheet, deleteFromRow, deleteToRow) {\n const numberRows = deleteToRow - deleteFromRow + 1;\n for (let [index, row] of Object.entries(sheet.rows)) {\n const rowIndex = parseInt(index, 10);\n if (rowIndex >= deleteFromRow && rowIndex <= deleteToRow) {\n for (let i in row.cells) {\n const colIndex = parseInt(i, 10);\n const cellId = row.cells[i];\n if (cellId) {\n this.dispatch(\"CLEAR_CELL\", {\n sheetId: sheet.id,\n col: colIndex,\n row: rowIndex,\n });\n }\n }\n }\n if (rowIndex > deleteToRow) {\n for (let i in row.cells) {\n const colIndex = parseInt(i, 10);\n const cellId = row.cells[i];\n if (cellId) {\n this.dispatch(\"UPDATE_CELL_POSITION\", {\n sheetId: sheet.id,\n cellId: cellId,\n col: colIndex,\n row: rowIndex - numberRows,\n });\n }\n }\n }\n }\n }\n updateRowsStructureOnDeletion(index, sheet) {\n const rows = [];\n const cellsQueue = sheet.rows.map((row) => row.cells);\n for (let i in sheet.rows) {\n if (parseInt(i, 10) === index) {\n continue;\n }\n rows.push({\n cells: cellsQueue.shift(),\n });\n }\n this.history.update(\"sheets\", sheet.id, \"rows\", rows);\n }\n /**\n * Update the rows of the sheet after an addition:\n * - Rename the rows\n *\n * @param sheet Sheet on which the deletion occurs\n * @param addedRow Index of the added row\n * @param rowsToAdd Number of the rows to add\n */\n updateRowsStructureOnAddition(sheet, addedRow, rowsToAdd) {\n const rows = [];\n const cellsQueue = sheet.rows.map((row) => row.cells);\n sheet.rows.forEach(() => rows.push({\n cells: cellsQueue.shift(),\n }));\n this.history.update(\"sheets\", sheet.id, \"rows\", rows);\n }\n /**\n * Add empty rows at the end of the rows\n *\n * @param sheet Sheet\n * @param quantity Number of rows to add\n */\n addEmptyRows(sheet, quantity) {\n const rows = sheet.rows.slice();\n for (let i = 0; i < quantity; i++) {\n rows.push({\n cells: {},\n });\n }\n this.history.update(\"sheets\", sheet.id, \"rows\", rows);\n }\n getImportedSheetSize(data) {\n const positions = Object.keys(data.cells).map(toCartesian);\n let rowNumber = data.rowNumber;\n let colNumber = data.colNumber;\n for (let { col, row } of positions) {\n rowNumber = Math.max(rowNumber, row + 1);\n colNumber = Math.max(colNumber, col + 1);\n }\n return { rowNumber, colNumber };\n }\n // ----------------------------------------------------\n // HIDE / SHOW\n // ----------------------------------------------------\n /**\n * Check that any \"sheetId\" in the command matches an existing\n * sheet.\n */\n checkSheetExists(cmd) {\n if (cmd.type !== \"CREATE_SHEET\" && \"sheetId\" in cmd && this.sheets[cmd.sheetId] === undefined) {\n return 27 /* CommandResult.InvalidSheetId */;\n }\n else if (cmd.type === \"CREATE_SHEET\" && this.sheets[cmd.sheetId] !== undefined) {\n return 12 /* CommandResult.DuplicatedSheetId */;\n }\n return 0 /* CommandResult.Success */;\n }\n /**\n * Check if zones in the command are well formed and\n * not outside the sheet.\n */\n checkZones(cmd) {\n const zones = [];\n if (\"zone\" in cmd) {\n zones.push(cmd.zone);\n }\n if (\"target\" in cmd && Array.isArray(cmd.target)) {\n zones.push(...cmd.target);\n }\n if (\"ranges\" in cmd && Array.isArray(cmd.ranges)) {\n zones.push(...cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone));\n }\n if (!zones.every(isZoneValid)) {\n return 25 /* CommandResult.InvalidRange */;\n }\n else if (zones.length && \"sheetId\" in cmd) {\n const sheetZone = this.getSheetZone(cmd.sheetId);\n return zones.every((zone) => isZoneInside(zone, sheetZone))\n ? 0 /* CommandResult.Success */\n : 18 /* CommandResult.TargetOutOfSheet */;\n }\n return 0 /* CommandResult.Success */;\n }\n }\n SheetPlugin.getters = [\n \"getSheetName\",\n \"tryGetSheetName\",\n \"getSheet\",\n \"tryGetSheet\",\n \"getSheetIdByName\",\n \"getSheetIds\",\n \"getVisibleSheetIds\",\n \"isSheetVisible\",\n \"getEvaluationSheets\",\n \"doesHeaderExist\",\n \"getCell\",\n \"getCellPosition\",\n \"getColsZone\",\n \"getRowCells\",\n \"getRowsZone\",\n \"getNumberCols\",\n \"getNumberRows\",\n \"getNumberHeaders\",\n \"getGridLinesVisibility\",\n \"getNextSheetName\",\n \"isEmpty\",\n \"getSheetSize\",\n \"getSheetZone\",\n \"getPaneDivisions\",\n ];\n\n /**\n * https://tomekdev.com/posts/sorting-colors-in-js\n */\n function sortWithClusters(colorsToSort) {\n const clusters = [\n { leadColor: rgba(255, 0, 0), colors: [] },\n { leadColor: rgba(255, 128, 0), colors: [] },\n { leadColor: rgba(128, 128, 0), colors: [] },\n { leadColor: rgba(128, 255, 0), colors: [] },\n { leadColor: rgba(0, 255, 0), colors: [] },\n { leadColor: rgba(0, 255, 128), colors: [] },\n { leadColor: rgba(0, 255, 255), colors: [] },\n { leadColor: rgba(0, 127, 255), colors: [] },\n { leadColor: rgba(0, 0, 255), colors: [] },\n { leadColor: rgba(127, 0, 255), colors: [] },\n { leadColor: rgba(128, 0, 128), colors: [] },\n { leadColor: rgba(255, 0, 128), colors: [] }, // rose\n ];\n for (const color of colorsToSort.map(colorToRGBA)) {\n let currentDistance = 500; //max distance is 441;\n let currentIndex = 0;\n clusters.forEach((cluster, clusterIndex) => {\n const distance = colorDistance(color, cluster.leadColor);\n if (currentDistance > distance) {\n currentDistance = distance;\n currentIndex = clusterIndex;\n }\n });\n clusters[currentIndex].colors.push(color);\n }\n return clusters\n .map((cluster) => cluster.colors.sort((a, b) => rgbaToHSLA(a).s - rgbaToHSLA(b).s))\n .flat()\n .map(rgbaToHex);\n }\n function colorDistance(color1, color2) {\n return Math.sqrt(Math.pow(color1.r - color2.r, 2) +\n Math.pow(color1.g - color2.g, 2) +\n Math.pow(color1.b - color2.b, 2));\n }\n /**\n * CustomColors plugin\n * This plugins aims to compute and keep to custom colors used in the\n * current spreadsheet\n */\n class CustomColorsPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.customColors = new Set();\n this.shouldUpdateColors = false;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"UPDATE_CELL\":\n case \"UPDATE_CHART\":\n case \"CREATE_CHART\":\n case \"ADD_CONDITIONAL_FORMAT\":\n this.shouldUpdateColors = true;\n }\n }\n finalize() {\n if (this.shouldUpdateColors) {\n this.shouldUpdateColors = false;\n for (const color of this.getCustomColors()) {\n this.tryToAddColor(color);\n }\n }\n }\n getCustomColors() {\n let usedColors = [];\n for (const sheetId of this.getters.getSheetIds()) {\n const cells = Object.values(this.getters.getCells(sheetId));\n usedColors = usedColors.concat(this.getColorsFromCells(cells), this.getFormattingColors(sheetId), this.getChartColors(sheetId));\n }\n return sortWithClusters([\n ...new Set(\n // remove duplicates first to check validity on a reduced\n // set of colors, then normalize to HEX and remove duplicates\n // again\n [...new Set([...usedColors, ...this.customColors])]\n .filter(isColorValid)\n .map((c) => toHex(c).toLowerCase())),\n ]).filter((color) => !COLOR_PICKER_DEFAULTS.includes(color));\n }\n getColorsFromCells(cells) {\n var _a, _b;\n const colors = new Set();\n for (const cell of cells) {\n if ((_a = cell.style) === null || _a === void 0 ? void 0 : _a.textColor) {\n colors.add(cell.style.textColor);\n }\n if ((_b = cell.style) === null || _b === void 0 ? void 0 : _b.fillColor) {\n colors.add(cell.style.fillColor);\n }\n }\n return [...colors];\n }\n getFormattingColors(sheetId) {\n const formats = this.getters.getConditionalFormats(sheetId);\n const formatColors = [];\n for (const format of formats) {\n const rule = format.rule;\n if (rule.type === \"CellIsRule\") {\n formatColors.push(rule.style.textColor);\n formatColors.push(rule.style.fillColor);\n }\n else if (rule.type === \"ColorScaleRule\") {\n formatColors.push(colorNumberString(rule.minimum.color));\n formatColors.push(rule.midpoint ? colorNumberString(rule.midpoint.color) : undefined);\n formatColors.push(colorNumberString(rule.maximum.color));\n }\n }\n return formatColors.filter(isDefined$1);\n }\n getChartColors(sheetId) {\n const charts = this.getters.getChartIds(sheetId).map((cid) => this.getters.getChart(cid));\n let chartsColors = new Set();\n for (let chart of charts) {\n if (chart === undefined) {\n continue;\n }\n const background = chart.getDefinition().background;\n if (background !== undefined) {\n chartsColors.add(background);\n }\n switch (chart.type) {\n case \"gauge\":\n const colors = chart.sectionRule.colors;\n chartsColors.add(colors.lowerColor);\n chartsColors.add(colors.middleColor);\n chartsColors.add(colors.upperColor);\n break;\n case \"scorecard\":\n const scoreChart = chart;\n chartsColors.add(scoreChart.baselineColorDown);\n chartsColors.add(scoreChart.baselineColorUp);\n break;\n }\n }\n return [...chartsColors];\n }\n tryToAddColor(color) {\n const formattedColor = toHex(color).toLowerCase();\n if (color && !COLOR_PICKER_DEFAULTS.includes(formattedColor)) {\n this.customColors.add(formattedColor);\n }\n }\n }\n CustomColorsPlugin.getters = [\"getCustomColors\"];\n\n const functionMap = functionRegistry.mapping;\n class EvaluationPlugin extends UIPlugin {\n constructor(config) {\n super(config);\n this.isUpToDate = false;\n this.evaluatedCells = {};\n this.evalContext = config.custom;\n this.lazyEvaluation = config.lazyEvaluation;\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n handle(cmd) {\n if (invalidateEvaluationCommands.has(cmd.type)) {\n this.isUpToDate = false;\n }\n switch (cmd.type) {\n case \"UPDATE_CELL\":\n if (\"content\" in cmd || \"format\" in cmd) {\n this.isUpToDate = false;\n }\n break;\n case \"EVALUATE_CELLS\":\n this.evaluate();\n break;\n }\n }\n finalize() {\n if (!this.isUpToDate) {\n this.evaluate();\n this.isUpToDate = true;\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n evaluateFormula(formulaString, sheetId = this.getters.getActiveSheetId()) {\n const compiledFormula = compile(formulaString);\n const params = this.getCompilationParameters((cell) => this.getEvaluatedCell(this.getters.getCellPosition(cell.id)));\n const ranges = [];\n for (let xc of compiledFormula.dependencies) {\n ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));\n }\n return compiledFormula.execute(ranges, ...params).value;\n }\n /**\n * Return the value of each cell in the range as they are displayed in the grid.\n */\n getRangeFormattedValues(range) {\n const sheet = this.getters.tryGetSheet(range.sheetId);\n if (sheet === undefined)\n return [];\n return this.getters\n .getEvaluatedCellsInZone(sheet.id, range.zone)\n .map((cell) => cell.formattedValue);\n }\n /**\n * Return the value of each cell in the range.\n */\n getRangeValues(range) {\n const sheet = this.getters.tryGetSheet(range.sheetId);\n if (sheet === undefined)\n return [];\n return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.value);\n }\n getEvaluatedCell({ sheetId, col, row }) {\n var _a, _b, _c;\n const cell = this.getters.getCell({ sheetId, col, row });\n if (cell === undefined) {\n return createEvaluatedCell(\"\");\n }\n // the cell might have been created by a command in the current\n // dispatch but the evaluation is not done yet.\n return ((_c = (_b = (_a = this.evaluatedCells[sheetId]) === null || _a === void 0 ? void 0 : _a[col]) === null || _b === void 0 ? void 0 : _b[row]) === null || _c === void 0 ? void 0 : _c.call(_b)) || createEvaluatedCell(\"\");\n }\n getEvaluatedCells(sheetId) {\n const rawCells = this.getters.getCells(sheetId) || {};\n const record = {};\n for (let cellId of Object.keys(rawCells)) {\n const position = this.getters.getCellPosition(cellId);\n record[cellId] = this.getEvaluatedCell(position);\n }\n return record;\n }\n /**\n * Returns all the evaluated cells of a col\n */\n getColEvaluatedCells(sheetId, col) {\n var _a;\n return Object.values(((_a = this.evaluatedCells[sheetId]) === null || _a === void 0 ? void 0 : _a[col]) || [])\n .filter(isDefined$1)\n .map((lazyCell) => lazyCell());\n }\n getEvaluatedCellsInZone(sheetId, zone) {\n return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));\n }\n // ---------------------------------------------------------------------------\n // Evaluator\n // ---------------------------------------------------------------------------\n setEvaluatedCell(cellId, evaluatedCell) {\n const { col, row, sheetId } = this.getters.getCellPosition(cellId);\n if (!this.evaluatedCells[sheetId]) {\n this.evaluatedCells[sheetId] = {};\n }\n if (!this.evaluatedCells[sheetId][col]) {\n this.evaluatedCells[sheetId][col] = {};\n }\n this.evaluatedCells[sheetId][col][row] = evaluatedCell;\n if (!this.lazyEvaluation) {\n this.evaluatedCells[sheetId][col][row]();\n }\n }\n *getAllCells() {\n // use a generator function to avoid re-building a new object\n for (const sheetId of this.getters.getSheetIds()) {\n const cells = this.getters.getCells(sheetId);\n for (const cellId in cells) {\n yield cells[cellId];\n }\n }\n }\n evaluate() {\n this.evaluatedCells = {};\n const cellsBeingComputed = new Set();\n const computeCell = (cell) => {\n var _a, _b;\n const cellId = cell.id;\n const { col, row, sheetId } = this.getters.getCellPosition(cellId);\n const lazyEvaluation = (_b = (_a = this.evaluatedCells[sheetId]) === null || _a === void 0 ? void 0 : _a[col]) === null || _b === void 0 ? void 0 : _b[row];\n if (lazyEvaluation) {\n return lazyEvaluation; // already computed\n }\n return lazy(() => {\n try {\n switch (cell.isFormula) {\n case true:\n return computeFormulaCell(cell);\n case false:\n return evaluateLiteral(cell.content, cell.format);\n }\n }\n catch (e) {\n return handleError(e, cell);\n }\n });\n };\n const handleError = (e, cell) => {\n if (!(e instanceof Error)) {\n e = new Error(e);\n }\n const msg = (e === null || e === void 0 ? void 0 : e.errorType) || CellErrorType.GenericError;\n // apply function name\n const __lastFnCalled = compilationParameters[2].__lastFnCalled || \"\";\n const error = new EvaluationError(msg, e.message.replace(\"[[FUNCTION_NAME]]\", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error);\n return errorCell(cell.content, error);\n };\n const computeFormulaCell = (cellData) => {\n const cellId = cellData.id;\n if (cellsBeingComputed.has(cellId)) {\n throw new CircularDependencyError();\n }\n compilationParameters[2].__originCellXC = () => {\n // compute the value lazily for performance reasons\n const position = compilationParameters[2].getters.getCellPosition(cellId);\n return toXC(position.col, position.row);\n };\n cellsBeingComputed.add(cellId);\n const computedCell = cellData.compiledFormula.execute(cellData.dependencies, ...compilationParameters);\n cellsBeingComputed.delete(cellId);\n if (Array.isArray(computedCell.value)) {\n // if a value returns an array (like =A1:A3)\n throw new Error(_lt(\"This formula depends on invalid values\"));\n }\n return createEvaluatedCell(computedCell.value, cellData.format || computedCell.format);\n };\n const compilationParameters = this.getCompilationParameters((cell) => computeCell(cell)());\n for (const cell of this.getAllCells()) {\n this.setEvaluatedCell(cell.id, computeCell(cell));\n }\n }\n /**\n * Return all functions necessary to properly evaluate a formula:\n * - a refFn function to read any reference, cell or range of a normalized formula\n * - a range function to convert any reference to a proper value array\n * - an evaluation context\n */\n getCompilationParameters(computeCell) {\n const evalContext = Object.assign(Object.create(functionMap), this.evalContext, {\n getters: this.getters,\n });\n const getters = this.getters;\n function readCell(range) {\n let cell;\n if (!getters.tryGetSheet(range.sheetId)) {\n throw new Error(_lt(\"Invalid sheet name\"));\n }\n cell = getters.getCell({ sheetId: range.sheetId, col: range.zone.left, row: range.zone.top });\n if (!cell || cell.content === \"\") {\n // magic \"empty\" value\n // Returning {value: null} instead of undefined will ensure that we don't\n // fall back on the default value of the argument provided to the formula's compute function\n return { value: null };\n }\n return getEvaluatedCell(cell);\n }\n const getEvaluatedCell = (cell) => {\n const evaluatedCell = computeCell(cell);\n if (evaluatedCell.type === CellValueType.error) {\n throw evaluatedCell.error;\n }\n return evaluatedCell;\n };\n /**\n * Return the values of the cell(s) used in reference, but always in the format of a range even\n * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as\n * range
etc.\n *\n * Note that each col is possibly sparse: it only contain the values of cells\n * that are actually present in the grid.\n */\n function range(range) {\n const sheetId = range.sheetId;\n if (!isZoneValid(range.zone)) {\n throw new InvalidReferenceError();\n }\n // Performance issue: Avoid fetching data on positions that are out of the spreadsheet\n // e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array\n const sheetZone = getters.getSheetZone(sheetId);\n const result = [];\n const zone = intersection(range.zone, sheetZone);\n if (!zone) {\n result.push([]);\n return result;\n }\n // Performance issue: nested loop is faster than a map here\n for (let col = zone.left; col <= zone.right; col++) {\n const rowValues = [];\n for (let row = zone.top; row <= zone.bottom; row++) {\n const cell = evalContext.getters.getCell({ sheetId: range.sheetId, col, row });\n rowValues.push(cell ? getEvaluatedCell(cell) : undefined);\n }\n result.push(rowValues);\n }\n return result;\n }\n /**\n * Returns the value of the cell(s) used in reference\n *\n * @param range the references used\n * @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the\n * function for which this parameter is used, we just return the string of the parameter.\n * The `compute` of the formula's function must process it completely\n */\n function refFn(range, isMeta, functionName, paramNumber) {\n if (isMeta) {\n // Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges\n return { value: zoneToXc(range.zone) };\n }\n if (!isZoneValid(range.zone)) {\n throw new InvalidReferenceError();\n }\n // if the formula definition could have accepted a range, we would pass through the _range function and not here\n if (range.zone.bottom !== range.zone.top || range.zone.left !== range.zone.right) {\n throw new Error(paramNumber\n ? _lt(\"Function %s expects the parameter %s to be a single value or a single cell reference, not a range.\", functionName.toString(), paramNumber.toString())\n : _lt(\"Function %s expects its parameters to be single values or single cell references, not ranges.\", functionName.toString()));\n }\n if (range.invalidSheetName) {\n throw new Error(_lt(\"Invalid sheet name: %s\", range.invalidSheetName));\n }\n return readCell(range);\n }\n return [refFn, range, evalContext];\n }\n // ---------------------------------------------------------------------------\n // Export\n // ---------------------------------------------------------------------------\n exportForExcel(data) {\n for (let sheet of data.sheets) {\n for (const xc in sheet.cells) {\n const position = { sheetId: sheet.id, ...toCartesian(xc) };\n const cell = this.getters.getCell(position);\n if (cell) {\n const exportedCellData = sheet.cells[xc];\n exportedCellData.value = this.getEvaluatedCell(position).value;\n exportedCellData.isFormula = cell.isFormula && !this.isBadExpression(cell.content);\n }\n }\n }\n }\n isBadExpression(formula) {\n try {\n compile(formula);\n return false;\n }\n catch (error) {\n return true;\n }\n }\n }\n EvaluationPlugin.getters = [\n \"evaluateFormula\",\n \"getRangeFormattedValues\",\n \"getRangeValues\",\n \"getEvaluatedCell\",\n \"getEvaluatedCells\",\n \"getColEvaluatedCells\",\n \"getEvaluatedCellsInZone\",\n ];\n\n class EvaluationChartPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.charts = {};\n this.createRuntimeChart = chartRuntimeFactory(this.getters);\n }\n handle(cmd) {\n if (invalidateEvaluationCommands.has(cmd.type) ||\n invalidateCFEvaluationCommands.has(cmd.type) ||\n cmd.type === \"EVALUATE_CELLS\" ||\n cmd.type === \"UPDATE_CELL\") {\n for (const chartId in this.charts) {\n this.charts[chartId] = undefined;\n }\n }\n switch (cmd.type) {\n case \"UPDATE_CHART\":\n case \"CREATE_CHART\":\n case \"DELETE_FIGURE\":\n this.charts[cmd.id] = undefined;\n break;\n case \"DELETE_SHEET\":\n for (let chartId in this.charts) {\n if (!this.getters.isChartDefined(chartId)) {\n this.charts[chartId] = undefined;\n }\n }\n break;\n }\n }\n getChartRuntime(figureId) {\n if (!this.charts[figureId]) {\n const chart = this.getters.getChart(figureId);\n if (!chart) {\n throw new Error(`No chart for the given id: ${figureId}`);\n }\n this.charts[figureId] = this.createRuntimeChart(chart);\n }\n return this.charts[figureId];\n }\n /**\n * Get the background color of a chart based on the color of the first cell of the main range\n * of the chart. In order of priority, it will return :\n *\n * - the chart background color if one is defined\n * - the fill color of the cell if one is defined\n * - the fill color of the cell from conditional formats if one is defined\n * - the default chart color if no other color is defined\n */\n getBackgroundOfSingleCellChart(chartBackground, mainRange) {\n if (chartBackground)\n return chartBackground;\n if (!mainRange) {\n return BACKGROUND_CHART_COLOR;\n }\n const col = mainRange.zone.left;\n const row = mainRange.zone.top;\n const sheetId = mainRange.sheetId;\n const style = this.getters.getCellComputedStyle({ sheetId, col, row });\n return style.fillColor || BACKGROUND_CHART_COLOR;\n }\n }\n EvaluationChartPlugin.getters = [\"getChartRuntime\", \"getBackgroundOfSingleCellChart\"];\n\n // -----------------------------------------------------------------------------\n // Constants\n // -----------------------------------------------------------------------------\n class EvaluationConditionalFormatPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.isStale = true;\n // stores the computed styles in the format of computedStyles.sheetName[col][row] = Style\n this.computedStyles = {};\n this.computedIcons = {};\n /**\n * Execute the predicate to know if a conditional formatting rule should be applied to a cell\n */\n this.rulePredicate = {\n CellIsRule: (cell, rule) => {\n if (cell.type === CellValueType.error) {\n return false;\n }\n const values = rule.values.map(parseLiteral);\n switch (rule.operator) {\n case \"IsEmpty\":\n return cell.value.toString().trim() === \"\";\n case \"IsNotEmpty\":\n return cell.value.toString().trim() !== \"\";\n case \"BeginsWith\":\n if (values[0] === \"\") {\n return false;\n }\n return cell.value.toString().startsWith(values[0].toString());\n case \"EndsWith\":\n if (values[0] === \"\") {\n return false;\n }\n return cell.value.toString().endsWith(values[0].toString());\n case \"Between\":\n return cell.value >= values[0] && cell.value <= values[1];\n case \"NotBetween\":\n return !(cell.value >= values[0] && cell.value <= values[1]);\n case \"ContainsText\":\n return cell.value.toString().indexOf(values[0].toString()) > -1;\n case \"NotContains\":\n return !cell.value || cell.value.toString().indexOf(values[0].toString()) == -1;\n case \"GreaterThan\":\n return cell.value > values[0];\n case \"GreaterThanOrEqual\":\n return cell.value >= values[0];\n case \"LessThan\":\n return cell.value < values[0];\n case \"LessThanOrEqual\":\n return cell.value <= values[0];\n case \"NotEqual\":\n if (values[0] === \"\") {\n return false;\n }\n return cell.value !== values[0];\n case \"Equal\":\n if (values[0] === \"\") {\n return true;\n }\n return cell.value === values[0];\n default:\n console.warn(_lt(\"Not implemented operator %s for kind of conditional formatting: %s\", rule.operator, rule.type));\n }\n return false;\n },\n };\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n handle(cmd) {\n if (invalidateCFEvaluationCommands.has(cmd.type) ||\n (cmd.type === \"UPDATE_CELL\" && \"content\" in cmd)) {\n this.isStale = true;\n }\n switch (cmd.type) {\n case \"ACTIVATE_SHEET\":\n const activeSheet = cmd.sheetIdTo;\n this.computedStyles[activeSheet] = this.computedStyles[activeSheet] || {};\n this.computedIcons[activeSheet] = this.computedIcons[activeSheet] || {};\n this.isStale = true;\n break;\n case \"AUTOFILL_CELL\":\n const sheetId = this.getters.getActiveSheetId();\n const cfOrigin = this.getters.getRulesByCell(sheetId, cmd.originCol, cmd.originRow);\n for (const cf of cfOrigin) {\n this.adaptRules(sheetId, cf, [toXC(cmd.col, cmd.row)], []);\n }\n break;\n case \"PASTE_CONDITIONAL_FORMAT\":\n this.pasteCf(cmd.origin, cmd.target, cmd.operation);\n break;\n }\n }\n finalize() {\n if (this.isStale) {\n this.computeStyles();\n this.isStale = false;\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getCellComputedStyle(position) {\n var _a;\n // TODO move this getter out of CF: it also depends on filters and link\n const { sheetId, col, row } = position;\n const cell = this.getters.getCell(position);\n const styles = this.computedStyles[sheetId];\n const cfStyle = styles && ((_a = styles[col]) === null || _a === void 0 ? void 0 : _a[row]);\n const computedStyle = {\n ...cell === null || cell === void 0 ? void 0 : cell.style,\n ...cfStyle,\n };\n const evaluatedCell = this.getters.getEvaluatedCell(position);\n if (evaluatedCell.link && !computedStyle.textColor) {\n computedStyle.textColor = LINK_COLOR;\n }\n if (this.getters.isFilterHeader(position)) {\n computedStyle.bold = true;\n }\n return computedStyle;\n }\n getConditionalIcon({ col, row }) {\n var _a;\n const activeSheet = this.getters.getActiveSheetId();\n const icon = this.computedIcons[activeSheet];\n return icon && ((_a = icon[col]) === null || _a === void 0 ? void 0 : _a[row]);\n }\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n /**\n * Compute the styles according to the conditional formatting.\n * This computation must happen after the cell values are computed if they change\n *\n * This result of the computation will be in the state.cell[XC].conditionalStyle and will be the union of all the style\n * properties of the rules applied (in order).\n * So if a cell has multiple conditional formatting applied to it, and each affect a different value of the style,\n * the resulting style will have the combination of all those values.\n * If multiple conditional formatting use the same style value, they will be applied in order so that the last applied wins\n */\n computeStyles() {\n var _a;\n const sheetId = this.getters.getActiveSheetId();\n this.computedStyles[sheetId] = {};\n this.computedIcons[sheetId] = {};\n const computedStyle = this.computedStyles[sheetId];\n for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {\n try {\n switch (cf.rule.type) {\n case \"ColorScaleRule\":\n for (let range of cf.ranges) {\n this.applyColorScale(range, cf.rule);\n }\n break;\n case \"IconSetRule\":\n for (let range of cf.ranges) {\n this.applyIcon(range, cf.rule);\n }\n break;\n default:\n for (let ref of cf.ranges) {\n const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;\n for (let row = zone.top; row <= zone.bottom; row++) {\n for (let col = zone.left; col <= zone.right; col++) {\n const pr = this.rulePredicate[cf.rule.type];\n let cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n if (pr && pr(cell, cf.rule)) {\n if (!computedStyle[col])\n computedStyle[col] = [];\n // we must combine all the properties of all the CF rules applied to the given cell\n computedStyle[col][row] = Object.assign(((_a = computedStyle[col]) === null || _a === void 0 ? void 0 : _a[row]) || {}, cf.rule.style);\n }\n }\n }\n }\n break;\n }\n }\n catch (_) {\n // we don't care about the errors within the evaluation of a rule\n }\n }\n }\n parsePoint(range, threshold, functionName) {\n const sheetId = this.getters.getActiveSheetId();\n const rangeValues = this.getters\n .getEvaluatedCellsInZone(sheetId, this.getters.getRangeFromSheetXC(sheetId, range).zone)\n .filter((cell) => cell.type === CellValueType.number)\n .map((cell) => cell.value);\n switch (threshold.type) {\n case \"value\":\n const result = functionName === \"max\" ? Math.max(...rangeValues) : Math.min(...rangeValues);\n return result;\n case \"number\":\n return Number(threshold.value);\n case \"percentage\":\n const min = Math.min(...rangeValues);\n const max = Math.max(...rangeValues);\n const delta = max - min;\n return min + (delta * Number(threshold.value)) / 100;\n case \"percentile\":\n return percentile(rangeValues, Number(threshold.value) / 100, true);\n case \"formula\":\n const value = threshold.value && this.getters.evaluateFormula(threshold.value);\n return !(value instanceof Promise) ? value : null;\n default:\n return null;\n }\n }\n applyIcon(range, rule) {\n const lowerInflectionPoint = this.parsePoint(range, rule.lowerInflectionPoint);\n const upperInflectionPoint = this.parsePoint(range, rule.upperInflectionPoint);\n if (lowerInflectionPoint === null ||\n upperInflectionPoint === null ||\n lowerInflectionPoint > upperInflectionPoint) {\n return;\n }\n const sheetId = this.getters.getActiveSheetId();\n const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;\n const computedIcons = this.computedIcons[sheetId];\n const iconSet = [rule.icons.upper, rule.icons.middle, rule.icons.lower];\n for (let row = zone.top; row <= zone.bottom; row++) {\n for (let col = zone.left; col <= zone.right; col++) {\n const cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n if (cell.type !== CellValueType.number) {\n continue;\n }\n const icon = this.computeIcon(cell.value, upperInflectionPoint, rule.upperInflectionPoint.operator, lowerInflectionPoint, rule.lowerInflectionPoint.operator, iconSet);\n if (!computedIcons[col]) {\n computedIcons[col] = [];\n }\n computedIcons[col][row] = icon;\n }\n }\n }\n computeIcon(value, upperInflectionPoint, upperOperator, lowerInflectionPoint, lowerOperator, icons) {\n if ((upperOperator === \"ge\" && value >= upperInflectionPoint) ||\n (upperOperator === \"gt\" && value > upperInflectionPoint)) {\n return icons[0];\n }\n else if ((lowerOperator === \"ge\" && value >= lowerInflectionPoint) ||\n (lowerOperator === \"gt\" && value > lowerInflectionPoint)) {\n return icons[1];\n }\n return icons[2];\n }\n applyColorScale(range, rule) {\n var _a;\n const minValue = this.parsePoint(range, rule.minimum, \"min\");\n const midValue = rule.midpoint ? this.parsePoint(range, rule.midpoint) : null;\n const maxValue = this.parsePoint(range, rule.maximum, \"max\");\n if (minValue === null ||\n maxValue === null ||\n minValue >= maxValue ||\n (midValue && (minValue >= midValue || midValue >= maxValue))) {\n return;\n }\n const sheetId = this.getters.getActiveSheetId();\n const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;\n const computedStyle = this.computedStyles[sheetId];\n const colorCellArgs = [];\n if (rule.midpoint && midValue) {\n colorCellArgs.push({\n minValue,\n minColor: rule.minimum.color,\n colorDiffUnit: this.computeColorDiffUnits(minValue, midValue, rule.minimum.color, rule.midpoint.color),\n });\n colorCellArgs.push({\n minValue: midValue,\n minColor: rule.midpoint.color,\n colorDiffUnit: this.computeColorDiffUnits(midValue, maxValue, rule.midpoint.color, rule.maximum.color),\n });\n }\n else {\n colorCellArgs.push({\n minValue,\n minColor: rule.minimum.color,\n colorDiffUnit: this.computeColorDiffUnits(minValue, maxValue, rule.minimum.color, rule.maximum.color),\n });\n }\n for (let row = zone.top; row <= zone.bottom; row++) {\n for (let col = zone.left; col <= zone.right; col++) {\n const cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n if (cell.type === CellValueType.number) {\n const value = clip(cell.value, minValue, maxValue);\n let color;\n if (colorCellArgs.length === 2 && midValue) {\n color =\n value <= midValue\n ? this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit)\n : this.colorCell(value, colorCellArgs[1].minValue, colorCellArgs[1].minColor, colorCellArgs[1].colorDiffUnit);\n }\n else {\n color = this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit);\n }\n if (!computedStyle[col])\n computedStyle[col] = [];\n computedStyle[col][row] = ((_a = computedStyle[col]) === null || _a === void 0 ? void 0 : _a[row]) || {};\n computedStyle[col][row].fillColor = colorNumberString(color);\n }\n }\n }\n }\n computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {\n const deltaValue = maxValue - minValue;\n const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);\n const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);\n const deltaColorB = (minColor % 256) - (maxColor % 256);\n const colorDiffUnitR = deltaColorR / deltaValue;\n const colorDiffUnitG = deltaColorG / deltaValue;\n const colorDiffUnitB = deltaColorB / deltaValue;\n return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];\n }\n colorCell(value, minValue, minColor, colorDiffUnit) {\n const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;\n const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));\n const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));\n const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));\n return (r << 16) | (g << 8) | b;\n }\n /**\n * Add or remove cells to a given conditional formatting rule.\n */\n adaptRules(sheetId, cf, toAdd, toRemove) {\n if (toAdd.length === 0 && toRemove.length === 0) {\n return;\n }\n const rules = this.getters.getConditionalFormats(sheetId);\n const replaceIndex = rules.findIndex((c) => c.id === cf.id);\n let currentRanges = [];\n if (replaceIndex > -1) {\n currentRanges = rules[replaceIndex].ranges;\n }\n currentRanges = currentRanges.concat(toAdd);\n const newRangesXC = recomputeZones(currentRanges, toRemove);\n this.dispatch(\"ADD_CONDITIONAL_FORMAT\", {\n cf: {\n id: cf.id,\n rule: cf.rule,\n stopIfTrue: cf.stopIfTrue,\n },\n ranges: newRangesXC.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),\n sheetId,\n });\n }\n pasteCf(origin, target, operation) {\n const xc = toXC(target.col, target.row);\n for (let rule of this.getters.getConditionalFormats(origin.sheetId)) {\n for (let range of rule.ranges) {\n if (isInside(origin.col, origin.row, this.getters.getRangeFromSheetXC(origin.sheetId, range).zone)) {\n const cf = rule;\n const toRemoveRange = [];\n if (operation === \"CUT\") {\n //remove from current rule\n toRemoveRange.push(toXC(origin.col, origin.row));\n }\n if (origin.sheetId === target.sheetId) {\n this.adaptRules(origin.sheetId, cf, [xc], toRemoveRange);\n }\n else {\n this.adaptRules(target.sheetId, cf, [xc], []);\n this.adaptRules(origin.sheetId, cf, [], toRemoveRange);\n }\n }\n }\n }\n }\n }\n EvaluationConditionalFormatPlugin.getters = [\"getConditionalIcon\", \"getCellComputedStyle\"];\n\n class FilterEvaluationPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.filterValues = {};\n this.hiddenRows = new Set();\n this.isEvaluationDirty = false;\n }\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"UPDATE_FILTER\":\n if (!this.getters.getFilterId(cmd)) {\n return 79 /* CommandResult.FilterNotFound */;\n }\n break;\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"UNDO\":\n case \"REDO\":\n case \"UPDATE_CELL\":\n case \"EVALUATE_CELLS\":\n case \"ACTIVATE_SHEET\":\n case \"REMOVE_FILTER_TABLE\":\n this.isEvaluationDirty = true;\n break;\n case \"START\":\n for (const sheetId of this.getters.getSheetIds()) {\n this.filterValues[sheetId] = {};\n for (const filter of this.getters.getFilters(sheetId)) {\n this.filterValues[sheetId][filter.id] = [];\n }\n }\n break;\n case \"CREATE_SHEET\":\n this.filterValues[cmd.sheetId] = {};\n break;\n case \"HIDE_COLUMNS_ROWS\":\n this.updateHiddenRows();\n break;\n case \"UPDATE_FILTER\":\n this.updateFilter(cmd);\n this.updateHiddenRows();\n break;\n case \"DUPLICATE_SHEET\":\n const filterValues = {};\n for (const newFilter of this.getters.getFilters(cmd.sheetIdTo)) {\n const zone = newFilter.zoneWithHeaders;\n filterValues[newFilter.id] = this.getFilterValues({\n sheetId: cmd.sheetId,\n col: zone.left,\n row: zone.top,\n });\n }\n this.filterValues[cmd.sheetIdTo] = filterValues;\n break;\n // If we don't handle DELETE_SHEET, on one hand we will have some residual data, on the other hand we keep the data\n // on DELETE_SHEET followed by undo\n }\n }\n finalize() {\n if (this.isEvaluationDirty) {\n this.updateHiddenRows();\n this.isEvaluationDirty = false;\n }\n }\n isRowFiltered(sheetId, row) {\n if (sheetId !== this.getters.getActiveSheetId()) {\n return false;\n }\n return this.hiddenRows.has(row);\n }\n getCellBorderWithFilterBorder(position) {\n const { sheetId, col, row } = position;\n let filterBorder = undefined;\n for (let filters of this.getters.getFilterTables(sheetId)) {\n const zone = filters.zone;\n if (isInside(col, row, zone)) {\n // The borders should be at the edges of the visible zone of the filter\n const visibleZone = this.intersectZoneWithViewport(sheetId, zone);\n filterBorder = {\n top: row === visibleZone.top ? DEFAULT_FILTER_BORDER_DESC : undefined,\n bottom: row === visibleZone.bottom ? DEFAULT_FILTER_BORDER_DESC : undefined,\n left: col === visibleZone.left ? DEFAULT_FILTER_BORDER_DESC : undefined,\n right: col === visibleZone.right ? DEFAULT_FILTER_BORDER_DESC : undefined,\n };\n }\n }\n const cellBorder = this.getters.getCellBorder(position);\n // Use removeFalsyAttributes to avoid overwriting filter borders with undefined values\n const border = { ...filterBorder, ...removeFalsyAttributes(cellBorder || {}) };\n return isObjectEmptyRecursive(border) ? null : border;\n }\n getFilterHeaders(sheetId) {\n const headers = [];\n for (let filters of this.getters.getFilterTables(sheetId)) {\n const zone = filters.zone;\n if (!zone) {\n continue;\n }\n const row = zone.top;\n for (let col = zone.left; col <= zone.right; col++) {\n if (this.getters.isColHidden(sheetId, col) || this.getters.isRowHidden(sheetId, row)) {\n continue;\n }\n headers.push({ col, row });\n }\n }\n return headers;\n }\n getFilterValues(position) {\n const id = this.getters.getFilterId(position);\n const sheetId = position.sheetId;\n if (!id || !this.filterValues[sheetId])\n return [];\n return this.filterValues[sheetId][id] || [];\n }\n isFilterHeader({ sheetId, col, row }) {\n const headers = this.getFilterHeaders(sheetId);\n return headers.some((header) => header.col === col && header.row === row);\n }\n isFilterActive(position) {\n var _a, _b;\n const id = this.getters.getFilterId(position);\n const sheetId = position.sheetId;\n return Boolean(id && ((_b = (_a = this.filterValues[sheetId]) === null || _a === void 0 ? void 0 : _a[id]) === null || _b === void 0 ? void 0 : _b.length));\n }\n intersectZoneWithViewport(sheetId, zone) {\n const colsRange = range(zone.left, zone.right + 1);\n const rowsRange = range(zone.top, zone.bottom + 1);\n return {\n left: this.getters.findVisibleHeader(sheetId, \"COL\", colsRange),\n right: this.getters.findVisibleHeader(sheetId, \"COL\", colsRange.reverse()),\n top: this.getters.findVisibleHeader(sheetId, \"ROW\", rowsRange),\n bottom: this.getters.findVisibleHeader(sheetId, \"ROW\", rowsRange.reverse()),\n };\n }\n updateFilter({ col, row, values, sheetId }) {\n const id = this.getters.getFilterId({ sheetId, col, row });\n if (!id)\n return;\n if (!this.filterValues[sheetId])\n this.filterValues[sheetId] = {};\n this.filterValues[sheetId][id] = values;\n }\n updateHiddenRows() {\n var _a, _b;\n const sheetId = this.getters.getActiveSheetId();\n const filters = this.getters\n .getFilters(sheetId)\n .sort((filter1, filter2) => filter1.zoneWithHeaders.top - filter2.zoneWithHeaders.top);\n const hiddenRows = new Set();\n for (let filter of filters) {\n // Disable filters whose header are hidden\n if (this.getters.isRowHiddenByUser(sheetId, filter.zoneWithHeaders.top))\n continue;\n if (hiddenRows.has(filter.zoneWithHeaders.top))\n continue;\n const filteredValues = (_b = (_a = this.filterValues[sheetId]) === null || _a === void 0 ? void 0 : _a[filter.id]) === null || _b === void 0 ? void 0 : _b.map(toLowerCase);\n if (!filteredValues || !filter.filteredZone)\n continue;\n for (let row = filter.filteredZone.top; row <= filter.filteredZone.bottom; row++) {\n const value = this.getCellValueAsString(sheetId, filter.col, row);\n if (filteredValues.includes(value)) {\n hiddenRows.add(row);\n }\n }\n }\n this.hiddenRows = hiddenRows;\n }\n getCellValueAsString(sheetId, col, row) {\n const value = this.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue;\n return value.toLowerCase();\n }\n exportForExcel(data) {\n for (const sheetData of data.sheets) {\n for (const tableData of sheetData.filterTables) {\n const tableZone = toZone(tableData.range);\n const filters = [];\n const headerNames = [];\n for (const i of range(0, zoneToDimension(tableZone).width)) {\n const position = {\n sheetId: sheetData.id,\n col: tableZone.left + i,\n row: tableZone.top,\n };\n const filteredValues = this.getFilterValues(position);\n const filter = this.getters.getFilter(position);\n if (!filter)\n continue;\n const valuesInFilterZone = filter.filteredZone\n ? positions(filter.filteredZone)\n .map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId: sheetData.id, col, row }))\n .filter((cell) => cell.type !== CellValueType.empty)\n .map((cell) => cell.formattedValue)\n : [];\n // In xlsx, filtered values = values that are displayed, not values that are hidden\n const xlsxFilteredValues = valuesInFilterZone.filter((val) => !filteredValues.includes(val));\n filters.push({ colId: i, filteredValues: [...new Set(xlsxFilteredValues)] });\n // In xlsx, filter header should ALWAYS be a string and should be unique\n const headerPosition = {\n col: filter.col,\n row: filter.zoneWithHeaders.top,\n sheetId: sheetData.id,\n };\n const headerString = this.getters.getEvaluatedCell(headerPosition).formattedValue;\n const headerName = this.getUniqueColNameForExcel(i, headerString, headerNames);\n headerNames.push(headerName);\n sheetData.cells[toXC(headerPosition.col, headerPosition.row)] = {\n ...sheetData.cells[toXC(headerPosition.col, headerPosition.row)],\n content: headerName,\n value: headerName,\n isFormula: false,\n };\n }\n tableData.filters = filters;\n }\n }\n }\n /**\n * Get an unique column name for the column at colIndex. If the column name is already in the array of used column names,\n * concatenate a number to the name until we find a new unique name (eg. \"ColName\" => \"ColName1\" => \"ColName2\" ...)\n */\n getUniqueColNameForExcel(colIndex, colName, usedColNames) {\n if (!colName) {\n colName = `Column${colIndex}`;\n }\n let currentColName = colName;\n let i = 2;\n while (usedColNames.includes(currentColName)) {\n currentColName = colName + String(i);\n i++;\n }\n return currentColName;\n }\n }\n FilterEvaluationPlugin.getters = [\n \"getCellBorderWithFilterBorder\",\n \"getFilterHeaders\",\n \"getFilterValues\",\n \"isFilterHeader\",\n \"isRowFiltered\",\n \"isFilterActive\",\n ];\n\n class InternalViewport {\n constructor(getters, sheetId, boundaries, sizeInGrid, options, offsets) {\n this.getters = getters;\n this.sheetId = sheetId;\n this.boundaries = boundaries;\n this.width = sizeInGrid.width;\n this.height = sizeInGrid.height;\n this.offsetScrollbarX = offsets.x;\n this.offsetScrollbarY = offsets.y;\n this.canScrollVertically = options.canScrollVertically;\n this.canScrollHorizontally = options.canScrollHorizontally;\n this.offsetCorrectionX = this.getters.getColDimensions(this.sheetId, this.boundaries.left).start;\n this.offsetCorrectionY = this.getters.getRowDimensions(this.sheetId, this.boundaries.top).start;\n this.adjustViewportOffsetX();\n this.adjustViewportOffsetY();\n }\n // PUBLIC\n getMaxSize() {\n const lastCol = this.getters.findLastVisibleColRowIndex(this.sheetId, \"COL\", {\n first: this.boundaries.left,\n last: this.boundaries.right,\n });\n const lastRow = this.getters.findLastVisibleColRowIndex(this.sheetId, \"ROW\", {\n first: this.boundaries.top,\n last: this.boundaries.bottom,\n });\n const { end: lastColEnd, size: lastColSize } = this.getters.getColDimensions(this.sheetId, lastCol);\n const { end: lastRowEnd, size: lastRowSize } = this.getters.getRowDimensions(this.sheetId, lastRow);\n const leftColIndex = this.searchHeaderIndex(\"COL\", lastColEnd - this.width, 0);\n const leftColSize = this.getters.getColSize(this.sheetId, leftColIndex);\n const leftRowIndex = this.searchHeaderIndex(\"ROW\", lastRowEnd - this.height, 0);\n const topRowSize = this.getters.getRowSize(this.sheetId, leftRowIndex);\n const width = lastColEnd -\n this.offsetCorrectionX +\n (this.canScrollHorizontally\n ? Math.max(DEFAULT_CELL_WIDTH, Math.min(leftColSize, this.width - lastColSize))\n : 0);\n const height = lastRowEnd -\n this.offsetCorrectionY +\n (this.canScrollVertically\n ? Math.max(DEFAULT_CELL_HEIGHT + 5, Math.min(topRowSize, this.height - lastRowSize))\n : 0);\n return { width, height };\n }\n /**\n * Return the index of a column given an offset x, based on the pane left\n * visible cell.\n * It returns -1 if no column is found.\n */\n getColIndex(x, absolute = false) {\n if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.width) {\n return -1;\n }\n return this.searchHeaderIndex(\"COL\", x - this.offsetCorrectionX, this.left, absolute);\n }\n /**\n * Return the index of a row given an offset y, based on the pane top\n * visible cell.\n * It returns -1 if no row is found.\n */\n getRowIndex(y, absolute = false) {\n if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.height) {\n return -1;\n }\n return this.searchHeaderIndex(\"ROW\", y - this.offsetCorrectionY, this.top, absolute);\n }\n /**\n * This function will make sure that the provided cell position (or current selected position) is part of\n * the pane that is actually displayed on the client. We therefore adjust the offset of the pane\n * until it contains the cell completely.\n */\n adjustPosition(position) {\n const sheetId = this.sheetId;\n if (!position) {\n position = this.getters.getSheetPosition(sheetId);\n }\n const mainCellPosition = this.getters.getMainCellPosition({ sheetId, ...position });\n const { col, row } = this.getters.getNextVisibleCellPosition(mainCellPosition);\n if (isInside(col, this.boundaries.top, this.boundaries)) {\n this.adjustPositionX(col);\n }\n if (isInside(this.boundaries.left, row, this.boundaries)) {\n this.adjustPositionY(row);\n }\n }\n adjustPositionX(col) {\n const sheetId = this.sheetId;\n const { start, end } = this.getters.getColDimensions(sheetId, col);\n while (end > this.offsetX + this.offsetCorrectionX + this.width &&\n this.offsetX + this.offsetCorrectionX < start) {\n this.offsetX = this.getters.getColDimensions(sheetId, this.left).end - this.offsetCorrectionX;\n this.offsetScrollbarX = this.offsetX;\n this.adjustViewportZoneX();\n }\n while (col < this.left) {\n let leftCol;\n for (leftCol = this.left; leftCol >= 0; leftCol--) {\n if (!this.getters.isColHidden(sheetId, leftCol)) {\n break;\n }\n }\n this.offsetX =\n this.getters.getColDimensions(sheetId, leftCol - 1).start - this.offsetCorrectionX;\n this.offsetScrollbarX = this.offsetX;\n this.adjustViewportZoneX();\n }\n }\n adjustPositionY(row) {\n const sheetId = this.sheetId;\n while (this.getters.getRowDimensions(sheetId, row).end >\n this.offsetY + this.height + this.offsetCorrectionY &&\n this.offsetY + this.offsetCorrectionY < this.getters.getRowDimensions(sheetId, row).start) {\n this.offsetY = this.getters.getRowDimensions(sheetId, this.top).end - this.offsetCorrectionY;\n this.offsetScrollbarY = this.offsetY;\n this.adjustViewportZoneY();\n }\n while (row < this.top) {\n let topRow;\n for (topRow = this.top; topRow >= 0; topRow--) {\n if (!this.getters.isRowHidden(sheetId, topRow)) {\n break;\n }\n }\n this.offsetY =\n this.getters.getRowDimensions(sheetId, topRow - 1).start - this.offsetCorrectionY;\n this.offsetScrollbarY = this.offsetY;\n this.adjustViewportZoneY();\n }\n }\n setViewportOffset(offsetX, offsetY) {\n this.setViewportOffsetX(offsetX);\n this.setViewportOffsetY(offsetY);\n }\n adjustViewportZone() {\n this.adjustViewportZoneX();\n this.adjustViewportZoneY();\n }\n /**\n *\n * @param zone\n * @returns Computes the absolute coordinate of a given zone inside the viewport\n */\n getRect(zone) {\n const targetZone = intersection(zone, this.zone);\n if (targetZone) {\n const x = this.getters.getColRowOffset(\"COL\", this.zone.left, targetZone.left) +\n this.offsetCorrectionX;\n const y = this.getters.getColRowOffset(\"ROW\", this.zone.top, targetZone.top) + this.offsetCorrectionY;\n const width = Math.min(this.getters.getColRowOffset(\"COL\", targetZone.left, targetZone.right + 1), this.width);\n const height = Math.min(this.getters.getColRowOffset(\"ROW\", targetZone.top, targetZone.bottom + 1), this.height);\n return {\n x,\n y,\n width,\n height,\n };\n }\n else {\n return undefined;\n }\n }\n isVisible(col, row) {\n const isInside = row <= this.bottom && row >= this.top && col >= this.left && col <= this.right;\n return (isInside &&\n !this.getters.isColHidden(this.sheetId, col) &&\n !this.getters.isRowHidden(this.sheetId, row));\n }\n // PRIVATE\n searchHeaderIndex(dimension, position, startIndex = 0, absolute = false) {\n let size = 0;\n const sheetId = this.sheetId;\n const headers = this.getters.getNumberHeaders(sheetId, dimension);\n for (let i = startIndex; i <= headers - 1; i++) {\n const isHiddenInViewport = !absolute && dimension === \"COL\"\n ? i < this.left && i > this.right\n : i < this.top && i > this.bottom;\n if (this.getters.isHeaderHidden(sheetId, dimension, i) || isHiddenInViewport) {\n continue;\n }\n size +=\n dimension === \"COL\"\n ? this.getters.getColSize(sheetId, i)\n : this.getters.getRowSize(sheetId, i);\n if (size > position) {\n return i;\n }\n }\n return -1;\n }\n get zone() {\n return { left: this.left, right: this.right, top: this.top, bottom: this.bottom };\n }\n setViewportOffsetX(offsetX) {\n if (!this.canScrollHorizontally) {\n return;\n }\n this.offsetScrollbarX = offsetX;\n this.adjustViewportZoneX();\n }\n setViewportOffsetY(offsetY) {\n if (!this.canScrollVertically) {\n return;\n }\n this.offsetScrollbarY = offsetY;\n this.adjustViewportZoneY();\n }\n /** Corrects the viewport's horizontal offset based on the current structure\n * To make sure that at least on column is visible inside the viewport.\n */\n adjustViewportOffsetX() {\n if (this.canScrollHorizontally) {\n const { width: viewportWidth } = this.getMaxSize();\n if (this.width + this.offsetScrollbarX > viewportWidth) {\n this.offsetScrollbarX = Math.max(0, viewportWidth - this.width);\n }\n }\n this.left = this.getColIndex(this.offsetScrollbarX, true);\n this.right = this.getColIndex(this.offsetScrollbarX + this.width, true);\n if (this.right === -1) {\n this.right = this.boundaries.right;\n }\n this.adjustViewportZoneX();\n }\n /** Corrects the viewport's vertical offset based on the current structure\n * To make sure that at least on row is visible inside the viewport.\n */\n adjustViewportOffsetY() {\n if (this.canScrollVertically) {\n const { height: paneHeight } = this.getMaxSize();\n if (this.height + this.offsetScrollbarY > paneHeight) {\n this.offsetScrollbarY = Math.max(0, paneHeight - this.height);\n }\n }\n this.top = this.getRowIndex(this.offsetScrollbarY, true);\n this.bottom = this.getRowIndex(this.offsetScrollbarY + this.width, true);\n if (this.bottom === -1) {\n this.bottom = this.boundaries.bottom;\n }\n this.adjustViewportZoneY();\n }\n /** Updates the pane zone and snapped offset based on its horizontal\n * offset (will find Left) and its width (will find Right) */\n adjustViewportZoneX() {\n const sheetId = this.sheetId;\n this.left = this.searchHeaderIndex(\"COL\", this.offsetScrollbarX, this.boundaries.left);\n this.right = Math.min(this.boundaries.right, this.searchHeaderIndex(\"COL\", this.width, this.left));\n if (this.right === -1) {\n this.right = this.getters.getNumberCols(sheetId) - 1;\n }\n this.offsetX =\n this.getters.getColDimensions(sheetId, this.left).start -\n this.getters.getColDimensions(sheetId, this.boundaries.left).start;\n }\n /** Updates the pane zone and snapped offset based on its vertical\n * offset (will find Top) and its width (will find Bottom) */\n adjustViewportZoneY() {\n const sheetId = this.sheetId;\n this.top = this.searchHeaderIndex(\"ROW\", this.offsetScrollbarY, this.boundaries.top);\n this.bottom = Math.min(this.boundaries.bottom, this.searchHeaderIndex(\"ROW\", this.height, this.top));\n if (this.bottom === -1) {\n this.bottom = this.getters.getNumberRows(sheetId) - 1;\n }\n this.offsetY =\n this.getters.getRowDimensions(sheetId, this.top).start -\n this.getters.getRowDimensions(sheetId, this.boundaries.top).start;\n }\n }\n\n /**\n * EdgeScrollCases Schema\n *\n * The dots/double dots represent a freeze (= a split of viewports)\n * In this example, we froze vertically between columns D and E\n * and horizontally between rows 4 and 5.\n *\n * One can see that we scrolled horizontally from column E to G and\n * vertically from row 5 to 7.\n *\n * A B C D G H I J K L M N O P Q R S T\n * _______________________________________________________\n * 1 | : |\n * 2 | : |\n * 3 | : B \u2191 6 |\n * 4 | : | | | |\n * \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7+\u00b7\u00b7\u00b7+\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7+\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\n * 7 | : | | | |\n * 8 | : \u2193 2 | |\n * 9 | : | |\n * 10 | A --+--\u2192 | |\n * 11 | : | |\n * 12 | : | |\n * 13 | \u2190--+-- 1 | |\n * 14 | : | 3 --+--\u2192\n * 15 | : | |\n * 16 | : | |\n * 17 | 5 --+-------------------------------------------+--\u2192\n * 18 | : | |\n * 19 | : 4 | |\n * 20 | : | | |\n * ______________________________+___________| ____________\n * | |\n * \u2193 \u2193\n */\n /**\n * Viewport plugin.\n *\n * This plugin manages all things related to all viewport states.\n *\n */\n class SheetViewPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.viewports = {};\n /**\n * The viewport dimensions are usually set by one of the components\n * (i.e. when grid component is mounted) to properly reflect its state in the DOM.\n * In the absence of a component (standalone model), is it mandatory to set reasonable default values\n * to ensure the correct operation of this plugin.\n */\n this.sheetViewWidth = DEFAULT_SHEETVIEW_SIZE;\n this.sheetViewHeight = DEFAULT_SHEETVIEW_SIZE;\n this.gridOffsetX = 0;\n this.gridOffsetY = 0;\n this.sheetsWithDirtyViewports = new Set();\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"SET_VIEWPORT_OFFSET\":\n return this.checkScrollingDirection(cmd);\n case \"RESIZE_SHEETVIEW\":\n return this.chainValidations(this.checkValuesAreDifferent, this.checkPositiveDimension)(cmd);\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handleEvent(event) {\n switch (event.type) {\n case \"HeadersSelected\":\n case \"AlterZoneCorner\":\n break;\n case \"ZonesSelected\":\n let { col, row } = findCellInNewZone(event.previousAnchor.zone, event.anchor.zone);\n if (event.mode === \"updateAnchor\") {\n const oldZone = event.previousAnchor.zone;\n const newZone = event.anchor.zone;\n // altering a zone should not move the viewport in a dimension that wasn't changed\n const { top, bottom, left, right } = this.getters.getActiveMainViewport();\n if (oldZone.left === newZone.left && oldZone.right === newZone.right) {\n col = left > col || col > right ? left : col;\n }\n if (oldZone.top === newZone.top && oldZone.bottom === newZone.bottom) {\n row = top > row || row > bottom ? top : row;\n }\n }\n const sheetId = this.getters.getActiveSheetId();\n col = Math.min(col, this.getters.getNumberCols(sheetId) - 1);\n row = Math.min(row, this.getters.getNumberRows(sheetId) - 1);\n this.refreshViewport(this.getters.getActiveSheetId(), { col, row });\n break;\n }\n }\n handle(cmd) {\n var _a;\n this.cleanViewports();\n switch (cmd.type) {\n case \"START\":\n this.selection.observe(this, {\n handleEvent: this.handleEvent.bind(this),\n });\n this.resetViewports(this.getters.getActiveSheetId());\n break;\n case \"UNDO\":\n case \"REDO\":\n this.resetSheetViews();\n break;\n case \"RESIZE_SHEETVIEW\":\n this.resizeSheetView(cmd.height, cmd.width, cmd.gridOffsetX, cmd.gridOffsetY);\n break;\n case \"SET_VIEWPORT_OFFSET\":\n this.setSheetViewOffset(cmd.offsetX, cmd.offsetY);\n break;\n case \"SHIFT_VIEWPORT_DOWN\":\n const { top } = this.getActiveMainViewport();\n const sheetId = this.getters.getActiveSheetId();\n const shiftedOffsetY = this.clipOffsetY(this.getters.getRowDimensions(sheetId, top).start + this.sheetViewHeight);\n this.shiftVertically(shiftedOffsetY);\n break;\n case \"SHIFT_VIEWPORT_UP\": {\n const { top } = this.getActiveMainViewport();\n const sheetId = this.getters.getActiveSheetId();\n const shiftedOffsetY = this.clipOffsetY(this.getters.getRowDimensions(sheetId, top).end - this.sheetViewHeight);\n this.shiftVertically(shiftedOffsetY);\n break;\n }\n case \"REMOVE_COLUMNS_ROWS\":\n case \"RESIZE_COLUMNS_ROWS\":\n case \"HIDE_COLUMNS_ROWS\":\n case \"ADD_COLUMNS_ROWS\":\n case \"UNHIDE_COLUMNS_ROWS\":\n case \"UPDATE_FILTER\":\n this.resetViewports(cmd.sheetId);\n break;\n case \"UPDATE_CELL\":\n // update cell content or format can change hidden rows because of data filters\n if (\"content\" in cmd || \"format\" in cmd || ((_a = cmd.style) === null || _a === void 0 ? void 0 : _a.fontSize) !== undefined) {\n this.sheetsWithDirtyViewports.add(cmd.sheetId);\n }\n break;\n case \"ACTIVATE_SHEET\":\n this.setViewports();\n this.refreshViewport(cmd.sheetIdTo);\n break;\n case \"UNFREEZE_ROWS\":\n case \"UNFREEZE_COLUMNS\":\n case \"FREEZE_COLUMNS\":\n case \"FREEZE_ROWS\":\n case \"UNFREEZE_COLUMNS_ROWS\":\n this.resetViewports(this.getters.getActiveSheetId());\n break;\n case \"DELETE_SHEET\":\n this.sheetsWithDirtyViewports.delete(cmd.sheetId);\n break;\n case \"START_EDITION\":\n const { col, row } = this.getters.getActivePosition();\n this.refreshViewport(this.getters.getActiveSheetId(), { col, row });\n break;\n }\n }\n finalize() {\n for (const sheetId of this.sheetsWithDirtyViewports) {\n this.resetViewports(sheetId);\n }\n this.sheetsWithDirtyViewports = new Set();\n this.setViewports();\n }\n setViewports() {\n var _a;\n const sheetIds = this.getters.getSheetIds();\n for (const sheetId of sheetIds) {\n if (!((_a = this.viewports[sheetId]) === null || _a === void 0 ? void 0 : _a.bottomRight)) {\n this.resetViewports(sheetId);\n }\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n /**\n * Return the index of a column given an offset x, based on the viewport left\n * visible cell.\n * It returns -1 if no column is found.\n */\n getColIndex(x) {\n const sheetId = this.getters.getActiveSheetId();\n return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getColIndex(x)));\n }\n /**\n * Return the index of a row given an offset y, based on the viewport top\n * visible cell.\n * It returns -1 if no row is found.\n */\n getRowIndex(y) {\n const sheetId = this.getters.getActiveSheetId();\n return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getRowIndex(y)));\n }\n getSheetViewDimensionWithHeaders() {\n return {\n width: this.sheetViewWidth + this.gridOffsetX,\n height: this.sheetViewHeight + this.gridOffsetY,\n };\n }\n getSheetViewDimension() {\n return {\n width: this.sheetViewWidth,\n height: this.sheetViewHeight,\n };\n }\n /** type as pane, not viewport but basically pane extends viewport */\n getActiveMainViewport() {\n const sheetId = this.getters.getActiveSheetId();\n return this.getMainViewport(sheetId);\n }\n /**\n * Return the scroll info of the active sheet, ie. the offset between the viewport left/top side and\n * the grid left/top side, snapped to the columns/rows.\n */\n getActiveSheetScrollInfo() {\n const sheetId = this.getters.getActiveSheetId();\n const viewport = this.getMainInternalViewport(sheetId);\n return {\n scrollX: viewport.offsetX,\n scrollY: viewport.offsetY,\n };\n }\n /**\n * Return the DOM scroll info of the active sheet, ie. the offset between the viewport left/top side and\n * the grid left/top side, corresponding to the scroll of the scrollbars and not snapped to the grid.\n */\n getActiveSheetDOMScrollInfo() {\n const sheetId = this.getters.getActiveSheetId();\n const viewport = this.getMainInternalViewport(sheetId);\n return {\n scrollX: viewport.offsetScrollbarX,\n scrollY: viewport.offsetScrollbarY,\n };\n }\n getSheetViewVisibleCols() {\n const sheetId = this.getters.getActiveSheetId();\n const viewports = this.getSubViewports(sheetId);\n return [...new Set(viewports.map((v) => range(v.left, v.right + 1)).flat())].filter((col) => !this.getters.isHeaderHidden(sheetId, \"COL\", col));\n }\n getSheetViewVisibleRows() {\n const sheetId = this.getters.getActiveSheetId();\n const viewports = this.getSubViewports(sheetId);\n return [...new Set(viewports.map((v) => range(v.top, v.bottom + 1)).flat())].filter((row) => !this.getters.isHeaderHidden(sheetId, \"ROW\", row));\n }\n /**\n * Return the main viewport maximum size. That is the zone dimension\n * with some bottom and right padding.\n */\n getMainViewportRect() {\n const sheetId = this.getters.getActiveSheetId();\n const viewport = this.getMainInternalViewport(sheetId);\n const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n let { width, height } = viewport.getMaxSize();\n const x = this.getters.getColDimensions(sheetId, xSplit).start;\n const y = this.getters.getRowDimensions(sheetId, ySplit).start;\n return { x, y, width, height };\n }\n getMaximumSheetOffset() {\n const sheetId = this.getters.getActiveSheetId();\n const { width, height } = this.getMainViewportRect();\n const viewport = this.getMainInternalViewport(sheetId);\n return {\n maxOffsetX: Math.max(0, width - viewport.width + 1),\n maxOffsetY: Math.max(0, height - viewport.height + 1),\n };\n }\n getColRowOffsetInViewport(dimension, referenceIndex, index) {\n const sheetId = this.getters.getActiveSheetId();\n const visibleCols = this.getters.getSheetViewVisibleCols();\n const visibleRows = this.getters.getSheetViewVisibleRows();\n if (index < referenceIndex) {\n return -this.getColRowOffsetInViewport(dimension, index, referenceIndex);\n }\n let offset = 0;\n const visibleIndexes = dimension === \"COL\" ? visibleCols : visibleRows;\n for (let i = referenceIndex; i < index; i++) {\n if (!visibleIndexes.includes(i)) {\n continue;\n }\n offset +=\n dimension === \"COL\"\n ? this.getters.getColSize(sheetId, i)\n : this.getters.getRowSize(sheetId, i);\n }\n return offset;\n }\n /**\n * Check if a given position is visible in the viewport.\n */\n isVisibleInViewport({ sheetId, col, row }) {\n return this.getSubViewports(sheetId).some((pane) => pane.isVisible(col, row));\n }\n // => return s the new offset\n getEdgeScrollCol(x, previousX, startingX) {\n let canEdgeScroll = false;\n let direction = 0;\n let delay = 0;\n /** 4 cases : See EdgeScrollCases Schema at the top\n * 1. previous in XRight > XLeft\n * 3. previous in XRight > outside\n * 5. previous in Left > outside\n * A. previous in Left > right\n * with X a position taken in the bottomRIght (aka scrollable) viewport\n */\n const { xSplit } = this.getters.getPaneDivisions(this.getters.getActiveSheetId());\n const { width } = this.getSheetViewDimension();\n const { x: offsetCorrectionX } = this.getMainViewportCoordinates();\n const currentOffsetX = this.getActiveSheetScrollInfo().scrollX;\n if (x > width) {\n // 3 & 5\n canEdgeScroll = true;\n delay = scrollDelay(x - width);\n direction = 1;\n }\n else if (x < offsetCorrectionX && startingX >= offsetCorrectionX && currentOffsetX > 0) {\n // 1\n canEdgeScroll = true;\n delay = scrollDelay(offsetCorrectionX - x);\n direction = -1;\n }\n else if (xSplit && previousX < offsetCorrectionX && x > offsetCorrectionX) {\n // A\n canEdgeScroll = true;\n delay = scrollDelay(x);\n direction = \"reset\";\n }\n return { canEdgeScroll, direction, delay };\n }\n getEdgeScrollRow(y, previousY, tartingY) {\n let canEdgeScroll = false;\n let direction = 0;\n let delay = 0;\n /** 4 cases : See EdgeScrollCases Schema at the top\n * 2. previous in XBottom > XTop\n * 4. previous in XRight > outside\n * 6. previous in Left > outside\n * B. previous in Left > right\n * with X a position taken in the bottomRIght (aka scrollable) viewport\n */\n const { ySplit } = this.getters.getPaneDivisions(this.getters.getActiveSheetId());\n const { height } = this.getSheetViewDimension();\n const { y: offsetCorrectionY } = this.getMainViewportCoordinates();\n const currentOffsetY = this.getActiveSheetScrollInfo().scrollY;\n if (y > height) {\n // 4 & 6\n canEdgeScroll = true;\n delay = scrollDelay(y - height);\n direction = 1;\n }\n else if (y < offsetCorrectionY && tartingY >= offsetCorrectionY && currentOffsetY > 0) {\n // 2\n canEdgeScroll = true;\n delay = scrollDelay(offsetCorrectionY - y);\n direction = -1;\n }\n else if (ySplit && previousY < offsetCorrectionY && y > offsetCorrectionY) {\n // B\n canEdgeScroll = true;\n delay = scrollDelay(y);\n direction = \"reset\";\n }\n return { canEdgeScroll, direction, delay };\n }\n /**\n * Computes the coordinates and size to draw the zone on the canvas\n */\n getVisibleRect(zone) {\n const sheetId = this.getters.getActiveSheetId();\n const viewportRects = this.getSubViewports(sheetId)\n .map((viewport) => viewport.getRect(zone))\n .filter(isDefined$1);\n if (viewportRects.length === 0) {\n return { x: 0, y: 0, width: 0, height: 0 };\n }\n const x = Math.min(...viewportRects.map((rect) => rect.x));\n const y = Math.min(...viewportRects.map((rect) => rect.y));\n const width = Math.max(...viewportRects.map((rect) => rect.x + rect.width)) - x;\n const height = Math.max(...viewportRects.map((rect) => rect.y + rect.height)) - y;\n return {\n x: x + this.gridOffsetX,\n y: y + this.gridOffsetY,\n width,\n height,\n };\n }\n /**\n * Returns the position of the MainViewport relatively to the start of the grid (without headers)\n * It corresponds to the summed dimensions of the visible cols/rows (in x/y respectively)\n * situated before the pane divisions.\n */\n getMainViewportCoordinates() {\n const sheetId = this.getters.getActiveSheetId();\n const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n const x = this.getters.getColDimensions(sheetId, xSplit).start;\n const y = this.getters.getRowDimensions(sheetId, ySplit).start;\n return { x, y };\n }\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n ensureMainViewportExist(sheetId) {\n if (!this.viewports[sheetId]) {\n this.resetViewports(sheetId);\n }\n }\n getSubViewports(sheetId) {\n this.ensureMainViewportExist(sheetId);\n return Object.values(this.viewports[sheetId]).filter(isDefined$1);\n }\n checkPositiveDimension(cmd) {\n if (cmd.width < 0 || cmd.height < 0) {\n return 68 /* CommandResult.InvalidViewportSize */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkValuesAreDifferent(cmd) {\n const { height, width } = this.getSheetViewDimension();\n if (cmd.gridOffsetX === this.gridOffsetX &&\n cmd.gridOffsetY === this.gridOffsetY &&\n cmd.width === width &&\n cmd.height === height) {\n return 76 /* CommandResult.ValuesNotChanged */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkScrollingDirection({ offsetX, offsetY, }) {\n const pane = this.getMainInternalViewport(this.getters.getActiveSheetId());\n if ((!pane.canScrollHorizontally && offsetX > 0) ||\n (!pane.canScrollVertically && offsetY > 0)) {\n return 69 /* CommandResult.InvalidScrollingDirection */;\n }\n return 0 /* CommandResult.Success */;\n }\n getMainViewport(sheetId) {\n const viewport = this.getMainInternalViewport(sheetId);\n return {\n top: viewport.top,\n left: viewport.left,\n bottom: viewport.bottom,\n right: viewport.right,\n };\n }\n getMainInternalViewport(sheetId) {\n this.ensureMainViewportExist(sheetId);\n return this.viewports[sheetId].bottomRight;\n }\n /** gets rid of deprecated sheetIds */\n cleanViewports() {\n const sheetIds = this.getters.getSheetIds();\n for (let sheetId of Object.keys(this.viewports)) {\n if (!sheetIds.includes(sheetId)) {\n delete this.viewports[sheetId];\n }\n }\n }\n resetSheetViews() {\n for (let sheetId of Object.keys(this.viewports)) {\n const position = this.getters.getSheetPosition(sheetId);\n this.resetViewports(sheetId);\n const viewports = this.getSubViewports(sheetId);\n Object.values(viewports).forEach((viewport) => {\n viewport.adjustPosition(position);\n });\n }\n }\n resizeSheetView(height, width, gridOffsetX = 0, gridOffsetY = 0) {\n this.sheetViewHeight = height;\n this.sheetViewWidth = width;\n this.gridOffsetX = gridOffsetX;\n this.gridOffsetY = gridOffsetY;\n this.recomputeViewports();\n }\n recomputeViewports() {\n for (let sheetId of Object.keys(this.viewports)) {\n this.resetViewports(sheetId);\n }\n }\n setSheetViewOffset(offsetX, offsetY) {\n const sheetId = this.getters.getActiveSheetId();\n const { maxOffsetX, maxOffsetY } = this.getMaximumSheetOffset();\n Object.values(this.getSubViewports(sheetId)).forEach((viewport) => viewport.setViewportOffset(clip(offsetX, 0, maxOffsetX), clip(offsetY, 0, maxOffsetY)));\n }\n /**\n * Clip the vertical offset within the allowed range.\n * Not above the sheet, nor below the sheet.\n */\n clipOffsetY(offsetY) {\n const { height } = this.getMainViewportRect();\n const maxOffset = height - this.sheetViewHeight;\n offsetY = Math.min(offsetY, maxOffset);\n offsetY = Math.max(offsetY, 0);\n return offsetY;\n }\n getViewportOffset(sheetId) {\n var _a, _b;\n return {\n x: ((_a = this.viewports[sheetId]) === null || _a === void 0 ? void 0 : _a.bottomRight.offsetScrollbarX) || 0,\n y: ((_b = this.viewports[sheetId]) === null || _b === void 0 ? void 0 : _b.bottomRight.offsetScrollbarY) || 0,\n };\n }\n resetViewports(sheetId) {\n if (!this.getters.tryGetSheet(sheetId)) {\n return;\n }\n const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n const nCols = this.getters.getNumberCols(sheetId);\n const nRows = this.getters.getNumberRows(sheetId);\n const colOffset = this.getters.getColRowOffset(\"COL\", 0, xSplit, sheetId);\n const rowOffset = this.getters.getColRowOffset(\"ROW\", 0, ySplit, sheetId);\n const { xRatio, yRatio } = this.getFrozenSheetViewRatio(sheetId);\n const canScrollHorizontally = xRatio < 1.0;\n const canScrollVertically = yRatio < 1.0;\n const previousOffset = this.getViewportOffset(sheetId);\n const sheetViewports = {\n topLeft: (ySplit &&\n xSplit &&\n new InternalViewport(this.getters, sheetId, { left: 0, right: xSplit - 1, top: 0, bottom: ySplit - 1 }, { width: colOffset, height: rowOffset }, { canScrollHorizontally: false, canScrollVertically: false }, { x: 0, y: 0 })) ||\n undefined,\n topRight: (ySplit &&\n new InternalViewport(this.getters, sheetId, { left: xSplit, right: nCols - 1, top: 0, bottom: ySplit - 1 }, { width: this.sheetViewWidth - colOffset, height: rowOffset }, { canScrollHorizontally, canScrollVertically: false }, { x: canScrollHorizontally ? previousOffset.x : 0, y: 0 })) ||\n undefined,\n bottomLeft: (xSplit &&\n new InternalViewport(this.getters, sheetId, { left: 0, right: xSplit - 1, top: ySplit, bottom: nRows - 1 }, { width: colOffset, height: this.sheetViewHeight - rowOffset }, { canScrollHorizontally: false, canScrollVertically }, { x: 0, y: canScrollVertically ? previousOffset.y : 0 })) ||\n undefined,\n bottomRight: new InternalViewport(this.getters, sheetId, { left: xSplit, right: nCols - 1, top: ySplit, bottom: nRows - 1 }, {\n width: this.sheetViewWidth - colOffset,\n height: this.sheetViewHeight - rowOffset,\n }, { canScrollHorizontally, canScrollVertically }, {\n x: canScrollHorizontally ? previousOffset.x : 0,\n y: canScrollVertically ? previousOffset.y : 0,\n }),\n };\n this.viewports[sheetId] = sheetViewports;\n }\n /**\n * Adjust the viewport such that the anchor position is visible\n */\n refreshViewport(sheetId, anchorPosition) {\n Object.values(this.getSubViewports(sheetId)).forEach((viewport) => {\n viewport.adjustViewportZone();\n viewport.adjustPosition(anchorPosition);\n });\n }\n /**\n * Shift the viewport vertically and move the selection anchor\n * such that it remains at the same place relative to the\n * viewport top.\n */\n shiftVertically(offset) {\n const { top } = this.getActiveMainViewport();\n const { scrollX } = this.getActiveSheetScrollInfo();\n this.setSheetViewOffset(scrollX, offset);\n const { anchor } = this.getters.getSelection();\n const deltaRow = this.getActiveMainViewport().top - top;\n this.selection.selectCell(anchor.cell.col, anchor.cell.row + deltaRow);\n }\n getVisibleFigures() {\n const sheetId = this.getters.getActiveSheetId();\n const result = [];\n const figures = this.getters.getFigures(sheetId);\n const { scrollX, scrollY } = this.getActiveSheetScrollInfo();\n const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();\n const { width, height } = this.getters.getSheetViewDimensionWithHeaders();\n for (const figure of figures) {\n if (figure.x >= offsetCorrectionX &&\n (figure.x + figure.width <= offsetCorrectionX + scrollX ||\n figure.x >= width + scrollX + offsetCorrectionX)) {\n continue;\n }\n if (figure.y >= offsetCorrectionY &&\n (figure.y + figure.height <= offsetCorrectionY + scrollY ||\n figure.y >= height + scrollY + offsetCorrectionY)) {\n continue;\n }\n result.push(figure);\n }\n return result;\n }\n getFrozenSheetViewRatio(sheetId) {\n const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n const offsetCorrectionX = this.getters.getColDimensions(sheetId, xSplit).start;\n const offsetCorrectionY = this.getters.getRowDimensions(sheetId, ySplit).start;\n const width = this.sheetViewWidth + this.gridOffsetX;\n const height = this.sheetViewHeight + this.gridOffsetY;\n return { xRatio: offsetCorrectionX / width, yRatio: offsetCorrectionY / height };\n }\n }\n SheetViewPlugin.getters = [\n \"getColIndex\",\n \"getRowIndex\",\n \"getActiveMainViewport\",\n \"getSheetViewDimension\",\n \"getSheetViewDimensionWithHeaders\",\n \"getMainViewportRect\",\n \"isVisibleInViewport\",\n \"getEdgeScrollCol\",\n \"getEdgeScrollRow\",\n \"getVisibleFigures\",\n \"getVisibleRect\",\n \"getColRowOffsetInViewport\",\n \"getMainViewportCoordinates\",\n \"getActiveSheetScrollInfo\",\n \"getActiveSheetDOMScrollInfo\",\n \"getSheetViewVisibleCols\",\n \"getSheetViewVisibleRows\",\n \"getFrozenSheetViewRatio\",\n ];\n\n /**\n * This plugin manage the autofill.\n *\n * The way it works is the next one:\n * For each line (row if the direction is left/right, col otherwise), we create\n * a \"AutofillGenerator\" object which is used to compute the cells to\n * autofill.\n *\n * When we need to autofill a cell, we compute the origin cell in the source.\n * EX: from A1:A2, autofill A3->A6.\n * Target | Origin cell\n * A3 | A1\n * A4 | A2\n * A5 | A1\n * A6 | A2\n * When we have the origin, we take the associated cell in the AutofillGenerator\n * and we apply the modifier (AutofillModifier) associated to the content of the\n * cell.\n */\n /**\n * This class is used to generate the next values to autofill.\n * It's done from a selection (the source) and describe how the next values\n * should be computed.\n */\n class AutofillGenerator {\n constructor(cells, getters, direction) {\n this.index = 0;\n this.cells = cells;\n this.getters = getters;\n this.direction = direction;\n }\n /**\n * Get the next value to autofill\n */\n next() {\n const genCell = this.cells[this.index++ % this.cells.length];\n const rule = genCell.rule;\n const { cellData, tooltip } = autofillModifiersRegistry\n .get(rule.type)\n .apply(rule, genCell.data, this.getters, this.direction);\n return {\n cellData,\n tooltip,\n origin: {\n col: genCell.data.col,\n row: genCell.data.row,\n },\n };\n }\n }\n /**\n * Autofill Plugin\n *\n */\n class AutofillPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.lastCellSelected = {};\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"AUTOFILL_SELECT\":\n const sheetId = this.getters.getActiveSheetId();\n this.lastCellSelected.col =\n cmd.col === -1\n ? this.lastCellSelected.col\n : clip(cmd.col, 0, this.getters.getNumberCols(sheetId));\n this.lastCellSelected.row =\n cmd.row === -1\n ? this.lastCellSelected.row\n : clip(cmd.row, 0, this.getters.getNumberRows(sheetId));\n if (this.lastCellSelected.col !== undefined && this.lastCellSelected.row !== undefined) {\n return 0 /* CommandResult.Success */;\n }\n return 45 /* CommandResult.InvalidAutofillSelection */;\n case \"AUTOFILL_AUTO\":\n const zone = this.getters.getSelectedZone();\n return zone.top === zone.bottom\n ? 0 /* CommandResult.Success */\n : 1 /* CommandResult.CancelledForUnknownReason */;\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"AUTOFILL\":\n this.autofill(true);\n break;\n case \"AUTOFILL_SELECT\":\n this.select(cmd.col, cmd.row);\n break;\n case \"AUTOFILL_AUTO\":\n this.autofillAuto();\n break;\n case \"AUTOFILL_CELL\":\n this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);\n const sheetId = this.getters.getActiveSheetId();\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col: cmd.col,\n row: cmd.row,\n style: cmd.style || null,\n content: cmd.content || \"\",\n format: cmd.format || \"\",\n });\n this.dispatch(\"SET_BORDER\", {\n sheetId,\n col: cmd.col,\n row: cmd.row,\n border: cmd.border,\n });\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getAutofillTooltip() {\n return this.tooltip;\n }\n // ---------------------------------------------------------------------------\n // Private methods\n // ---------------------------------------------------------------------------\n /**\n * Autofill the autofillZone from the current selection\n * @param apply Flag set to true to apply the autofill in the model. It's\n * useful to set it to false when we need to fill the tooltip\n */\n autofill(apply) {\n if (!this.autofillZone || !this.steps || this.direction === undefined) {\n this.tooltip = undefined;\n return;\n }\n const source = this.getters.getSelectedZone();\n const target = this.autofillZone;\n switch (this.direction) {\n case \"down\" /* DIRECTION.DOWN */:\n for (let col = source.left; col <= source.right; col++) {\n const xcs = [];\n for (let row = source.top; row <= source.bottom; row++) {\n xcs.push(toXC(col, row));\n }\n const generator = this.createGenerator(xcs);\n for (let row = target.top; row <= target.bottom; row++) {\n this.computeNewCell(generator, col, row, apply);\n }\n }\n break;\n case \"up\" /* DIRECTION.UP */:\n for (let col = source.left; col <= source.right; col++) {\n const xcs = [];\n for (let row = source.bottom; row >= source.top; row--) {\n xcs.push(toXC(col, row));\n }\n const generator = this.createGenerator(xcs);\n for (let row = target.bottom; row >= target.top; row--) {\n this.computeNewCell(generator, col, row, apply);\n }\n }\n break;\n case \"left\" /* DIRECTION.LEFT */:\n for (let row = source.top; row <= source.bottom; row++) {\n const xcs = [];\n for (let col = source.right; col >= source.left; col--) {\n xcs.push(toXC(col, row));\n }\n const generator = this.createGenerator(xcs);\n for (let col = target.right; col >= target.left; col--) {\n this.computeNewCell(generator, col, row, apply);\n }\n }\n break;\n case \"right\" /* DIRECTION.RIGHT */:\n for (let row = source.top; row <= source.bottom; row++) {\n const xcs = [];\n for (let col = source.left; col <= source.right; col++) {\n xcs.push(toXC(col, row));\n }\n const generator = this.createGenerator(xcs);\n for (let col = target.left; col <= target.right; col++) {\n this.computeNewCell(generator, col, row, apply);\n }\n }\n break;\n }\n if (apply) {\n this.autofillZone = undefined;\n this.selection.resizeAnchorZone(this.direction, this.steps);\n this.lastCellSelected = {};\n this.direction = undefined;\n this.steps = 0;\n this.tooltip = undefined;\n }\n }\n /**\n * Select a cell which becomes the last cell of the autofillZone\n */\n select(col, row) {\n const source = this.getters.getSelectedZone();\n if (isInside(col, row, source)) {\n this.autofillZone = undefined;\n return;\n }\n this.direction = this.getDirection(col, row);\n switch (this.direction) {\n case \"up\" /* DIRECTION.UP */:\n this.saveZone(row, source.top - 1, source.left, source.right);\n this.steps = source.top - row;\n break;\n case \"down\" /* DIRECTION.DOWN */:\n this.saveZone(source.bottom + 1, row, source.left, source.right);\n this.steps = row - source.bottom;\n break;\n case \"left\" /* DIRECTION.LEFT */:\n this.saveZone(source.top, source.bottom, col, source.left - 1);\n this.steps = source.left - col;\n break;\n case \"right\" /* DIRECTION.RIGHT */:\n this.saveZone(source.top, source.bottom, source.right + 1, col);\n this.steps = col - source.right;\n break;\n }\n this.autofill(false);\n }\n /**\n * Computes the autofillZone to autofill when the user double click on the\n * autofiller\n */\n autofillAuto() {\n const zone = this.getters.getSelectedZone();\n const sheetId = this.getters.getActiveSheetId();\n let col = zone.left;\n let row = zone.bottom;\n if (col > 0) {\n let left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });\n while (left.type !== CellValueType.empty) {\n row += 1;\n left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });\n }\n }\n if (row === zone.bottom) {\n col = zone.right;\n if (col <= this.getters.getNumberCols(sheetId)) {\n let right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });\n while (right.type !== CellValueType.empty) {\n row += 1;\n right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });\n }\n }\n }\n if (row !== zone.bottom) {\n this.select(zone.left, row - 1);\n this.autofill(true);\n }\n }\n /**\n * Generate the next cell\n */\n computeNewCell(generator, col, row, apply) {\n const { cellData, tooltip, origin } = generator.next();\n const { content, style, border, format } = cellData;\n this.tooltip = tooltip;\n if (apply) {\n this.dispatch(\"AUTOFILL_CELL\", {\n originCol: origin.col,\n originRow: origin.row,\n col,\n row,\n content,\n style,\n border,\n format,\n });\n }\n }\n /**\n * Get the rule associated to the current cell\n */\n getRule(cell, cells) {\n const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);\n const rule = rules.find((rule) => rule.condition(cell, cells));\n return rule && rule.generateRule(cell, cells);\n }\n /**\n * Create the generator to be able to autofill the next cells.\n */\n createGenerator(source) {\n const nextCells = [];\n const cellsData = [];\n const sheetId = this.getters.getActiveSheetId();\n for (let xc of source) {\n const { col, row } = toCartesian(xc);\n const cell = this.getters.getCell({ sheetId, col, row });\n cellsData.push({\n col,\n row,\n cell,\n sheetId,\n });\n }\n const cells = cellsData.map((cellData) => cellData.cell);\n for (let cellData of cellsData) {\n let rule = { type: \"COPY_MODIFIER\" };\n if (cellData && cellData.cell) {\n const newRule = this.getRule(cellData.cell, cells);\n rule = newRule || rule;\n }\n const border = this.getters.getCellBorder(cellData) || undefined;\n nextCells.push({\n data: { ...cellData, border },\n rule,\n });\n }\n return new AutofillGenerator(nextCells, this.getters, this.direction);\n }\n saveZone(top, bottom, left, right) {\n this.autofillZone = { top, bottom, left, right };\n }\n /**\n * Compute the direction of the autofill from the last selected zone and\n * a given cell (col, row)\n */\n getDirection(col, row) {\n const source = this.getters.getSelectedZone();\n const position = {\n up: { number: source.top - row, value: \"up\" /* DIRECTION.UP */ },\n down: { number: row - source.bottom, value: \"down\" /* DIRECTION.DOWN */ },\n left: { number: source.left - col, value: \"left\" /* DIRECTION.LEFT */ },\n right: { number: col - source.right, value: \"right\" /* DIRECTION.RIGHT */ },\n };\n if (Object.values(position)\n .map((x) => (x.number > 0 ? 1 : 0))\n .reduce((acc, value) => acc + value) === 1) {\n return Object.values(position).find((x) => (x.number > 0 ? 1 : 0)).value;\n }\n const first = position.up.number > 0 ? \"up\" : \"down\";\n const second = position.left.number > 0 ? \"left\" : \"right\";\n return Math.abs(position[first].number) >= Math.abs(position[second].number)\n ? position[first].value\n : position[second].value;\n }\n autoFillMerge(originCol, originRow, col, row) {\n const sheetId = this.getters.getActiveSheetId();\n const position = { sheetId, col, row };\n const originPosition = { sheetId, col: originCol, row: originRow };\n if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {\n const zone = this.getters.getMerge(position);\n if (zone) {\n this.dispatch(\"REMOVE_MERGE\", {\n sheetId,\n target: [zone],\n });\n }\n }\n const originMerge = this.getters.getMerge(originPosition);\n if ((originMerge === null || originMerge === void 0 ? void 0 : originMerge.topLeft.col) === originCol && (originMerge === null || originMerge === void 0 ? void 0 : originMerge.topLeft.row) === originRow) {\n this.dispatch(\"ADD_MERGE\", {\n sheetId,\n target: [\n {\n top: row,\n bottom: row + originMerge.bottom - originMerge.top,\n left: col,\n right: col + originMerge.right - originMerge.left,\n },\n ],\n });\n }\n }\n // ---------------------------------------------------------------------------\n // Grid rendering\n // ---------------------------------------------------------------------------\n drawGrid(renderingContext) {\n if (!this.autofillZone) {\n return;\n }\n const { ctx, thinLineWidth } = renderingContext;\n const { x, y, width, height } = this.getters.getVisibleRect(this.autofillZone);\n if (width > 0 && height > 0) {\n ctx.strokeStyle = \"black\";\n ctx.lineWidth = thinLineWidth;\n ctx.setLineDash([3]);\n ctx.strokeRect(x, y, width, height);\n ctx.setLineDash([]);\n }\n }\n }\n AutofillPlugin.layers = [5 /* LAYERS.Autofill */];\n AutofillPlugin.getters = [\"getAutofillTooltip\"];\n\n class AutomaticSumPlugin extends UIPlugin {\n handle(cmd) {\n switch (cmd.type) {\n case \"SUM_SELECTION\":\n const sheetId = this.getters.getActiveSheetId();\n const { zones, anchor } = this.getters.getSelection();\n for (const zone of zones) {\n const sums = this.getAutomaticSums(sheetId, zone, anchor.cell);\n this.dispatchCellUpdates(sheetId, sums);\n }\n break;\n }\n }\n getAutomaticSums(sheetId, zone, anchor) {\n return this.shouldFindData(sheetId, zone)\n ? this.sumAdjacentData(sheetId, zone, anchor)\n : this.sumData(sheetId, zone);\n }\n // ---------------------------------------------------------------------------\n // Private methods\n // ---------------------------------------------------------------------------\n sumData(sheetId, zone) {\n const dimensions = this.dimensionsToSum(sheetId, zone);\n const sums = this.sumDimensions(sheetId, zone, dimensions).filter(({ zone }) => !this.getters.isEmpty(sheetId, zone));\n if (dimensions.has(\"ROW\") && dimensions.has(\"COL\")) {\n sums.push(this.sumTotal(zone));\n }\n return sums;\n }\n sumAdjacentData(sheetId, zone, anchor) {\n const { col, row } = isInside(anchor.col, anchor.row, zone)\n ? anchor\n : { col: zone.left, row: zone.top };\n const dataZone = this.findAdjacentData(sheetId, col, row);\n if (!dataZone) {\n return [];\n }\n if (this.getters.isSingleCellOrMerge(sheetId, zone) ||\n isOneDimensional(union(dataZone, zone))) {\n return [{ position: { col, row }, zone: dataZone }];\n }\n else {\n return this.sumDimensions(sheetId, union(dataZone, zone), this.transpose(this.dimensionsToSum(sheetId, zone)));\n }\n }\n /**\n * Find a zone to automatically sum a column or row of numbers.\n *\n * We first decide which direction will be summed (column or row).\n * Here is the strategy:\n * 1. If the left cell is a number and the top cell is not: choose horizontal\n * 2. Try to find a valid vertical zone. If it's valid: choose vertical\n * 3. Try to find a valid horizontal zone. If it's valid: choose horizontal\n * 4. Otherwise, no zone is returned\n *\n * Now, how to find a valid zone?\n * The zone starts directly above or on the left of the starting point\n * (depending on the direction).\n * The zone ends where the first continuous sequence of numbers ends.\n * Empty or text cells can be part of the zone while no number has been found.\n * Other kind of cells (boolean, dates, etc.) are not valid in the zone and the\n * search stops immediately if one is found.\n *\n * ------- -------\n * | 1 | | 1 |\n * ------- -------\n * | | | |\n * ------- <= end of the sequence, stop here -------\n * | 2 | | 2 |\n * ------- -------\n * | 3 | <= start of the number sequence | 3 |\n * ------- -------\n * | | <= ignored | FALSE | <= invalid, no zone is found\n * ------- -------\n * | A | <= ignored | A | <= ignored\n * ------- -------\n */\n findAdjacentData(sheetId, col, row) {\n const sheet = this.getters.getSheet(sheetId);\n const mainCellPosition = this.getters.getMainCellPosition({ sheetId, col, row });\n const zone = this.findSuitableZoneToSum(sheet, mainCellPosition.col, mainCellPosition.row);\n if (zone) {\n return this.getters.expandZone(sheetId, zone);\n }\n return undefined;\n }\n /**\n * Return the zone to sum if a valid one is found.\n * @see getAutomaticSumZone\n */\n findSuitableZoneToSum(sheet, col, row) {\n const topCell = this.getters.getEvaluatedCell({ sheetId: sheet.id, col, row: row - 1 });\n const leftCell = this.getters.getEvaluatedCell({ sheetId: sheet.id, col: col - 1, row });\n if (this.isNumber(leftCell) && !this.isNumber(topCell)) {\n return this.findHorizontalZone(sheet, col, row);\n }\n const verticalZone = this.findVerticalZone(sheet, col, row);\n if (this.isZoneValid(verticalZone)) {\n return verticalZone;\n }\n const horizontalZone = this.findHorizontalZone(sheet, col, row);\n if (this.isZoneValid(horizontalZone)) {\n return horizontalZone;\n }\n return undefined;\n }\n findVerticalZone(sheet, col, row) {\n const zone = {\n top: 0,\n bottom: row - 1,\n left: col,\n right: col,\n };\n const top = this.reduceZoneStart(sheet, zone, zone.bottom);\n return { ...zone, top };\n }\n findHorizontalZone(sheet, col, row) {\n const zone = {\n top: row,\n bottom: row,\n left: 0,\n right: col - 1,\n };\n const left = this.reduceZoneStart(sheet, zone, zone.right);\n return { ...zone, left };\n }\n /**\n * Reduces a column or row zone to a valid zone for the automatic sum.\n * @see getAutomaticSumZone\n * @param sheet\n * @param zone one dimensional zone (a single row or a single column). The zone is\n * assumed to start at the beginning of the column (top=0) or the row (left=0)\n * @param end end index of the zone (`bottom` or `right` depending on the dimension)\n * @returns the starting position of the valid zone or Infinity if the zone is not valid.\n */\n reduceZoneStart(sheet, zone, end) {\n const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);\n const cellPositions = range(end, -1, -1);\n const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);\n const maxValidPosition = Math.max(...invalidCells);\n const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));\n const firstSequence = numberSequences[0] || [];\n if (Math.max(...firstSequence) < maxValidPosition) {\n return Infinity;\n }\n return Math.min(...firstSequence);\n }\n shouldFindData(sheetId, zone) {\n return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);\n }\n isNumber(cell) {\n var _a;\n return cell.type === CellValueType.number && !((_a = cell.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_FORMAT));\n }\n isZoneValid(zone) {\n return zone.bottom >= zone.top && zone.right >= zone.left;\n }\n lastColIsEmpty(sheetId, zone) {\n return this.getters.isEmpty(sheetId, { ...zone, left: zone.right });\n }\n lastRowIsEmpty(sheetId, zone) {\n return this.getters.isEmpty(sheetId, { ...zone, top: zone.bottom });\n }\n /**\n * Decides which dimensions (columns or rows) should be summed\n * based on its shape and what's inside the zone.\n */\n dimensionsToSum(sheetId, zone) {\n const dimensions = new Set();\n if (isOneDimensional(zone)) {\n dimensions.add(zoneToDimension(zone).width === 1 ? \"COL\" : \"ROW\");\n return dimensions;\n }\n if (this.lastColIsEmpty(sheetId, zone)) {\n dimensions.add(\"ROW\");\n }\n if (this.lastRowIsEmpty(sheetId, zone)) {\n dimensions.add(\"COL\");\n }\n if (dimensions.size === 0) {\n dimensions.add(\"COL\");\n }\n return dimensions;\n }\n /**\n * Sum each column and/or row in the zone in the appropriate cells,\n * depending on the available space.\n */\n sumDimensions(sheetId, zone, dimensions) {\n return [\n ...(dimensions.has(\"COL\") ? this.sumColumns(zone, sheetId) : []),\n ...(dimensions.has(\"ROW\") ? this.sumRows(zone, sheetId) : []),\n ];\n }\n /**\n * Sum the total of the zone in the bottom right cell, assuming\n * the last row contains summed columns.\n */\n sumTotal(zone) {\n const { bottom, right } = zone;\n return {\n position: { col: right, row: bottom },\n zone: { ...zone, top: bottom, right: right - 1 },\n };\n }\n sumColumns(zone, sheetId) {\n const target = this.nextEmptyRow(sheetId, { ...zone, bottom: zone.bottom - 1 });\n zone = { ...zone, bottom: Math.min(zone.bottom, target.bottom - 1) };\n return positions(target).map((position) => ({\n position,\n zone: { ...zone, right: position.col, left: position.col },\n }));\n }\n sumRows(zone, sheetId) {\n const target = this.nextEmptyCol(sheetId, { ...zone, right: zone.right - 1 });\n zone = { ...zone, right: Math.min(zone.right, target.right - 1) };\n return positions(target).map((position) => ({\n position,\n zone: { ...zone, top: position.row, bottom: position.row },\n }));\n }\n dispatchCellUpdates(sheetId, sums) {\n for (const sum of sums) {\n const { col, row } = sum.position;\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n content: `=SUM(${this.getters.zoneToXC(sheetId, sum.zone)})`,\n });\n }\n }\n /**\n * Find the first row where all cells below the zone are empty.\n */\n nextEmptyRow(sheetId, zone) {\n let start = zone.bottom + 1;\n const { left, right } = zone;\n while (!this.getters.isEmpty(sheetId, { bottom: start, top: start, left, right })) {\n start++;\n }\n return {\n ...zone,\n top: start,\n bottom: start,\n };\n }\n /**\n * Find the first column where all cells right of the zone are empty.\n */\n nextEmptyCol(sheetId, zone) {\n let start = zone.right + 1;\n const { top, bottom } = zone;\n while (!this.getters.isEmpty(sheetId, { left: start, right: start, top, bottom })) {\n start++;\n }\n return {\n ...zone,\n left: start,\n right: start,\n };\n }\n /**\n * Transpose the given dimensions.\n * COL becomes ROW\n * ROW becomes COL\n */\n transpose(dimensions) {\n return new Set([...dimensions.values()].map((dimension) => (dimension === \"COL\" ? \"ROW\" : \"COL\")));\n }\n }\n AutomaticSumPlugin.getters = [\"getAutomaticSums\"];\n\n /**\n * Plugin managing the display of components next to cells.\n */\n class CellPopoverPlugin extends UIPlugin {\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"OPEN_CELL_POPOVER\":\n try {\n cellPopoverRegistry.get(cmd.popoverType);\n }\n catch (error) {\n return 72 /* CommandResult.InvalidCellPopover */;\n }\n return 0 /* CommandResult.Success */;\n default:\n return 0 /* CommandResult.Success */;\n }\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"ACTIVATE_SHEET\":\n this.persistentPopover = undefined;\n break;\n case \"OPEN_CELL_POPOVER\":\n this.persistentPopover = {\n col: cmd.col,\n row: cmd.row,\n sheetId: this.getters.getActiveSheetId(),\n type: cmd.popoverType,\n };\n break;\n case \"CLOSE_CELL_POPOVER\":\n this.persistentPopover = undefined;\n break;\n }\n }\n getCellPopover({ col, row }) {\n var _a, _b;\n const sheetId = this.getters.getActiveSheetId();\n if (this.persistentPopover && this.getters.isVisibleInViewport(this.persistentPopover)) {\n const position = this.getters.getMainCellPosition(this.persistentPopover);\n const popover = (_b = (_a = cellPopoverRegistry\n .get(this.persistentPopover.type)).onOpen) === null || _b === void 0 ? void 0 : _b.call(_a, position, this.getters);\n return !(popover === null || popover === void 0 ? void 0 : popover.isOpen)\n ? { isOpen: false }\n : {\n ...popover,\n anchorRect: this.computePopoverAnchorRect(this.persistentPopover),\n };\n }\n if (col === undefined ||\n row === undefined ||\n !this.getters.isVisibleInViewport({ sheetId, col, row })) {\n return { isOpen: false };\n }\n const position = this.getters.getMainCellPosition({ sheetId, col, row });\n const popover = cellPopoverRegistry\n .getAll()\n .map((matcher) => { var _a; return (_a = matcher.onHover) === null || _a === void 0 ? void 0 : _a.call(matcher, position, this.getters); })\n .find((popover) => popover === null || popover === void 0 ? void 0 : popover.isOpen);\n return !(popover === null || popover === void 0 ? void 0 : popover.isOpen)\n ? { isOpen: false }\n : {\n ...popover,\n anchorRect: this.computePopoverAnchorRect(position),\n };\n }\n hasOpenedPopover() {\n return this.persistentPopover !== undefined;\n }\n getPersistentPopoverTypeAtPosition({ col, row }) {\n if (this.persistentPopover &&\n this.persistentPopover.col === col &&\n this.persistentPopover.row === row) {\n return this.persistentPopover.type;\n }\n return undefined;\n }\n computePopoverAnchorRect({ col, row }) {\n const sheetId = this.getters.getActiveSheetId();\n const merge = this.getters.getMerge({ sheetId, col, row });\n if (merge) {\n return this.getters.getVisibleRect(merge);\n }\n return this.getters.getVisibleRect(positionToZone({ col, row }));\n }\n }\n CellPopoverPlugin.getters = [\n \"getCellPopover\",\n \"getPersistentPopoverTypeAtPosition\",\n \"hasOpenedPopover\",\n ];\n\n const BORDER_COLOR = \"#8B008B\";\n const BACKGROUND_COLOR = \"#8B008B33\";\n var Direction;\n (function (Direction) {\n Direction[Direction[\"previous\"] = -1] = \"previous\";\n Direction[Direction[\"current\"] = 0] = \"current\";\n Direction[Direction[\"next\"] = 1] = \"next\";\n })(Direction || (Direction = {}));\n /**\n * Find and Replace Plugin\n *\n * This plugin is used in combination with the find_and_replace sidePanel\n * It is used to 'highlight' cells that match an input string according to\n * the given searchOptions. The second part of this plugin makes it possible\n * (again with the find_and_replace sidePanel), to replace the values that match\n * the search with a new value.\n */\n class FindAndReplacePlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.searchMatches = [];\n this.selectedMatchIndex = null;\n this.currentSearchRegex = null;\n this.searchOptions = {\n matchCase: false,\n exactMatch: false,\n searchFormulas: false,\n };\n this.toSearch = \"\";\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n handle(cmd) {\n switch (cmd.type) {\n case \"UPDATE_SEARCH\":\n this.updateSearch(cmd.toSearch, cmd.searchOptions);\n break;\n case \"CLEAR_SEARCH\":\n this.clearSearch();\n break;\n case \"SELECT_SEARCH_PREVIOUS_MATCH\":\n this.selectNextCell(Direction.previous);\n break;\n case \"SELECT_SEARCH_NEXT_MATCH\":\n this.selectNextCell(Direction.next);\n break;\n case \"REPLACE_SEARCH\":\n this.replace(cmd.replaceWith);\n break;\n case \"REPLACE_ALL_SEARCH\":\n this.replaceAll(cmd.replaceWith);\n break;\n case \"UNDO\":\n case \"REDO\":\n case \"REMOVE_COLUMNS_ROWS\":\n case \"ADD_COLUMNS_ROWS\":\n this.clearSearch();\n break;\n case \"ACTIVATE_SHEET\":\n case \"REFRESH_SEARCH\":\n this.refreshSearch();\n break;\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getSearchMatches() {\n return this.searchMatches;\n }\n getCurrentSelectedMatchIndex() {\n return this.selectedMatchIndex;\n }\n // ---------------------------------------------------------------------------\n // Search\n // ---------------------------------------------------------------------------\n /**\n * Will update the current searchOptions and accordingly update the regex.\n * It will then search for matches using the regex and store them.\n */\n updateSearch(toSearch, searchOptions) {\n this.searchOptions = searchOptions;\n if (toSearch !== this.toSearch) {\n this.selectedMatchIndex = null;\n }\n this.toSearch = toSearch;\n this.updateRegex();\n this.refreshSearch();\n }\n /**\n * refresh the matches according to the current search options\n */\n refreshSearch() {\n const matches = this.findMatches();\n this.searchMatches = matches;\n this.selectNextCell(Direction.current);\n }\n /**\n * Updates the regex based on the current searchOptions and\n * the value toSearch\n */\n updateRegex() {\n let searchValue = escapeRegExp(this.toSearch);\n const flags = !this.searchOptions.matchCase ? \"i\" : \"\";\n if (this.searchOptions.exactMatch) {\n searchValue = `^${searchValue}$`;\n }\n this.currentSearchRegex = RegExp(searchValue, flags);\n }\n /**\n * Find matches using the current regex\n */\n findMatches() {\n const sheetId = this.getters.getActiveSheetId();\n const cells = this.getters.getCells(sheetId);\n const matches = [];\n if (this.toSearch) {\n for (const cell of Object.values(cells)) {\n const { col, row } = this.getters.getCellPosition(cell.id);\n if (cell &&\n this.currentSearchRegex &&\n this.currentSearchRegex.test(this.getSearchableString({ sheetId, col, row }))) {\n const match = { col, row, selected: false };\n matches.push(match);\n }\n }\n }\n return matches.sort(this.sortByRowThenColumn);\n }\n sortByRowThenColumn(a, b) {\n if (a.row === b.row) {\n return a.col - b.col;\n }\n return a.row > b.row ? 1 : -1;\n }\n /**\n * Changes the selected search cell. Given a direction it will\n * Change the selection to the previous, current or nextCell,\n * if it exists otherwise it will set the selectedMatchIndex to null.\n * It will also reset the index to 0 if the search has changed.\n * It is also used to keep coherence between the selected searchMatch\n * and selectedMatchIndex.\n */\n selectNextCell(indexChange) {\n const matches = this.searchMatches;\n if (!matches.length) {\n this.selectedMatchIndex = null;\n return;\n }\n let nextIndex;\n if (this.selectedMatchIndex === null) {\n nextIndex = 0;\n }\n else {\n nextIndex = this.selectedMatchIndex + indexChange;\n }\n //modulo of negative value to be able to cycle in both directions with previous and next\n nextIndex = ((nextIndex % matches.length) + matches.length) % matches.length;\n this.selectedMatchIndex = nextIndex;\n this.selection.selectCell(matches[nextIndex].col, matches[nextIndex].row);\n for (let index = 0; index < this.searchMatches.length; index++) {\n this.searchMatches[index].selected = index === this.selectedMatchIndex;\n }\n }\n clearSearch() {\n this.toSearch = \"\";\n this.searchMatches = [];\n this.selectedMatchIndex = null;\n this.currentSearchRegex = null;\n this.searchOptions = {\n matchCase: false,\n exactMatch: false,\n searchFormulas: false,\n };\n }\n // ---------------------------------------------------------------------------\n // Replace\n // ---------------------------------------------------------------------------\n /**\n * Replace the value of the currently selected match\n */\n replace(replaceWith) {\n if (this.selectedMatchIndex === null || !this.currentSearchRegex) {\n return;\n }\n const matches = this.searchMatches;\n const selectedMatch = matches[this.selectedMatchIndex];\n const sheetId = this.getters.getActiveSheetId();\n const cell = this.getters.getCell({ sheetId, ...selectedMatch });\n if ((cell === null || cell === void 0 ? void 0 : cell.isFormula) && !this.searchOptions.searchFormulas) {\n this.selectNextCell(Direction.next);\n }\n else {\n const replaceRegex = new RegExp(this.currentSearchRegex.source, this.currentSearchRegex.flags + \"g\");\n const toReplace = this.getSearchableString({\n sheetId,\n col: selectedMatch.col,\n row: selectedMatch.row,\n });\n const newContent = toReplace.replace(replaceRegex, replaceWith);\n this.dispatch(\"UPDATE_CELL\", {\n sheetId: this.getters.getActiveSheetId(),\n col: selectedMatch.col,\n row: selectedMatch.row,\n content: newContent,\n });\n this.searchMatches.splice(this.selectedMatchIndex, 1);\n this.selectNextCell(Direction.current);\n }\n }\n /**\n * Apply the replace function to all the matches one time.\n */\n replaceAll(replaceWith) {\n const matchCount = this.searchMatches.length;\n for (let i = 0; i < matchCount; i++) {\n this.replace(replaceWith);\n }\n }\n getSearchableString(position) {\n const cell = this.getters.getCell(position);\n if (this.searchOptions.searchFormulas && (cell === null || cell === void 0 ? void 0 : cell.isFormula)) {\n return cell.content;\n }\n return this.getters.getEvaluatedCell(position).formattedValue;\n }\n // ---------------------------------------------------------------------------\n // Grid rendering\n // ---------------------------------------------------------------------------\n drawGrid(renderingContext) {\n const { ctx } = renderingContext;\n const sheetId = this.getters.getActiveSheetId();\n for (const match of this.searchMatches) {\n const merge = this.getters.getMerge({ sheetId, col: match.col, row: match.row });\n const left = merge ? merge.left : match.col;\n const right = merge ? merge.right : match.col;\n const top = merge ? merge.top : match.row;\n const bottom = merge ? merge.bottom : match.row;\n const { x, y, width, height } = this.getters.getVisibleRect({ top, left, right, bottom });\n if (width > 0 && height > 0) {\n ctx.fillStyle = BACKGROUND_COLOR;\n ctx.fillRect(x, y, width, height);\n if (match.selected) {\n ctx.strokeStyle = BORDER_COLOR;\n ctx.strokeRect(x, y, width, height);\n }\n }\n }\n }\n }\n FindAndReplacePlugin.layers = [3 /* LAYERS.Search */];\n FindAndReplacePlugin.getters = [\"getSearchMatches\", \"getCurrentSelectedMatchIndex\"];\n\n class FormatPlugin extends UIPlugin {\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n handle(cmd) {\n switch (cmd.type) {\n case \"SET_DECIMAL\":\n this.setDecimal(cmd.sheetId, cmd.target, cmd.step);\n break;\n }\n }\n /**\n * This function allows to adjust the quantity of decimal places after a decimal\n * point on cells containing number value. It does this by changing the cells\n * format. Values aren't modified.\n *\n * The change of the decimal quantity is done one by one, the sign of the step\n * variable indicates whether we are increasing or decreasing.\n *\n * If several cells are in the zone, the format resulting from the change of the\n * first cell (with number type) will be applied to the whole zone.\n */\n setDecimal(sheetId, zones, step) {\n // Find the first cell with a number value and get the format\n const numberFormat = this.searchNumberFormat(sheetId, zones);\n if (numberFormat !== undefined) {\n // Depending on the step sign, increase or decrease the decimal representation\n // of the format\n const newFormat = changeDecimalPlaces(numberFormat, step);\n // Apply the new format on the whole zone\n this.dispatch(\"SET_FORMATTING\", {\n sheetId,\n target: zones,\n format: newFormat,\n });\n }\n }\n /**\n * Take a range of cells and return the format of the first cell containing a\n * number value. Returns a default format if the cell hasn't format. Returns\n * undefined if no number value in the range.\n */\n searchNumberFormat(sheetId, zones) {\n var _a;\n for (let zone of zones) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n for (let col = zone.left; col <= zone.right; col++) {\n const cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n if (cell.type === CellValueType.number &&\n !((_a = cell.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_FORMAT)) // reject dates\n ) {\n return cell.format || createDefaultFormat(cell.value);\n }\n }\n }\n }\n return undefined;\n }\n }\n\n class HeaderVisibilityUIPlugin extends UIPlugin {\n isRowHidden(sheetId, index) {\n return (this.getters.isRowHiddenByUser(sheetId, index) || this.getters.isRowFiltered(sheetId, index));\n }\n isColHidden(sheetId, index) {\n return this.getters.isColHiddenByUser(sheetId, index);\n }\n isHeaderHidden(sheetId, dimension, index) {\n return dimension === \"COL\"\n ? this.isColHidden(sheetId, index)\n : this.isRowHidden(sheetId, index);\n }\n getNextVisibleCellPosition({ sheetId, col, row }) {\n return {\n sheetId,\n col: this.findVisibleHeader(sheetId, \"COL\", range(col, this.getters.getNumberCols(sheetId))),\n row: this.findVisibleHeader(sheetId, \"ROW\", range(row, this.getters.getNumberRows(sheetId))),\n };\n }\n findVisibleHeader(sheetId, dimension, indexes) {\n return indexes.find((index) => this.getters.doesHeaderExist(sheetId, dimension, index) &&\n !this.isHeaderHidden(sheetId, dimension, index));\n }\n findLastVisibleColRowIndex(sheetId, dimension, { last, first }) {\n const lastVisibleIndex = range(last, first, -1).find((index) => !this.isHeaderHidden(sheetId, dimension, index));\n return lastVisibleIndex || first;\n }\n findFirstVisibleColRowIndex(sheetId, dimension) {\n const numberOfHeaders = this.getters.getNumberHeaders(sheetId, dimension);\n for (let i = 0; i < numberOfHeaders - 1; i++) {\n if (dimension === \"COL\" && !this.isColHidden(sheetId, i)) {\n return i;\n }\n if (dimension === \"ROW\" && !this.isRowHidden(sheetId, i)) {\n return i;\n }\n }\n return undefined;\n }\n exportForExcel(data) {\n for (const sheetData of data.sheets) {\n for (const [row, rowData] of Object.entries(sheetData.rows)) {\n const isHidden = this.isRowHidden(sheetData.id, Number(row));\n rowData.isHidden = isHidden;\n }\n }\n }\n }\n HeaderVisibilityUIPlugin.getters = [\n \"getNextVisibleCellPosition\",\n \"findVisibleHeader\",\n \"findLastVisibleColRowIndex\",\n \"findFirstVisibleColRowIndex\",\n \"isRowHidden\",\n \"isColHidden\",\n \"isHeaderHidden\",\n ];\n\n /**\n * HighlightPlugin\n */\n class HighlightPlugin extends UIPlugin {\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getHighlights() {\n return this.prepareHighlights(this.getters.getComposerHighlights().concat(this.getters.getSelectionInputHighlights()));\n }\n // ---------------------------------------------------------------------------\n // Other\n // ---------------------------------------------------------------------------\n prepareHighlights(highlights) {\n return highlights\n .filter((x) => x.zone.top >= 0 &&\n x.zone.left >= 0 &&\n x.zone.bottom < this.getters.getNumberRows(x.sheetId) &&\n x.zone.right < this.getters.getNumberCols(x.sheetId))\n .map((highlight) => {\n const { height, width } = zoneToDimension(highlight.zone);\n const zone = height * width === 1\n ? this.getters.expandZone(highlight.sheetId, highlight.zone)\n : highlight.zone;\n return {\n ...highlight,\n zone,\n };\n });\n }\n // ---------------------------------------------------------------------------\n // Grid rendering\n // ---------------------------------------------------------------------------\n drawGrid(renderingContext) {\n // rendering selection highlights\n const { ctx, thinLineWidth } = renderingContext;\n const sheetId = this.getters.getActiveSheetId();\n const lineWidth = 3 * thinLineWidth;\n ctx.lineWidth = lineWidth;\n /**\n * We only need to draw the highlights of the current sheet.\n *\n * Note that there can be several times the same highlight in 'this.highlights'.\n * In order to avoid superposing the same color layer and modifying the final\n * opacity, we filter highlights to remove duplicates.\n */\n const highlights = this.getHighlights();\n for (let h of highlights.filter((highlight, index) => \n // For every highlight in the sheet, deduplicated by zone\n highlights.findIndex((h) => isEqual(h.zone, highlight.zone) && h.sheetId === sheetId) ===\n index)) {\n const { x, y, width, height } = this.getters.getVisibleRect(h.zone);\n if (width > 0 && height > 0) {\n ctx.strokeStyle = h.color;\n ctx.strokeRect(x + lineWidth / 2, y + lineWidth / 2, width - lineWidth, height - lineWidth);\n ctx.globalCompositeOperation = \"source-over\";\n ctx.fillStyle = h.color + \"20\";\n ctx.fillRect(x + lineWidth, y + lineWidth, width - 2 * lineWidth, height - 2 * lineWidth);\n }\n }\n }\n }\n HighlightPlugin.layers = [1 /* LAYERS.Highlights */];\n HighlightPlugin.getters = [\"getHighlights\"];\n\n class RendererPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.boxes = [];\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n /**\n * Returns the size, start and end coordinates of a column relative to the left\n * column of the current viewport\n */\n getColDimensionsInViewport(sheetId, col) {\n const left = Math.min(...this.getters.getSheetViewVisibleCols());\n const start = this.getters.getColRowOffsetInViewport(\"COL\", left, col);\n const size = this.getters.getColSize(sheetId, col);\n const isColHidden = this.getters.isColHidden(sheetId, col);\n return {\n start,\n size: size,\n end: start + (isColHidden ? 0 : size),\n };\n }\n /**\n * Returns the size, start and end coordinates of a row relative to the top row\n * of the current viewport\n */\n getRowDimensionsInViewport(sheetId, row) {\n const top = Math.min(...this.getters.getSheetViewVisibleRows());\n const start = this.getters.getColRowOffsetInViewport(\"ROW\", top, row);\n const size = this.getters.getRowSize(sheetId, row);\n const isRowHidden = this.getters.isRowHidden(sheetId, row);\n return {\n start,\n size: size,\n end: start + (isRowHidden ? 0 : size),\n };\n }\n /**\n * Get the offset of a header (see getColRowOffsetInViewport), adjusted with the header\n * size (HEADER_HEIGHT and HEADER_WIDTH)\n */\n getHeaderOffset(dimension, start, index) {\n let size = this.getters.getColRowOffsetInViewport(dimension, start, index);\n if (!this.getters.isDashboard()) {\n size += dimension === \"ROW\" ? HEADER_HEIGHT : HEADER_WIDTH;\n }\n return size;\n }\n // ---------------------------------------------------------------------------\n // Grid rendering\n // ---------------------------------------------------------------------------\n drawGrid(renderingContext, layer) {\n switch (layer) {\n case 0 /* LAYERS.Background */:\n this.boxes = this.getGridBoxes();\n this.drawBackground(renderingContext);\n this.drawOverflowingCellBackground(renderingContext);\n this.drawCellBackground(renderingContext);\n this.drawBorders(renderingContext);\n this.drawTexts(renderingContext);\n this.drawIcon(renderingContext);\n this.drawFrozenPanes(renderingContext);\n break;\n case 7 /* LAYERS.Headers */:\n if (!this.getters.isDashboard()) {\n this.drawHeaders(renderingContext);\n this.drawFrozenPanesHeaders(renderingContext);\n }\n break;\n }\n }\n drawBackground(renderingContext) {\n const { ctx, thinLineWidth } = renderingContext;\n const { width, height } = this.getters.getSheetViewDimensionWithHeaders();\n // white background\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(0, 0, width + CANVAS_SHIFT, height + CANVAS_SHIFT);\n const areGridLinesVisible = !this.getters.isDashboard() &&\n this.getters.getGridLinesVisibility(this.getters.getActiveSheetId());\n const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;\n if (areGridLinesVisible) {\n for (const box of this.boxes) {\n ctx.strokeStyle = CELL_BORDER_COLOR;\n ctx.lineWidth = thinLineWidth;\n ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);\n }\n }\n }\n drawCellBackground(renderingContext) {\n const { ctx } = renderingContext;\n for (const box of this.boxes) {\n let style = box.style;\n if (style.fillColor && style.fillColor !== \"#ffffff\") {\n ctx.fillStyle = style.fillColor || \"#ffffff\";\n ctx.fillRect(box.x, box.y, box.width, box.height);\n }\n if (box.error) {\n ctx.fillStyle = \"red\";\n ctx.beginPath();\n ctx.moveTo(box.x + box.width - 5, box.y);\n ctx.lineTo(box.x + box.width, box.y);\n ctx.lineTo(box.x + box.width, box.y + 5);\n ctx.fill();\n }\n }\n }\n drawOverflowingCellBackground(renderingContext) {\n var _a, _b;\n const { ctx, thinLineWidth } = renderingContext;\n for (const box of this.boxes) {\n if (box.content && box.isOverflow) {\n const align = box.content.align || \"left\";\n let x;\n let width;\n const y = box.y + thinLineWidth / 2;\n const height = box.height - thinLineWidth;\n const clipWidth = Math.min(((_a = box.clipRect) === null || _a === void 0 ? void 0 : _a.width) || Infinity, box.content.width);\n if (align === \"left\") {\n x = box.x + thinLineWidth / 2;\n width = clipWidth - 2 * thinLineWidth;\n }\n else if (align === \"right\") {\n x = box.x + box.width - thinLineWidth / 2;\n width = -clipWidth + 2 * thinLineWidth;\n }\n else {\n x =\n (((_b = box.clipRect) === null || _b === void 0 ? void 0 : _b.x) || box.x + box.width / 2 - box.content.width / 2) + thinLineWidth / 2;\n width = clipWidth - 2 * thinLineWidth;\n }\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(x, y, width, height);\n }\n }\n }\n drawBorders(renderingContext) {\n const { ctx, thinLineWidth } = renderingContext;\n for (let box of this.boxes) {\n const border = box.border;\n if (border) {\n const { x, y, width, height } = box;\n if (border.left) {\n drawBorder(border.left, x, y, x, y + height);\n }\n if (border.top) {\n drawBorder(border.top, x, y, x + width, y);\n }\n if (border.right) {\n drawBorder(border.right, x + width, y, x + width, y + height);\n }\n if (border.bottom) {\n drawBorder(border.bottom, x, y + height, x + width, y + height);\n }\n }\n }\n function drawBorder([style, color], x1, y1, x2, y2) {\n ctx.strokeStyle = color;\n ctx.lineWidth = (style === \"thin\" ? 2 : 3) * thinLineWidth;\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.lineTo(x2, y2);\n ctx.stroke();\n }\n }\n drawTexts(renderingContext) {\n const { ctx, thinLineWidth } = renderingContext;\n ctx.textBaseline = \"top\";\n let currentFont;\n for (let box of this.boxes) {\n if (box.content) {\n const style = box.style || {};\n const align = box.content.align || \"left\";\n // compute font and textColor\n const font = computeTextFont(style);\n if (font !== currentFont) {\n currentFont = font;\n ctx.font = font;\n }\n ctx.fillStyle = style.textColor || \"#000\";\n // compute horizontal align start point parameter\n let x = box.x;\n if (align === \"left\") {\n x += MIN_CELL_TEXT_MARGIN + (box.image ? box.image.size + MIN_CF_ICON_MARGIN : 0);\n }\n else if (align === \"right\") {\n x +=\n box.width -\n MIN_CELL_TEXT_MARGIN -\n (box.isFilterHeader ? ICON_EDGE_LENGTH + FILTER_ICON_MARGIN : 0);\n }\n else {\n x += box.width / 2;\n }\n // horizontal align text direction\n ctx.textAlign = align;\n // clip rect if needed\n if (box.clipRect) {\n ctx.save();\n ctx.beginPath();\n const { x, y, width, height } = box.clipRect;\n ctx.rect(x, y, width, height);\n ctx.clip();\n }\n // compute vertical align start point parameter:\n const textLineHeight = computeTextFontSizeInPixels(style);\n const numberOfLines = box.content.textLines.length;\n let y = this.computeTextYCoordinate(box, textLineHeight, numberOfLines);\n // use the horizontal and the vertical start points to:\n // fill text / fill strikethrough / fill underline\n for (let brokenLine of box.content.textLines) {\n ctx.fillText(brokenLine, Math.round(x), Math.round(y));\n if (style.strikethrough || style.underline) {\n const lineWidth = computeTextWidth(ctx, brokenLine, style);\n let _x = x;\n if (align === \"right\") {\n _x -= lineWidth;\n }\n else if (align === \"center\") {\n _x -= lineWidth / 2;\n }\n if (style.strikethrough) {\n ctx.fillRect(_x, y + textLineHeight / 2, lineWidth, 2.6 * thinLineWidth);\n }\n if (style.underline) {\n ctx.fillRect(_x, y + textLineHeight + 1, lineWidth, 1.3 * thinLineWidth);\n }\n }\n y += MIN_CELL_TEXT_MARGIN + textLineHeight;\n }\n if (box.clipRect) {\n ctx.restore();\n }\n }\n }\n }\n drawIcon(renderingContext) {\n const { ctx } = renderingContext;\n for (const box of this.boxes) {\n if (box.image) {\n const icon = box.image.image;\n if (box.image.clipIcon) {\n ctx.save();\n ctx.beginPath();\n const { x, y, width, height } = box.image.clipIcon;\n ctx.rect(x, y, width, height);\n ctx.clip();\n }\n const iconSize = box.image.size;\n const y = this.computeTextYCoordinate(box, iconSize);\n ctx.drawImage(icon, box.x + MIN_CF_ICON_MARGIN, y, iconSize, iconSize);\n if (box.image.clipIcon) {\n ctx.restore();\n }\n }\n }\n }\n /** Compute the vertical start point from which a text line should be draw.\n *\n * Note that in case the cell does not have enough spaces to display its text lines,\n * (wrapping cell case) then the vertical align should be at the top.\n * */\n computeTextYCoordinate(box, textLineHeight, numberOfLines = 1) {\n const y = box.y + 1;\n const textHeight = computeTextLinesHeight(textLineHeight, numberOfLines);\n const hasEnoughSpaces = box.height > textHeight + MIN_CELL_TEXT_MARGIN * 2;\n const verticalAlign = box.verticalAlign || \"middle\";\n if (hasEnoughSpaces) {\n if (verticalAlign === \"middle\") {\n return y + (box.height - textHeight) / 2;\n }\n if (verticalAlign === \"bottom\") {\n return y + box.height - textHeight - MIN_CELL_TEXT_MARGIN;\n }\n }\n return y + MIN_CELL_TEXT_MARGIN;\n }\n drawHeaders(renderingContext) {\n const { ctx, thinLineWidth } = renderingContext;\n const visibleCols = this.getters.getSheetViewVisibleCols();\n const left = visibleCols[0];\n const right = visibleCols[visibleCols.length - 1];\n const visibleRows = this.getters.getSheetViewVisibleRows();\n const top = visibleRows[0];\n const bottom = visibleRows[visibleRows.length - 1];\n const { width, height } = this.getters.getSheetViewDimensionWithHeaders();\n const selection = this.getters.getSelectedZones();\n const selectedCols = getZonesCols(selection);\n const selectedRows = getZonesRows(selection);\n const sheetId = this.getters.getActiveSheetId();\n const numberOfCols = this.getters.getNumberCols(sheetId);\n const numberOfRows = this.getters.getNumberRows(sheetId);\n const activeCols = this.getters.getActiveCols();\n const activeRows = this.getters.getActiveRows();\n ctx.font = `400 ${HEADER_FONT_SIZE}px ${DEFAULT_FONT}`;\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.lineWidth = thinLineWidth;\n ctx.strokeStyle = \"#333\";\n // Columns headers background\n for (let col = left; col <= right; col++) {\n const colZone = { left: col, right: col, top: 0, bottom: numberOfRows - 1 };\n const { x, width } = this.getters.getVisibleRect(colZone);\n const colHasFilter = this.getters.doesZonesContainFilter(sheetId, [colZone]);\n const isColActive = activeCols.has(col);\n const isColSelected = selectedCols.has(col);\n if (isColActive) {\n ctx.fillStyle = colHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;\n }\n else if (isColSelected) {\n ctx.fillStyle = colHasFilter\n ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR\n : BACKGROUND_HEADER_SELECTED_COLOR;\n }\n else {\n ctx.fillStyle = colHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;\n }\n ctx.fillRect(x, 0, width, HEADER_HEIGHT);\n }\n // Rows headers background\n for (let row = top; row <= bottom; row++) {\n const rowZone = { top: row, bottom: row, left: 0, right: numberOfCols - 1 };\n const { y, height } = this.getters.getVisibleRect(rowZone);\n const rowHasFilter = this.getters.doesZonesContainFilter(sheetId, [rowZone]);\n const isRowActive = activeRows.has(row);\n const isRowSelected = selectedRows.has(row);\n if (isRowActive) {\n ctx.fillStyle = rowHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;\n }\n else if (isRowSelected) {\n ctx.fillStyle = rowHasFilter\n ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR\n : BACKGROUND_HEADER_SELECTED_COLOR;\n }\n else {\n ctx.fillStyle = rowHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;\n }\n ctx.fillRect(0, y, HEADER_WIDTH, height);\n }\n // 2 main lines\n ctx.beginPath();\n ctx.moveTo(HEADER_WIDTH, 0);\n ctx.lineTo(HEADER_WIDTH, height);\n ctx.moveTo(0, HEADER_HEIGHT);\n ctx.lineTo(width, HEADER_HEIGHT);\n ctx.strokeStyle = HEADER_BORDER_COLOR;\n ctx.stroke();\n ctx.beginPath();\n // column text + separator\n for (const i of visibleCols) {\n const colSize = this.getters.getColSize(sheetId, i);\n const colName = numberToLetters(i);\n ctx.fillStyle = activeCols.has(i) ? \"#fff\" : TEXT_HEADER_COLOR;\n let colStart = this.getHeaderOffset(\"COL\", left, i);\n ctx.fillText(colName, colStart + colSize / 2, HEADER_HEIGHT / 2);\n ctx.moveTo(colStart + colSize, 0);\n ctx.lineTo(colStart + colSize, HEADER_HEIGHT);\n }\n // row text + separator\n for (const i of visibleRows) {\n const rowSize = this.getters.getRowSize(sheetId, i);\n ctx.fillStyle = activeRows.has(i) ? \"#fff\" : TEXT_HEADER_COLOR;\n let rowStart = this.getHeaderOffset(\"ROW\", top, i);\n ctx.fillText(String(i + 1), HEADER_WIDTH / 2, rowStart + rowSize / 2);\n ctx.moveTo(0, rowStart + rowSize);\n ctx.lineTo(HEADER_WIDTH, rowStart + rowSize);\n }\n ctx.stroke();\n }\n drawFrozenPanesHeaders(renderingContext) {\n const { ctx, thinLineWidth } = renderingContext;\n const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();\n const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;\n const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;\n ctx.lineWidth = 6 * thinLineWidth;\n ctx.strokeStyle = \"#BCBCBC\";\n ctx.beginPath();\n if (offsetCorrectionX) {\n ctx.moveTo(widthCorrection + offsetCorrectionX, 0);\n ctx.lineTo(widthCorrection + offsetCorrectionX, heightCorrection);\n }\n if (offsetCorrectionY) {\n ctx.moveTo(0, heightCorrection + offsetCorrectionY);\n ctx.lineTo(widthCorrection, heightCorrection + offsetCorrectionY);\n }\n ctx.stroke();\n }\n drawFrozenPanes(renderingContext) {\n const { ctx, thinLineWidth } = renderingContext;\n const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();\n const visibleCols = this.getters.getSheetViewVisibleCols();\n const left = visibleCols[0];\n const right = visibleCols[visibleCols.length - 1];\n const visibleRows = this.getters.getSheetViewVisibleRows();\n const top = visibleRows[0];\n const bottom = visibleRows[visibleRows.length - 1];\n const viewport = { left, right, top, bottom };\n const rect = this.getters.getVisibleRect(viewport);\n const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;\n const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;\n ctx.lineWidth = 6 * thinLineWidth;\n ctx.strokeStyle = \"#DADFE8\";\n ctx.beginPath();\n if (offsetCorrectionX) {\n ctx.moveTo(widthCorrection + offsetCorrectionX, heightCorrection);\n ctx.lineTo(widthCorrection + offsetCorrectionX, rect.height + heightCorrection);\n }\n if (offsetCorrectionY) {\n ctx.moveTo(widthCorrection, heightCorrection + offsetCorrectionY);\n ctx.lineTo(rect.width + widthCorrection, heightCorrection + offsetCorrectionY);\n }\n ctx.stroke();\n }\n findNextEmptyCol(base, max, row) {\n const sheetId = this.getters.getActiveSheetId();\n let col = base;\n while (col < max) {\n const position = { sheetId, col: col + 1, row };\n const nextCell = this.getters.getEvaluatedCell(position);\n const nextCellBorder = this.getters.getCellBorderWithFilterBorder(position);\n if (nextCell.type !== CellValueType.empty ||\n this.getters.isInMerge(position) ||\n (nextCellBorder === null || nextCellBorder === void 0 ? void 0 : nextCellBorder.left)) {\n return col;\n }\n col++;\n }\n return col;\n }\n findPreviousEmptyCol(base, min, row) {\n const sheetId = this.getters.getActiveSheetId();\n let col = base;\n while (col > min) {\n const position = { sheetId, col: col - 1, row };\n const previousCell = this.getters.getEvaluatedCell(position);\n const previousCellBorder = this.getters.getCellBorderWithFilterBorder(position);\n if (previousCell.type !== CellValueType.empty ||\n this.getters.isInMerge(position) ||\n (previousCellBorder === null || previousCellBorder === void 0 ? void 0 : previousCellBorder.right)) {\n return col;\n }\n col--;\n }\n return col;\n }\n computeCellAlignment(position, isOverflowing) {\n const cell = this.getters.getCell(position);\n if ((cell === null || cell === void 0 ? void 0 : cell.isFormula) && this.getters.shouldShowFormulas()) {\n return \"left\";\n }\n const { align } = this.getters.getCellStyle(position);\n const evaluatedCell = this.getters.getEvaluatedCell(position);\n if (isOverflowing && evaluatedCell.type === CellValueType.number) {\n return align !== \"center\" ? \"left\" : align;\n }\n return align || evaluatedCell.defaultAlign;\n }\n createZoneBox(sheetId, zone, viewport) {\n const { left, right } = viewport;\n const col = zone.left;\n const row = zone.top;\n const position = { sheetId, col, row };\n const cell = this.getters.getEvaluatedCell(position);\n const showFormula = this.getters.shouldShowFormulas();\n const { x, y, width, height } = this.getters.getVisibleRect(zone);\n const { verticalAlign } = this.getters.getCellStyle(position);\n const box = {\n x,\n y,\n width,\n height,\n border: this.getters.getCellBorderWithFilterBorder(position) || undefined,\n style: this.getters.getCellComputedStyle(position),\n verticalAlign,\n };\n if (cell.type === CellValueType.empty) {\n return box;\n }\n /** Icon CF */\n const cfIcon = this.getters.getConditionalIcon(position);\n const fontSizePX = computeTextFontSizeInPixels(box.style);\n const iconBoxWidth = cfIcon ? MIN_CF_ICON_MARGIN + fontSizePX : 0;\n if (cfIcon) {\n box.image = {\n type: \"icon\",\n size: fontSizePX,\n clipIcon: { x: box.x, y: box.y, width: Math.min(iconBoxWidth, width), height },\n image: ICONS[cfIcon].img,\n };\n }\n /** Filter Header */\n box.isFilterHeader = this.getters.isFilterHeader(position);\n const headerIconWidth = box.isFilterHeader ? FILTER_ICON_EDGE_LENGTH + FILTER_ICON_MARGIN : 0;\n /** Content */\n const text = this.getters.getCellText(position, showFormula);\n const textWidth = this.getters.getTextWidth(position) + MIN_CELL_TEXT_MARGIN;\n const wrapping = this.getters.getCellStyle(position).wrapping || \"overflow\";\n const multiLineText = wrapping === \"wrap\"\n ? this.getters.getCellMultiLineText(position, width - 2 * MIN_CELL_TEXT_MARGIN)\n : [text];\n const contentWidth = iconBoxWidth + textWidth + headerIconWidth;\n const align = this.computeCellAlignment(position, contentWidth > width);\n box.content = {\n textLines: multiLineText,\n width: wrapping === \"overflow\" ? textWidth : width,\n align,\n };\n /** Error */\n if (cell.type === CellValueType.error && cell.error.logLevel > CellErrorLevel.silent) {\n box.error = cell.error.message;\n }\n /** ClipRect */\n const isOverflowing = contentWidth > width || fontSizePX > height;\n if (cfIcon || box.isFilterHeader) {\n box.clipRect = {\n x: box.x + iconBoxWidth,\n y: box.y,\n width: Math.max(0, width - iconBoxWidth - headerIconWidth),\n height,\n };\n }\n else if (isOverflowing && wrapping === \"overflow\") {\n let nextColIndex, previousColIndex;\n const isCellInMerge = this.getters.isInMerge(position);\n if (isCellInMerge) {\n // Always clip merges\n nextColIndex = this.getters.getMerge(position).right;\n previousColIndex = col;\n }\n else {\n nextColIndex = this.findNextEmptyCol(col, right, row);\n previousColIndex = this.findPreviousEmptyCol(col, left, row);\n box.isOverflow = true;\n }\n switch (align) {\n case \"left\": {\n const emptyZoneOnTheLeft = positionToZone({ col: nextColIndex, row });\n const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheLeft));\n if (width < contentWidth || fontSizePX > height) {\n box.clipRect = { x, y, width, height };\n }\n break;\n }\n case \"right\": {\n const emptyZoneOnTheRight = positionToZone({ col: previousColIndex, row });\n const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheRight));\n if (width < contentWidth || fontSizePX > height) {\n box.clipRect = { x, y, width, height };\n }\n break;\n }\n case \"center\": {\n const emptyZone = {\n ...zone,\n left: previousColIndex,\n right: nextColIndex,\n };\n const { x, y, height, width } = this.getters.getVisibleRect(emptyZone);\n const halfContentWidth = contentWidth / 2;\n const boxMiddle = box.x + box.width / 2;\n if (x + width < boxMiddle + halfContentWidth ||\n x > boxMiddle - halfContentWidth ||\n fontSizePX > height) {\n const clipX = x > boxMiddle - halfContentWidth ? x : boxMiddle - halfContentWidth;\n const clipWidth = x + width - clipX;\n box.clipRect = { x: clipX, y, width: clipWidth, height };\n }\n break;\n }\n }\n }\n else if (wrapping === \"clip\" || wrapping === \"wrap\") {\n box.clipRect = {\n x: box.x,\n y: box.y,\n width,\n height,\n };\n }\n return box;\n }\n getGridBoxes() {\n const boxes = [];\n const visibleCols = this.getters.getSheetViewVisibleCols();\n const left = visibleCols[0];\n const right = visibleCols[visibleCols.length - 1];\n const visibleRows = this.getters.getSheetViewVisibleRows();\n const top = visibleRows[0];\n const bottom = visibleRows[visibleRows.length - 1];\n const viewport = { left, right, top, bottom };\n const sheetId = this.getters.getActiveSheetId();\n for (const row of visibleRows) {\n for (const col of visibleCols) {\n const position = { sheetId, col, row };\n if (this.getters.isInMerge(position)) {\n continue;\n }\n boxes.push(this.createZoneBox(sheetId, positionToZone(position), viewport));\n }\n }\n for (const merge of this.getters.getMerges(sheetId)) {\n if (this.getters.isMergeHidden(sheetId, merge)) {\n continue;\n }\n if (overlap(merge, viewport)) {\n const box = this.createZoneBox(sheetId, merge, viewport);\n const borderBottomRight = this.getters.getCellBorder({\n sheetId,\n col: merge.right,\n row: merge.bottom,\n });\n box.border = {\n ...box.border,\n bottom: borderBottomRight ? borderBottomRight.bottom : undefined,\n right: borderBottomRight ? borderBottomRight.right : undefined,\n };\n box.isMerge = true;\n boxes.push(box);\n }\n }\n return boxes;\n }\n }\n RendererPlugin.layers = [0 /* LAYERS.Background */, 7 /* LAYERS.Headers */];\n RendererPlugin.getters = [\"getColDimensionsInViewport\", \"getRowDimensionsInViewport\"];\n\n /**\n * Selection input Plugin\n *\n * The SelectionInput component input and output are both arrays of strings, but\n * it requires an intermediary internal state to work.\n * This plugin handles this internal state.\n */\n class SelectionInputPlugin extends UIPlugin {\n constructor(config, initialRanges, inputHasSingleRange) {\n super(config);\n this.inputHasSingleRange = inputHasSingleRange;\n this.ranges = [];\n this.focusedRangeIndex = null;\n this.willAddNewRange = false;\n this.insertNewRange(0, initialRanges);\n this.activeSheet = this.getters.getActiveSheetId();\n if (this.ranges.length === 0) {\n this.insertNewRange(this.ranges.length, [\"\"]);\n this.focusLast();\n }\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"ADD_EMPTY_RANGE\":\n if (this.inputHasSingleRange && this.ranges.length === 1) {\n return 30 /* CommandResult.MaximumRangesReached */;\n }\n break;\n }\n return 0 /* CommandResult.Success */;\n }\n handleEvent(event) {\n const xc = zoneToXc(event.anchor.zone);\n const inputSheetId = this.activeSheet;\n const sheetId = this.getters.getActiveSheetId();\n const sheetName = this.getters.getSheetName(sheetId);\n this.add([sheetId === inputSheetId ? xc : `${getComposerSheetName(sheetName)}!${xc}`]);\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"UNFOCUS_SELECTION_INPUT\":\n this.unfocus();\n break;\n case \"FOCUS_RANGE\":\n this.focus(this.getIndex(cmd.rangeId));\n break;\n case \"CHANGE_RANGE\": {\n const index = this.getIndex(cmd.rangeId);\n if (index !== null && this.focusedRangeIndex !== index) {\n this.focus(index);\n }\n if (index !== null) {\n const values = cmd.value.split(\",\").map((reference) => reference.trim());\n this.setRange(index, values);\n }\n break;\n }\n case \"ADD_EMPTY_RANGE\":\n this.insertNewRange(this.ranges.length, [\"\"]);\n this.focusLast();\n break;\n case \"REMOVE_RANGE\":\n const index = this.getIndex(cmd.rangeId);\n if (index !== null) {\n this.removeRange(index);\n }\n break;\n case \"STOP_SELECTION_INPUT\":\n this.willAddNewRange = false;\n break;\n case \"PREPARE_SELECTION_INPUT_EXPANSION\": {\n const index = this.focusedRangeIndex;\n if (index !== null && !this.inputHasSingleRange) {\n this.willAddNewRange = this.ranges[index].xc.trim() !== \"\";\n }\n break;\n }\n case \"ACTIVATE_SHEET\": {\n if (cmd.sheetIdFrom !== cmd.sheetIdTo) {\n const { col, row } = this.getters.getNextVisibleCellPosition({\n sheetId: cmd.sheetIdTo,\n col: 0,\n row: 0,\n });\n const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));\n this.selection.resetAnchor(this, { cell: { col, row }, zone });\n }\n }\n }\n }\n unsubscribe() {\n this.unfocus();\n }\n // ---------------------------------------------------------------------------\n // Getters || only callable by the parent\n // ---------------------------------------------------------------------------\n getSelectionInputValue() {\n return this.cleanInputs(this.ranges.map((range) => {\n return range.xc ? range.xc : \"\";\n }));\n }\n getSelectionInputHighlights() {\n return this.ranges.map((input) => this.inputToHighlights(input)).flat();\n }\n // ---------------------------------------------------------------------------\n // Other\n // ---------------------------------------------------------------------------\n /**\n * Focus a given range or remove the focus.\n */\n focus(index) {\n this.focusedRangeIndex = index;\n }\n focusLast() {\n this.focus(this.ranges.length - 1);\n }\n unfocus() {\n this.focusedRangeIndex = null;\n }\n add(newRanges) {\n if (this.focusedRangeIndex === null || newRanges.length === 0) {\n return;\n }\n if (this.willAddNewRange) {\n this.insertNewRange(this.ranges.length, newRanges);\n this.focusLast();\n this.willAddNewRange = false;\n }\n else {\n this.setRange(this.focusedRangeIndex, newRanges);\n }\n }\n setContent(index, xc) {\n this.ranges[index] = {\n ...this.ranges[index],\n xc,\n };\n }\n /**\n * Insert new inputs after the given index.\n */\n insertNewRange(index, values) {\n this.ranges.splice(index, 0, ...values.map((xc, i) => ({\n xc,\n id: (this.ranges.length + i + 1).toString(),\n color: colors$1[(this.ranges.length + i) % colors$1.length],\n })));\n }\n /**\n * Set a new value in a given range input. If more than one value is provided,\n * new inputs will be added.\n */\n setRange(index, values) {\n const [, ...additionalValues] = values;\n this.setContent(index, values[0]);\n this.insertNewRange(index + 1, additionalValues);\n // focus the last newly added range\n if (additionalValues.length) {\n this.focus(index + additionalValues.length);\n }\n }\n removeRange(index) {\n this.ranges.splice(index, 1);\n if (this.focusedRangeIndex !== null) {\n this.focusLast();\n }\n }\n /**\n * Convert highlights input format to the command format.\n * The first xc in the input range will keep its color.\n * Invalid ranges and ranges from other sheets than the active sheets\n * are ignored.\n */\n inputToHighlights({ xc, color }) {\n const XCs = this.cleanInputs([xc])\n .filter((range) => this.getters.isRangeValid(range))\n .filter((reference) => this.shouldBeHighlighted(this.activeSheet, reference));\n return XCs.map((xc) => {\n const { sheetName } = splitReference(xc);\n return {\n zone: this.getters.getRangeFromSheetXC(this.activeSheet, xc).zone,\n sheetId: (sheetName && this.getters.getSheetIdByName(sheetName)) || this.activeSheet,\n color,\n };\n });\n }\n cleanInputs(ranges) {\n return ranges\n .map((xc) => xc.split(\",\"))\n .flat()\n .map((xc) => xc.trim())\n .filter((xc) => xc !== \"\");\n }\n /**\n * Check if a cell or range reference should be highlighted.\n * It should be highlighted if it references the current active sheet.\n * Note that if no sheet name is given in the reference (\"A1\"), it refers to the\n * active sheet when the selection input was enabled which might be different from\n * the current active sheet.\n */\n shouldBeHighlighted(inputSheetId, reference) {\n const { sheetName } = splitReference(reference);\n const sheetId = this.getters.getSheetIdByName(sheetName);\n const activeSheetId = this.getters.getActiveSheet().id;\n const valid = this.getters.isRangeValid(reference);\n return (valid &&\n (sheetId === activeSheetId || (sheetId === undefined && activeSheetId === inputSheetId)));\n }\n /**\n * Return the index of a range given its id\n * or `null` if the range is not found.\n */\n getIndex(rangeId) {\n const index = this.ranges.findIndex((range) => range.id === rangeId);\n return index >= 0 ? index : null;\n }\n }\n SelectionInputPlugin.layers = [1 /* LAYERS.Highlights */];\n SelectionInputPlugin.getters = [];\n\n /**\n * Selection input Plugin\n *\n * The SelectionInput component input and output are both arrays of strings, but\n * it requires an intermediary internal state to work.\n * This plugin handles this internal state.\n */\n class SelectionInputsManagerPlugin extends UIPlugin {\n constructor(config) {\n super(config);\n this.config = config;\n this.inputs = {};\n this.focusedInputId = null;\n }\n get currentInput() {\n return this.focusedInputId ? this.inputs[this.focusedInputId] : null;\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n var _a, _b;\n switch (cmd.type) {\n case \"FOCUS_RANGE\":\n const index = (_a = this.currentInput) === null || _a === void 0 ? void 0 : _a.getIndex(cmd.rangeId);\n if (this.focusedInputId === cmd.id && ((_b = this.currentInput) === null || _b === void 0 ? void 0 : _b.focusedRangeIndex) === index) {\n return 29 /* CommandResult.InputAlreadyFocused */;\n }\n break;\n }\n if (this.currentInput) {\n return this.currentInput.allowDispatch(cmd);\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n var _a;\n switch (cmd.type) {\n case \"ENABLE_NEW_SELECTION_INPUT\":\n this.initInput(cmd.id, cmd.initialRanges || [], cmd.hasSingleRange);\n break;\n case \"DISABLE_SELECTION_INPUT\":\n if (this.focusedInputId === cmd.id) {\n this.unfocus();\n }\n delete this.inputs[cmd.id];\n break;\n case \"UNFOCUS_SELECTION_INPUT\":\n this.unfocus();\n break;\n case \"ADD_EMPTY_RANGE\":\n case \"REMOVE_RANGE\":\n if (cmd.id !== this.focusedInputId) {\n const input = this.inputs[cmd.id];\n this.selection.capture(input, { cell: { col: 0, row: 0 }, zone: positionToZone({ col: 0, row: 0 }) }, { handleEvent: input.handleEvent.bind(input) });\n this.focusedInputId = cmd.id;\n }\n break;\n case \"FOCUS_RANGE\":\n case \"CHANGE_RANGE\":\n if (cmd.id !== this.focusedInputId) {\n const input = this.inputs[cmd.id];\n const range = input.ranges.find((range) => range.id === cmd.rangeId);\n const sheetId = this.getters.getActiveSheetId();\n const zone = this.getters.getRangeFromSheetXC(sheetId, (range === null || range === void 0 ? void 0 : range.xc) || \"A1\").zone;\n this.selection.capture(input, { cell: { col: zone.left, row: zone.top }, zone }, { handleEvent: input.handleEvent.bind(input) });\n this.focusedInputId = cmd.id;\n }\n break;\n }\n (_a = this.currentInput) === null || _a === void 0 ? void 0 : _a.handle(cmd);\n }\n unsubscribe() {\n this.unfocus();\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n /**\n * Return a list of all valid XCs.\n * e.g. [\"A1\", \"Sheet2!B3\", \"E12\"]\n */\n getSelectionInput(id) {\n if (!this.inputs[id]) {\n return [];\n }\n return this.inputs[id].ranges.map((input, index) => Object.assign({}, input, {\n color: this.focusedInputId === id &&\n this.inputs[id].focusedRangeIndex !== null &&\n this.isRangeValid(input.xc)\n ? input.color\n : null,\n isFocused: this.focusedInputId === id && this.inputs[id].focusedRangeIndex === index,\n }));\n }\n isRangeValid(reference) {\n if (!reference) {\n return false;\n }\n const { xc, sheetName } = splitReference(reference);\n return (xc.match(rangeReference) !== null &&\n (!sheetName || this.getters.getSheetIdByName(sheetName) !== undefined));\n }\n getSelectionInputValue(id) {\n return this.inputs[id].getSelectionInputValue();\n }\n getSelectionInputHighlights() {\n if (!this.focusedInputId) {\n return [];\n }\n return this.inputs[this.focusedInputId].getSelectionInputHighlights();\n }\n // ---------------------------------------------------------------------------\n // Other\n // ---------------------------------------------------------------------------\n initInput(id, initialRanges, inputHasSingleRange = false) {\n this.inputs[id] = new SelectionInputPlugin(this.config, initialRanges, inputHasSingleRange);\n if (initialRanges.length === 0) {\n const input = this.inputs[id];\n const anchor = {\n zone: positionToZone({ col: 0, row: 0 }),\n cell: { col: 0, row: 0 },\n };\n this.selection.capture(input, anchor, { handleEvent: input.handleEvent.bind(input) });\n this.focusedInputId = id;\n }\n }\n unfocus() {\n this.selection.release(this.currentInput);\n this.focusedInputId = null;\n }\n }\n SelectionInputsManagerPlugin.layers = [1 /* LAYERS.Highlights */];\n SelectionInputsManagerPlugin.getters = [\n \"getSelectionInput\",\n \"getSelectionInputValue\",\n \"isRangeValid\",\n \"getSelectionInputHighlights\",\n ];\n\n /**\n * This is a generic event bus based on the Owl event bus.\n * This bus however ensures type safety across events and subscription callbacks.\n */\n class EventBus {\n constructor() {\n this.subscriptions = {};\n }\n /**\n * Add a listener for the 'eventType' events.\n *\n * Note that the 'owner' of this event can be anything, but will more likely\n * be a component or a class. The idea is that the callback will be called with\n * the proper owner bound.\n *\n * Also, the owner should be kind of unique. This will be used to remove the\n * listener.\n */\n on(type, owner, callback) {\n if (!callback) {\n throw new Error(\"Missing callback\");\n }\n if (!this.subscriptions[type]) {\n this.subscriptions[type] = [];\n }\n this.subscriptions[type].push({\n owner,\n callback,\n });\n }\n /**\n * Emit an event of type 'eventType'. Any extra arguments will be passed to\n * the listeners callback.\n */\n trigger(type, payload) {\n const subs = this.subscriptions[type] || [];\n for (let i = 0, iLen = subs.length; i < iLen; i++) {\n const sub = subs[i];\n sub.callback.call(sub.owner, payload);\n }\n }\n /**\n * Remove a listener\n */\n off(eventType, owner) {\n const subs = this.subscriptions[eventType];\n if (subs) {\n this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner);\n }\n }\n /**\n * Remove all subscriptions.\n */\n clear() {\n this.subscriptions = {};\n }\n }\n\n /*\n * This file contains the specifics transformations\n */\n otRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"CREATE_CHART\", \"UPDATE_CHART\"], updateChartRangesTransformation);\n otRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"CREATE_CHART\", \"UPDATE_CHART\"], updateChartRangesTransformation);\n otRegistry.addTransformation(\"DELETE_SHEET\", [\"MOVE_RANGES\"], transformTargetSheetId);\n otRegistry.addTransformation(\"DELETE_FIGURE\", [\"UPDATE_FIGURE\", \"UPDATE_CHART\"], updateChartFigure);\n otRegistry.addTransformation(\"CREATE_SHEET\", [\"CREATE_SHEET\"], createSheetTransformation);\n otRegistry.addTransformation(\"ADD_MERGE\", [\"ADD_MERGE\", \"REMOVE_MERGE\", \"CREATE_FILTER_TABLE\"], mergeTransformation);\n otRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"FREEZE_COLUMNS\", \"FREEZE_ROWS\"], freezeTransformation);\n otRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"FREEZE_COLUMNS\", \"FREEZE_ROWS\"], freezeTransformation);\n otRegistry.addTransformation(\"CREATE_FILTER_TABLE\", [\"CREATE_FILTER_TABLE\", \"ADD_MERGE\"], createTableTransformation);\n function transformTargetSheetId(cmd, executed) {\n const deletedSheetId = executed.sheetId;\n if (cmd.targetSheetId === deletedSheetId || cmd.sheetId === deletedSheetId) {\n return undefined;\n }\n return cmd;\n }\n function updateChartFigure(toTransform, executed) {\n if (toTransform.id === executed.id) {\n return undefined;\n }\n return toTransform;\n }\n function updateChartRangesTransformation(toTransform, executed) {\n return {\n ...toTransform,\n definition: transformDefinition(toTransform.definition, executed),\n };\n }\n function createSheetTransformation(cmd, executed) {\n var _a;\n if (cmd.name === executed.name) {\n return {\n ...cmd,\n name: ((_a = cmd.name) === null || _a === void 0 ? void 0 : _a.match(/\\d+/))\n ? cmd.name.replace(/\\d+/, (n) => (parseInt(n) + 1).toString())\n : `${cmd.name}~`,\n position: cmd.position + 1,\n };\n }\n return cmd;\n }\n function mergeTransformation(cmd, executed) {\n if (cmd.sheetId !== executed.sheetId) {\n return cmd;\n }\n const target = [];\n for (const zone1 of cmd.target) {\n for (const zone2 of executed.target) {\n if (!overlap(zone1, zone2)) {\n target.push({ ...zone1 });\n }\n }\n }\n if (target.length) {\n return { ...cmd, target };\n }\n return undefined;\n }\n function freezeTransformation(cmd, executed) {\n if (cmd.sheetId !== executed.sheetId) {\n return cmd;\n }\n const dimension = cmd.type === \"FREEZE_COLUMNS\" ? \"COL\" : \"ROW\";\n if (dimension !== executed.dimension) {\n return cmd;\n }\n let quantity = cmd[\"quantity\"];\n if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n const executedElements = [...executed.elements].sort((a, b) => b - a);\n for (let removedElement of executedElements) {\n if (quantity > removedElement) {\n quantity--;\n }\n }\n }\n if (executed.type === \"ADD_COLUMNS_ROWS\") {\n const executedBase = executed.position === \"before\" ? executed.base - 1 : executed.base;\n quantity = quantity > executedBase ? quantity + executed.quantity : quantity;\n }\n return quantity > 0 ? { ...cmd, quantity } : undefined;\n }\n /**\n * Cancel CREATE_FILTER_TABLE and ADD_MERGE commands if they overlap a filter\n */\n function createTableTransformation(cmd, executed) {\n if (cmd.sheetId !== executed.sheetId) {\n return cmd;\n }\n for (const cmdTarget of cmd.target) {\n for (const executedCmdTarget of executed.target) {\n if (overlap(executedCmdTarget, cmdTarget)) {\n return undefined;\n }\n }\n }\n return cmd;\n }\n\n const transformations = [\n { match: isSheetDependent, fn: transformSheetId },\n { match: isTargetDependent, fn: transformTarget },\n { match: isPositionDependent, fn: transformPosition },\n { match: isGridDependent, fn: transformDimension },\n { match: isRangeDependant, fn: transformRangeData },\n ];\n /**\n * Get the result of applying the operation transformations on the given command\n * to transform based on the executed command.\n * Let's see a small example:\n * Given\n * - command A: set the content of C1 to \"Hello\"\n * - command B: add a column after A\n *\n * If command B has been executed locally and not transmitted (yet) to\n * other clients, and command A arrives from an other client to be executed locally.\n * Command A is no longer valid and no longer reflects the user intention.\n * It needs to be transformed knowing that command B is already executed.\n * transform(A, B) => set the content of D1 to \"Hello\"\n */\n function transform(toTransform, executed) {\n const specificTransform = otRegistry.getTransformation(toTransform.type, executed.type);\n return specificTransform\n ? specificTransform(toTransform, executed)\n : genericTransform(toTransform, executed);\n }\n /**\n * Get the result of applying the operation transformations on all the given\n * commands to transform for each executed commands.\n */\n function transformAll(toTransform, executed) {\n let transformedCommands = [...toTransform];\n for (const executedCommand of executed) {\n transformedCommands = transformedCommands\n .map((cmd) => transform(cmd, executedCommand))\n .filter(isDefined$1);\n }\n return transformedCommands;\n }\n /**\n * Apply all generic transformation based on the characteristic of the given commands.\n */\n function genericTransform(cmd, executed) {\n for (const { match, fn } of transformations) {\n if (match(cmd)) {\n const result = fn(cmd, executed);\n if (result === \"SKIP_TRANSFORMATION\") {\n continue;\n }\n if (result === \"IGNORE_COMMAND\") {\n return undefined;\n }\n cmd = result;\n }\n }\n return cmd;\n }\n function transformSheetId(cmd, executed) {\n const deleteSheet = executed.type === \"DELETE_SHEET\" && executed.sheetId;\n if (cmd.sheetId === deleteSheet) {\n return \"IGNORE_COMMAND\";\n }\n else if (cmd.type === \"CREATE_SHEET\" ||\n executed.type === \"CREATE_SHEET\" ||\n cmd.sheetId !== executed.sheetId) {\n return cmd;\n }\n return \"SKIP_TRANSFORMATION\";\n }\n function transformTarget(cmd, executed) {\n const transformSheetResult = transformSheetId(cmd, executed);\n if (transformSheetResult !== \"SKIP_TRANSFORMATION\") {\n return transformSheetResult === \"IGNORE_COMMAND\" ? \"IGNORE_COMMAND\" : cmd;\n }\n const target = [];\n for (const zone of cmd.target) {\n const newZone = transformZone(zone, executed);\n if (newZone) {\n target.push(newZone);\n }\n }\n if (!target.length) {\n return \"IGNORE_COMMAND\";\n }\n return { ...cmd, target };\n }\n function transformRangeData(cmd, executed) {\n const ranges = [];\n const deletedSheet = executed.type === \"DELETE_SHEET\" && executed.sheetId;\n for (const range of cmd.ranges) {\n if (range._sheetId !== executed.sheetId) {\n ranges.push({ ...range, _zone: range._zone });\n }\n else {\n const newZone = transformZone(range._zone, executed);\n if (newZone && deletedSheet !== range._sheetId) {\n ranges.push({ ...range, _zone: newZone });\n }\n }\n }\n if (!ranges.length) {\n return \"IGNORE_COMMAND\";\n }\n return { ...cmd, ranges };\n }\n function transformDimension(cmd, executed) {\n const transformSheetResult = transformSheetId(cmd, executed);\n if (transformSheetResult !== \"SKIP_TRANSFORMATION\") {\n return transformSheetResult === \"IGNORE_COMMAND\" ? \"IGNORE_COMMAND\" : cmd;\n }\n if (executed.type === \"ADD_COLUMNS_ROWS\" || executed.type === \"REMOVE_COLUMNS_ROWS\") {\n if (executed.dimension !== cmd.dimension) {\n return cmd;\n }\n const isUnique = cmd.type === \"ADD_COLUMNS_ROWS\";\n const field = isUnique ? \"base\" : \"elements\";\n let elements = isUnique ? [cmd[field]] : cmd[field];\n if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n elements = elements\n .map((element) => {\n if (executed.elements.includes(element)) {\n return undefined;\n }\n const executedElements = executed.elements.sort((a, b) => b - a);\n for (let removedElement of executedElements) {\n if (element > removedElement) {\n element--;\n }\n }\n return element;\n })\n .filter(isDefined$1);\n }\n if (executed.type === \"ADD_COLUMNS_ROWS\") {\n const base = executed.position === \"before\" ? executed.base - 1 : executed.base;\n elements = elements.map((el) => (el > base ? el + executed.quantity : el));\n }\n if (elements.length) {\n let result = elements;\n if (isUnique) {\n result = elements[0];\n }\n return { ...cmd, [field]: result };\n }\n return \"IGNORE_COMMAND\";\n }\n return \"SKIP_TRANSFORMATION\";\n }\n /**\n * Transform a PositionDependentCommand. It could be impacted by a grid command\n * (Add/remove cols/rows) and a merge\n */\n function transformPosition(cmd, executed) {\n const transformSheetResult = transformSheetId(cmd, executed);\n if (transformSheetResult !== \"SKIP_TRANSFORMATION\") {\n return transformSheetResult === \"IGNORE_COMMAND\" ? \"IGNORE_COMMAND\" : cmd;\n }\n if (executed.type === \"ADD_COLUMNS_ROWS\" || executed.type === \"REMOVE_COLUMNS_ROWS\") {\n return transformPositionWithGrid(cmd, executed);\n }\n if (executed.type === \"ADD_MERGE\") {\n return transformPositionWithMerge(cmd, executed);\n }\n return \"SKIP_TRANSFORMATION\";\n }\n /**\n * Transform a PositionDependentCommand after a grid shape modification. This\n * transformation consists of updating the position.\n */\n function transformPositionWithGrid(cmd, executed) {\n const field = executed.dimension === \"COL\" ? \"col\" : \"row\";\n let base = cmd[field];\n if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n const elements = [...executed.elements].sort((a, b) => b - a);\n if (elements.includes(base)) {\n return \"IGNORE_COMMAND\";\n }\n for (let removedElement of elements) {\n if (base >= removedElement) {\n base--;\n }\n }\n }\n if (executed.type === \"ADD_COLUMNS_ROWS\") {\n if (base > executed.base || (base === executed.base && executed.position === \"before\")) {\n base = base + executed.quantity;\n }\n }\n return { ...cmd, [field]: base };\n }\n /**\n * Transform a PositionDependentCommand after a merge. This transformation\n * consists of checking that the position is not inside the merged zones\n */\n function transformPositionWithMerge(cmd, executed) {\n for (const zone of executed.target) {\n const sameTopLeft = cmd.col === zone.left && cmd.row === zone.top;\n if (!sameTopLeft && isInside(cmd.col, cmd.row, zone)) {\n return \"IGNORE_COMMAND\";\n }\n }\n return cmd;\n }\n\n class Revision {\n /**\n * A revision represents a whole client action (Create a sheet, merge a Zone, Undo, ...).\n * A revision contains the following information:\n * - id: ID of the revision\n * - commands: CoreCommands that are linked to the action, and should be\n * dispatched in other clients\n * - clientId: Client who initiated the action\n * - changes: List of changes applied on the state.\n */\n constructor(id, clientId, commands, changes) {\n this._commands = [];\n this._changes = [];\n this.id = id;\n this.clientId = clientId;\n this._commands = [...commands];\n this._changes = changes ? [...changes] : [];\n }\n setChanges(changes) {\n this._changes = changes;\n }\n get commands() {\n return this._commands;\n }\n get changes() {\n return this._changes;\n }\n }\n\n class ClientDisconnectedError extends Error {\n }\n class Session extends EventBus {\n /**\n * Manages the collaboration between multiple users on the same spreadsheet.\n * It can forward local state changes to other users to ensure they all eventually\n * reach the same state.\n * It also manages the positions of each clients in the spreadsheet to provide\n * a visual indication of what other users are doing in the spreadsheet.\n *\n * @param revisions\n * @param transportService communication channel used to send and receive messages\n * between all connected clients\n * @param client the client connected locally\n * @param serverRevisionId\n */\n constructor(revisions, transportService, serverRevisionId = DEFAULT_REVISION_ID) {\n super();\n this.revisions = revisions;\n this.transportService = transportService;\n this.serverRevisionId = serverRevisionId;\n /**\n * Positions of the others client.\n */\n this.clients = {};\n this.clientId = \"local\";\n this.pendingMessages = [];\n this.waitingAck = false;\n /**\n * Flag used to block all commands when an undo or redo is triggered, until\n * it is accepted on the server\n */\n this.waitingUndoRedoAck = false;\n this.isReplayingInitialRevisions = false;\n this.processedRevisions = new Set();\n this.uuidGenerator = new UuidGenerator();\n this.debouncedMove = debounce(this._move.bind(this), DEBOUNCE_TIME);\n }\n canApplyOptimisticUpdate() {\n return !this.waitingUndoRedoAck;\n }\n /**\n * Add a new revision to the collaborative session.\n * It will be transmitted to all other connected clients.\n */\n save(commands, changes) {\n if (!commands.length || !changes.length || !this.canApplyOptimisticUpdate())\n return;\n const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, changes);\n this.revisions.append(revision.id, revision);\n this.trigger(\"new-local-state-update\", { id: revision.id });\n this.sendUpdateMessage({\n type: \"REMOTE_REVISION\",\n version: MESSAGE_VERSION,\n serverRevisionId: this.serverRevisionId,\n nextRevisionId: revision.id,\n clientId: revision.clientId,\n commands: revision.commands,\n });\n }\n undo(revisionId) {\n this.waitingUndoRedoAck = true;\n this.sendUpdateMessage({\n type: \"REVISION_UNDONE\",\n version: MESSAGE_VERSION,\n serverRevisionId: this.serverRevisionId,\n nextRevisionId: this.uuidGenerator.uuidv4(),\n undoneRevisionId: revisionId,\n });\n }\n redo(revisionId) {\n this.waitingUndoRedoAck = true;\n this.sendUpdateMessage({\n type: \"REVISION_REDONE\",\n version: MESSAGE_VERSION,\n serverRevisionId: this.serverRevisionId,\n nextRevisionId: this.uuidGenerator.uuidv4(),\n redoneRevisionId: revisionId,\n });\n }\n /**\n * Notify that the position of the client has changed\n */\n move(position) {\n this.debouncedMove(position);\n }\n join(client) {\n if (client) {\n this.clients[client.id] = client;\n this.clientId = client.id;\n }\n else {\n this.clients[\"local\"] = { id: \"local\", name: \"local\" };\n this.clientId = \"local\";\n }\n this.transportService.onNewMessage(this.clientId, this.onMessageReceived.bind(this));\n }\n loadInitialMessages(messages) {\n this.isReplayingInitialRevisions = true;\n this.on(\"unexpected-revision-id\", this, ({ revisionId }) => {\n throw new Error(`The spreadsheet could not be loaded. Revision ${revisionId} is corrupted.`);\n });\n for (const message of messages) {\n this.onMessageReceived(message);\n }\n this.off(\"unexpected-revision-id\", this);\n this.isReplayingInitialRevisions = false;\n }\n /**\n * Notify the server that the user client left the collaborative session\n */\n leave() {\n delete this.clients[this.clientId];\n this.transportService.leave(this.clientId);\n this.transportService.sendMessage({\n type: \"CLIENT_LEFT\",\n clientId: this.clientId,\n version: MESSAGE_VERSION,\n });\n }\n /**\n * Send a snapshot of the spreadsheet to the collaboration server\n */\n snapshot(data) {\n const snapshotId = this.uuidGenerator.uuidv4();\n this.transportService.sendMessage({\n type: \"SNAPSHOT\",\n nextRevisionId: snapshotId,\n serverRevisionId: this.serverRevisionId,\n data: { ...data, revisionId: snapshotId },\n version: MESSAGE_VERSION,\n });\n }\n getClient() {\n const client = this.clients[this.clientId];\n if (!client) {\n throw new ClientDisconnectedError(\"The client left the session\");\n }\n return client;\n }\n getConnectedClients() {\n return new Set(Object.values(this.clients).filter(isDefined$1));\n }\n getRevisionId() {\n return this.serverRevisionId;\n }\n isFullySynchronized() {\n return this.pendingMessages.length === 0;\n }\n _move(position) {\n var _a;\n // this method is debounced and might be called after the client\n // left the session.\n if (!this.clients[this.clientId])\n return;\n const currentPosition = (_a = this.clients[this.clientId]) === null || _a === void 0 ? void 0 : _a.position;\n if ((currentPosition === null || currentPosition === void 0 ? void 0 : currentPosition.col) === position.col &&\n currentPosition.row === position.row &&\n currentPosition.sheetId === position.sheetId) {\n return;\n }\n const type = currentPosition ? \"CLIENT_MOVED\" : \"CLIENT_JOINED\";\n const client = this.getClient();\n this.clients[this.clientId] = { ...client, position };\n this.transportService.sendMessage({\n type,\n version: MESSAGE_VERSION,\n client: { ...client, position },\n });\n }\n /**\n * Handles messages received from other clients in the collaborative\n * session.\n */\n onMessageReceived(message) {\n if (this.isAlreadyProcessed(message))\n return;\n switch (message.type) {\n case \"CLIENT_MOVED\":\n this.onClientMoved(message);\n break;\n case \"CLIENT_JOINED\":\n this.onClientJoined(message);\n break;\n case \"CLIENT_LEFT\":\n this.onClientLeft(message);\n break;\n case \"REVISION_REDONE\": {\n this.revisions.redo(message.redoneRevisionId, message.nextRevisionId, message.serverRevisionId);\n this.trigger(\"revision-redone\", {\n revisionId: message.redoneRevisionId,\n commands: this.revisions.get(message.redoneRevisionId).commands,\n });\n break;\n }\n case \"REVISION_UNDONE\":\n this.revisions.undo(message.undoneRevisionId, message.nextRevisionId, message.serverRevisionId);\n this.trigger(\"revision-undone\", {\n revisionId: message.undoneRevisionId,\n commands: this.revisions.get(message.undoneRevisionId).commands,\n });\n break;\n case \"REMOTE_REVISION\":\n if (message.serverRevisionId !== this.serverRevisionId) {\n this.trigger(\"unexpected-revision-id\", { revisionId: message.serverRevisionId });\n return;\n }\n const { clientId, commands } = message;\n const revision = new Revision(message.nextRevisionId, clientId, commands);\n if (revision.clientId !== this.clientId) {\n this.revisions.insert(revision.id, revision, message.serverRevisionId);\n const pendingCommands = this.pendingMessages\n .filter((msg) => msg.type === \"REMOTE_REVISION\")\n .map((msg) => msg.commands)\n .flat();\n this.trigger(\"remote-revision-received\", {\n commands: transformAll(commands, pendingCommands),\n });\n }\n break;\n case \"SNAPSHOT_CREATED\": {\n const revision = new Revision(message.nextRevisionId, \"server\", []);\n this.revisions.insert(revision.id, revision, message.serverRevisionId);\n this.dropPendingHistoryMessages();\n this.trigger(\"snapshot\");\n break;\n }\n }\n this.acknowledge(message);\n this.trigger(\"collaborative-event-received\");\n }\n onClientMoved(message) {\n if (message.client.id !== this.clientId) {\n this.clients[message.client.id] = message.client;\n }\n }\n /**\n * Register the new client and send your\n * own position back.\n */\n onClientJoined(message) {\n if (message.client.id !== this.clientId) {\n this.clients[message.client.id] = message.client;\n const client = this.clients[this.clientId];\n if (client) {\n const { position } = client;\n if (position) {\n this.transportService.sendMessage({\n type: \"CLIENT_MOVED\",\n version: MESSAGE_VERSION,\n client: { ...client, position },\n });\n }\n }\n }\n }\n onClientLeft(message) {\n if (message.clientId !== this.clientId) {\n delete this.clients[message.clientId];\n }\n }\n sendUpdateMessage(message) {\n this.pendingMessages.push(message);\n if (this.waitingAck) {\n return;\n }\n this.waitingAck = true;\n this.sendPendingMessage();\n }\n /**\n * Send the next pending message\n */\n sendPendingMessage() {\n let message = this.pendingMessages[0];\n if (!message)\n return;\n if (message.type === \"REMOTE_REVISION\") {\n const revision = this.revisions.get(message.nextRevisionId);\n if (revision.commands.length === 0) {\n /**\n * The command is empty, we have to drop all the next local revisions\n * to avoid issues with undo/redo\n */\n this.revisions.drop(revision.id);\n const revisionIds = this.pendingMessages\n .filter((message) => message.type === \"REMOTE_REVISION\")\n .map((message) => message.nextRevisionId);\n this.trigger(\"pending-revisions-dropped\", { revisionIds });\n this.waitingAck = false;\n this.waitingUndoRedoAck = false;\n this.pendingMessages = [];\n return;\n }\n message = {\n ...message,\n clientId: revision.clientId,\n commands: revision.commands,\n };\n }\n if (this.isReplayingInitialRevisions) {\n throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.\n ${JSON.stringify(message)}`);\n }\n this.transportService.sendMessage({\n ...message,\n serverRevisionId: this.serverRevisionId,\n });\n }\n acknowledge(message) {\n if (message.type === \"REVISION_UNDONE\" || message.type === \"REVISION_REDONE\") {\n this.waitingUndoRedoAck = false;\n }\n switch (message.type) {\n case \"REMOTE_REVISION\":\n case \"REVISION_REDONE\":\n case \"REVISION_UNDONE\":\n case \"SNAPSHOT_CREATED\":\n this.waitingAck = false;\n this.pendingMessages = this.pendingMessages.filter((msg) => msg.nextRevisionId !== message.nextRevisionId);\n this.serverRevisionId = message.nextRevisionId;\n this.processedRevisions.add(message.nextRevisionId);\n this.sendPendingMessage();\n break;\n }\n }\n isAlreadyProcessed(message) {\n if (message.type === \"CLIENT_MOVED\" && message.client.id === this.clientId) {\n return true;\n }\n switch (message.type) {\n case \"REMOTE_REVISION\":\n case \"REVISION_REDONE\":\n case \"REVISION_UNDONE\":\n return this.processedRevisions.has(message.nextRevisionId);\n default:\n return false;\n }\n }\n dropPendingHistoryMessages() {\n this.waitingUndoRedoAck = false;\n this.pendingMessages = this.pendingMessages.filter(({ type }) => type !== \"REVISION_REDONE\" && type !== \"REVISION_UNDONE\");\n }\n }\n\n function randomChoice(arr) {\n return arr[Math.floor(Math.random() * arr.length)];\n }\n const colors = [\n \"#ff851b\",\n \"#0074d9\",\n \"#7fdbff\",\n \"#b10dc9\",\n \"#39cccc\",\n \"#f012be\",\n \"#3d9970\",\n \"#111111\",\n \"#ff4136\",\n \"#aaaaaa\",\n \"#85144b\",\n \"#001f3f\",\n ];\n class SelectionMultiUserPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.availableColors = new Set(colors);\n this.colors = {};\n }\n isPositionValid(position) {\n return (position.row < this.getters.getNumberRows(position.sheetId) &&\n position.col < this.getters.getNumberCols(position.sheetId));\n }\n chooseNewColor() {\n if (this.availableColors.size === 0) {\n this.availableColors = new Set(colors);\n }\n const color = randomChoice([...this.availableColors.values()]);\n this.availableColors.delete(color);\n return color;\n }\n /**\n * Get the list of others connected clients which are present in the same sheet\n * and with a valid position\n */\n getClientsToDisplay() {\n try {\n this.getters.getClient();\n }\n catch (e) {\n if (e instanceof ClientDisconnectedError) {\n return [];\n }\n else {\n throw e;\n }\n }\n const sheetId = this.getters.getActiveSheetId();\n const clients = [];\n for (const client of this.getters.getConnectedClients()) {\n if (client.id !== this.getters.getClient().id &&\n client.position &&\n client.position.sheetId === sheetId &&\n this.isPositionValid(client.position)) {\n const position = client.position;\n if (!this.colors[client.id]) {\n this.colors[client.id] = this.chooseNewColor();\n }\n const color = this.colors[client.id];\n clients.push({ ...client, position, color });\n }\n }\n return clients;\n }\n drawGrid(renderingContext) {\n if (this.getters.isDashboard()) {\n return;\n }\n const { ctx, thinLineWidth } = renderingContext;\n const activeSheetId = this.getters.getActiveSheetId();\n for (const client of this.getClientsToDisplay()) {\n const { row, col } = client.position;\n const zone = this.getters.expandZone(activeSheetId, {\n top: row,\n bottom: row,\n left: col,\n right: col,\n });\n const { x, y, width, height } = this.getters.getVisibleRect(zone);\n if (width <= 0 || height <= 0) {\n continue;\n }\n const color = client.color;\n /* Cell background */\n const cellBackgroundColor = `${color}10`;\n ctx.fillStyle = cellBackgroundColor;\n ctx.lineWidth = 4 * thinLineWidth;\n ctx.strokeStyle = color;\n ctx.globalCompositeOperation = \"multiply\";\n ctx.fillRect(x, y, width, height);\n /* Cell border */\n ctx.globalCompositeOperation = \"source-over\";\n ctx.strokeRect(x, y, width, height);\n /* client name background */\n ctx.font = `bold ${DEFAULT_FONT_SIZE + 1}px ${DEFAULT_FONT}`;\n }\n }\n }\n SelectionMultiUserPlugin.getters = [\"getClientsToDisplay\"];\n SelectionMultiUserPlugin.layers = [6 /* LAYERS.Selection */];\n\n class SortPlugin extends UIPlugin {\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"SORT_CELLS\":\n if (!isInside(cmd.col, cmd.row, cmd.zone)) {\n throw new Error(_lt(\"The anchor must be part of the provided zone\"));\n }\n return this.checkValidations(cmd, this.checkMerge, this.checkMergeSizes);\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"SORT_CELLS\":\n this.sortZone(cmd.sheetId, cmd, cmd.zone, cmd.sortDirection, cmd.sortOptions || {});\n break;\n }\n }\n checkMerge({ sheetId, zone }) {\n if (!this.getters.doesIntersectMerge(sheetId, zone)) {\n return 0 /* CommandResult.Success */;\n }\n /*Test the presence of single cells*/\n const singleCells = positions(zone).some(({ col, row }) => !this.getters.isInMerge({ sheetId, col, row }));\n if (singleCells) {\n return 63 /* CommandResult.InvalidSortZone */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkMergeSizes({ sheetId, zone }) {\n if (!this.getters.doesIntersectMerge(sheetId, zone)) {\n return 0 /* CommandResult.Success */;\n }\n const merges = this.getters.getMerges(sheetId).filter((merge) => overlap(merge, zone));\n /*Test the presence of merges of different sizes*/\n const mergeDimension = zoneToDimension(merges[0]);\n let [widthFirst, heightFirst] = [mergeDimension.width, mergeDimension.height];\n if (!merges.every((merge) => {\n let [widthCurrent, heightCurrent] = [\n merge.right - merge.left + 1,\n merge.bottom - merge.top + 1,\n ];\n return widthCurrent === widthFirst && heightCurrent === heightFirst;\n })) {\n return 63 /* CommandResult.InvalidSortZone */;\n }\n return 0 /* CommandResult.Success */;\n }\n // getContiguousZone helpers\n /**\n * safe-version of expandZone to make sure we don't get out of the grid\n */\n expand(sheetId, z) {\n const { left, right, top, bottom } = this.getters.expandZone(sheetId, z);\n return {\n left: Math.max(0, left),\n right: Math.min(this.getters.getNumberCols(sheetId) - 1, right),\n top: Math.max(0, top),\n bottom: Math.min(this.getters.getNumberRows(sheetId) - 1, bottom),\n };\n }\n /**\n * verifies the presence of at least one non-empty cell in the given zone\n */\n checkExpandedValues(sheetId, z) {\n const expandedZone = this.expand(sheetId, z);\n let cell;\n if (this.getters.doesIntersectMerge(sheetId, expandedZone)) {\n const { left, right, top, bottom } = expandedZone;\n for (let c = left; c <= right; c++) {\n for (let r = top; r <= bottom; r++) {\n const { col, row } = this.getters.getMainCellPosition({ sheetId, col: c, row: r });\n cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n if (cell.formattedValue) {\n return true;\n }\n }\n }\n }\n else {\n for (let cell of this.getters.getEvaluatedCellsInZone(sheetId, expandedZone)) {\n if (cell.formattedValue) {\n return true;\n }\n }\n }\n return false;\n }\n /**\n * This function will expand the provided zone in directions (top, bottom, left, right) for which there\n * are non-null cells on the external boundary of the zone in the given direction.\n *\n * Example:\n * A B C D E\n * ___ ___ ___ ___ ___\n * 1 | | D | | | |\n * ___ ___ ___ ___ ___\n * 2 | 5 | | 1 | D | |\n * ___ ___ ___ ___ ___\n * 3 | | | A | X | |\n * ___ ___ ___ ___ ___\n * 4 | | | | | |\n * ___ ___ ___ ___ ___\n *\n * Let's consider a provided zone corresponding to (C2:D3) - (left:2, right: 3, top:1, bottom:2)\n * - the top external boundary is (B1:E1)\n * Since we have B1='D' != \"\", we expand to the top: => (C1:D3)\n * The top boundary having reached the top of the grid, we cannot expand in that direction anymore\n *\n * - the left boundary is (B1:B4)\n * since we have B1 again, we expand to the left => (B1:D3)\n *\n * - the right and bottom boundaries are a dead end for now as (E1:E4) and (A4:E4) are empty.\n *\n * - the left boundary is now (A1:A4)\n * Since we have A2=5 != \"\", we can therefore expand to the left => (A1:D3)\n *\n * This will be the final zone as left and top have reached the boundaries of the grid and\n * the other boundaries (E1:E4) and (A4:E4) are empty.\n *\n * @param sheetId UID of concerned sheet\n * @param zone Zone\n *\n */\n getContiguousZone(sheetId, zone) {\n let { top, bottom, left, right } = zone;\n let canExpand;\n let stop = false;\n while (!stop) {\n stop = true;\n /** top row external boundary */\n if (top > 0) {\n canExpand = this.checkExpandedValues(sheetId, {\n left: left - 1,\n right: right + 1,\n top: top - 1,\n bottom: top - 1,\n });\n if (canExpand) {\n stop = false;\n top--;\n }\n }\n /** left column external boundary */\n if (left > 0) {\n canExpand = this.checkExpandedValues(sheetId, {\n left: left - 1,\n right: left - 1,\n top: top - 1,\n bottom: bottom + 1,\n });\n if (canExpand) {\n stop = false;\n left--;\n }\n }\n /** right column external boundary */\n if (right < this.getters.getNumberCols(sheetId) - 1) {\n canExpand = this.checkExpandedValues(sheetId, {\n left: right + 1,\n right: right + 1,\n top: top - 1,\n bottom: bottom + 1,\n });\n if (canExpand) {\n stop = false;\n right++;\n }\n }\n /** bottom row external boundary */\n if (bottom < this.getters.getNumberRows(sheetId) - 1) {\n canExpand = this.checkExpandedValues(sheetId, {\n left: left - 1,\n right: right + 1,\n top: bottom + 1,\n bottom: bottom + 1,\n });\n if (canExpand) {\n stop = false;\n bottom++;\n }\n }\n }\n return { left, right, top, bottom };\n }\n /**\n * This function evaluates if the top row of a provided zone can be considered as a `header`\n * by checking the following criteria:\n * * If the left-most column top row value (topLeft) is empty, we ignore it while evaluating the criteria.\n * 1 - Apart from the left-most column, every element of the top row must be non-empty, i.e. a cell should be present in the sheet.\n * 2 - There should be at least one column in which the type (CellValueType) of the rop row cell differs from the type of the cell below.\n * For the second criteria, we ignore columns on which the cell below is empty.\n *\n */\n hasHeader(sheetId, items) {\n if (items[0].length === 1)\n return false;\n let cells = items.map((col) => col.map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }).type));\n // ignore left-most column when topLeft cell is empty\n const topLeft = cells[0][0];\n if (topLeft === CellValueType.empty) {\n cells = cells.slice(1);\n }\n if (cells.some((item) => item[0] === CellValueType.empty)) {\n return false;\n }\n else if (cells.some((item) => item[1] !== CellValueType.empty && item[0] !== item[1])) {\n return true;\n }\n else {\n return false;\n }\n }\n sortZone(sheetId, anchor, zone, sortDirection, options) {\n const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);\n let sortingCol = this.getters.getMainCellPosition({\n sheetId,\n col: anchor.col,\n row: anchor.row,\n }).col; // fetch anchor\n let sortZone = Object.assign({}, zone);\n // Update in case of merges in the zone\n let cellPositions = this.mainCells(sheetId, zone);\n if (!options.sortHeaders && this.hasHeader(sheetId, cellPositions)) {\n sortZone.top += stepY;\n }\n cellPositions = this.mainCells(sheetId, sortZone);\n const sortingCells = cellPositions[sortingCol - sortZone.left];\n const sortedIndexOfSortTypeCells = sortCells(sortingCells.map((position) => this.getters.getEvaluatedCell(position)), sortDirection, Boolean(options.emptyCellAsZero));\n const sortedIndex = sortedIndexOfSortTypeCells.map((x) => x.index);\n const [width, height] = [cellPositions.length, cellPositions[0].length];\n const updateCellCommands = [];\n for (let c = 0; c < width; c++) {\n for (let r = 0; r < height; r++) {\n let { col, row, sheetId } = cellPositions[c][sortedIndex[r]];\n const cell = this.getters.getCell({ sheetId, col, row });\n let newCol = sortZone.left + c * stepX;\n let newRow = sortZone.top + r * stepY;\n let newCellValues = {\n sheetId: sheetId,\n col: newCol,\n row: newRow,\n content: \"\",\n };\n if (cell) {\n let content = cell.content;\n if (cell.isFormula) {\n const position = this.getters.getCellPosition(cell.id);\n const offsetY = newRow - position.row;\n // we only have a vertical offset\n const ranges = this.getters.createAdaptedRanges(cell.dependencies, 0, offsetY, sheetId);\n content = this.getters.buildFormulaContent(sheetId, cell, ranges);\n }\n newCellValues.style = cell.style;\n newCellValues.content = content;\n newCellValues.format = cell.format;\n }\n updateCellCommands.push(newCellValues);\n }\n for (const cmd of updateCellCommands) {\n this.dispatch(\"UPDATE_CELL\", cmd);\n }\n }\n }\n /**\n * Return the distances between main merge cells in the zone.\n * (1 if there are no merges).\n * Note: it is assumed all merges are the same in the zone.\n */\n mainCellsSteps(sheetId, zone) {\n const merge = this.getters.getMerge({ sheetId, col: zone.left, row: zone.top });\n const stepX = merge ? merge.right - merge.left + 1 : 1;\n const stepY = merge ? merge.bottom - merge.top + 1 : 1;\n return [stepX, stepY];\n }\n /**\n * Return a 2D array of cells in the zone (main merge cells if there are merges)\n */\n mainCells(sheetId, zone) {\n const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);\n const cells = [];\n const cols = range(zone.left, zone.right + 1, stepX);\n const rows = range(zone.top, zone.bottom + 1, stepY);\n for (const col of cols) {\n const colCells = [];\n cells.push(colCells);\n for (const row of rows) {\n colCells.push({ sheetId, col, row });\n }\n }\n return cells;\n }\n }\n SortPlugin.getters = [\"getContiguousZone\"];\n\n class UIOptionsPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.showFormulas = false;\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n handle(cmd) {\n switch (cmd.type) {\n case \"SET_FORMULA_VISIBILITY\":\n this.showFormulas = cmd.show;\n break;\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n shouldShowFormulas() {\n return this.showFormulas;\n }\n }\n UIOptionsPlugin.getters = [\"shouldShowFormulas\"];\n\n class SheetUIPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.ctx = document.createElement(\"canvas\").getContext(\"2d\");\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"AUTORESIZE_ROWS\":\n case \"AUTORESIZE_COLUMNS\":\n try {\n this.getters.getSheet(cmd.sheetId);\n break;\n }\n catch (error) {\n return 27 /* CommandResult.InvalidSheetId */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"AUTORESIZE_COLUMNS\":\n for (let col of cmd.cols) {\n const size = this.getColMaxWidth(cmd.sheetId, col);\n if (size !== 0) {\n this.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n elements: [col],\n dimension: \"COL\",\n size,\n sheetId: cmd.sheetId,\n });\n }\n }\n break;\n case \"AUTORESIZE_ROWS\":\n for (let row of cmd.rows) {\n this.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n elements: [row],\n dimension: \"ROW\",\n size: null,\n sheetId: cmd.sheetId,\n });\n }\n break;\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getCellWidth(position) {\n let contentWidth = this.getTextWidth(position);\n const icon = this.getters.getConditionalIcon(position);\n if (icon) {\n contentWidth += computeIconWidth(this.getters.getCellStyle(position));\n }\n const isFilterHeader = this.getters.isFilterHeader(position);\n if (isFilterHeader) {\n contentWidth += ICON_EDGE_LENGTH + FILTER_ICON_MARGIN;\n }\n if (contentWidth > 0) {\n contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;\n if (this.getters.getCellStyle(position).wrapping === \"wrap\") {\n const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);\n return Math.min(colWidth, contentWidth);\n }\n }\n return contentWidth;\n }\n getTextWidth(position) {\n const text = this.getters.getCellText(position, this.getters.shouldShowFormulas());\n return computeTextWidth(this.ctx, text, this.getters.getCellComputedStyle(position));\n }\n getCellText(position, showFormula = false) {\n const cell = this.getters.getCell(position);\n if (showFormula && (cell === null || cell === void 0 ? void 0 : cell.isFormula)) {\n return cell.content;\n }\n else {\n return this.getters.getEvaluatedCell(position).formattedValue;\n }\n }\n getCellMultiLineText(position, width) {\n const style = this.getters.getCellStyle(position);\n const text = this.getters.getCellText(position);\n const words = text.split(\" \");\n const brokenText = [];\n let textLine = \"\";\n let availableWidth = width;\n for (let word of words) {\n const splitWord = this.splitWordToSpecificWidth(this.ctx, word, width, style);\n const lastPart = splitWord.pop();\n const lastPartWidth = computeTextWidth(this.ctx, lastPart, style);\n // At this step: \"splitWord\" is an array composed of parts of word whose\n // length is at most equal to \"width\".\n // Last part contains the end of the word.\n // Note that: When word length is less than width, then lastPart is equal\n // to word and splitWord is empty\n if (splitWord.length) {\n if (textLine !== \"\") {\n brokenText.push(textLine);\n textLine = \"\";\n availableWidth = width;\n }\n splitWord.forEach((wordPart) => {\n brokenText.push(wordPart);\n });\n textLine = lastPart;\n availableWidth = width - lastPartWidth;\n }\n else {\n // here \"lastPart\" is equal to \"word\" and the \"word\" size is smaller than \"width\"\n const _word = textLine === \"\" ? lastPart : \" \" + lastPart;\n const wordWidth = computeTextWidth(this.ctx, _word, style);\n if (wordWidth <= availableWidth) {\n textLine += _word;\n availableWidth -= wordWidth;\n }\n else {\n brokenText.push(textLine);\n textLine = lastPart;\n availableWidth = width - lastPartWidth;\n }\n }\n }\n if (textLine !== \"\") {\n brokenText.push(textLine);\n }\n return brokenText;\n }\n /**\n * Returns the size, start and end coordinates of a column on an unfolded sheet\n */\n getColDimensions(sheetId, col) {\n const start = this.getColRowOffset(\"COL\", 0, col, sheetId);\n const size = this.getters.getColSize(sheetId, col);\n const isColHidden = this.getters.isColHidden(sheetId, col);\n return {\n start,\n size,\n end: start + (isColHidden ? 0 : size),\n };\n }\n /**\n * Returns the size, start and end coordinates of a row an unfolded sheet\n */\n getRowDimensions(sheetId, row) {\n const start = this.getColRowOffset(\"ROW\", 0, row, sheetId);\n const size = this.getters.getRowSize(sheetId, row);\n const isRowHidden = this.getters.isRowHidden(sheetId, row);\n return {\n start,\n size: size,\n end: start + (isRowHidden ? 0 : size),\n };\n }\n /**\n * Returns the offset of a header (determined by the dimension) at the given index\n * based on the referenceIndex given. If start === 0, this method will return\n * the start attribute of the header.\n *\n * i.e. The size from A to B is the distance between A.start and B.end\n */\n getColRowOffset(dimension, referenceIndex, index, sheetId = this.getters.getActiveSheetId()) {\n if (index < referenceIndex) {\n return -this.getColRowOffset(dimension, index, referenceIndex);\n }\n let offset = 0;\n for (let i = referenceIndex; i < index; i++) {\n if (this.getters.isHeaderHidden(sheetId, dimension, i)) {\n continue;\n }\n offset +=\n dimension === \"COL\"\n ? this.getters.getColSize(sheetId, i)\n : this.getters.getRowSize(sheetId, i);\n }\n return offset;\n }\n // ---------------------------------------------------------------------------\n // Grid manipulation\n // ---------------------------------------------------------------------------\n getColMaxWidth(sheetId, index) {\n const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));\n const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));\n return Math.max(0, ...sizes);\n }\n splitWordToSpecificWidth(ctx, word, width, style) {\n const wordWidth = computeTextWidth(ctx, word, style);\n if (wordWidth <= width) {\n return [word];\n }\n const splitWord = [];\n let wordPart = \"\";\n for (let l of word) {\n const wordPartWidth = computeTextWidth(ctx, wordPart + l, style);\n if (wordPartWidth > width) {\n splitWord.push(wordPart);\n wordPart = l;\n }\n else {\n wordPart += l;\n }\n }\n splitWord.push(wordPart);\n return splitWord;\n }\n }\n SheetUIPlugin.getters = [\n \"getCellWidth\",\n \"getTextWidth\",\n \"getCellText\",\n \"getCellMultiLineText\",\n \"getColDimensions\",\n \"getRowDimensions\",\n \"getColRowOffset\",\n ];\n\n /** Abstract state of the clipboard when copying/cutting content that is pasted in cells of the sheet */\n class ClipboardCellsAbstractState {\n constructor(operation, getters, dispatch, selection) {\n this.getters = getters;\n this.dispatch = dispatch;\n this.selection = selection;\n this.operation = operation;\n this.sheetId = getters.getActiveSheetId();\n }\n isCutAllowed(target) {\n return 0 /* CommandResult.Success */;\n }\n isPasteAllowed(target, clipboardOption) {\n return 0 /* CommandResult.Success */;\n }\n /**\n * Add columns and/or rows to ensure that col + width and row + height are still\n * in the sheet\n */\n addMissingDimensions(width, height, col, row) {\n const sheetId = this.getters.getActiveSheetId();\n const missingRows = height + row - this.getters.getNumberRows(sheetId);\n if (missingRows > 0) {\n this.dispatch(\"ADD_COLUMNS_ROWS\", {\n dimension: \"ROW\",\n base: this.getters.getNumberRows(sheetId) - 1,\n sheetId,\n quantity: missingRows,\n position: \"after\",\n });\n }\n const missingCols = width + col - this.getters.getNumberCols(sheetId);\n if (missingCols > 0) {\n this.dispatch(\"ADD_COLUMNS_ROWS\", {\n dimension: \"COL\",\n base: this.getters.getNumberCols(sheetId) - 1,\n sheetId,\n quantity: missingCols,\n position: \"after\",\n });\n }\n }\n isColRowDirtyingClipboard(position, dimension) {\n return false;\n }\n drawClipboard(renderingContext) { }\n }\n\n /** State of the clipboard when copying/cutting cells */\n class ClipboardCellsState extends ClipboardCellsAbstractState {\n constructor(zones, operation, getters, dispatch, selection) {\n super(operation, getters, dispatch, selection);\n if (!zones.length) {\n this.cells = [[]];\n this.zones = [];\n this.copiedTables = [];\n return;\n }\n const lefts = new Set(zones.map((z) => z.left));\n const rights = new Set(zones.map((z) => z.right));\n const tops = new Set(zones.map((z) => z.top));\n const bottoms = new Set(zones.map((z) => z.bottom));\n const areZonesCompatible = (tops.size === 1 && bottoms.size === 1) || (lefts.size === 1 && rights.size === 1);\n // In order to don't paste several times the same cells in intersected zones\n // --> we merge zones that have common cells\n const clippedZones = areZonesCompatible\n ? mergeOverlappingZones(zones)\n : [zones[zones.length - 1]];\n const cellsPosition = clippedZones.map((zone) => positions(zone)).flat();\n const columnsIndex = [...new Set(cellsPosition.map((p) => p.col))].sort((a, b) => a - b);\n const rowsIndex = [...new Set(cellsPosition.map((p) => p.row))].sort((a, b) => a - b);\n const cellsInClipboard = [];\n const sheetId = getters.getActiveSheetId();\n for (let row of rowsIndex) {\n let cellsInRow = [];\n for (let col of columnsIndex) {\n const position = { col, row, sheetId };\n cellsInRow.push({\n cell: getters.getCell(position),\n style: getters.getCellComputedStyle(position),\n evaluatedCell: getters.getEvaluatedCell(position),\n border: getters.getCellBorder(position) || undefined,\n position,\n });\n }\n cellsInClipboard.push(cellsInRow);\n }\n const tables = [];\n for (const zone of zones) {\n for (const table of this.getters.getFilterTablesInZone(sheetId, zone)) {\n const values = [];\n for (const col of range(table.zone.left, table.zone.right + 1)) {\n values.push(this.getters.getFilterValues({ sheetId, col, row: table.zone.top }));\n }\n tables.push({ filtersValues: values, zone: table.zone });\n }\n }\n this.cells = cellsInClipboard;\n this.zones = clippedZones;\n this.copiedTables = tables;\n }\n isCutAllowed(target) {\n if (target.length !== 1) {\n return 19 /* CommandResult.WrongCutSelection */;\n }\n return 0 /* CommandResult.Success */;\n }\n isPasteAllowed(target, clipboardOption) {\n const sheetId = this.getters.getActiveSheetId();\n if (this.operation === \"CUT\" && (clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) !== undefined) {\n // cannot paste only format or only value if the previous operation is a CUT\n return 21 /* CommandResult.WrongPasteOption */;\n }\n if (target.length > 1) {\n // cannot paste if we have a clipped zone larger than a cell and multiple\n // zones selected\n if (this.cells.length > 1 || this.cells[0].length > 1) {\n return 20 /* CommandResult.WrongPasteSelection */;\n }\n }\n const clipboardHeight = this.cells.length;\n const clipboardWidth = this.cells[0].length;\n for (let zone of this.getPasteZones(target)) {\n if (this.getters.doesIntersectMerge(sheetId, zone)) {\n if (target.length > 1 ||\n !this.getters.isSingleCellOrMerge(sheetId, target[0]) ||\n clipboardHeight * clipboardWidth !== 1) {\n return 2 /* CommandResult.WillRemoveExistingMerge */;\n }\n }\n }\n const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n for (const zone of this.getPasteZones(target)) {\n if ((zone.left < xSplit && zone.right >= xSplit) ||\n (zone.top < ySplit && zone.bottom >= ySplit)) {\n return 75 /* CommandResult.FrozenPaneOverlap */;\n }\n }\n return 0 /* CommandResult.Success */;\n }\n /**\n * Paste the clipboard content in the given target\n */\n paste(target, options) {\n if (this.operation === \"COPY\") {\n this.pasteFromCopy(target, options);\n }\n else {\n this.pasteFromCut(target, options);\n }\n const height = this.cells.length;\n const width = this.cells[0].length;\n const isCutOperation = this.operation === \"CUT\";\n if (options === null || options === void 0 ? void 0 : options.selectTarget) {\n this.selectPastedZone(width, height, isCutOperation, target);\n }\n }\n pasteFromCopy(target, options) {\n if (target.length === 1) {\n // in this specific case, due to the isPasteAllowed function:\n // state.cells can contains several cells.\n // So if the target zone is larger than the copied zone,\n // we duplicate each cells as many times as possible to fill the zone.\n const height = this.cells.length;\n const width = this.cells[0].length;\n const pasteZones = this.pastedZones(target, width, height);\n for (const zone of pasteZones) {\n this.pasteZone(zone.left, zone.top, options);\n }\n }\n else {\n // in this case, due to the isPasteAllowed function: state.cells contains\n // only one cell\n for (const zone of target) {\n for (let col = zone.left; col <= zone.right; col++) {\n for (let row = zone.top; row <= zone.bottom; row++) {\n this.pasteZone(col, row, options);\n }\n }\n }\n }\n if ((options === null || options === void 0 ? void 0 : options.pasteOption) === undefined) {\n this.pasteCopiedTables(target);\n }\n }\n pasteFromCut(target, options) {\n this.clearClippedZones();\n const selection = target[0];\n this.pasteZone(selection.left, selection.top, options);\n this.dispatch(\"MOVE_RANGES\", {\n target: this.zones,\n sheetId: this.sheetId,\n targetSheetId: this.getters.getActiveSheetId(),\n col: selection.left,\n row: selection.top,\n });\n for (const filterTable of this.copiedTables) {\n this.dispatch(\"REMOVE_FILTER_TABLE\", {\n sheetId: this.getters.getActiveSheetId(),\n target: [filterTable.zone],\n });\n }\n this.pasteCopiedTables(target);\n }\n /**\n * The clipped zone is copied as many times as it fits in the target.\n * This returns the list of zones where the clipped zone is copy-pasted.\n */\n pastedZones(target, originWidth, originHeight) {\n const selection = target[0];\n const repeatHorizontally = Math.max(1, Math.floor((selection.right + 1 - selection.left) / originWidth));\n const repeatVertically = Math.max(1, Math.floor((selection.bottom + 1 - selection.top) / originHeight));\n const zones = [];\n for (let x = 0; x < repeatHorizontally; x++) {\n for (let y = 0; y < repeatVertically; y++) {\n const top = selection.top + y * originHeight;\n const left = selection.left + x * originWidth;\n zones.push({\n left,\n top,\n bottom: top + originHeight - 1,\n right: left + originWidth - 1,\n });\n }\n }\n return zones;\n }\n /**\n * Compute the complete zones where to paste the current clipboard\n */\n getPasteZones(target) {\n const cells = this.cells;\n if (!cells.length || !cells[0].length) {\n return target;\n }\n const pasteZones = [];\n const height = cells.length;\n const width = cells[0].length;\n const selection = target[target.length - 1];\n const col = selection.left;\n const row = selection.top;\n const repetitionCol = Math.max(1, Math.floor((selection.right + 1 - col) / width));\n const repetitionRow = Math.max(1, Math.floor((selection.bottom + 1 - row) / height));\n for (let x = 1; x <= repetitionCol; x++) {\n for (let y = 1; y <= repetitionRow; y++) {\n pasteZones.push({\n left: col,\n top: row,\n right: col - 1 + x * width,\n bottom: row - 1 + y * height,\n });\n }\n }\n return pasteZones;\n }\n /**\n * Update the selection with the newly pasted zone\n */\n selectPastedZone(width, height, isCutOperation, target) {\n const selection = target[0];\n const col = selection.left;\n const row = selection.top;\n if (height > 1 || width > 1 || isCutOperation) {\n const zones = this.pastedZones(target, width, height);\n const newZone = isCutOperation ? zones[0] : union(...zones);\n this.selection.selectZone({ cell: { col, row }, zone: newZone });\n }\n }\n /**\n * Clear the clipped zones: remove the cells and clear the formatting\n */\n clearClippedZones() {\n for (const row of this.cells) {\n for (const cell of row) {\n if (cell.cell) {\n this.dispatch(\"CLEAR_CELL\", cell.position);\n }\n }\n }\n this.dispatch(\"CLEAR_FORMATTING\", {\n sheetId: this.sheetId,\n target: this.zones,\n });\n }\n pasteZone(col, row, clipboardOptions) {\n const height = this.cells.length;\n const width = this.cells[0].length;\n // This condition is used to determine if we have to paste the CF or not.\n // We have to do it when the command handled is \"PASTE\", not \"INSERT_CELL\"\n // or \"DELETE_CELL\". So, the state should be the local state\n const shouldPasteCF = (clipboardOptions === null || clipboardOptions === void 0 ? void 0 : clipboardOptions.pasteOption) !== \"onlyValue\" && (clipboardOptions === null || clipboardOptions === void 0 ? void 0 : clipboardOptions.shouldPasteCF);\n const sheetId = this.getters.getActiveSheetId();\n // first, add missing cols/rows if needed\n this.addMissingDimensions(width, height, col, row);\n // then, perform the actual paste operation\n for (let r = 0; r < height; r++) {\n const rowCells = this.cells[r];\n for (let c = 0; c < width; c++) {\n const origin = rowCells[c];\n const position = { col: col + c, row: row + r, sheetId: sheetId };\n // TODO: refactor this part. the \"Paste merge\" action is also executed with\n // MOVE_RANGES in pasteFromCut. Adding a condition on the operation type here\n // is not appropriate\n if (this.operation !== \"CUT\") {\n this.pasteMergeIfExist(origin.position, position);\n }\n this.pasteCell(origin, position, this.operation, clipboardOptions);\n if (shouldPasteCF) {\n this.dispatch(\"PASTE_CONDITIONAL_FORMAT\", {\n origin: origin.position,\n target: position,\n operation: this.operation,\n });\n }\n }\n }\n }\n /**\n * Paste the cell at the given position to the target position\n */\n pasteCell(origin, target, operation, clipboardOption) {\n const { sheetId, col, row } = target;\n const targetCell = this.getters.getEvaluatedCell(target);\n if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) !== \"onlyValue\") {\n const targetBorders = this.getters.getCellBorder(target);\n const originBorders = origin.border;\n const border = {\n top: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.top) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.top),\n bottom: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.bottom) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.bottom),\n left: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.left) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.left),\n right: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.right) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.right),\n };\n this.dispatch(\"SET_BORDER\", { sheetId, col, row, border });\n }\n if (origin.cell) {\n if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === \"onlyFormat\") {\n this.dispatch(\"UPDATE_CELL\", {\n ...target,\n style: origin.cell.style,\n format: origin.evaluatedCell.format,\n });\n return;\n }\n if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === \"onlyValue\") {\n const content = formatValue(origin.evaluatedCell.value);\n this.dispatch(\"UPDATE_CELL\", { ...target, content });\n return;\n }\n let content = origin.cell.content;\n if (origin.cell.isFormula && operation === \"COPY\") {\n const offsetX = col - origin.position.col;\n const offsetY = row - origin.position.row;\n content = this.getUpdatedContent(sheetId, origin.cell, offsetX, offsetY, operation);\n }\n this.dispatch(\"UPDATE_CELL\", {\n ...target,\n content,\n style: origin.cell.style || null,\n format: origin.cell.format,\n });\n }\n else if (targetCell) {\n if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === \"onlyValue\") {\n this.dispatch(\"UPDATE_CELL\", { ...target, content: \"\" });\n }\n else if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === \"onlyFormat\") {\n this.dispatch(\"UPDATE_CELL\", { ...target, style: null, format: \"\" });\n }\n else {\n this.dispatch(\"CLEAR_CELL\", target);\n }\n }\n }\n /**\n * Get the newly updated formula, after applying offsets\n */\n getUpdatedContent(sheetId, cell, offsetX, offsetY, operation) {\n const ranges = this.getters.createAdaptedRanges(cell.dependencies, offsetX, offsetY, sheetId);\n return this.getters.buildFormulaContent(sheetId, cell, ranges);\n }\n /**\n * If the origin position given is the top left of a merge, merge the target\n * position.\n */\n pasteMergeIfExist(origin, target) {\n let { sheetId, col, row } = origin;\n const { col: mainCellColOrigin, row: mainCellRowOrigin } = this.getters.getMainCellPosition(origin);\n if (mainCellColOrigin === col && mainCellRowOrigin === row) {\n const merge = this.getters.getMerge(origin);\n if (!merge) {\n return;\n }\n ({ sheetId, col, row } = target);\n this.dispatch(\"ADD_MERGE\", {\n sheetId,\n force: true,\n target: [\n {\n left: col,\n top: row,\n right: col + merge.right - merge.left,\n bottom: row + merge.bottom - merge.top,\n },\n ],\n });\n }\n }\n /** Paste the filter tables that are in the state */\n pasteCopiedTables(target) {\n const sheetId = this.getters.getActiveSheetId();\n const selection = target[0];\n const cutZone = this.zones[0];\n const cutOffset = [\n selection.left - cutZone.left,\n selection.top - cutZone.top,\n ];\n for (const table of this.copiedTables) {\n const newTableZone = createAdaptedZone(table.zone, \"both\", \"MOVE\", cutOffset);\n this.dispatch(\"CREATE_FILTER_TABLE\", { sheetId, target: [newTableZone] });\n for (const i of range(0, table.filtersValues.length)) {\n this.dispatch(\"UPDATE_FILTER\", {\n sheetId,\n col: newTableZone.left + i,\n row: newTableZone.top,\n values: table.filtersValues[i],\n });\n }\n }\n }\n getClipboardContent() {\n return {\n [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),\n [ClipboardMIMEType.Html]: this.getHTMLContent(),\n };\n }\n getPlainTextContent() {\n return (this.cells\n .map((cells) => {\n return cells\n .map((c) => c.cell ? this.getters.getCellText(c.position, this.getters.shouldShowFormulas()) : \"\")\n .join(\"\\t\");\n })\n .join(\"\\n\") || \"\\t\");\n }\n getHTMLContent() {\n if (this.cells.length == 1 && this.cells[0].length == 1) {\n return this.getters.getCellText(this.cells[0][0].position);\n }\n let htmlTable = '';\n for (const row of this.cells) {\n htmlTable += \"\";\n for (const cell of row) {\n const cssStyle = cssPropertiesToCss(cellStyleToCss(cell.style), false);\n const cellText = this.getters.getCellText(cell.position);\n htmlTable += `` + xmlEscape(cellText) + \" \";\n }\n htmlTable += \" \";\n }\n htmlTable += \"
\";\n return htmlTable;\n }\n isColRowDirtyingClipboard(position, dimension) {\n if (!this.zones)\n return false;\n for (let zone of this.zones) {\n if (dimension === \"COL\" && position <= zone.right) {\n return true;\n }\n if (dimension === \"ROW\" && position <= zone.bottom) {\n return true;\n }\n }\n return false;\n }\n drawClipboard(renderingContext) {\n const { ctx, thinLineWidth } = renderingContext;\n if (this.sheetId !== this.getters.getActiveSheetId() || !this.zones || !this.zones.length) {\n return;\n }\n ctx.setLineDash([8, 5]);\n ctx.strokeStyle = SELECTION_BORDER_COLOR;\n ctx.lineWidth = 3.3 * thinLineWidth;\n for (const zone of this.zones) {\n const { x, y, width, height } = this.getters.getVisibleRect(zone);\n if (width > 0 && height > 0) {\n ctx.strokeRect(x, y, width, height);\n }\n }\n }\n }\n\n /** State of the clipboard when copying/cutting figures */\n class ClipboardFigureState {\n constructor(operation, getters, dispatch) {\n this.operation = operation;\n this.getters = getters;\n this.dispatch = dispatch;\n this.sheetId = getters.getActiveSheetId();\n const copiedFigureId = getters.getSelectedFigureId();\n if (!copiedFigureId) {\n throw new Error(`No figure selected`);\n }\n const figure = getters.getFigure(this.sheetId, copiedFigureId);\n if (!figure) {\n throw new Error(`No figure for the given id: ${copiedFigureId}`);\n }\n this.copiedFigure = { ...figure };\n switch (figure.tag) {\n case \"chart\":\n this.copiedFigureContent = new ClipboardFigureChart(dispatch, getters, this.sheetId, copiedFigureId);\n break;\n case \"image\":\n this.copiedFigureContent = new ClipboardFigureImage(dispatch, getters, this.sheetId, copiedFigureId);\n break;\n default:\n throw new Error(`Unknow tag '${figure.tag}' for the given figure id: ${copiedFigureId}`);\n }\n }\n isCutAllowed(target) {\n return 0 /* CommandResult.Success */;\n }\n isPasteAllowed(target, option) {\n if (target.length === 0) {\n return 73 /* CommandResult.EmptyTarget */;\n }\n if ((option === null || option === void 0 ? void 0 : option.pasteOption) !== undefined) {\n return 22 /* CommandResult.WrongFigurePasteOption */;\n }\n return 0 /* CommandResult.Success */;\n }\n /**\n * Paste the clipboard content in the given target\n */\n paste(target) {\n const sheetId = this.getters.getActiveSheetId();\n const position = {\n x: this.getters.getColDimensions(sheetId, target[0].left).start,\n y: this.getters.getRowDimensions(sheetId, target[0].top).start,\n };\n const size = { height: this.copiedFigure.height, width: this.copiedFigure.width };\n const newId = new UuidGenerator().uuidv4();\n this.copiedFigureContent.paste(sheetId, newId, position, size);\n if (this.operation === \"CUT\") {\n this.dispatch(\"DELETE_FIGURE\", {\n sheetId: this.copiedFigureContent.sheetId,\n id: this.copiedFigure.id,\n });\n }\n this.dispatch(\"SELECT_FIGURE\", { id: newId });\n }\n getClipboardContent() {\n return { [ClipboardMIMEType.PlainText]: \"\\t\" };\n }\n isColRowDirtyingClipboard(position, dimension) {\n return false;\n }\n drawClipboard(renderingContext) { }\n }\n class ClipboardFigureChart {\n constructor(dispatch, getters, sheetId, copiedFigureId) {\n this.dispatch = dispatch;\n this.sheetId = sheetId;\n const chart = getters.getChart(copiedFigureId);\n if (!chart) {\n throw new Error(`No chart for the given id: ${copiedFigureId}`);\n }\n this.copiedChart = chart.copyInSheetId(sheetId);\n }\n paste(sheetId, figureId, position, size) {\n const copy = this.copiedChart.copyInSheetId(sheetId);\n this.dispatch(\"CREATE_CHART\", {\n id: figureId,\n sheetId,\n position,\n size,\n definition: copy.getDefinition(),\n });\n }\n }\n class ClipboardFigureImage {\n constructor(dispatch, getters, sheetId, copiedFigureId) {\n this.dispatch = dispatch;\n this.sheetId = sheetId;\n const image = getters.getImage(copiedFigureId);\n this.copiedImage = deepCopy(image);\n }\n paste(sheetId, figureId, position, size) {\n const copy = deepCopy(this.copiedImage);\n this.dispatch(\"CREATE_IMAGE\", {\n figureId,\n sheetId,\n position,\n size,\n definition: copy,\n });\n }\n }\n\n /** State of the clipboard when copying/cutting from the OS clipboard*/\n class ClipboardOsState extends ClipboardCellsAbstractState {\n constructor(content, getters, dispatch, selection) {\n super(\"COPY\", getters, dispatch, selection);\n this.values = content\n .replace(/\\r/g, \"\")\n .split(\"\\n\")\n .map((vals) => vals.split(\"\\t\"));\n }\n isPasteAllowed(target, clipboardOption) {\n const sheetId = this.getters.getActiveSheetId();\n const pasteZone = this.getPasteZone(target);\n if (this.getters.doesIntersectMerge(sheetId, pasteZone)) {\n return 2 /* CommandResult.WillRemoveExistingMerge */;\n }\n return 0 /* CommandResult.Success */;\n }\n paste(target) {\n const values = this.values;\n const pasteZone = this.getPasteZone(target);\n const { left: activeCol, top: activeRow } = pasteZone;\n const { width, height } = zoneToDimension(pasteZone);\n const sheetId = this.getters.getActiveSheetId();\n this.addMissingDimensions(width, height, activeCol, activeRow);\n for (let i = 0; i < values.length; i++) {\n for (let j = 0; j < values[i].length; j++) {\n this.dispatch(\"UPDATE_CELL\", {\n row: activeRow + i,\n col: activeCol + j,\n content: values[i][j],\n sheetId,\n });\n }\n }\n const zone = {\n left: activeCol,\n top: activeRow,\n right: activeCol + width - 1,\n bottom: activeRow + height - 1,\n };\n this.selection.selectZone({ cell: { col: activeCol, row: activeRow }, zone });\n }\n getClipboardContent() {\n return {\n [ClipboardMIMEType.PlainText]: this.values.map((values) => values.join(\"\\t\")).join(\"\\n\"),\n };\n }\n getPasteZone(target) {\n const height = this.values.length;\n const width = Math.max(...this.values.map((a) => a.length));\n const { left: activeCol, top: activeRow } = target[0];\n return {\n top: activeRow,\n left: activeCol,\n bottom: activeRow + height - 1,\n right: activeCol + width - 1,\n };\n }\n }\n\n /**\n * Clipboard Plugin\n *\n * This clipboard manages all cut/copy/paste interactions internal to the\n * application, and with the OS clipboard as well.\n */\n class ClipboardPlugin extends UIPlugin {\n constructor() {\n super(...arguments);\n this.status = \"invisible\";\n this._isPaintingFormat = false;\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"CUT\":\n const zones = cmd.target || this.getters.getSelectedZones();\n const state = this.getClipboardState(zones, cmd.type);\n return state.isCutAllowed(zones);\n case \"PASTE\":\n if (!this.state) {\n return 23 /* CommandResult.EmptyClipboard */;\n }\n const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? \"onlyFormat\" : undefined);\n return this.state.isPasteAllowed(cmd.target, { pasteOption });\n case \"PASTE_FROM_OS_CLIPBOARD\": {\n const state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection);\n return state.isPasteAllowed(cmd.target);\n }\n case \"INSERT_CELL\": {\n const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);\n const state = this.getClipboardStateForCopyCells(cut, \"CUT\");\n return state.isPasteAllowed(paste);\n }\n case \"DELETE_CELL\": {\n const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);\n const state = this.getClipboardStateForCopyCells(cut, \"CUT\");\n return state.isPasteAllowed(paste);\n }\n }\n return 0 /* CommandResult.Success */;\n }\n handle(cmd) {\n var _a, _b;\n switch (cmd.type) {\n case \"COPY\":\n case \"CUT\":\n const zones = (\"target\" in cmd && cmd.target) || this.getters.getSelectedZones();\n this.state = this.getClipboardState(zones, cmd.type);\n this.status = \"visible\";\n break;\n case \"PASTE\":\n if (!this.state) {\n break;\n }\n const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? \"onlyFormat\" : undefined);\n this._isPaintingFormat = false;\n this.state.paste(cmd.target, { pasteOption, shouldPasteCF: true, selectTarget: true });\n if (this.state.operation === \"CUT\") {\n this.state = undefined;\n }\n this.status = \"invisible\";\n break;\n case \"CLEAN_CLIPBOARD_HIGHLIGHT\":\n this.status = \"invisible\";\n break;\n case \"DELETE_CELL\": {\n const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);\n if (!isZoneValid(cut[0])) {\n for (const { col, row } of positions(cmd.zone)) {\n this.dispatch(\"CLEAR_CELL\", { col, row, sheetId: this.getters.getActiveSheetId() });\n }\n break;\n }\n const state = this.getClipboardStateForCopyCells(cut, \"CUT\");\n state.paste(paste);\n break;\n }\n case \"INSERT_CELL\": {\n const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);\n const state = this.getClipboardStateForCopyCells(cut, \"CUT\");\n state.paste(paste);\n break;\n }\n case \"ADD_COLUMNS_ROWS\": {\n this.status = \"invisible\";\n // If we add a col/row inside or before the cut area, we invalidate the clipboard\n if (((_a = this.state) === null || _a === void 0 ? void 0 : _a.operation) !== \"CUT\") {\n return;\n }\n const isClipboardDirty = this.state.isColRowDirtyingClipboard(cmd.position === \"before\" ? cmd.base : cmd.base + 1, cmd.dimension);\n if (isClipboardDirty) {\n this.state = undefined;\n }\n break;\n }\n case \"REMOVE_COLUMNS_ROWS\": {\n this.status = \"invisible\";\n // If we remove a col/row inside or before the cut area, we invalidate the clipboard\n if (((_b = this.state) === null || _b === void 0 ? void 0 : _b.operation) !== \"CUT\") {\n return;\n }\n for (let el of cmd.elements) {\n const isClipboardDirty = this.state.isColRowDirtyingClipboard(el, cmd.dimension);\n if (isClipboardDirty) {\n this.state = undefined;\n break;\n }\n }\n this.status = \"invisible\";\n break;\n }\n case \"PASTE_FROM_OS_CLIPBOARD\":\n const state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection);\n state.paste(cmd.target);\n this.status = \"invisible\";\n break;\n case \"ACTIVATE_PAINT_FORMAT\": {\n const zones = this.getters.getSelectedZones();\n this.state = this.getClipboardStateForCopyCells(zones, \"COPY\");\n this._isPaintingFormat = true;\n this.status = \"visible\";\n break;\n }\n default:\n if (isCoreCommand(cmd)) {\n this.status = \"invisible\";\n }\n }\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n /**\n * Format the current clipboard to a string suitable for being pasted in other\n * programs.\n *\n * - add a tab character between each consecutive cells\n * - add a newline character between each line\n *\n * Note that it returns \\t if the clipboard is empty. This is necessary for the\n * clipboard copy event to add it as data, otherwise an empty string is not\n * considered as a copy content.\n */\n getClipboardContent() {\n var _a;\n return ((_a = this.state) === null || _a === void 0 ? void 0 : _a.getClipboardContent()) || { [ClipboardMIMEType.PlainText]: \"\\t\" };\n }\n getClipboardTextContent() {\n var _a;\n return ((_a = this.state) === null || _a === void 0 ? void 0 : _a.getClipboardContent()[ClipboardMIMEType.PlainText]) || \"\\t\";\n }\n isCutOperation() {\n return this.state ? this.state.operation === \"CUT\" : false;\n }\n isPaintingFormat() {\n return this._isPaintingFormat;\n }\n // ---------------------------------------------------------------------------\n // Private methods\n // ---------------------------------------------------------------------------\n getDeleteCellsTargets(zone, dimension) {\n const sheetId = this.getters.getActiveSheetId();\n let cut;\n if (dimension === \"COL\") {\n cut = {\n ...zone,\n left: zone.right + 1,\n right: this.getters.getNumberCols(sheetId) - 1,\n };\n }\n else {\n cut = {\n ...zone,\n top: zone.bottom + 1,\n bottom: this.getters.getNumberRows(sheetId) - 1,\n };\n }\n return { cut: [cut], paste: [zone] };\n }\n getInsertCellsTargets(zone, dimension) {\n const sheetId = this.getters.getActiveSheetId();\n let cut;\n let paste;\n if (dimension === \"COL\") {\n cut = {\n ...zone,\n right: this.getters.getNumberCols(sheetId) - 1,\n };\n paste = {\n ...zone,\n left: zone.right + 1,\n right: zone.right + 1,\n };\n }\n else {\n cut = {\n ...zone,\n bottom: this.getters.getNumberRows(sheetId) - 1,\n };\n paste = { ...zone, top: zone.bottom + 1, bottom: this.getters.getNumberRows(sheetId) - 1 };\n }\n return { cut: [cut], paste: [paste] };\n }\n getClipboardStateForCopyCells(zones, operation) {\n return new ClipboardCellsState(zones, operation, this.getters, this.dispatch, this.selection);\n }\n /**\n * Get the clipboard state from the given zones.\n */\n getClipboardState(zones, operation) {\n const selectedFigureId = this.getters.getSelectedFigureId();\n if (selectedFigureId) {\n return new ClipboardFigureState(operation, this.getters, this.dispatch);\n }\n return new ClipboardCellsState(zones, operation, this.getters, this.dispatch, this.selection);\n }\n // ---------------------------------------------------------------------------\n // Grid rendering\n // ---------------------------------------------------------------------------\n drawGrid(renderingContext) {\n if (this.status !== \"visible\" || !this.state) {\n return;\n }\n this.state.drawClipboard(renderingContext);\n }\n }\n ClipboardPlugin.layers = [2 /* LAYERS.Clipboard */];\n ClipboardPlugin.getters = [\n \"getClipboardContent\",\n \"getClipboardTextContent\",\n \"isCutOperation\",\n \"isPaintingFormat\",\n ];\n\n const selectionStatisticFunctions = [\n {\n name: _lt(\"Sum\"),\n types: [CellValueType.number],\n compute: (values) => SUM.compute([values]),\n },\n {\n name: _lt(\"Avg\"),\n types: [CellValueType.number],\n compute: (values) => AVERAGE.compute([values]),\n },\n {\n name: _lt(\"Min\"),\n types: [CellValueType.number],\n compute: (values) => MIN.compute([values]),\n },\n {\n name: _lt(\"Max\"),\n types: [CellValueType.number],\n compute: (values) => MAX.compute([values]),\n },\n {\n name: _lt(\"Count\"),\n types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],\n compute: (values) => COUNTA.compute([values]),\n },\n {\n name: _lt(\"Count Numbers\"),\n types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],\n compute: (values) => COUNT.compute([values]),\n },\n ];\n /**\n * SelectionPlugin\n */\n class GridSelectionPlugin extends UIPlugin {\n constructor(config) {\n super(config);\n this.gridSelection = {\n anchor: {\n cell: { col: 0, row: 0 },\n zone: { top: 0, left: 0, bottom: 0, right: 0 },\n },\n zones: [{ top: 0, left: 0, bottom: 0, right: 0 }],\n };\n this.selectedFigureId = null;\n this.sheetsData = {};\n // This flag is used to avoid to historize the ACTIVE_SHEET command when it's\n // the main command.\n this.activeSheet = null;\n this.moveClient = config.moveClient;\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"ACTIVATE_SHEET\":\n try {\n this.getters.getSheet(cmd.sheetIdTo);\n break;\n }\n catch (error) {\n return 27 /* CommandResult.InvalidSheetId */;\n }\n case \"MOVE_COLUMNS_ROWS\":\n return this.isMoveElementAllowed(cmd);\n }\n return 0 /* CommandResult.Success */;\n }\n handleEvent(event) {\n const anchor = event.anchor;\n let zones = [];\n switch (event.mode) {\n case \"overrideSelection\":\n zones = [anchor.zone];\n break;\n case \"updateAnchor\":\n zones = [...this.gridSelection.zones];\n const index = zones.findIndex((z) => isEqual(z, event.previousAnchor.zone));\n if (index >= 0) {\n zones[index] = anchor.zone;\n }\n break;\n case \"newAnchor\":\n zones = [...this.gridSelection.zones, anchor.zone];\n break;\n }\n this.setSelectionMixin(event.anchor, zones);\n /** Any change to the selection has to be reflected in the selection processor. */\n this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));\n const { col, row } = this.gridSelection.anchor.cell;\n this.moveClient({\n sheetId: this.getters.getActiveSheetId(),\n col,\n row,\n });\n this.selectedFigureId = null;\n }\n handle(cmd) {\n switch (cmd.type) {\n case \"START_EDITION\":\n case \"ACTIVATE_SHEET\":\n this.selectedFigureId = null;\n break;\n case \"DELETE_FIGURE\":\n if (this.selectedFigureId === cmd.id) {\n this.selectedFigureId = null;\n }\n break;\n case \"DELETE_SHEET\":\n if (this.selectedFigureId && this.getters.getFigure(cmd.sheetId, this.selectedFigureId)) {\n this.selectedFigureId = null;\n }\n break;\n }\n switch (cmd.type) {\n case \"START\":\n const firstSheetId = this.getters.getVisibleSheetIds()[0];\n this.dispatch(\"ACTIVATE_SHEET\", {\n sheetIdTo: firstSheetId,\n sheetIdFrom: firstSheetId,\n });\n const { col, row } = this.getters.getNextVisibleCellPosition({\n sheetId: firstSheetId,\n col: 0,\n row: 0,\n });\n this.selectCell(col, row);\n this.selection.registerAsDefault(this, this.gridSelection.anchor, {\n handleEvent: this.handleEvent.bind(this),\n });\n this.moveClient({ sheetId: firstSheetId, col: 0, row: 0 });\n break;\n case \"ACTIVATE_SHEET\": {\n if (!this.getters.isSheetVisible(cmd.sheetIdTo)) {\n this.dispatch(\"SHOW_SHEET\", { sheetId: cmd.sheetIdTo });\n }\n this.setActiveSheet(cmd.sheetIdTo);\n this.sheetsData[cmd.sheetIdFrom] = {\n gridSelection: deepCopy(this.gridSelection),\n };\n if (cmd.sheetIdTo in this.sheetsData) {\n Object.assign(this, this.sheetsData[cmd.sheetIdTo]);\n this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));\n }\n else {\n const { col, row } = this.getters.getNextVisibleCellPosition({\n sheetId: cmd.sheetIdTo,\n col: 0,\n row: 0,\n });\n this.selectCell(col, row);\n }\n break;\n }\n case \"REMOVE_COLUMNS_ROWS\": {\n const sheetId = this.getters.getActiveSheetId();\n if (cmd.sheetId === sheetId) {\n if (cmd.dimension === \"COL\") {\n this.onColumnsRemoved(cmd);\n }\n else {\n this.onRowsRemoved(cmd);\n }\n const { col, row } = this.gridSelection.anchor.cell;\n this.moveClient({ sheetId, col, row });\n }\n break;\n }\n case \"ADD_COLUMNS_ROWS\": {\n const sheetId = this.getters.getActiveSheetId();\n if (cmd.sheetId === sheetId) {\n this.onAddElements(cmd);\n const { col, row } = this.gridSelection.anchor.cell;\n this.moveClient({ sheetId, col, row });\n }\n break;\n }\n case \"MOVE_COLUMNS_ROWS\":\n if (cmd.sheetId === this.getActiveSheetId()) {\n this.onMoveElements(cmd);\n }\n break;\n case \"SELECT_FIGURE\":\n this.selectedFigureId = cmd.id;\n break;\n case \"ACTIVATE_NEXT_SHEET\":\n this.activateNextSheet(\"right\");\n break;\n case \"ACTIVATE_PREVIOUS_SHEET\":\n this.activateNextSheet(\"left\");\n break;\n case \"HIDE_SHEET\":\n if (cmd.sheetId === this.getActiveSheetId()) {\n this.dispatch(\"ACTIVATE_SHEET\", {\n sheetIdFrom: cmd.sheetId,\n sheetIdTo: this.getters.getVisibleSheetIds()[0],\n });\n }\n break;\n case \"UNDO\":\n case \"REDO\":\n case \"DELETE_SHEET\":\n const deletedSheetIds = Object.keys(this.sheetsData).filter((sheetId) => !this.getters.tryGetSheet(sheetId));\n for (const sheetId of deletedSheetIds) {\n delete this.sheetsData[sheetId];\n }\n for (const sheetId in this.sheetsData) {\n const gridSelection = this.clipSelection(sheetId, this.sheetsData[sheetId].gridSelection);\n this.sheetsData[sheetId] = {\n gridSelection: deepCopy(gridSelection),\n };\n }\n if (!this.getters.tryGetSheet(this.getters.getActiveSheetId())) {\n const currentSheetIds = this.getters.getVisibleSheetIds();\n this.activeSheet = this.getters.getSheet(currentSheetIds[0]);\n if (this.activeSheet.id in this.sheetsData) {\n const { anchor } = this.clipSelection(this.activeSheet.id, this.sheetsData[this.activeSheet.id].gridSelection);\n this.selectCell(anchor.cell.col, anchor.cell.row);\n }\n else {\n this.selectCell(0, 0);\n }\n const { col, row } = this.gridSelection.anchor.cell;\n this.moveClient({\n sheetId: this.getters.getActiveSheetId(),\n col,\n row,\n });\n }\n const sheetId = this.getters.getActiveSheetId();\n this.gridSelection.zones = this.gridSelection.zones.map((z) => this.getters.expandZone(sheetId, z));\n this.gridSelection.anchor.zone = this.getters.expandZone(sheetId, this.gridSelection.anchor.zone);\n this.setSelectionMixin(this.gridSelection.anchor, this.gridSelection.zones);\n break;\n }\n /** Any change to the selection has to be reflected in the selection processor. */\n this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));\n }\n // ---------------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------------\n getActiveSheet() {\n return this.activeSheet;\n }\n getActiveSheetId() {\n return this.activeSheet.id;\n }\n getActiveCell() {\n return this.getters.getEvaluatedCell(this.getActivePosition());\n }\n getActiveCols() {\n const activeCols = new Set();\n for (let zone of this.gridSelection.zones) {\n if (zone.top === 0 &&\n zone.bottom === this.getters.getNumberRows(this.getters.getActiveSheetId()) - 1) {\n for (let i = zone.left; i <= zone.right; i++) {\n activeCols.add(i);\n }\n }\n }\n return activeCols;\n }\n getActiveRows() {\n const activeRows = new Set();\n const sheetId = this.getters.getActiveSheetId();\n for (let zone of this.gridSelection.zones) {\n if (zone.left === 0 && zone.right === this.getters.getNumberCols(sheetId) - 1) {\n for (let i = zone.top; i <= zone.bottom; i++) {\n activeRows.add(i);\n }\n }\n }\n return activeRows;\n }\n getCurrentStyle() {\n const zone = this.getters.getSelectedZone();\n const sheetId = this.getters.getActiveSheetId();\n return this.getters.getCellStyle({ sheetId, col: zone.left, row: zone.top });\n }\n getSelectedZones() {\n return deepCopy(this.gridSelection.zones);\n }\n getSelectedZone() {\n return deepCopy(this.gridSelection.anchor.zone);\n }\n getSelection() {\n return deepCopy(this.gridSelection);\n }\n getSelectedFigureId() {\n return this.selectedFigureId;\n }\n getActivePosition() {\n return this.getters.getMainCellPosition({\n sheetId: this.getActiveSheetId(),\n col: this.gridSelection.anchor.cell.col,\n row: this.gridSelection.anchor.cell.row,\n });\n }\n getSheetPosition(sheetId) {\n if (sheetId === this.getters.getActiveSheetId()) {\n return this.getActivePosition();\n }\n else {\n const sheetData = this.sheetsData[sheetId];\n return sheetData\n ? {\n sheetId,\n col: sheetData.gridSelection.anchor.cell.col,\n row: sheetData.gridSelection.anchor.cell.row,\n }\n : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });\n }\n }\n getStatisticFnResults() {\n // get deduplicated cells in zones\n const cells = new Set(this.gridSelection.zones\n .map((zone) => this.getters.getEvaluatedCellsInZone(this.getters.getActiveSheetId(), zone))\n .flat()\n .filter((cell) => cell.type !== CellValueType.empty));\n let cellsTypes = new Set();\n let cellsValues = [];\n for (let cell of cells) {\n cellsTypes.add(cell.type);\n cellsValues.push(cell.value);\n }\n let statisticFnResults = {};\n for (let fn of selectionStatisticFunctions) {\n // We don't want to display statistical information when there is no interest:\n // We set the statistical result to undefined if the data handled by the selection\n // does not match the data handled by the function.\n // Ex: if there are only texts in the selection, we prefer that the SUM result\n // be displayed as undefined rather than 0.\n let fnResult = undefined;\n if (fn.types.some((t) => cellsTypes.has(t))) {\n fnResult = fn.compute(cellsValues);\n }\n statisticFnResults[fn.name] = fnResult;\n }\n return statisticFnResults;\n }\n getAggregate() {\n let aggregate = 0;\n let n = 0;\n const sheetId = this.getters.getActiveSheetId();\n const cellPositions = this.gridSelection.zones.map(positions).flat();\n for (const { col, row } of cellPositions) {\n const cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n if (cell.type === CellValueType.number) {\n n++;\n aggregate += cell.value;\n }\n }\n return n < 2 ? null : formatValue(aggregate);\n }\n isSelected(zone) {\n return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));\n }\n /**\n * Returns a sorted array of indexes of all columns (respectively rows depending\n * on the dimension parameter) intersected by the currently selected zones.\n *\n * example:\n * assume selectedZones: [{left:0, right: 2, top :2, bottom: 4}, {left:5, right: 6, top :3, bottom: 5}]\n *\n * if dimension === \"COL\" => [0,1,2,5,6]\n * if dimension === \"ROW\" => [2,3,4,5]\n */\n getElementsFromSelection(dimension) {\n if (dimension === \"COL\" && this.getters.getActiveCols().size === 0) {\n return [];\n }\n if (dimension === \"ROW\" && this.getters.getActiveRows().size === 0) {\n return [];\n }\n const zones = this.getters.getSelectedZones();\n let elements = [];\n const start = dimension === \"COL\" ? \"left\" : \"top\";\n const end = dimension === \"COL\" ? \"right\" : \"bottom\";\n for (const zone of zones) {\n const zoneRows = Array.from({ length: zone[end] - zone[start] + 1 }, (_, i) => zone[start] + i);\n elements = elements.concat(zoneRows);\n }\n return [...new Set(elements)].sort();\n }\n // ---------------------------------------------------------------------------\n // Other\n // ---------------------------------------------------------------------------\n /**\n * Ensure selections are not outside sheet boundaries.\n * They are clipped to fit inside the sheet if needed.\n */\n setSelectionMixin(anchor, zones) {\n const { anchor: clippedAnchor, zones: clippedZones } = this.clipSelection(this.getters.getActiveSheetId(), { anchor, zones });\n this.gridSelection.anchor = clippedAnchor;\n this.gridSelection.zones = uniqueZones(clippedZones);\n }\n /**\n * Change the anchor of the selection active cell to an absolute col and row index.\n *\n * This is a non trivial task. We need to stop the editing process and update\n * properly the current selection. Also, this method can optionally create a new\n * range in the selection.\n */\n selectCell(col, row) {\n const sheetId = this.getters.getActiveSheetId();\n const zone = this.getters.expandZone(sheetId, { left: col, right: col, top: row, bottom: row });\n this.setSelectionMixin({ zone, cell: { col, row } }, [zone]);\n }\n setActiveSheet(id) {\n const sheet = this.getters.getSheet(id);\n this.activeSheet = sheet;\n }\n activateNextSheet(direction) {\n const sheetIds = this.getters.getSheetIds();\n const oldSheetPosition = sheetIds.findIndex((id) => id === this.activeSheet.id);\n const delta = direction === \"left\" ? sheetIds.length - 1 : 1;\n const newPosition = (oldSheetPosition + delta) % sheetIds.length;\n this.dispatch(\"ACTIVATE_SHEET\", {\n sheetIdFrom: this.getActiveSheetId(),\n sheetIdTo: sheetIds[newPosition],\n });\n }\n onColumnsRemoved(cmd) {\n const { cell, zone } = this.gridSelection.anchor;\n const selectedZone = updateSelectionOnDeletion(zone, \"left\", [...cmd.elements]);\n let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };\n anchorZone = updateSelectionOnDeletion(anchorZone, \"left\", [...cmd.elements]);\n const anchor = {\n cell: {\n col: anchorZone.left,\n row: anchorZone.top,\n },\n zone: selectedZone,\n };\n this.setSelectionMixin(anchor, [selectedZone]);\n }\n onRowsRemoved(cmd) {\n const { cell, zone } = this.gridSelection.anchor;\n const selectedZone = updateSelectionOnDeletion(zone, \"top\", [...cmd.elements]);\n let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };\n anchorZone = updateSelectionOnDeletion(anchorZone, \"top\", [...cmd.elements]);\n const anchor = {\n cell: {\n col: anchorZone.left,\n row: anchorZone.top,\n },\n zone: selectedZone,\n };\n this.setSelectionMixin(anchor, [selectedZone]);\n }\n onAddElements(cmd) {\n const selection = this.gridSelection.anchor.zone;\n const zone = updateSelectionOnInsertion(selection, cmd.dimension === \"COL\" ? \"left\" : \"top\", cmd.base, cmd.position, cmd.quantity);\n const anchor = { cell: { col: zone.left, row: zone.top }, zone };\n this.setSelectionMixin(anchor, [zone]);\n }\n onMoveElements(cmd) {\n const thickness = cmd.elements.length;\n this.dispatch(\"ADD_COLUMNS_ROWS\", {\n dimension: cmd.dimension,\n sheetId: cmd.sheetId,\n base: cmd.base,\n quantity: thickness,\n position: \"before\",\n });\n const isCol = cmd.dimension === \"COL\";\n const start = cmd.elements[0];\n const end = cmd.elements[thickness - 1];\n const isBasedBefore = cmd.base < start;\n const deltaCol = isBasedBefore && isCol ? thickness : 0;\n const deltaRow = isBasedBefore && !isCol ? thickness : 0;\n this.dispatch(\"CUT\", {\n target: [\n {\n left: isCol ? start + deltaCol : 0,\n right: isCol ? end + deltaCol : this.getters.getNumberCols(cmd.sheetId) - 1,\n top: !isCol ? start + deltaRow : 0,\n bottom: !isCol ? end + deltaRow : this.getters.getNumberRows(cmd.sheetId) - 1,\n },\n ],\n });\n this.dispatch(\"PASTE\", {\n target: [\n {\n left: isCol ? cmd.base : 0,\n right: isCol ? cmd.base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,\n top: !isCol ? cmd.base : 0,\n bottom: !isCol ? cmd.base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,\n },\n ],\n });\n const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;\n let currentIndex = cmd.base;\n for (const element of toRemove) {\n const size = cmd.dimension === \"COL\"\n ? this.getters.getColSize(cmd.sheetId, element)\n : this.getters.getRowSize(cmd.sheetId, element);\n this.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n dimension: cmd.dimension,\n sheetId: cmd.sheetId,\n size,\n elements: [currentIndex],\n });\n currentIndex += 1;\n }\n this.dispatch(\"REMOVE_COLUMNS_ROWS\", {\n dimension: cmd.dimension,\n sheetId: cmd.sheetId,\n elements: toRemove,\n });\n }\n isMoveElementAllowed(cmd) {\n const isCol = cmd.dimension === \"COL\";\n const start = cmd.elements[0];\n const end = cmd.elements[cmd.elements.length - 1];\n const id = cmd.sheetId;\n const doesElementsHaveCommonMerges = isCol\n ? this.getters.doesColumnsHaveCommonMerges\n : this.getters.doesRowsHaveCommonMerges;\n if (doesElementsHaveCommonMerges(id, start - 1, start) ||\n doesElementsHaveCommonMerges(id, end, end + 1) ||\n doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {\n return 2 /* CommandResult.WillRemoveExistingMerge */;\n }\n return 0 /* CommandResult.Success */;\n }\n //-------------------------------------------\n // Helpers for extensions\n // ------------------------------------------\n /**\n * Clip the selection if it spans outside the sheet\n */\n clipSelection(sheetId, selection) {\n const cols = this.getters.getNumberCols(sheetId) - 1;\n const rows = this.getters.getNumberRows(sheetId) - 1;\n const zones = selection.zones.map((z) => {\n return {\n left: clip(z.left, 0, cols),\n right: clip(z.right, 0, cols),\n top: clip(z.top, 0, rows),\n bottom: clip(z.bottom, 0, rows),\n };\n });\n const anchorCol = clip(selection.anchor.cell.col, 0, cols);\n const anchorRow = clip(selection.anchor.cell.row, 0, rows);\n const anchorZone = {\n left: clip(selection.anchor.zone.left, 0, cols),\n right: clip(selection.anchor.zone.right, 0, cols),\n top: clip(selection.anchor.zone.top, 0, rows),\n bottom: clip(selection.anchor.zone.bottom, 0, rows),\n };\n return {\n zones,\n anchor: {\n cell: { col: anchorCol, row: anchorRow },\n zone: anchorZone,\n },\n };\n }\n // ---------------------------------------------------------------------------\n // Grid rendering\n // ---------------------------------------------------------------------------\n drawGrid(renderingContext) {\n if (this.getters.isDashboard()) {\n return;\n }\n const { ctx, thinLineWidth } = renderingContext;\n // selection\n const zones = this.getSelectedZones();\n ctx.fillStyle = \"#f3f7fe\";\n const onlyOneCell = zones.length === 1 && zones[0].left === zones[0].right && zones[0].top === zones[0].bottom;\n ctx.fillStyle = onlyOneCell ? \"#f3f7fe\" : \"#e9f0ff\";\n ctx.strokeStyle = SELECTION_BORDER_COLOR;\n ctx.lineWidth = 1.5 * thinLineWidth;\n for (const zone of zones) {\n const { x, y, width, height } = this.getters.getVisibleRect(zone);\n ctx.globalCompositeOperation = \"multiply\";\n ctx.fillRect(x, y, width, height);\n ctx.globalCompositeOperation = \"source-over\";\n ctx.strokeRect(x, y, width, height);\n }\n ctx.globalCompositeOperation = \"source-over\";\n // active zone\n const position = this.getActivePosition();\n ctx.strokeStyle = SELECTION_BORDER_COLOR;\n ctx.lineWidth = 3 * thinLineWidth;\n let zone;\n if (this.getters.isInMerge(position)) {\n zone = this.getters.getMerge(position);\n }\n else {\n zone = positionToZone(position);\n }\n const { x, y, width, height } = this.getters.getVisibleRect(zone);\n if (width > 0 && height > 0) {\n ctx.strokeRect(x, y, width, height);\n }\n }\n }\n GridSelectionPlugin.layers = [6 /* LAYERS.Selection */];\n GridSelectionPlugin.getters = [\n \"getActiveSheet\",\n \"getActiveSheetId\",\n \"getActiveCell\",\n \"getActiveCols\",\n \"getActiveRows\",\n \"getCurrentStyle\",\n \"getSelectedZones\",\n \"getSelectedZone\",\n \"getStatisticFnResults\",\n \"getAggregate\",\n \"getSelectedFigureId\",\n \"getSelection\",\n \"getActivePosition\",\n \"getSheetPosition\",\n \"isSelected\",\n \"getElementsFromSelection\",\n ];\n\n const corePluginRegistry = new Registry()\n .add(\"sheet\", SheetPlugin)\n .add(\"header visibility\", HeaderVisibilityPlugin)\n .add(\"filters\", FiltersPlugin)\n .add(\"cell\", CellPlugin)\n .add(\"merge\", MergePlugin)\n .add(\"headerSize\", HeaderSizePlugin)\n .add(\"borders\", BordersPlugin)\n .add(\"conditional formatting\", ConditionalFormatPlugin)\n .add(\"figures\", FigurePlugin)\n .add(\"chart\", ChartPlugin)\n .add(\"image\", ImagePlugin);\n // Plugins which handle a specific feature, without handling any core commands\n const featurePluginRegistry = new Registry()\n .add(\"ui_sheet\", SheetUIPlugin)\n .add(\"header_visibility_ui\", HeaderVisibilityUIPlugin)\n .add(\"ui_options\", UIOptionsPlugin)\n .add(\"selectionInputManager\", SelectionInputsManagerPlugin)\n .add(\"highlight\", HighlightPlugin)\n .add(\"grid renderer\", RendererPlugin)\n .add(\"autofill\", AutofillPlugin)\n .add(\"find_and_replace\", FindAndReplacePlugin)\n .add(\"sort\", SortPlugin)\n .add(\"automatic_sum\", AutomaticSumPlugin)\n .add(\"format\", FormatPlugin)\n .add(\"cell_popovers\", CellPopoverPlugin)\n .add(\"selection_multiuser\", SelectionMultiUserPlugin);\n // Plugins which have a state, but which should not be shared in collaborative\n const statefulUIPluginRegistry = new Registry()\n .add(\"selection\", GridSelectionPlugin)\n .add(\"clipboard\", ClipboardPlugin)\n .add(\"edition\", EditionPlugin);\n // Plugins which have a derived state from core data\n const coreViewsPluginRegistry = new Registry()\n .add(\"evaluation\", EvaluationPlugin)\n .add(\"evaluation_filter\", FilterEvaluationPlugin)\n .add(\"evaluation_chart\", EvaluationChartPlugin)\n .add(\"evaluation_cf\", EvaluationConditionalFormatPlugin)\n .add(\"viewport\", SheetViewPlugin)\n .add(\"custom_colors\", CustomColorsPlugin);\n\n const clickableCellRegistry = new Registry();\n clickableCellRegistry.add(\"link\", {\n condition: (position, env) => !!env.model.getters.getEvaluatedCell(position).link,\n action: (position, env) => openLink(env.model.getters.getEvaluatedCell(position).link, env),\n sequence: 5,\n });\n\n class ImageProvider {\n constructor(fileStore) {\n this.fileStore = fileStore;\n }\n async requestImage() {\n const file = await this.getImageFromUser();\n const path = await this.fileStore.upload(file);\n const size = await this.getImageSize(path);\n return { path, size };\n }\n getImageFromUser() {\n return new Promise((resolve, reject) => {\n const input = document.createElement(\"input\");\n input.setAttribute(\"type\", \"file\");\n input.setAttribute(\"accept\", \"image/*\");\n input.addEventListener(\"change\", async () => {\n if (input.files === null || input.files.length != 1) {\n reject();\n }\n else {\n resolve(input.files[0]);\n }\n });\n input.click();\n });\n }\n getImageSize(path) {\n return new Promise((resolve, reject) => {\n const image = new Image();\n image.src = path;\n image.addEventListener(\"load\", () => {\n const size = { width: image.width, height: image.height };\n resolve(size);\n });\n image.addEventListener(\"error\", reject);\n });\n }\n }\n\n // -----------------------------------------------------------------------------\n // SpreadSheet\n // -----------------------------------------------------------------------------\n css /* scss */ `\n .o-spreadsheet-bottom-bar {\n background-color: ${BACKGROUND_GRAY_COLOR};\n padding-left: ${HEADER_WIDTH}px;\n display: flex;\n align-items: center;\n font-size: 15px;\n border-top: 1px solid lightgrey;\n overflow: hidden;\n\n .o-add-sheet,\n .o-list-sheets {\n margin-right: 5px;\n }\n\n .o-add-sheet.disabled {\n cursor: not-allowed;\n }\n\n .o-sheet-item {\n display: flex;\n align-items: center;\n padding: 5px;\n cursor: pointer;\n &:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n }\n\n .o-all-sheets {\n display: flex;\n align-items: center;\n max-width: 80%;\n overflow: hidden;\n }\n\n .o-sheet {\n color: #666;\n padding: 0 15px;\n padding-right: 10px;\n height: ${BOTTOMBAR_HEIGHT}px;\n line-height: ${BOTTOMBAR_HEIGHT}px;\n user-select: none;\n white-space: nowrap;\n border-left: 1px solid #c1c1c1;\n\n &:last-child {\n border-right: 1px solid #c1c1c1;\n }\n\n &.active {\n color: #484;\n background-color: #ffffff;\n box-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n }\n\n .o-sheet-icon {\n margin-left: 5px;\n\n &:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n }\n }\n\n .o-selection-statistic {\n background-color: #ffffff;\n margin-left: auto;\n font-size: 14px;\n margin-right: 20px;\n padding: 4px 8px;\n color: #333;\n border-radius: 3px;\n box-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n user-select: none;\n cursor: pointer;\n &:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n }\n\n .fade-enter-active {\n transition: opacity 0.5s;\n }\n\n .fade-enter {\n opacity: 0;\n }\n }\n`;\n class BottomBar extends owl.Component {\n constructor() {\n super(...arguments);\n this.bottomBarRef = owl.useRef(\"bottomBar\");\n this.menuState = owl.useState({\n isOpen: false,\n menuId: undefined,\n position: null,\n menuItems: [],\n });\n this.selectedStatisticFn = \"\";\n }\n setup() {\n owl.onMounted(() => this.focusSheet());\n owl.onPatched(() => this.focusSheet());\n }\n focusSheet() {\n const div = this.bottomBarRef.el.querySelector(`[data-id=\"${this.env.model.getters.getActiveSheetId()}\"]`);\n if (div && div.scrollIntoView) {\n div.scrollIntoView();\n }\n }\n addSheet() {\n const activeSheetId = this.env.model.getters.getActiveSheetId();\n const position = this.env.model.getters.getSheetIds().findIndex((sheetId) => sheetId === activeSheetId) + 1;\n const sheetId = this.env.model.uuidGenerator.uuidv4();\n const name = this.env.model.getters.getNextSheetName(this.env._t(\"Sheet\"));\n this.env.model.dispatch(\"CREATE_SHEET\", { sheetId, position, name });\n this.env.model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom: activeSheetId, sheetIdTo: sheetId });\n }\n getVisibleSheets() {\n return this.env.model.getters\n .getVisibleSheetIds()\n .map((sheetId) => this.env.model.getters.getSheet(sheetId));\n }\n listSheets(ev) {\n const registry = new MenuItemRegistry();\n const from = this.env.model.getters.getActiveSheetId();\n let i = 0;\n for (const sheetId of this.env.model.getters.getSheetIds()) {\n const sheet = this.env.model.getters.getSheet(sheetId);\n registry.add(sheetId, {\n name: sheet.name,\n sequence: i,\n isReadonlyAllowed: true,\n textColor: sheet.isVisible ? undefined : \"grey\",\n action: (env) => {\n env.model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom: from, sheetIdTo: sheetId });\n },\n });\n i++;\n }\n const target = ev.currentTarget;\n const { top, left } = target.getBoundingClientRect();\n this.openContextMenu(left, top, registry);\n }\n activateSheet(name) {\n this.env.model.dispatch(\"ACTIVATE_SHEET\", {\n sheetIdFrom: this.env.model.getters.getActiveSheetId(),\n sheetIdTo: name,\n });\n }\n onDblClick(sheetId) {\n interactiveRenameSheet(this.env, sheetId);\n }\n openContextMenu(x, y, registry, menuId) {\n this.menuState.isOpen = true;\n this.menuState.menuItems = registry.getAll().filter((x) => x.isVisible(this.env));\n this.menuState.position = { x, y };\n this.menuState.menuId = menuId;\n }\n onIconClick(sheetId, ev) {\n if (this.env.model.getters.getActiveSheetId() !== sheetId) {\n this.activateSheet(sheetId);\n }\n if (ev.closedMenuId === sheetId) {\n this.menuState.isOpen = false;\n this.menuState.menuId = undefined;\n }\n else {\n const target = ev.currentTarget.parentElement;\n const { top, left } = target.getBoundingClientRect();\n this.openContextMenu(left, top, sheetMenuRegistry, sheetId);\n }\n }\n onContextMenu(sheetId, ev) {\n if (this.env.model.getters.getActiveSheetId() !== sheetId) {\n this.activateSheet(sheetId);\n }\n const target = ev.currentTarget;\n const { top, left } = target.getBoundingClientRect();\n this.openContextMenu(left, top, sheetMenuRegistry, sheetId);\n }\n getSelectedStatistic() {\n const statisticFnResults = this.env.model.getters.getStatisticFnResults();\n // don't display button if no function has a result\n if (Object.values(statisticFnResults).every((result) => result === undefined)) {\n return undefined;\n }\n if (this.selectedStatisticFn === \"\") {\n this.selectedStatisticFn = Object.keys(statisticFnResults)[0];\n }\n return this.getComposedFnName(this.selectedStatisticFn, statisticFnResults[this.selectedStatisticFn]);\n }\n listSelectionStatistics(ev) {\n const registry = new MenuItemRegistry();\n let i = 0;\n for (let [fnName, fnValue] of Object.entries(this.env.model.getters.getStatisticFnResults())) {\n registry.add(fnName, {\n name: this.getComposedFnName(fnName, fnValue),\n sequence: i,\n isReadonlyAllowed: true,\n action: () => {\n this.selectedStatisticFn = fnName;\n },\n });\n i++;\n }\n const target = ev.currentTarget;\n const { top, left, width } = target.getBoundingClientRect();\n this.openContextMenu(left + width, top, registry);\n }\n getComposedFnName(fnName, fnValue) {\n return fnName + \": \" + (fnValue !== undefined ? formatValue(fnValue) : \"__\");\n }\n }\n BottomBar.template = \"o-spreadsheet-BottomBar\";\n BottomBar.components = { Menu };\n BottomBar.props = {\n onClick: Function,\n };\n\n css /* scss */ `\n .o-dashboard-clickable-cell {\n position: absolute;\n cursor: pointer;\n }\n`;\n let tKey = 1;\n class SpreadsheetDashboard extends owl.Component {\n setup() {\n const gridRef = owl.useRef(\"grid\");\n this.canvasPosition = useAbsoluteBoundingRect(gridRef);\n this.hoveredCell = owl.useState({ col: undefined, row: undefined });\n owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });\n useGridDrawing(\"canvas\", this.env.model, () => this.env.model.getters.getSheetViewDimension());\n this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {\n this.moveCanvas(deltaX, deltaY);\n this.hoveredCell.col = undefined;\n this.hoveredCell.row = undefined;\n });\n }\n onCellHovered({ col, row }) {\n this.hoveredCell.col = col;\n this.hoveredCell.row = row;\n }\n get gridContainer() {\n const sheetId = this.env.model.getters.getActiveSheetId();\n const { right } = this.env.model.getters.getSheetZone(sheetId);\n const { end } = this.env.model.getters.getColDimensions(sheetId, right);\n return `\n max-width: ${end}px;\n `;\n }\n get gridOverlayDimensions() {\n return `\n height: 100%;\n width: 100%\n `;\n }\n getCellClickableStyle(coordinates) {\n return `\n top: ${coordinates.y}px;\n left: ${coordinates.x}px;\n width: ${coordinates.width}px;\n height: ${coordinates.height}px;\n `;\n }\n /**\n * Get all the boxes for the cell in the sheet view that are clickable.\n * This function is used to render an overlay over each clickable cell in\n * order to display a pointer cursor.\n *\n */\n getClickableCells() {\n const cells = [];\n const sheetId = this.env.model.getters.getActiveSheetId();\n for (const col of this.env.model.getters.getSheetViewVisibleCols()) {\n for (const row of this.env.model.getters.getSheetViewVisibleRows()) {\n const position = { sheetId, col, row };\n const action = this.getClickableAction(position);\n if (!action) {\n continue;\n }\n let zone;\n if (this.env.model.getters.isInMerge(position)) {\n zone = this.env.model.getters.getMerge(position);\n }\n else {\n zone = positionToZone({ col, row });\n }\n const rect = this.env.model.getters.getVisibleRect(zone);\n cells.push({\n coordinates: rect,\n position: { col, row },\n action,\n // we can't rely on position only because a row or a column could\n // be inserted at any time.\n tKey: `${tKey}-${col}-${row}`,\n });\n }\n }\n tKey++;\n return cells;\n }\n getClickableAction(position) {\n for (const items of clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence)) {\n if (items.condition(position, this.env)) {\n return items.action;\n }\n }\n return false;\n }\n selectClickableCell(clickableCell) {\n const { position, action } = clickableCell;\n action({ ...position, sheetId: this.env.model.getters.getActiveSheetId() }, this.env);\n }\n onClosePopover() {\n this.env.model.dispatch(\"CLOSE_CELL_POPOVER\");\n }\n onGridResized({ height, width }) {\n this.env.model.dispatch(\"RESIZE_SHEETVIEW\", {\n width: width,\n height: height,\n gridOffsetX: 0,\n gridOffsetY: 0,\n });\n }\n moveCanvas(deltaX, deltaY) {\n const { scrollX, scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n offsetX: scrollX + deltaX,\n offsetY: scrollY + deltaY,\n });\n }\n getGridRect() {\n return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() };\n }\n }\n SpreadsheetDashboard.template = \"o-spreadsheet-SpreadsheetDashboard\";\n SpreadsheetDashboard.components = {\n GridOverlay,\n GridPopover,\n Popover,\n VerticalScrollBar,\n HorizontalScrollBar,\n FilterIconsOverlay,\n };\n SpreadsheetDashboard.props = {};\n\n css /* scss */ `\n .o-sidePanel {\n display: flex;\n flex-direction: column;\n overflow-x: hidden;\n background-color: white;\n border: 1px solid darkgray;\n user-select: none;\n .o-sidePanelHeader {\n padding: 6px;\n height: 30px;\n background-color: ${BACKGROUND_HEADER_COLOR};\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid darkgray;\n border-top: 1px solid darkgray;\n font-weight: bold;\n .o-sidePanelTitle {\n font-weight: bold;\n padding: 5px 10px;\n color: dimgrey;\n }\n .o-sidePanelClose {\n font-size: 1.5rem;\n padding: 5px 10px;\n cursor: pointer;\n &:hover {\n background-color: WhiteSmoke;\n }\n }\n }\n .o-sidePanelBody {\n overflow: auto;\n width: 100%;\n height: 100%;\n\n .o-section {\n padding: 16px;\n\n .o-section-title {\n font-weight: bold;\n color: dimgrey;\n margin-bottom: 5px;\n }\n\n .o-section-subtitle {\n color: dimgrey;\n font-weight: 500;\n font-size: 12px;\n line-height: 14px;\n margin: 8px 0 4px 0;\n }\n\n .o-subsection-left {\n display: inline-block;\n width: 47%;\n margin-right: 3%;\n }\n\n .o-subsection-right {\n display: inline-block;\n width: 47%;\n margin-left: 3%;\n }\n }\n }\n\n .o-sidepanel-error {\n color: red;\n margin-top: 10px;\n }\n\n .o-sidePanelButtons {\n padding: 16px;\n text-align: right;\n }\n\n .o-sidePanelButton {\n border: 1px solid lightgrey;\n padding: 0px 20px 0px 20px;\n border-radius: 4px;\n font-weight: 500;\n font-size: 14px;\n height: 30px;\n line-height: 16px;\n background: white;\n margin-right: 8px;\n &:hover:enabled {\n background-color: rgba(0, 0, 0, 0.08);\n }\n }\n .o-sidePanelButton:enabled {\n cursor: pointer;\n }\n .o-sidePanelButton:last-child {\n margin-right: 0px;\n }\n\n .o-input {\n color: #666666;\n border-radius: 4px;\n min-width: 0px;\n padding: 4px 6px;\n box-sizing: border-box;\n line-height: 1;\n width: 100%;\n height: 28px;\n .o-type-selector {\n background-position: right 5px top 11px;\n }\n }\n input.o-required,\n select.o-required {\n border-color: #4c4c4c;\n }\n input.o-optional,\n select.o-optional {\n border: 1px solid #a9a9a9;\n }\n input.o-invalid {\n border-color: red;\n }\n select.o-input {\n background-color: white;\n text-align: left;\n }\n\n .o-inflection {\n .o-inflection-icon-button {\n display: inline-block;\n border: 1px solid #dadce0;\n border-radius: 4px;\n cursor: pointer;\n padding: 1px 2px;\n }\n .o-inflection-icon-button:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n table {\n table-layout: fixed;\n margin-top: 2%;\n display: table;\n text-align: left;\n font-size: 12px;\n line-height: 18px;\n width: 100%;\n }\n th.o-inflection-iconset-icons {\n width: 8%;\n }\n th.o-inflection-iconset-text {\n width: 28%;\n }\n th.o-inflection-iconset-operator {\n width: 14%;\n }\n th.o-inflection-iconset-type {\n width: 28%;\n }\n th.o-inflection-iconset-value {\n width: 26%;\n }\n input,\n select {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n }\n }\n\n .o-dropdown {\n position: relative;\n .o-dropdown-content {\n position: absolute;\n top: calc(100% + 5px);\n left: 0;\n z-index: ${ComponentsImportance.Dropdown};\n box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);\n background-color: #f6f6f6;\n\n .o-dropdown-item {\n padding: 7px 10px;\n }\n .o-dropdown-item:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n .o-dropdown-line {\n display: flex;\n padding: 3px 6px;\n .o-line-item {\n width: 16px;\n height: 16px;\n margin: 1px 3px;\n &:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n }\n }\n }\n }\n\n .o-tools {\n color: #333;\n font-size: 13px;\n cursor: default;\n display: flex;\n\n .o-tool {\n display: flex;\n align-items: center;\n margin: 2px;\n padding: 0 3px;\n border-radius: 2px;\n }\n\n .o-tool.active,\n .o-tool:not(.o-disabled):hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n\n .o-with-color > span {\n border-bottom: 4px solid;\n height: 16px;\n margin-top: 2px;\n }\n .o-with-color {\n .o-line-item:hover {\n outline: 1px solid gray;\n }\n }\n .o-border {\n .o-line-item {\n padding: 4px;\n margin: 1px;\n }\n }\n }\n }\n`;\n class SidePanel extends owl.Component {\n setup() {\n this.state = owl.useState({\n panel: sidePanelRegistry.get(this.props.component),\n });\n owl.onWillUpdateProps((nextProps) => (this.state.panel = sidePanelRegistry.get(nextProps.component)));\n }\n getTitle() {\n return typeof this.state.panel.title === \"function\"\n ? this.state.panel.title(this.env)\n : this.state.panel.title;\n }\n }\n SidePanel.template = \"o-spreadsheet-SidePanel\";\n SidePanel.props = {\n component: String,\n panelProps: { type: Object, optional: true },\n onCloseSidePanel: Function,\n };\n\n const AddMergeInteractiveContent = {\n MergeIsDestructive: _lt(\"Merging these cells will only preserve the top-leftmost value. Merge anyway?\"),\n MergeInFilter: _lt(\"You can't merge cells inside of an existing filter.\"),\n };\n function interactiveAddMerge(env, sheetId, target) {\n const result = env.model.dispatch(\"ADD_MERGE\", { sheetId, target });\n if (result.isCancelledBecause(80 /* CommandResult.MergeInFilter */)) {\n env.raiseError(AddMergeInteractiveContent.MergeInFilter);\n }\n else if (result.isCancelledBecause(3 /* CommandResult.MergeIsDestructive */)) {\n env.askConfirmation(AddMergeInteractiveContent.MergeIsDestructive, () => {\n env.model.dispatch(\"ADD_MERGE\", { sheetId, target, force: true });\n });\n }\n }\n\n const FORMATS = [\n { name: \"automatic\", text: NumberFormatTerms.Automatic },\n { name: \"number\", text: NumberFormatTerms.Number, description: \"1,000.12\", value: \"#,##0.00\" },\n { name: \"percent\", text: NumberFormatTerms.Percent, description: \"10.12%\", value: \"0.00%\" },\n {\n name: \"currency\",\n text: NumberFormatTerms.Currency,\n description: \"$1,000.12\",\n value: \"[$$]#,##0.00\",\n },\n {\n name: \"currency_rounded\",\n text: NumberFormatTerms.CurrencyRounded,\n description: \"$1,000\",\n value: \"[$$]#,##0\",\n },\n { name: \"date\", text: NumberFormatTerms.Date, description: \"9/26/2008\", value: \"m/d/yyyy\" },\n { name: \"time\", text: NumberFormatTerms.Time, description: \"10:43:00 PM\", value: \"hh:mm:ss a\" },\n {\n name: \"datetime\",\n text: NumberFormatTerms.DateTime,\n description: \"9/26/2008 22:43:00\",\n value: \"m/d/yyyy hh:mm:ss\",\n },\n {\n name: \"duration\",\n text: NumberFormatTerms.Duration,\n description: \"27:51:38\",\n value: \"hhhh:mm:ss\",\n },\n ];\n const CUSTOM_FORMATS = [\n { name: \"custom_currency\", text: NumberFormatTerms.CustomCurrency, sidePanel: \"CustomCurrency\" },\n ];\n // -----------------------------------------------------------------------------\n // TopBar\n // -----------------------------------------------------------------------------\n css /* scss */ `\n .o-spreadsheet-topbar {\n background-color: white;\n line-height: 1.2;\n display: flex;\n flex-direction: column;\n font-size: 13px;\n line-height: 1.2;\n user-select: none;\n\n .o-topbar-top {\n border-bottom: 1px solid #e0e2e4;\n display: flex;\n padding: 2px 10px;\n justify-content: space-between;\n\n /* Menus */\n .o-topbar-topleft {\n display: flex;\n .o-topbar-menu {\n padding: 4px 6px;\n margin: 0 2px;\n cursor: pointer;\n }\n\n .o-topbar-menu:hover,\n .o-topbar-menu-active {\n background-color: #f1f3f4;\n border-radius: 2px;\n }\n }\n\n .o-topbar-topright {\n display: flex;\n justify-content: flex-end;\n }\n }\n /* Toolbar + Cell Content */\n .o-topbar-toolbar {\n border-bottom: 1px solid #e0e2e4;\n display: flex;\n\n .o-readonly-toolbar {\n display: flex;\n align-items: center;\n background-color: ${BACKGROUND_HEADER_COLOR};\n padding-left: 18px;\n padding-right: 18px;\n }\n .o-composer-container {\n height: 34px;\n border: 1px solid #e0e2e4;\n margin-top: -1px;\n margin-bottom: -1px;\n }\n\n /* Toolbar */\n .o-toolbar-tools {\n display: flex;\n flex-shrink: 0;\n margin-left: 16px;\n color: #333;\n cursor: default;\n\n .o-tool {\n display: flex;\n align-items: center;\n margin: 2px;\n padding: 0px 3px;\n border-radius: 2px;\n cursor: pointer;\n min-width: fit-content;\n }\n\n .o-tool-outlined {\n background-color: rgba(0, 0, 0, 0.08);\n }\n\n .o-filter-tool {\n margin-right: 8px;\n }\n\n .o-tool.active,\n .o-tool:not(.o-disabled):hover {\n background-color: #f1f3f4;\n }\n\n .o-with-color > span {\n border-bottom: 4px solid;\n height: 16px;\n margin-top: 2px;\n }\n\n .o-with-color {\n .o-line-item:hover {\n outline: 1px solid gray;\n }\n }\n\n .o-border-dropdown {\n padding: 4px;\n }\n\n .o-divider {\n display: inline-block;\n border-right: 1px solid #e0e2e4;\n width: 0;\n margin: 0 6px;\n }\n\n .o-disabled {\n opacity: 0.6;\n cursor: default;\n }\n\n .o-dropdown {\n position: relative;\n display: flex;\n align-items: center;\n\n .o-dropdown-button {\n height: 30px;\n }\n\n .o-text-icon {\n height: 100%;\n line-height: 30px;\n }\n\n .o-text-options > div {\n line-height: 26px;\n padding: 3px 12px;\n &:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n }\n\n .o-dropdown-content {\n position: absolute;\n top: 100%;\n left: 0;\n overflow-y: auto;\n overflow-x: hidden;\n z-index: ${ComponentsImportance.Dropdown};\n box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);\n background-color: white;\n\n .o-dropdown-item {\n cursor: pointer;\n }\n\n .o-dropdown-item:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n\n .o-dropdown-line {\n display: flex;\n margin: 1px;\n\n .o-line-item {\n padding: 4px;\n width: 18px;\n height: 18px;\n cursor: pointer;\n\n &:hover {\n background-color: rgba(0, 0, 0, 0.08);\n }\n }\n }\n\n &.o-format-tool {\n padding: 5px 0;\n width: 250px;\n font-size: 12px;\n > div {\n padding: 0 20px;\n white-space: nowrap;\n\n &.active:before {\n content: \"\u2713\";\n font-weight: bold;\n position: absolute;\n left: 5px;\n }\n }\n }\n\n .o-dropdown-align-item {\n padding: 7px 10px;\n }\n }\n }\n }\n\n /* Cell Content */\n .o-toolbar-cell-content {\n font-size: 12px;\n font-weight: 500;\n padding: 0 12px;\n margin: 0;\n line-height: 34px;\n white-space: nowrap;\n user-select: text;\n }\n }\n }\n`;\n class TopBar extends owl.Component {\n constructor() {\n super(...arguments);\n this.DEFAULT_FONT_SIZE = DEFAULT_FONT_SIZE;\n this.commonFormats = FORMATS;\n this.customFormats = CUSTOM_FORMATS;\n this.currentFormatName = \"automatic\";\n this.fontSizes = fontSizes;\n this.style = {};\n this.state = owl.useState({\n menuState: { isOpen: false, position: null, menuItems: [] },\n activeTool: \"\",\n });\n this.isSelectingMenu = false;\n this.openedEl = null;\n this.inMerge = false;\n this.cannotMerge = false;\n this.undoTool = false;\n this.redoTool = false;\n this.paintFormatTool = false;\n this.fillColor = \"#ffffff\";\n this.textColor = \"#000000\";\n this.menus = [];\n this.composerStyle = `\n line-height: 34px;\n padding-left: 8px;\n height: 34px;\n background-color: white;\n `;\n }\n get dropdownStyle() {\n return `max-height:${this.props.dropdownMaxHeight}px`;\n }\n setup() {\n owl.useExternalListener(window, \"click\", this.onExternalClick);\n owl.onWillStart(() => this.updateCellState());\n owl.onWillUpdateProps(() => this.updateCellState());\n }\n get topbarComponents() {\n return topbarComponentRegistry\n .getAll()\n .filter((item) => !item.isVisible || item.isVisible(this.env));\n }\n onExternalClick(ev) {\n // TODO : manage click events better. We need this piece of code\n // otherwise the event opening the menu would close it on the same frame.\n // And we cannot stop the event propagation because it's used in an\n // external listener of the Menu component to close the context menu when\n // clicking on the top bar\n if (this.openedEl === ev.target) {\n return;\n }\n this.closeMenus();\n }\n onClick() {\n this.props.onClick();\n this.closeMenus();\n }\n toggleStyle(style) {\n setStyle(this.env, { [style]: !this.style[style] });\n }\n toggleFormat(formatName) {\n const formatter = FORMATS.find((f) => f.name === formatName);\n const value = (formatter && formatter.value) || \"\";\n setFormatter(this.env, value);\n }\n toggleAlign(align) {\n setStyle(this.env, { [\"align\"]: align });\n this.onClick();\n }\n onMenuMouseOver(menu, ev) {\n if (this.isSelectingMenu) {\n this.openMenu(menu, ev);\n }\n }\n toggleDropdownTool(tool, ev) {\n const isOpen = this.state.activeTool === tool;\n this.closeMenus();\n this.state.activeTool = isOpen ? \"\" : tool;\n this.openedEl = isOpen ? null : ev.target;\n }\n toggleContextMenu(menu, ev) {\n if (this.state.menuState.isOpen) {\n this.closeMenus();\n }\n else {\n this.openMenu(menu, ev);\n }\n }\n openMenu(menu, ev) {\n const { left, top, height } = ev.target.getBoundingClientRect();\n this.state.menuState.isOpen = true;\n this.state.menuState.position = { x: left, y: top + height };\n this.state.menuState.menuItems = getMenuChildren(menu, this.env).filter((item) => !item.isVisible || item.isVisible(this.env));\n this.state.menuState.parentMenu = menu;\n this.isSelectingMenu = true;\n this.openedEl = ev.target;\n this.env.model.dispatch(\"STOP_EDITION\");\n }\n closeMenus() {\n this.state.activeTool = \"\";\n this.state.menuState.isOpen = false;\n this.state.menuState.parentMenu = undefined;\n this.isSelectingMenu = false;\n this.openedEl = null;\n }\n updateCellState() {\n const zones = this.env.model.getters.getSelectedZones();\n const { col, row, sheetId } = this.env.model.getters.getActivePosition();\n this.inMerge = false;\n const { top, left, right, bottom } = this.env.model.getters.getSelectedZone();\n const { xSplit, ySplit } = this.env.model.getters.getPaneDivisions(sheetId);\n this.cannotMerge =\n zones.length > 1 ||\n (top === bottom && left === right) ||\n (left < xSplit && xSplit <= right) ||\n (top < ySplit && ySplit <= bottom);\n if (!this.cannotMerge) {\n const zone = this.env.model.getters.expandZone(sheetId, positionToZone({ col, row }));\n this.inMerge = isEqual(zones[0], zone);\n }\n this.undoTool = this.env.model.getters.canUndo();\n this.redoTool = this.env.model.getters.canRedo();\n this.paintFormatTool = this.env.model.getters.isPaintingFormat();\n const cell = this.env.model.getters.getActiveCell();\n if (cell.format) {\n const currentFormat = this.commonFormats.find((f) => f.value === cell.format);\n this.currentFormatName = currentFormat ? currentFormat.name : \"\";\n }\n else {\n this.currentFormatName = \"automatic\";\n }\n this.style = { ...this.env.model.getters.getCurrentStyle() };\n this.style.align = this.style.align || cell.defaultAlign;\n this.fillColor = this.style.fillColor || \"#ffffff\";\n this.textColor = this.style.textColor || \"#000000\";\n this.menus = topbarMenuRegistry\n .getAll()\n .filter((item) => !item.isVisible || item.isVisible(this.env));\n }\n getMenuName(menu) {\n return getMenuName(menu, this.env);\n }\n toggleMerge() {\n if (this.cannotMerge) {\n return;\n }\n const zones = this.env.model.getters.getSelectedZones();\n const target = [zones[zones.length - 1]];\n const sheetId = this.env.model.getters.getActiveSheetId();\n if (this.inMerge) {\n this.env.model.dispatch(\"REMOVE_MERGE\", { sheetId, target });\n }\n else {\n interactiveAddMerge(this.env, sheetId, target);\n }\n }\n setColor(target, color) {\n setStyle(this.env, { [target]: color });\n this.onClick();\n }\n setBorder(command) {\n this.env.model.dispatch(\"SET_FORMATTING\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n border: command,\n });\n this.onClick();\n }\n setFormat(format, custom) {\n if (!custom) {\n this.toggleFormat(format);\n }\n else {\n this.openCustomFormatSidePanel(format);\n }\n this.onClick();\n }\n openCustomFormatSidePanel(custom) {\n const customFormatter = CUSTOM_FORMATS.find((c) => c.name === custom);\n const sidePanel = (customFormatter && customFormatter.sidePanel) || \"\";\n this.env.openSidePanel(sidePanel);\n }\n setDecimal(step) {\n this.env.model.dispatch(\"SET_DECIMAL\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n step: step,\n });\n }\n paintFormat() {\n this.env.model.dispatch(\"ACTIVATE_PAINT_FORMAT\", {\n target: this.env.model.getters.getSelectedZones(),\n });\n }\n clearFormatting() {\n this.env.model.dispatch(\"CLEAR_FORMATTING\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n });\n }\n setSize(fontSizeStr) {\n const fontSize = parseFloat(fontSizeStr);\n setStyle(this.env, { fontSize });\n this.onClick();\n }\n doAction(action) {\n action(this.env);\n this.closeMenus();\n }\n undo() {\n this.env.model.dispatch(\"REQUEST_UNDO\");\n }\n redo() {\n this.env.model.dispatch(\"REQUEST_REDO\");\n }\n get selectionContainsFilter() {\n const sheetId = this.env.model.getters.getActiveSheetId();\n const selectedZones = this.env.model.getters.getSelectedZones();\n return this.env.model.getters.doesZonesContainFilter(sheetId, selectedZones);\n }\n get cannotCreateFilter() {\n return !areZonesContinuous(...this.env.model.getters.getSelectedZones());\n }\n createFilter() {\n if (this.cannotCreateFilter) {\n return;\n }\n const sheetId = this.env.model.getters.getActiveSheetId();\n const selection = this.env.model.getters.getSelectedZones();\n interactiveAddFilter(this.env, sheetId, selection);\n }\n removeFilter() {\n this.env.model.dispatch(\"REMOVE_FILTER_TABLE\", {\n sheetId: this.env.model.getters.getActiveSheetId(),\n target: this.env.model.getters.getSelectedZones(),\n });\n }\n }\n TopBar.template = \"o-spreadsheet-TopBar\";\n TopBar.components = { ColorPicker, Menu, Composer };\n TopBar.props = {\n onClick: Function,\n focusComposer: String,\n onComposerContentFocused: Function,\n dropdownMaxHeight: Number,\n };\n\n function instantiateClipboard() {\n return new WebClipboardWrapper(navigator.clipboard);\n }\n class WebClipboardWrapper {\n // Can be undefined because navigator.clipboard doesn't exist in old browsers\n constructor(clipboard) {\n this.clipboard = clipboard;\n }\n async write(clipboardContent) {\n var _a;\n try {\n (_a = this.clipboard) === null || _a === void 0 ? void 0 : _a.write(this.getClipboardItems(clipboardContent));\n }\n catch (e) { }\n }\n async writeText(text) {\n var _a;\n try {\n (_a = this.clipboard) === null || _a === void 0 ? void 0 : _a.writeText(text);\n }\n catch (e) { }\n }\n async readText() {\n let permissionResult = undefined;\n try {\n //@ts-ignore - clipboard-read is not implemented in all browsers\n permissionResult = await navigator.permissions.query({ name: \"clipboard-read\" });\n }\n catch (e) { }\n try {\n const clipboardContent = await this.clipboard.readText();\n return { status: \"ok\", content: clipboardContent };\n }\n catch (e) {\n const status = (permissionResult === null || permissionResult === void 0 ? void 0 : permissionResult.state) === \"denied\" ? \"permissionDenied\" : \"notImplemented\";\n return { status };\n }\n }\n getClipboardItems(content) {\n return [\n new ClipboardItem({\n [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),\n [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),\n }),\n ];\n }\n getBlob(clipboardContent, type) {\n return new Blob([clipboardContent[type] || \"\"], {\n type,\n });\n }\n }\n\n css /* scss */ `\n .o-spreadsheet {\n position: relative;\n display: grid;\n grid-template-columns: auto 350px;\n color: #333;\n input {\n background-color: white;\n }\n .text-muted {\n color: grey !important;\n }\n button {\n color: #333;\n }\n\n * {\n font-family: \"Roboto\", \"RobotoDraft\", Helvetica, Arial, sans-serif;\n }\n &,\n *,\n *:before,\n *:after {\n box-sizing: content-box;\n }\n .o-separator {\n border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid #e0e2e4;\n margin-top: ${MENU_SEPARATOR_PADDING}px;\n margin-bottom: ${MENU_SEPARATOR_PADDING}px;\n }\n }\n\n .o-two-columns {\n grid-column: 1 / 3;\n }\n\n .o-icon {\n width: ${ICON_EDGE_LENGTH}px;\n height: ${ICON_EDGE_LENGTH}px;\n opacity: 0.6;\n vertical-align: middle;\n }\n\n .o-cf-icon {\n width: ${CF_ICON_EDGE_LENGTH}px;\n height: ${CF_ICON_EDGE_LENGTH}px;\n vertical-align: sub;\n }\n`;\n // -----------------------------------------------------------------------------\n // GRID STYLE\n // -----------------------------------------------------------------------------\n css /* scss */ `\n .o-grid {\n position: relative;\n overflow: hidden;\n background-color: ${BACKGROUND_GRAY_COLOR};\n &:focus {\n outline: none;\n }\n\n > canvas {\n border-top: 1px solid #e2e3e3;\n border-bottom: 1px solid #e2e3e3;\n }\n .o-scrollbar {\n &.corner {\n right: 0px;\n bottom: 0px;\n height: ${SCROLLBAR_WIDTH$1}px;\n width: ${SCROLLBAR_WIDTH$1}px;\n border-top: 1px solid #e2e3e3;\n border-left: 1px solid #e2e3e3;\n }\n }\n\n .o-grid-overlay {\n position: absolute;\n outline: none;\n }\n }\n`;\n const t = (s) => s;\n class Spreadsheet extends owl.Component {\n constructor() {\n super(...arguments);\n this.isViewportTooSmall = false;\n }\n get model() {\n return this.props.model;\n }\n getStyle() {\n if (this.env.isDashboard()) {\n return `grid-template-rows: auto;`;\n }\n return `grid-template-rows: ${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;\n }\n setup() {\n this.sidePanel = owl.useState({ isOpen: false, panelProps: {} });\n this.composer = owl.useState({\n topBarFocus: \"inactive\",\n gridFocusMode: \"inactive\",\n });\n this.keyDownMapping = {\n \"CTRL+H\": () => this.toggleSidePanel(\"FindAndReplace\", {}),\n \"CTRL+F\": () => this.toggleSidePanel(\"FindAndReplace\", {}),\n };\n const fileStore = this.model.config.external.fileStore;\n owl.useSubEnv({\n model: this.model,\n imageProvider: fileStore ? new ImageProvider(fileStore) : undefined,\n loadCurrencies: this.model.config.external.loadCurrencies,\n isDashboard: () => this.model.getters.isDashboard(),\n openSidePanel: this.openSidePanel.bind(this),\n toggleSidePanel: this.toggleSidePanel.bind(this),\n _t: Spreadsheet._t,\n clipboard: this.env.clipboard || instantiateClipboard(),\n });\n owl.useExternalListener(window, \"resize\", () => this.render(true));\n owl.useExternalListener(window, \"beforeunload\", this.unbindModelEvents.bind(this));\n this.bindModelEvents();\n owl.onMounted(() => {\n this.checkViewportSize();\n });\n owl.onWillUnmount(() => this.unbindModelEvents());\n owl.onPatched(() => {\n this.checkViewportSize();\n });\n }\n get focusTopBarComposer() {\n return this.model.getters.getEditionMode() === \"inactive\"\n ? \"inactive\"\n : this.composer.topBarFocus;\n }\n get focusGridComposer() {\n return this.model.getters.getEditionMode() === \"inactive\"\n ? \"inactive\"\n : this.composer.gridFocusMode;\n }\n bindModelEvents() {\n this.model.on(\"update\", this, () => this.render(true));\n this.model.on(\"notify-ui\", this, this.onNotifyUI);\n }\n unbindModelEvents() {\n this.model.off(\"update\", this);\n this.model.off(\"notify-ui\", this);\n }\n checkViewportSize() {\n const { xRatio, yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());\n if (yRatio > MAXIMAL_FREEZABLE_RATIO || xRatio > MAXIMAL_FREEZABLE_RATIO) {\n if (this.isViewportTooSmall) {\n return;\n }\n this.env.notifyUser({\n text: _lt(\"The current window is too small to display this sheet properly. Consider resizing your browser window or adjusting frozen rows and columns.\"),\n tag: \"viewportTooSmall\",\n });\n this.isViewportTooSmall = true;\n }\n else {\n this.isViewportTooSmall = false;\n }\n }\n onNotifyUI(payload) {\n switch (payload.type) {\n case \"ERROR\":\n this.env.raiseError(payload.text);\n break;\n }\n }\n openSidePanel(panel, panelProps) {\n this.sidePanel.component = panel;\n this.sidePanel.panelProps = panelProps;\n this.sidePanel.isOpen = true;\n }\n closeSidePanel() {\n this.sidePanel.isOpen = false;\n this.focusGrid();\n }\n toggleSidePanel(panel, panelProps) {\n if (this.sidePanel.isOpen && panel === this.sidePanel.component) {\n this.sidePanel.isOpen = false;\n this.focusGrid();\n }\n else {\n this.openSidePanel(panel, panelProps);\n }\n }\n focusGrid() {\n if (!this._focusGrid) {\n throw new Error(\"_focusGrid should be exposed by the grid component\");\n }\n this._focusGrid();\n }\n onKeydown(ev) {\n let keyDownString = \"\";\n if (ev.ctrlKey || ev.metaKey) {\n keyDownString += \"CTRL+\";\n }\n keyDownString += ev.key.toUpperCase();\n let handler = this.keyDownMapping[keyDownString];\n if (handler) {\n ev.preventDefault();\n ev.stopPropagation();\n handler();\n return;\n }\n }\n onTopBarComposerFocused(selection) {\n if (this.model.getters.isReadonly()) {\n return;\n }\n this.model.dispatch(\"UNFOCUS_SELECTION_INPUT\");\n this.composer.topBarFocus = \"contentFocus\";\n this.composer.gridFocusMode = \"inactive\";\n this.setComposerContent({ selection } || {});\n }\n onGridComposerContentFocused() {\n if (this.model.getters.isReadonly()) {\n return;\n }\n this.model.dispatch(\"UNFOCUS_SELECTION_INPUT\");\n this.composer.topBarFocus = \"inactive\";\n this.composer.gridFocusMode = \"contentFocus\";\n this.setComposerContent({});\n }\n onGridComposerCellFocused(content, selection) {\n if (this.model.getters.isReadonly()) {\n return;\n }\n this.model.dispatch(\"UNFOCUS_SELECTION_INPUT\");\n this.composer.topBarFocus = \"inactive\";\n this.composer.gridFocusMode = \"cellFocus\";\n this.setComposerContent({ content, selection } || {});\n }\n /**\n * Start the edition or update the content if it's already started.\n */\n setComposerContent({ content, selection, }) {\n if (this.model.getters.getEditionMode() === \"inactive\") {\n this.model.dispatch(\"START_EDITION\", { text: content, selection });\n }\n else if (content) {\n this.model.dispatch(\"SET_CURRENT_CONTENT\", { content, selection });\n }\n }\n get gridHeight() {\n const { height } = this.env.model.getters.getSheetViewDimension();\n return height;\n }\n }\n Spreadsheet.template = \"o-spreadsheet-Spreadsheet\";\n Spreadsheet.components = { TopBar, Grid, BottomBar, SidePanel, SpreadsheetDashboard };\n Spreadsheet._t = t;\n Spreadsheet.props = {\n model: Object,\n };\n\n class LocalTransportService {\n constructor() {\n this.listeners = [];\n }\n sendMessage(message) {\n for (const { callback } of this.listeners) {\n callback(message);\n }\n }\n onNewMessage(id, callback) {\n this.listeners.push({ id, callback });\n }\n leave(id) {\n this.listeners = this.listeners.filter((listener) => listener.id !== id);\n }\n }\n\n function inverseCommand(cmd) {\n return inverseCommandRegistry.get(cmd.type)(cmd);\n }\n\n /**\n * Create an empty structure according to the type of the node key:\n * string: object\n * number: array\n */\n function createEmptyStructure(node) {\n if (typeof node === \"string\") {\n return {};\n }\n else if (typeof node === \"number\") {\n return [];\n }\n throw new Error(`Cannot create new node`);\n }\n\n /**\n * A branch holds a sequence of operations.\n * It can be represented as \"A - B - C - D\" if A, B, C and D are executed one\n * after the other.\n *\n * @param buildTransformation Factory to build transformations\n * @param operations initial operations\n */\n class Branch {\n constructor(buildTransformation, operations = []) {\n this.buildTransformation = buildTransformation;\n this.operations = operations;\n }\n getOperations() {\n return this.operations;\n }\n getOperation(operationId) {\n const operation = this.operations.find((op) => op.id === operationId);\n if (!operation) {\n throw new Error(`Operation ${operationId} not found`);\n }\n return operation;\n }\n getLastOperationId() {\n var _a;\n return (_a = this.operations[this.operations.length - 1]) === null || _a === void 0 ? void 0 : _a.id;\n }\n /**\n * Get the id of the operation appears first in the list of operations\n */\n getFirstOperationAmong(op1, op2) {\n for (const operation of this.operations) {\n if (operation.id === op1)\n return op1;\n if (operation.id === op2)\n return op2;\n }\n throw new Error(`Operation ${op1} and ${op2} not found`);\n }\n contains(operationId) {\n return !!this.operations.find((operation) => operation.id === operationId);\n }\n /**\n * Add the given operation as the first operation\n */\n prepend(operation) {\n const transformation = this.buildTransformation.with(operation.data);\n this.operations = [\n operation,\n ...this.operations.map((operation) => operation.transformed(transformation)),\n ];\n }\n /**\n * add the given operation after the given predecessorOpId\n */\n insert(newOperation, predecessorOpId) {\n const transformation = this.buildTransformation.with(newOperation.data);\n const { before, operation, after } = this.locateOperation(predecessorOpId);\n this.operations = [\n ...before,\n operation,\n newOperation,\n ...after.map((operation) => operation.transformed(transformation)),\n ];\n }\n /**\n * Add the given operation as the last operation\n */\n append(operation) {\n this.operations.push(operation);\n }\n /**\n * Append operations in the given branch to this branch.\n */\n appendBranch(branch) {\n this.operations = this.operations.concat(branch.operations);\n }\n /**\n * Create and return a copy of this branch, starting after the given operationId\n */\n fork(operationId) {\n const { after } = this.locateOperation(operationId);\n return new Branch(this.buildTransformation, after);\n }\n /**\n * Transform all the operations in this branch with the given transformation\n */\n transform(transformation) {\n this.operations = this.operations.map((operation) => operation.transformed(transformation));\n }\n /**\n * Cut the branch before the operation, meaning the operation\n * and all following operations are dropped.\n */\n cutBefore(operationId) {\n this.operations = this.locateOperation(operationId).before;\n }\n /**\n * Cut the branch after the operation, meaning all following operations are dropped.\n */\n cutAfter(operationId) {\n const { before, operation } = this.locateOperation(operationId);\n this.operations = before.concat([operation]);\n }\n /**\n * Find an operation in this branch based on its id.\n * This returns the operation itself, operations which comes before it\n * and operation which comes after it.\n */\n locateOperation(operationId) {\n const operationIndex = this.operations.findIndex((step) => step.id === operationId);\n if (operationIndex === -1) {\n throw new Error(`Operation ${operationId} not found`);\n }\n return {\n before: this.operations.slice(0, operationIndex),\n operation: this.operations[operationIndex],\n after: this.operations.slice(operationIndex + 1),\n };\n }\n }\n\n /**\n * An Operation can be executed to change a data structure from state A\n * to state B.\n * It should hold the necessary data used to perform this transition.\n * It should be possible to revert the changes made by this operation.\n *\n * In the context of o-spreadsheet, the data from an operation would\n * be a revision (the commands are used to execute it, the `changes` are used\n * to revert it).\n */\n class Operation {\n constructor(id, data) {\n this.id = id;\n this.data = data;\n }\n transformed(transformation) {\n return new LazyOperation(this.id, lazy(() => transformation(this.data)));\n }\n }\n class LazyOperation {\n constructor(id, lazyData) {\n this.id = id;\n this.lazyData = lazyData;\n }\n get data() {\n return this.lazyData();\n }\n transformed(transformation) {\n return new LazyOperation(this.id, this.lazyData.map(transformation));\n }\n }\n\n /**\n * An execution object is a sequence of executionSteps (each execution step is an operation in a branch).\n *\n * You can iterate over the steps of an execution\n * ```js\n * for (const operation of execution) {\n * // ... do something\n * }\n * ```\n */\n class OperationSequence {\n constructor(operations) {\n this.operations = operations;\n }\n [Symbol.iterator]() {\n return this.operations[Symbol.iterator]();\n }\n /**\n * Stop the operation sequence at a given operation\n * @param operationId included\n */\n stopWith(operationId) {\n function* filter(execution, operationId) {\n for (const step of execution) {\n yield step;\n if (step.operation.id === operationId) {\n return;\n }\n }\n }\n return new OperationSequence(filter(this.operations, operationId));\n }\n /**\n * Stop the operation sequence before a given operation\n * @param operationId excluded\n */\n stopBefore(operationId) {\n function* filter(execution, operationId) {\n for (const step of execution) {\n if (step.operation.id === operationId) {\n return;\n }\n yield step;\n }\n }\n return new OperationSequence(filter(this.operations, operationId));\n }\n /**\n * Start the operation sequence at a given operation\n * @param operationId excluded\n */\n startAfter(operationId) {\n function* filter(execution, operationId) {\n let skip = true;\n for (const step of execution) {\n if (!skip) {\n yield step;\n }\n if (step.operation.id === operationId) {\n skip = false;\n }\n }\n }\n return new OperationSequence(filter(this.operations, operationId));\n }\n }\n\n /**\n * The tree is a data structure used to maintain the different branches of the\n * SelectiveHistory.\n *\n * Branches can be \"stacked\" on each other and an execution path can be derived\n * from any stack of branches. The rules to derive this path is explained below.\n *\n * An operation can be cancelled/undone by inserting a new branch below\n * this operation.\n * e.g\n * Given the branch A B C\n * To undo B, a new branching branch is inserted at operation B.\n * ```txt\n * A B C D\n * > C' D'\n * ```\n * A new execution path can now be derived. At each operation:\n * - if there is a lower branch, don't execute it and go to the operation below\n * - if not, execute it and go to the operation on the right.\n * The execution path is A C' D'\n * Operation C and D have been adapted (transformed) in the lower branch\n * since operation B is not executed in this branch.\n *\n */\n class Tree {\n constructor(buildTransformation, initialBranch) {\n this.buildTransformation = buildTransformation;\n this.branchingOperationIds = new Map();\n this.branches = [initialBranch];\n }\n /**\n * Return the last branch of the entire stack of branches.\n */\n getLastBranch() {\n return this.branches[this.branches.length - 1];\n }\n /**\n * Return the sequence of operations from this branch\n * until the very last branch.\n */\n execution(branch) {\n return new OperationSequence(linkNext(this._execution(branch), this._execution(branch)));\n }\n /**\n * Return the sequence of operations from this branch\n * to the very first branch.\n */\n revertedExecution(branch) {\n return new OperationSequence(linkNext(this._revertedExecution(branch), this._revertedExecution(branch)));\n }\n /**\n * Append an operation to the end of the tree.\n * Also insert the (transformed) operation in all previous branches.\n *\n * Adding operation `D` to the last branch\n * ```txt\n * A1 B1 C1\n * > B2 C2\n * ```\n * will give\n * ```txt\n * A1 B1 C1 D' with D' = D transformed with A1\n * > B2 C2 D\n * ```\n */\n insertOperationLast(branch, operation) {\n var _a;\n const insertAfter = branch.getLastOperationId() || ((_a = this.previousBranch(branch)) === null || _a === void 0 ? void 0 : _a.getLastOperationId());\n branch.append(operation);\n if (insertAfter) {\n this.insertPrevious(branch, operation, insertAfter);\n }\n }\n /**\n * Insert a new operation after an other operation.\n * The operation will be inserted in this branch, in next branches (transformed)\n * and in previous branches (also transformed).\n *\n * Given\n * ```txt\n * 1: A1 B1 C1\n * 2: > B2 C2\n * 3: > C3\n * ```\n * Inserting D to branch 2 gives\n * ```txt\n * 1: A1 B1 C1 D1 D1 = D transformed with A1\n * 2: > B2 C2 D with D = D\n * 3: > C3 D2 D2 = D transformed without B2 (B2\u207b\u00b9)\n * ```\n */\n insertOperationAfter(branch, operation, predecessorOpId) {\n branch.insert(operation, predecessorOpId);\n this.updateNextWith(branch, operation, predecessorOpId);\n this.insertPrevious(branch, operation, predecessorOpId);\n }\n /**\n * Create a new branching branch at the given operation.\n * This cancels the operation from the execution path.\n */\n undo(branch, operation) {\n const transformation = this.buildTransformation.without(operation.data);\n const branchingId = this.branchingOperationIds.get(branch);\n this.branchingOperationIds.set(branch, operation.id);\n const nextBranch = branch.fork(operation.id);\n if (branchingId) {\n this.branchingOperationIds.set(nextBranch, branchingId);\n }\n this.insertBranchAfter(branch, nextBranch);\n this.transform(nextBranch, transformation);\n }\n /**\n * Remove the branch just after this one. This un-cancels (redo) the branching\n * operation. Lower branches will be transformed accordingly.\n *\n * Given\n * ```txt\n * 1: A1 B1 C1\n * 2: > B2 C2\n * 3: > C3\n * ```\n * removing the next branch of 1 gives\n *\n * ```txt\n * 1: A1 B1 C1\n * 2: > C3' with C3' = C1 transformed without B1 (B1\u207b\u00b9)\n * ```\n */\n redo(branch) {\n const removedBranch = this.nextBranch(branch);\n if (!removedBranch)\n return;\n const nextBranch = this.nextBranch(removedBranch);\n this.removeBranchFromTree(removedBranch);\n const undoBranchingId = this.branchingOperationIds.get(removedBranch);\n if (undoBranchingId) {\n this.branchingOperationIds.set(branch, undoBranchingId);\n }\n else {\n this.branchingOperationIds.delete(branch);\n }\n if (nextBranch) {\n this.rebaseUp(nextBranch);\n }\n }\n /**\n * Drop the operation and all following operations in every\n * branch\n */\n drop(operationId) {\n for (const branch of this.branches) {\n if (branch.contains(operationId)) {\n branch.cutBefore(operationId);\n }\n }\n }\n /**\n * Find the operation in the execution path.\n */\n findOperation(branch, operationId) {\n for (const operation of this.revertedExecution(branch)) {\n if (operation.operation.id === operationId) {\n return operation;\n }\n }\n throw new Error(`Operation ${operationId} not found`);\n }\n /**\n * Rebuild transformed operations of this branch based on the upper branch.\n *\n * Given the following structure:\n * ```txt\n * 1: A1 B1 C1\n * 2: > B2 C2\n * 3: > C3\n * ```\n * Rebasing branch \"2\" gives\n * ```txt\n * 1: A1 B1 C1\n * 2: > B2' C2' With B2' = B1 transformed without A1 and C2' = C1 transformed without A1\n * 3: > C3' C3' = C2' transformed without B2'\n * ```\n */\n rebaseUp(branch) {\n const { previousBranch, branchingOperation } = this.findPreviousBranchingOperation(branch);\n if (!previousBranch || !branchingOperation)\n return;\n const rebaseTransformation = this.buildTransformation.without(branchingOperation.data);\n const newBranch = previousBranch.fork(branchingOperation.id);\n this.branchingOperationIds.set(newBranch, this.branchingOperationIds.get(branch));\n this.removeBranchFromTree(branch);\n this.insertBranchAfter(previousBranch, newBranch);\n newBranch.transform(rebaseTransformation);\n const nextBranch = this.nextBranch(newBranch);\n if (nextBranch) {\n this.rebaseUp(nextBranch);\n }\n }\n removeBranchFromTree(branch) {\n const index = this.branches.findIndex((l) => l === branch);\n this.branches.splice(index, 1);\n }\n insertBranchAfter(branch, toInsert) {\n const index = this.branches.findIndex((l) => l === branch);\n this.branches.splice(index + 1, 0, toInsert);\n }\n /**\n * Update the branching branch of this branch, either by (1) inserting the new\n * operation in it or (2) by transforming it.\n * (1) If the operation is positioned before the branching branch, the branching\n * branch should be transformed with this operation.\n * (2) If it's positioned after, the operation should be inserted in the\n * branching branch.\n */\n updateNextWith(branch, operation, predecessorOpId) {\n const branchingId = this.branchingOperationIds.get(branch);\n const nextBranch = this.nextBranch(branch);\n if (!branchingId || !nextBranch) {\n return;\n }\n if (branch.getFirstOperationAmong(predecessorOpId, branchingId) === branchingId) {\n const transformedOperation = this.addToNextBranch(branch, nextBranch, branchingId, operation, predecessorOpId);\n this.updateNextWith(nextBranch, transformedOperation, predecessorOpId);\n }\n else {\n const transformation = this.buildTransformation.with(operation.data);\n this.transform(nextBranch, transformation);\n }\n }\n addToNextBranch(branch, nextBranch, branchingId, operation, predecessorOpId) {\n // If the operation is inserted after the branching operation, it should\n // be positioned first.\n let transformedOperation = operation;\n if (predecessorOpId === branchingId) {\n transformedOperation = this.getTransformedOperation(branch, branchingId, operation);\n nextBranch.prepend(transformedOperation);\n }\n else if (nextBranch.contains(predecessorOpId)) {\n transformedOperation = this.getTransformedOperation(branch, branchingId, operation);\n nextBranch.insert(transformedOperation, predecessorOpId);\n }\n else {\n nextBranch.append(operation);\n }\n return transformedOperation;\n }\n getTransformedOperation(branch, branchingId, operation) {\n const branchingOperation = branch.getOperation(branchingId);\n const branchingTransformation = this.buildTransformation.without(branchingOperation.data);\n return operation.transformed(branchingTransformation);\n }\n /**\n * Check if this branch should execute the given operation.\n * i.e. If the operation is not cancelled by a branching branch.\n */\n shouldExecute(branch, operation) {\n return operation.id !== this.branchingOperationIds.get(branch);\n }\n transform(branch, transformation) {\n branch.transform(transformation);\n const nextBranch = this.nextBranch(branch);\n if (nextBranch) {\n this.transform(nextBranch, transformation);\n }\n }\n /**\n * Insert a new operation in previous branches. The operations which are\n * positioned after the inserted operations are transformed with the newly\n * inserted operations. This one is also transformed, with the branching\n * operation.\n */\n insertPrevious(branch, newOperation, insertAfter) {\n const { previousBranch, branchingOperation } = this.findPreviousBranchingOperation(branch);\n if (!previousBranch || !branchingOperation)\n return;\n const transformation = this.buildTransformation.with(branchingOperation.data);\n const branchTail = branch.fork(insertAfter);\n branchTail.transform(transformation);\n previousBranch.cutAfter(insertAfter);\n previousBranch.appendBranch(branchTail);\n const operationToInsert = newOperation.transformed(transformation);\n this.insertPrevious(previousBranch, operationToInsert, insertAfter);\n }\n findPreviousBranchingOperation(branch) {\n const previousBranch = this.previousBranch(branch);\n if (!previousBranch)\n return { previousBranch: undefined, branchingOperation: undefined };\n const previousBranchingId = this.branchingOperationIds.get(previousBranch);\n if (!previousBranchingId)\n return { previousBranch: undefined, branchingOperation: undefined };\n return {\n previousBranch,\n branchingOperation: previousBranch.getOperation(previousBranchingId),\n };\n }\n /**\n * Retrieve the next branch of the given branch\n */\n nextBranch(branch) {\n const index = this.branches.findIndex((l) => l === branch);\n if (index === -1) {\n return undefined;\n }\n return this.branches[index + 1];\n }\n /**\n * Retrieve the previous branch of the given branch\n */\n previousBranch(branch) {\n const index = this.branches.findIndex((l) => l === branch);\n if (index === -1) {\n return undefined;\n }\n return this.branches[index - 1];\n }\n /**\n * Yields the sequence of operations to execute, in reverse order.\n */\n *_revertedExecution(branch) {\n const branchingOperationId = this.branchingOperationIds.get(branch);\n let afterBranchingPoint = !!branchingOperationId;\n const operations = branch.getOperations();\n for (let i = operations.length - 1; i >= 0; i--) {\n const operation = operations[i];\n if (operation.id === branchingOperationId) {\n afterBranchingPoint = false;\n }\n if (!afterBranchingPoint) {\n yield {\n operation: operation,\n branch: branch,\n isCancelled: !this.shouldExecute(branch, operation),\n };\n }\n }\n const previous = this.previousBranch(branch);\n yield* previous ? this._revertedExecution(previous) : [];\n }\n /**\n * Yields the sequence of operations to execute\n */\n *_execution(branch) {\n for (const operation of branch.getOperations()) {\n yield {\n operation: operation,\n branch: branch,\n isCancelled: !this.shouldExecute(branch, operation),\n };\n if (operation.id === this.branchingOperationIds.get(branch)) {\n const next = this.nextBranch(branch);\n yield* next ? this._execution(next) : [];\n return;\n }\n }\n if (!this.branchingOperationIds.get(branch)) {\n const next = this.nextBranch(branch);\n yield* next ? this._execution(next) : [];\n }\n }\n }\n\n class SelectiveHistory {\n /**\n * The selective history is a data structure used to register changes/updates of a state.\n * Each change/update is called an \"operation\".\n * The data structure allows to easily cancel (and redo) any operation individually.\n * An operation can be represented by any data structure. It can be a \"command\", a \"diff\", etc.\n * However it must have the following properties:\n * - it can be applied to modify the state\n * - it can be reverted on the state such that it was never executed.\n * - it can be transformed given other operation (Operational Transformation)\n *\n * Since this data structure doesn't know anything about the state nor the structure of\n * operations, the actual work must be performed by external functions given as parameters.\n * @param initialOperationId\n * @param applyOperation a function which can apply an operation to the state\n * @param revertOperation a function which can revert an operation from the state\n * @param buildEmpty a function returning an \"empty\" operation.\n * i.e an operation that leaves the state unmodified once applied or reverted\n * (used for internal implementation)\n * @param buildTransformation Factory used to build transformations\n */\n constructor(initialOperationId, applyOperation, revertOperation, buildEmpty, buildTransformation) {\n this.applyOperation = applyOperation;\n this.revertOperation = revertOperation;\n this.buildEmpty = buildEmpty;\n this.buildTransformation = buildTransformation;\n this.HEAD_BRANCH = new Branch(this.buildTransformation);\n this.tree = new Tree(buildTransformation, this.HEAD_BRANCH);\n const initial = new Operation(initialOperationId, buildEmpty(initialOperationId));\n this.tree.insertOperationLast(this.HEAD_BRANCH, initial);\n this.HEAD_OPERATION = initial;\n }\n /**\n * Return the operation identified by its id.\n */\n get(operationId) {\n return this.tree.findOperation(this.HEAD_BRANCH, operationId).operation.data;\n }\n /**\n * Append a new operation as the last one\n */\n append(operationId, data) {\n const operation = new Operation(operationId, data);\n const branch = this.tree.getLastBranch();\n this.tree.insertOperationLast(branch, operation);\n this.HEAD_BRANCH = branch;\n this.HEAD_OPERATION = operation;\n }\n /**\n * Insert a new operation after a specific operation (may not be the last operation).\n * Following operations will be transformed according\n * to the new operation.\n */\n insert(operationId, data, insertAfter) {\n const operation = new Operation(operationId, data);\n this.revertTo(insertAfter);\n this.tree.insertOperationAfter(this.HEAD_BRANCH, operation, insertAfter);\n this.fastForward();\n }\n /**\n * @param operationId operation to undo\n * @param undoId the id of the \"undo operation\"\n * @param insertAfter the id of the operation after which to insert the undo\n */\n undo(operationId, undoId, insertAfter) {\n const { branch, operation } = this.tree.findOperation(this.HEAD_BRANCH, operationId);\n this.revertBefore(operationId);\n this.tree.undo(branch, operation);\n this.fastForward();\n this.insert(undoId, this.buildEmpty(undoId), insertAfter);\n }\n /**\n * @param operationId operation to redo\n * @param redoId the if of the \"redo operation\"\n * @param insertAfter the id of the operation after which to insert the redo\n */\n redo(operationId, redoId, insertAfter) {\n const { branch } = this.tree.findOperation(this.HEAD_BRANCH, operationId);\n this.revertBefore(operationId);\n this.tree.redo(branch);\n this.fastForward();\n this.insert(redoId, this.buildEmpty(redoId), insertAfter);\n }\n drop(operationId) {\n this.revertBefore(operationId);\n this.tree.drop(operationId);\n }\n /**\n * Revert the state as it was *before* the given operation was executed.\n */\n revertBefore(operationId) {\n const execution = this.tree.revertedExecution(this.HEAD_BRANCH).stopWith(operationId);\n this.revert(execution);\n }\n /**\n * Revert the state as it was *after* the given operation was executed.\n */\n revertTo(operationId) {\n const execution = operationId\n ? this.tree.revertedExecution(this.HEAD_BRANCH).stopBefore(operationId)\n : this.tree.revertedExecution(this.HEAD_BRANCH);\n this.revert(execution);\n }\n /**\n * Revert an execution\n */\n revert(execution) {\n for (const { next, operation, isCancelled } of execution) {\n if (!isCancelled) {\n this.revertOperation(operation.data);\n }\n if (next) {\n this.HEAD_BRANCH = next.branch;\n this.HEAD_OPERATION = next.operation;\n }\n }\n }\n /**\n * Replay the operations between the current HEAD_BRANCH and the end of the tree\n */\n fastForward() {\n const operations = this.HEAD_OPERATION\n ? this.tree.execution(this.HEAD_BRANCH).startAfter(this.HEAD_OPERATION.id)\n : this.tree.execution(this.HEAD_BRANCH);\n for (const { operation: operation, branch, isCancelled } of operations) {\n if (!isCancelled) {\n this.applyOperation(operation.data);\n }\n this.HEAD_OPERATION = operation;\n this.HEAD_BRANCH = branch;\n }\n }\n }\n\n function buildRevisionLog(initialRevisionId, recordChanges, dispatch) {\n return new SelectiveHistory(initialRevisionId, (revision) => {\n const commands = revision.commands.slice();\n const { changes } = recordChanges(() => {\n for (const command of commands) {\n dispatch(command);\n }\n });\n revision.setChanges(changes);\n }, (revision) => revertChanges([revision]), (id) => new Revision(id, \"empty\", [], []), {\n with: (revision) => (toTransform) => {\n return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands));\n },\n without: (revision) => (toTransform) => {\n return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands.map(inverseCommand).flat()));\n },\n });\n }\n /**\n * Revert changes from the given revisions\n */\n function revertChanges(revisions) {\n for (const revision of revisions.slice().reverse()) {\n for (let i = revision.changes.length - 1; i >= 0; i--) {\n const change = revision.changes[i];\n applyChange(change, \"before\");\n }\n }\n }\n /**\n * Apply the changes of the given HistoryChange to the state\n */\n function applyChange(change, target) {\n let val = change.root;\n let key = change.path[change.path.length - 1];\n for (let pathIndex = 0; pathIndex < change.path.slice(0, -1).length; pathIndex++) {\n const p = change.path[pathIndex];\n if (val[p] === undefined) {\n const nextPath = change.path[pathIndex + 1];\n val[p] = createEmptyStructure(nextPath);\n }\n val = val[p];\n }\n if (change[target] === undefined) {\n delete val[key];\n }\n else {\n val[key] = change[target];\n }\n }\n\n /**\n * Local History\n *\n * The local history is responsible of tracking the locally state updates\n * It maintains the local undo and redo stack to allow to undo/redo only local\n * changes\n */\n class LocalHistory extends owl.EventBus {\n constructor(dispatch, session) {\n super();\n this.dispatch = dispatch;\n this.session = session;\n /**\n * Ids of the revisions which can be undone\n */\n this.undoStack = [];\n /**\n * Ids of the revisions which can be redone\n */\n this.redoStack = [];\n this.session.on(\"new-local-state-update\", this, this.onNewLocalStateUpdate);\n this.session.on(\"revision-undone\", this, ({ commands }) => this.selectiveUndo(commands));\n this.session.on(\"revision-redone\", this, ({ commands }) => this.selectiveRedo(commands));\n this.session.on(\"pending-revisions-dropped\", this, ({ revisionIds }) => this.drop(revisionIds));\n this.session.on(\"snapshot\", this, () => {\n this.undoStack = [];\n this.redoStack = [];\n });\n }\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"REQUEST_UNDO\":\n if (!this.canUndo()) {\n return 6 /* CommandResult.EmptyUndoStack */;\n }\n break;\n case \"REQUEST_REDO\":\n if (!this.canRedo()) {\n return 7 /* CommandResult.EmptyRedoStack */;\n }\n break;\n }\n return 0 /* CommandResult.Success */;\n }\n beforeHandle(cmd) { }\n handle(cmd) {\n switch (cmd.type) {\n case \"REQUEST_UNDO\":\n case \"REQUEST_REDO\":\n // History changes (undo & redo) are *not* applied optimistically on the local state.\n // We wait a global confirmation from the server. The goal is to avoid handling concurrent\n // history changes on multiple clients which are very hard to manage correctly.\n this.requestHistoryChange(cmd.type === \"REQUEST_UNDO\" ? \"UNDO\" : \"REDO\");\n }\n }\n finalize() { }\n requestHistoryChange(type) {\n const id = type === \"UNDO\" ? this.undoStack.pop() : this.redoStack.pop();\n if (!id) {\n return;\n }\n if (type === \"UNDO\") {\n this.session.undo(id);\n this.redoStack.push(id);\n }\n else {\n this.session.redo(id);\n this.undoStack.push(id);\n }\n }\n canUndo() {\n return this.undoStack.length > 0;\n }\n canRedo() {\n return this.redoStack.length > 0;\n }\n drop(revisionIds) {\n this.undoStack = this.undoStack.filter((id) => !revisionIds.includes(id));\n this.redoStack = [];\n }\n onNewLocalStateUpdate({ id }) {\n this.undoStack.push(id);\n this.redoStack = [];\n if (this.undoStack.length > MAX_HISTORY_STEPS) {\n this.undoStack.shift();\n }\n }\n selectiveUndo(commands) {\n this.dispatch(\"UNDO\", { commands });\n }\n selectiveRedo(commands) {\n this.dispatch(\"REDO\", { commands });\n }\n }\n\n /**\n * Stateless sequence of events that can be processed by consumers.\n *\n * There are three kind of consumers:\n * - the main consumer\n * - the default consumer\n * - observer consumers\n *\n * Main consumer\n * -------------\n * Anyone can capture the event stream and become the main consumer.\n * If there is already a main consumer, it is kicked off and it will no longer\n * receive events.\n * The main consumer can release the stream at any moment to stop listening\n * events.\n *\n * Default consumer\n * ----------------\n * When the main consumer releases the stream and until the stream is captured\n * again, all events are transmitted to the default consumer.\n *\n * Observer consumers\n * ------------------\n * Observers permanently receive events.\n *\n */\n class EventStream {\n constructor() {\n this.observers = [];\n }\n registerAsDefault(owner, callbacks) {\n this.defaultSubscription = { owner, callbacks };\n if (!this.mainSubscription) {\n this.mainSubscription = this.defaultSubscription;\n }\n }\n /**\n * Register callbacks to observe the stream\n */\n observe(owner, callbacks) {\n this.observers.push({ owner, callbacks });\n }\n /**\n * Capture the stream for yourself\n */\n capture(owner, callbacks) {\n var _a, _b, _c;\n if (this.observers.find((sub) => sub.owner === owner)) {\n throw new Error(\"You are already subscribed forever\");\n }\n if ((_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.owner) {\n (_c = (_b = this.mainSubscription.callbacks).release) === null || _c === void 0 ? void 0 : _c.call(_b);\n }\n this.mainSubscription = { owner, callbacks };\n }\n release(owner) {\n var _a, _b, _c, _d;\n if (((_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.owner) !== owner ||\n this.observers.find((sub) => sub.owner === owner)) {\n return;\n }\n (_d = (_b = this.mainSubscription) === null || _b === void 0 ? void 0 : (_c = _b.callbacks).release) === null || _d === void 0 ? void 0 : _d.call(_c);\n this.mainSubscription = this.defaultSubscription;\n }\n /**\n * Release whichever subscription in charge and get back to the default subscription\n */\n getBackToDefault() {\n var _a, _b, _c;\n if (this.mainSubscription === this.defaultSubscription) {\n return;\n }\n (_c = (_a = this.mainSubscription) === null || _a === void 0 ? void 0 : (_b = _a.callbacks).release) === null || _c === void 0 ? void 0 : _c.call(_b);\n this.mainSubscription = this.defaultSubscription;\n }\n /**\n * Check if you are currently the main stream consumer\n */\n isListening(owner) {\n var _a;\n return ((_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.owner) === owner;\n }\n /**\n * Push an event to the stream and broadcast it to consumers\n */\n send(event) {\n var _a;\n (_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.callbacks.handleEvent(event);\n [...this.observers].forEach((sub) => sub.callbacks.handleEvent(event));\n }\n }\n\n /**\n * Processes all selection updates (usually from user inputs) and emits an event\n * with the new selected anchor\n */\n class SelectionStreamProcessor {\n constructor(getters) {\n this.getters = getters;\n this.stream = new EventStream();\n this.anchor = { cell: { col: 0, row: 0 }, zone: positionToZone({ col: 0, row: 0 }) };\n this.defaultAnchor = this.anchor;\n }\n capture(owner, anchor, callbacks) {\n this.stream.capture(owner, callbacks);\n this.anchor = anchor;\n }\n /**\n * Register as default subscriber and capture the event stream.\n */\n registerAsDefault(owner, anchor, callbacks) {\n this.checkAnchorZoneOrThrow(anchor);\n this.stream.registerAsDefault(owner, callbacks);\n this.defaultAnchor = anchor;\n this.capture(owner, anchor, callbacks);\n }\n resetDefaultAnchor(owner, anchor) {\n this.checkAnchorZoneOrThrow(anchor);\n if (this.stream.isListening(owner)) {\n this.anchor = anchor;\n }\n this.defaultAnchor = anchor;\n }\n resetAnchor(owner, anchor) {\n this.checkAnchorZoneOrThrow(anchor);\n if (this.stream.isListening(owner)) {\n this.anchor = anchor;\n }\n }\n observe(owner, callbacks) {\n this.stream.observe(owner, callbacks);\n }\n release(owner) {\n if (this.stream.isListening(owner)) {\n this.stream.release(owner);\n this.anchor = this.defaultAnchor;\n }\n }\n getBackToDefault() {\n this.stream.getBackToDefault();\n }\n /**\n * Select a new anchor\n */\n selectZone(anchor, mode = \"overrideSelection\") {\n const sheetId = this.getters.getActiveSheetId();\n anchor = {\n ...anchor,\n zone: this.getters.expandZone(sheetId, anchor.zone),\n };\n return this.processEvent({\n type: \"ZonesSelected\",\n anchor,\n mode,\n });\n }\n /**\n * Select a single cell as the new anchor.\n */\n selectCell(col, row) {\n const zone = positionToZone({ col, row });\n return this.selectZone({ zone, cell: { col, row } });\n }\n /**\n * Set the selection to one of the cells adjacent to the current anchor cell.\n */\n moveAnchorCell(direction, step = 1) {\n if (step !== \"end\" && step <= 0) {\n return new DispatchResult(83 /* CommandResult.InvalidSelectionStep */);\n }\n const { col, row } = this.getNextAvailablePosition(direction, step);\n return this.selectCell(col, row);\n }\n /**\n * Update the current anchor such that it includes the given\n * cell position.\n */\n setAnchorCorner(col, row) {\n const sheetId = this.getters.getActiveSheetId();\n const { col: anchorCol, row: anchorRow } = this.anchor.cell;\n const zone = {\n left: Math.min(anchorCol, col),\n top: Math.min(anchorRow, row),\n right: Math.max(anchorCol, col),\n bottom: Math.max(anchorRow, row),\n };\n const expandedZone = this.getters.expandZone(sheetId, zone);\n const anchor = { zone: expandedZone, cell: { col: anchorCol, row: anchorRow } };\n return this.processEvent({\n type: \"AlterZoneCorner\",\n mode: \"updateAnchor\",\n anchor: anchor,\n });\n }\n /**\n * Add a new cell to the current selection\n */\n addCellToSelection(col, row) {\n const sheetId = this.getters.getActiveSheetId();\n ({ col, row } = this.getters.getMainCellPosition({ sheetId, col, row }));\n const zone = this.getters.expandZone(sheetId, positionToZone({ col, row }));\n return this.processEvent({\n type: \"ZonesSelected\",\n anchor: { zone, cell: { col, row } },\n mode: \"newAnchor\",\n });\n }\n /**\n * Increase or decrease the size of the current anchor zone.\n * The anchor cell remains where it is. It's the opposite side\n * of the anchor zone which moves.\n */\n resizeAnchorZone(direction, step = 1) {\n if (step !== \"end\" && step <= 0) {\n return new DispatchResult(83 /* CommandResult.InvalidSelectionStep */);\n }\n const sheetId = this.getters.getActiveSheetId();\n const anchor = this.anchor;\n const { col: anchorCol, row: anchorRow } = anchor.cell;\n const { left, right, top, bottom } = anchor.zone;\n const starting = this.getStartingPosition(direction);\n let [deltaCol, deltaRow] = this.deltaToTarget(starting, direction, step);\n if (deltaCol === 0 && deltaRow === 0) {\n return DispatchResult.Success;\n }\n let result = anchor.zone;\n const expand = (z) => {\n z = organizeZone(z);\n const { left, right, top, bottom } = this.getters.expandZone(sheetId, z);\n return {\n left: Math.max(0, left),\n right: Math.min(this.getters.getNumberCols(sheetId) - 1, right),\n top: Math.max(0, top),\n bottom: Math.min(this.getters.getNumberRows(sheetId) - 1, bottom),\n };\n };\n const { col: refCol, row: refRow } = this.getReferencePosition();\n // check if we can shrink selection\n let n = 0;\n while (result !== null) {\n n++;\n if (deltaCol < 0) {\n const newRight = this.getNextAvailableCol(deltaCol, right - (n - 1), refRow);\n result = refCol <= right - n ? expand({ top, left, bottom, right: newRight }) : null;\n }\n if (deltaCol > 0) {\n const newLeft = this.getNextAvailableCol(deltaCol, left + (n - 1), refRow);\n result = left + n <= refCol ? expand({ top, left: newLeft, bottom, right }) : null;\n }\n if (deltaRow < 0) {\n const newBottom = this.getNextAvailableRow(deltaRow, refCol, bottom - (n - 1));\n result = refRow <= bottom - n ? expand({ top, left, bottom: newBottom, right }) : null;\n }\n if (deltaRow > 0) {\n const newTop = this.getNextAvailableRow(deltaRow, refCol, top + (n - 1));\n result = top + n <= refRow ? expand({ top: newTop, left, bottom, right }) : null;\n }\n result = result ? organizeZone(result) : result;\n if (result && !isEqual(result, anchor.zone)) {\n return this.processEvent({\n type: \"ZonesSelected\",\n mode: \"updateAnchor\",\n anchor: { zone: result, cell: { col: anchorCol, row: anchorRow } },\n });\n }\n }\n const currentZone = {\n top: anchorRow,\n bottom: anchorRow,\n left: anchorCol,\n right: anchorCol,\n };\n const zoneWithDelta = organizeZone({\n top: this.getNextAvailableRow(deltaRow, refCol, top),\n left: this.getNextAvailableCol(deltaCol, left, refRow),\n bottom: this.getNextAvailableRow(deltaRow, refCol, bottom),\n right: this.getNextAvailableCol(deltaCol, right, refRow),\n });\n result = expand(union(currentZone, zoneWithDelta));\n const newAnchor = { zone: result, cell: { col: anchorCol, row: anchorRow } };\n return this.processEvent({\n type: \"ZonesSelected\",\n anchor: newAnchor,\n mode: \"updateAnchor\",\n });\n }\n selectColumn(index, mode) {\n const sheetId = this.getters.getActiveSheetId();\n const bottom = this.getters.getNumberRows(sheetId) - 1;\n let zone = { left: index, right: index, top: 0, bottom };\n const top = this.getters.findFirstVisibleColRowIndex(sheetId, \"ROW\");\n let col, row;\n switch (mode) {\n case \"overrideSelection\":\n case \"newAnchor\":\n col = index;\n row = top;\n break;\n case \"updateAnchor\":\n ({ col, row } = this.anchor.cell);\n zone = union(zone, { left: col, right: col, top, bottom });\n break;\n }\n return this.processEvent({\n type: \"HeadersSelected\",\n anchor: { zone, cell: { col, row } },\n mode,\n });\n }\n selectRow(index, mode) {\n const sheetId = this.getters.getActiveSheetId();\n const right = this.getters.getNumberCols(sheetId) - 1;\n let zone = { top: index, bottom: index, left: 0, right };\n const left = this.getters.findFirstVisibleColRowIndex(sheetId, \"COL\");\n let col, row;\n switch (mode) {\n case \"overrideSelection\":\n case \"newAnchor\":\n col = left;\n row = index;\n break;\n case \"updateAnchor\":\n ({ col, row } = this.anchor.cell);\n zone = union(zone, { left, right, top: row, bottom: row });\n break;\n }\n return this.processEvent({\n type: \"HeadersSelected\",\n anchor: { zone, cell: { col, row } },\n mode,\n });\n }\n /**\n * Loop the current selection while keeping the same anchor. The selection will loop through:\n * 1) the smallest zone that contain the anchor and that have only empty cells bordering it\n * 2) the whole sheet\n * 3) the anchor cell\n */\n loopSelection() {\n const sheetId = this.getters.getActiveSheetId();\n const anchor = this.anchor;\n // The whole sheet is selected, select the anchor cell\n if (isEqual(this.anchor.zone, this.getters.getSheetZone(sheetId))) {\n return this.selectZone({ ...anchor, zone: positionToZone(anchor.cell) });\n }\n const tableZone = this.expandZoneToTable(anchor.zone);\n return !deepEquals(tableZone, anchor.zone)\n ? this.selectZone({ ...anchor, zone: tableZone })\n : this.selectAll();\n }\n /**\n * Select a \"table\" around the current selection.\n * We define a table by the smallest zone that contain the anchor and that have only empty\n * cells bordering it\n */\n selectTableAroundSelection() {\n const tableZone = this.expandZoneToTable(this.anchor.zone);\n return this.selectZone({ ...this.anchor, zone: tableZone }, \"updateAnchor\");\n }\n /**\n * Select the entire sheet\n */\n selectAll() {\n const sheetId = this.getters.getActiveSheetId();\n const bottom = this.getters.getNumberRows(sheetId) - 1;\n const right = this.getters.getNumberCols(sheetId) - 1;\n const zone = { left: 0, top: 0, bottom, right };\n return this.processEvent({\n type: \"HeadersSelected\",\n mode: \"overrideSelection\",\n anchor: { zone, cell: this.anchor.cell },\n });\n }\n /**\n * Process a new anchor selection event. If the new anchor is inside\n * the sheet boundaries, the event is pushed to the event stream to\n * be processed.\n */\n processEvent(newAnchorEvent) {\n const event = { ...newAnchorEvent, previousAnchor: deepCopy(this.anchor) };\n const commandResult = this.checkEventAnchorZone(event);\n if (commandResult !== 0 /* CommandResult.Success */) {\n return new DispatchResult(commandResult);\n }\n this.anchor = event.anchor;\n this.stream.send(event);\n return DispatchResult.Success;\n }\n checkEventAnchorZone(event) {\n return this.checkAnchorZone(event.anchor);\n }\n checkAnchorZone(anchor) {\n const { cell, zone } = anchor;\n if (!isInside(cell.col, cell.row, zone)) {\n return 16 /* CommandResult.InvalidAnchorZone */;\n }\n const { left, right, top, bottom } = zone;\n const sheetId = this.getters.getActiveSheetId();\n const refCol = this.getters.findVisibleHeader(sheetId, \"COL\", range(left, right + 1));\n const refRow = this.getters.findVisibleHeader(sheetId, \"ROW\", range(top, bottom + 1));\n if (refRow === undefined || refCol === undefined) {\n return 17 /* CommandResult.SelectionOutOfBound */;\n }\n return 0 /* CommandResult.Success */;\n }\n checkAnchorZoneOrThrow(anchor) {\n const result = this.checkAnchorZone(anchor);\n if (result === 16 /* CommandResult.InvalidAnchorZone */) {\n throw new Error(_t(\"The provided anchor is invalid. The cell must be part of the zone.\"));\n }\n }\n /**\n * ---- PRIVATE ----\n */\n /** Computes the next cell position in the direction of deltaX and deltaY\n * by crossing through merges and skipping hidden cells.\n * Note that the resulting position might be out of the sheet, it needs to be validated.\n */\n getNextAvailablePosition(direction, step = 1) {\n const { col, row } = this.anchor.cell;\n const delta = this.deltaToTarget({ col, row }, direction, step);\n return {\n col: this.getNextAvailableCol(delta[0], col, row),\n row: this.getNextAvailableRow(delta[1], col, row),\n };\n }\n getNextAvailableCol(delta, colIndex, rowIndex) {\n const sheetId = this.getters.getActiveSheetId();\n const position = { col: colIndex, row: rowIndex };\n const isInPositionMerge = (nextCol) => this.getters.isInSameMerge(sheetId, colIndex, rowIndex, nextCol, rowIndex);\n return this.getNextAvailableHeader(delta, \"COL\", colIndex, position, isInPositionMerge);\n }\n getNextAvailableRow(delta, colIndex, rowIndex) {\n const sheetId = this.getters.getActiveSheetId();\n const position = { col: colIndex, row: rowIndex };\n const isInPositionMerge = (nextRow) => this.getters.isInSameMerge(sheetId, colIndex, rowIndex, colIndex, nextRow);\n return this.getNextAvailableHeader(delta, \"ROW\", rowIndex, position, isInPositionMerge);\n }\n getNextAvailableHeader(delta, dimension, startingHeaderIndex, position, isInPositionMerge) {\n const sheetId = this.getters.getActiveSheetId();\n if (delta === 0) {\n return startingHeaderIndex;\n }\n const step = Math.sign(delta);\n let header = startingHeaderIndex + delta;\n while (isInPositionMerge(header)) {\n header += step;\n }\n while (this.getters.isHeaderHidden(sheetId, dimension, header)) {\n header += step;\n }\n const outOfBound = header < 0 || header > this.getters.getNumberHeaders(sheetId, dimension) - 1;\n if (outOfBound) {\n if (this.getters.isHeaderHidden(sheetId, dimension, startingHeaderIndex)) {\n return this.getNextAvailableHeader(-step, dimension, startingHeaderIndex, position, isInPositionMerge);\n }\n else {\n return startingHeaderIndex;\n }\n }\n return header;\n }\n /**\n * Finds a visible cell in the currently selected zone starting with the anchor.\n * If the anchor is hidden, browses from left to right and top to bottom to\n * find a visible cell.\n */\n getReferencePosition() {\n const sheetId = this.getters.getActiveSheetId();\n const anchor = this.anchor;\n const { left, right, top, bottom } = anchor.zone;\n const { col: anchorCol, row: anchorRow } = anchor.cell;\n return {\n col: this.getters.isColHidden(sheetId, anchorCol)\n ? this.getters.findVisibleHeader(sheetId, \"COL\", range(left, right + 1)) || anchorCol\n : anchorCol,\n row: this.getters.isRowHidden(sheetId, anchorRow)\n ? this.getters.findVisibleHeader(sheetId, \"ROW\", range(top, bottom + 1)) || anchorRow\n : anchorRow,\n };\n }\n deltaToTarget(position, direction, step) {\n switch (direction) {\n case \"up\":\n return step !== \"end\"\n ? [0, -step]\n : [0, this.getEndOfCluster(position, \"rows\", -1) - position.row];\n case \"down\":\n return step !== \"end\"\n ? [0, step]\n : [0, this.getEndOfCluster(position, \"rows\", 1) - position.row];\n case \"left\":\n return step !== \"end\"\n ? [-step, 0]\n : [this.getEndOfCluster(position, \"cols\", -1) - position.col, 0];\n case \"right\":\n return step !== \"end\"\n ? [step, 0]\n : [this.getEndOfCluster(position, \"cols\", 1) - position.col, 0];\n }\n }\n // TODO rename this\n getStartingPosition(direction) {\n let { col, row } = this.getPosition();\n const zone = this.anchor.zone;\n switch (direction) {\n case \"down\":\n case \"up\":\n row = row === zone.top ? zone.bottom : zone.top;\n break;\n case \"left\":\n case \"right\":\n col = col === zone.right ? zone.left : zone.right;\n break;\n }\n return { col, row };\n }\n /**\n * Given a starting position, compute the end of the cluster containing the position in the given\n * direction or the start of the next cluster. We define cluster here as side-by-side cells that\n * all have a content.\n *\n * We will return the end of the cluster if the given cell is inside a cluster, and the start of the\n * next cluster if the given cell is outside a cluster or at the border of a cluster in the given direction.\n */\n getEndOfCluster(startPosition, dim, dir) {\n const sheet = this.getters.getActiveSheet();\n let currentPosition = startPosition;\n // If both the current cell and the next cell are not empty, we want to go to the end of the cluster\n const nextCellPosition = this.getNextCellPosition(startPosition, dim, dir);\n let mode = !this.isCellEmpty(currentPosition, sheet.id) && !this.isCellEmpty(nextCellPosition, sheet.id)\n ? \"endOfCluster\"\n : \"nextCluster\";\n while (true) {\n const nextCellPosition = this.getNextCellPosition(currentPosition, dim, dir);\n // Break if nextPosition == currentPosition, which happens if there's no next valid position\n if (currentPosition.col === nextCellPosition.col &&\n currentPosition.row === nextCellPosition.row) {\n break;\n }\n const isNextCellEmpty = this.isCellEmpty(nextCellPosition, sheet.id);\n if (mode === \"endOfCluster\" && isNextCellEmpty) {\n break;\n }\n else if (mode === \"nextCluster\" && !isNextCellEmpty) {\n // We want to return the start of the next cluster, not the end of the empty zone\n currentPosition = nextCellPosition;\n break;\n }\n currentPosition = nextCellPosition;\n }\n return dim === \"cols\" ? currentPosition.col : currentPosition.row;\n }\n /**\n * Check if a cell is empty or undefined in the model. If the cell is part of a merge,\n * check if the merge containing the cell is empty.\n */\n isCellEmpty({ col, row }, sheetId = this.getters.getActiveSheetId()) {\n const position = this.getters.getMainCellPosition({ sheetId, col, row });\n const cell = this.getters.getEvaluatedCell(position);\n return cell.type === CellValueType.empty;\n }\n /** Computes the next cell position in the given direction by crossing through merges and skipping hidden cells.\n *\n * This has the same behaviour as getNextAvailablePosition() for certain arguments, but use this method instead\n * inside directionToDelta(), which is called in getNextAvailablePosition(), to avoid possible infinite\n * recursion.\n */\n getNextCellPosition(currentPosition, dimension, direction) {\n const dimOfInterest = dimension === \"cols\" ? \"col\" : \"row\";\n const startingPosition = { ...currentPosition };\n const nextCoord = dimension === \"cols\"\n ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)\n : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);\n startingPosition[dimOfInterest] = nextCoord;\n return { col: startingPosition.col, row: startingPosition.row };\n }\n getPosition() {\n return { ...this.anchor.cell };\n }\n /**\n * Expand the given zone to a table.\n * We define a table by the smallest zone that contain the anchor and that have only empty\n * cells bordering it\n */\n expandZoneToTable(zoneToExpand) {\n /** Try to expand the zone by one col/row in any direction to include a new non-empty cell */\n const expandZone = (zone) => {\n for (const col of range(zone.left, zone.right + 1)) {\n if (!this.isCellEmpty({ col, row: zone.top - 1 })) {\n return { ...zone, top: zone.top - 1 };\n }\n if (!this.isCellEmpty({ col, row: zone.bottom + 1 })) {\n return { ...zone, bottom: zone.bottom + 1 };\n }\n }\n for (const row of range(zone.top, zone.bottom + 1)) {\n if (!this.isCellEmpty({ col: zone.left - 1, row })) {\n return { ...zone, left: zone.left - 1 };\n }\n if (!this.isCellEmpty({ col: zone.right + 1, row })) {\n return { ...zone, right: zone.right + 1 };\n }\n }\n return zone;\n };\n let hasExpanded = false;\n let zone = zoneToExpand;\n do {\n hasExpanded = false;\n const newZone = expandZone(zone);\n if (!isEqual(zone, newZone)) {\n hasExpanded = true;\n zone = newZone;\n continue;\n }\n } while (hasExpanded);\n return zone;\n }\n }\n\n class StateObserver {\n constructor() {\n this.changes = [];\n this.commands = [];\n }\n /**\n * Record the changes which could happen in the given callback, save them in a\n * new revision with the given id and userId.\n */\n recordChanges(callback) {\n this.changes = [];\n this.commands = [];\n callback();\n return { changes: this.changes, commands: this.commands };\n }\n addCommand(command) {\n this.commands.push(command);\n }\n addChange(...args) {\n const val = args.pop();\n const [root, ...path] = args;\n let value = root;\n let key = path[path.length - 1];\n for (let pathIndex = 0; pathIndex <= path.length - 2; pathIndex++) {\n const p = path[pathIndex];\n if (value[p] === undefined) {\n const nextPath = path[pathIndex + 1];\n value[p] = createEmptyStructure(nextPath);\n }\n value = value[p];\n }\n if (value[key] === val) {\n return;\n }\n this.changes.push({\n root,\n path,\n before: value[key],\n after: val,\n });\n if (val === undefined) {\n delete value[key];\n }\n else {\n value[key] = val;\n }\n }\n }\n\n /**\n * Each axis present inside a graph needs to be identified by an unsigned integer\n * The value does not matter, it can be hardcoded.\n */\n const catAxId = 17781237;\n const valAxId = 88853993;\n function createChart(chart, chartSheetIndex, data) {\n const namespaces = [\n [\"xmlns:r\", RELATIONSHIP_NSR],\n [\"xmlns:a\", DRAWING_NS_A],\n [\"xmlns:c\", DRAWING_NS_C],\n ];\n const chartShapeProperty = shapeProperty({\n backgroundColor: chart.data.backgroundColor,\n line: { color: \"000000\" },\n });\n // to manually position the chart in the figure container\n let title = escapeXml ``;\n if (chart.data.title) {\n title = escapeXml /*xml*/ `\n \n ${insertText(chart.data.title, chart.data.fontColor)}\n \n \n `;\n }\n // switch on chart type\n let plot = escapeXml ``;\n switch (chart.data.type) {\n case \"bar\":\n plot = addBarChart(chart.data);\n break;\n case \"line\":\n plot = addLineChart(chart.data);\n break;\n case \"pie\":\n plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });\n break;\n }\n let position = \"t\";\n switch (chart.data.legendPosition) {\n case \"bottom\":\n position = \"b\";\n break;\n case \"left\":\n position = \"l\";\n break;\n case \"right\":\n position = \"r\";\n break;\n case \"top\":\n position = \"t\";\n break;\n }\n const fontColor = chart.data.fontColor;\n const xml = escapeXml /*xml*/ `\n \n \n \n ${chartShapeProperty}\n \n ${title}\n \n \n \n \n ${plot}\n ${shapeProperty({ backgroundColor: chart.data.backgroundColor })}\n \n ${addLegend(position, fontColor)}\n \n \n `;\n return parseXML(xml);\n }\n function shapeProperty(params) {\n return escapeXml /*xml*/ `\n \n ${params.backgroundColor ? solidFill(params.backgroundColor) : \"\"}\n ${params.line ? lineAttributes(params.line) : \"\"}\n \n `;\n }\n function solidFill(color) {\n return escapeXml /*xml*/ `\n \n \n \n `;\n }\n function lineAttributes(params) {\n const attrs = [[\"cmpd\", \"sng\"]];\n if (params.width) {\n attrs.push([\"w\", convertDotValueToEMU(params.width)]);\n }\n const lineStyle = params.style ? escapeXml /*xml*/ `` : \"\";\n return escapeXml /*xml*/ `\n \n ${solidFill(params.color)}\n ${lineStyle}\n \n `;\n }\n function insertText(text, fontColor = \"000000\", fontsize = 22) {\n return escapeXml /*xml*/ `\n \n \n \n \n \n \n \n ${solidFill(fontColor)}\n \n \n \n \n \n ${text} \n \n \n \n \n `;\n }\n function insertTextProperties(fontsize = 12, fontColor = \"000000\", bold = false, italic = false) {\n const defPropertiesAttributes = [\n [\"b\", bold ? \"1\" : \"0\"],\n [\"i\", italic ? \"1\" : \"0\"],\n [\"sz\", fontsize * 100],\n ];\n return escapeXml /*xml*/ `\n \n \n \n \n \n \n ${solidFill(fontColor)}\n \n \n \n \n \n `;\n }\n function addBarChart(chart) {\n // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)\n // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB\n // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB\n //\n // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.\n // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage\n const colors = new ChartColors();\n const dataSetsNodes = [];\n for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {\n const color = toXlsxHexColor(colors.next());\n const dataShapeProperty = shapeProperty({\n backgroundColor: color,\n line: { color },\n });\n dataSetsNodes.push(escapeXml /*xml*/ `\n \n \n \n ${dataset.label ? escapeXml /*xml*/ `${stringRef(dataset.label)} ` : \"\"}\n ${dataShapeProperty}\n ${chart.labelRange ? escapeXml /*xml*/ `${stringRef(chart.labelRange)} ` : \"\"} \n \n ${numberRef(dataset.range)}\n \n \n `);\n }\n // Excel does not support this feature\n const axisPos = chart.verticalAxisPosition === \"left\" ? \"l\" : \"r\";\n const grouping = chart.stacked ? \"stacked\" : \"clustered\";\n const overlap = chart.stacked ? 100 : -20;\n return escapeXml /*xml*/ `\n \n \n \n \n \n \n \n ${joinXmlNodes(dataSetsNodes)}\n \n \n \n ${addAx(\"b\", \"c:catAx\", catAxId, valAxId, { fontColor: chart.fontColor })}\n ${addAx(axisPos, \"c:valAx\", valAxId, catAxId, { fontColor: chart.fontColor })}\n `;\n }\n function addLineChart(chart) {\n const colors = new ChartColors();\n const dataSetsNodes = [];\n for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {\n const dataShapeProperty = shapeProperty({\n line: {\n width: 2.5,\n style: \"solid\",\n color: toXlsxHexColor(colors.next()),\n },\n });\n dataSetsNodes.push(escapeXml /*xml*/ `\n \n \n \n \n \n \n \n \n ${dataset.label ? escapeXml `${stringRef(dataset.label)} ` : \"\"}\n ${dataShapeProperty}\n ${chart.labelRange ? escapeXml `${stringRef(chart.labelRange)} ` : \"\"} \n \n ${numberRef(dataset.range)}\n \n \n `);\n }\n // Excel does not support this feature\n const axisPos = chart.verticalAxisPosition === \"left\" ? \"l\" : \"r\";\n const grouping = chart.stacked ? \"stacked\" : \"standard\";\n return escapeXml /*xml*/ `\n \n \n \n \n ${joinXmlNodes(dataSetsNodes)}\n \n \n \n ${addAx(\"b\", \"c:catAx\", catAxId, valAxId, { fontColor: chart.fontColor })}\n ${addAx(axisPos, \"c:valAx\", valAxId, catAxId, { fontColor: chart.fontColor })}\n `;\n }\n function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {\n const colors = new ChartColors();\n const maxLength = Math.max(...chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));\n const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));\n const dataSetsNodes = [];\n for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {\n //dataset slice labels\n const dsSize = getRangeSize(dataset.range, chartSheetIndex, data);\n const dataPoints = [];\n for (const index of range(0, dsSize)) {\n const pointShapeProperty = shapeProperty({\n backgroundColor: doughnutColors[index],\n line: { color: \"FFFFFF\", width: 1.5 },\n });\n dataPoints.push(escapeXml /*xml*/ `\n \n \n ${pointShapeProperty}\n \n `);\n }\n dataSetsNodes.push(escapeXml /*xml*/ `\n \n \n \n ${dataset.label ? escapeXml `${stringRef(dataset.label)} ` : \"\"}\n ${joinXmlNodes(dataPoints)}\n ${insertDataLabels({ showLeaderLines: true })}\n ${chart.labelRange ? escapeXml `${stringRef(chart.labelRange)} ` : \"\"}\n \n ${numberRef(dataset.range)}\n \n \n `);\n }\n return escapeXml /*xml*/ `\n \n \n \n ${insertDataLabels()}\n ${joinXmlNodes(dataSetsNodes)}\n \n `;\n }\n function insertDataLabels({ showLeaderLines } = { showLeaderLines: false }) {\n return escapeXml /*xml*/ `\n \n \n \n \n \n \n \n \n \n `;\n }\n function addAx(position, axisName, axId, crossAxId, { fontColor }) {\n // Each Axis present inside a graph needs to be identified by an unsigned integer in order to be referenced by its crossAxis.\n // I.e. x-axis, will reference y-axis and vice-versa.\n return escapeXml /*xml*/ `\n <${axisName}>\n \n \n \n \n \n \n \n ${insertMajorGridLines()}\n \n \n \n \n ${insertText(\"\")}\n \n ${insertTextProperties(10, fontColor)}\n ${axisName}>\n \n `;\n }\n function addLegend(position, fontColor) {\n return escapeXml /*xml*/ `\n \n \n \n ${insertTextProperties(10, fontColor)}\n \n `;\n }\n function insertMajorGridLines(color = \"B7B7B7\") {\n return escapeXml /*xml*/ `\n \n ${shapeProperty({ line: { color } })}\n \n `;\n }\n function stringRef(reference) {\n return escapeXml /*xml*/ `\n \n ${reference} \n \n `;\n }\n function numberRef(reference) {\n return escapeXml /*xml*/ `\n \n ${reference} \n \n \n `;\n }\n\n function addFormula(cell) {\n const formula = cell.content;\n const functions = functionRegistry.content;\n const tokens = tokenize(formula);\n const attrs = [];\n let node = escapeXml ``;\n const isExported = tokens\n .filter((tk) => tk.type === \"FUNCTION\")\n .every((tk) => functions[tk.value.toUpperCase()].isExported);\n if (isExported) {\n let cycle = escapeXml ``;\n const XlsxFormula = adaptFormulaToExcel(formula);\n // hack for cycles : if we don't set a value (be it 0 or #VALUE!), it will appear as invisible on excel,\n // Making it very hard for the client to find where the recursion is.\n if (cell.value === CellErrorType.CircularDependency) {\n attrs.push([\"t\", \"str\"]);\n cycle = escapeXml /*xml*/ `${cell.value} `;\n }\n node = escapeXml /*xml*/ `\n \n ${XlsxFormula}\n \n ${cycle}\n `;\n return { attrs, node };\n }\n else {\n // Shouldn't we always output the value then ?\n const value = cell.value;\n // what if value = 0? Is this condition correct?\n if (value) {\n const type = getCellType(value);\n attrs.push([\"t\", type]);\n node = escapeXml /*xml*/ `${value} `;\n }\n return { attrs, node };\n }\n }\n function addContent(content, sharedStrings, forceString = false) {\n let value = content;\n const attrs = [];\n if (!forceString && [\"TRUE\", \"FALSE\"].includes(value.trim())) {\n value = value === \"TRUE\" ? \"1\" : \"0\";\n attrs.push([\"t\", \"b\"]);\n }\n else if (forceString || !isNumber(value)) {\n const { id } = pushElement(content, sharedStrings);\n value = id.toString();\n attrs.push([\"t\", \"s\"]);\n }\n return { attrs, node: escapeXml /*xml*/ `${value} ` };\n }\n function adaptFormulaToExcel(formulaText) {\n if (formulaText[0] === \"=\") {\n formulaText = formulaText.slice(1);\n }\n let ast;\n try {\n ast = parse(formulaText);\n }\n catch (error) {\n return formulaText;\n }\n ast = convertAstNodes(ast, \"STRING\", convertDateFormat);\n ast = convertAstNodes(ast, \"FUNCALL\", (ast) => {\n ast = { ...ast, value: ast.value.toUpperCase() };\n ast = prependNonRetrocompatibleFunction(ast);\n ast = addMissingRequiredArgs(ast);\n return ast;\n });\n return ast ? astToFormula(ast) : formulaText;\n }\n /**\n * Some Excel function need required args that might not be mandatory in o-spreadsheet.\n * This adds those missing args.\n */\n function addMissingRequiredArgs(ast) {\n const formulaName = ast.value.toUpperCase();\n const args = ast.args;\n const exportDefaultArgs = FORCE_DEFAULT_ARGS_FUNCTIONS[formulaName];\n if (exportDefaultArgs) {\n const requiredArgs = functionRegistry.content[formulaName].args.filter((el) => !el.optional);\n const diffArgs = requiredArgs.length - ast.args.length;\n if (diffArgs) {\n // We know that we have at least 1 default Value missing\n for (let i = ast.args.length; i < requiredArgs.length; i++) {\n const currentDefaultArg = exportDefaultArgs[i - diffArgs];\n args.push({ type: currentDefaultArg.type, value: currentDefaultArg.value });\n }\n }\n }\n return { ...ast, args };\n }\n /**\n * Prepend function names that are not compatible with Old Excel versions\n */\n function prependNonRetrocompatibleFunction(ast) {\n const formulaName = ast.value.toUpperCase();\n return {\n ...ast,\n value: NON_RETROCOMPATIBLE_FUNCTIONS.includes(formulaName)\n ? `_xlfn.${formulaName}`\n : formulaName,\n };\n }\n /**\n * Convert strings that correspond to a date to the format YYYY-DD-MM\n */\n function convertDateFormat(ast) {\n const value = ast.value.replace(new RegExp('\"', \"g\"), \"\");\n const internalDate = parseDateTime(value);\n if (internalDate) {\n let format = [];\n if (value.match(mdyDateRegexp) || value.match(ymdDateRegexp)) {\n format.push(\"yyyy-mm-dd\");\n }\n if (value.match(timeRegexp)) {\n format.push(\"hh:mm:ss\");\n }\n return {\n ...ast,\n value: formatValue(internalDate.value, format.join(\" \")),\n };\n }\n else {\n return { ...ast, value: ast.value.replace(/\\\\\"/g, `\"\"`) };\n }\n }\n\n function addConditionalFormatting(dxfs, conditionalFormats) {\n // Conditional Formats\n const cfNodes = [];\n for (const cf of conditionalFormats) {\n // Special case for each type of rule: might be better to extract that logic in dedicated functions\n switch (cf.rule.type) {\n case \"CellIsRule\":\n cfNodes.push(addCellIsRule(cf, cf.rule, dxfs));\n break;\n case \"ColorScaleRule\":\n cfNodes.push(addColorScaleRule(cf, cf.rule));\n break;\n case \"IconSetRule\":\n cfNodes.push(addIconSetRule(cf, cf.rule));\n break;\n default:\n // @ts-ignore Typescript knows it will never happen at compile time\n console.warn(`Conditional formatting ${cf.rule.type} not implemented`);\n break;\n }\n }\n return cfNodes;\n }\n // ----------------------\n // RULES\n // ----------------------\n function addCellIsRule(cf, rule, dxfs) {\n const ruleAttributes = commonCfAttributes(cf);\n const operator = convertOperator(rule.operator);\n ruleAttributes.push(...cellRuleTypeAttributes(rule), [\"operator\", operator]);\n const formulas = cellRuleFormula(cf.ranges, rule).map((formula) => escapeXml /*xml*/ `${formula} `);\n const dxf = {\n font: {\n color: { rgb: rule.style.textColor },\n bold: rule.style.bold,\n italic: rule.style.italic,\n strike: rule.style.strikethrough,\n underline: rule.style.underline,\n },\n };\n if (rule.style.fillColor) {\n dxf.fill = { fgColor: { rgb: rule.style.fillColor } };\n }\n const { id } = pushElement(dxf, dxfs);\n ruleAttributes.push([\"dxfId\", id]);\n return escapeXml /*xml*/ `\n \n \n ${joinXmlNodes(formulas)}\n \n \n `;\n }\n function cellRuleFormula(ranges, rule) {\n const firstCell = ranges[0].split(\":\")[0];\n const values = rule.values;\n switch (rule.operator) {\n case \"ContainsText\":\n return [`NOT(ISERROR(SEARCH(\"${values[0]}\",${firstCell})))`];\n case \"NotContains\":\n return [`ISERROR(SEARCH(\"${values[0]}\",${firstCell}))`];\n case \"BeginsWith\":\n return [`LEFT(${firstCell},LEN(\"${values[0]}\"))=\"${values[0]}\"`];\n case \"EndsWith\":\n return [`RIGHT(${firstCell},LEN(\"${values[0]}\"))=\"${values[0]}\"`];\n case \"IsEmpty\":\n return [`LEN(TRIM(${firstCell}))=0`];\n case \"IsNotEmpty\":\n return [`LEN(TRIM(${firstCell}))>0`];\n case \"Equal\":\n case \"NotEqual\":\n case \"GreaterThan\":\n case \"GreaterThanOrEqual\":\n case \"LessThan\":\n case \"LessThanOrEqual\":\n return [values[0]];\n case \"Between\":\n case \"NotBetween\":\n return [values[0], values[1]];\n }\n }\n function cellRuleTypeAttributes(rule) {\n const operator = convertOperator(rule.operator);\n switch (rule.operator) {\n case \"ContainsText\":\n case \"NotContains\":\n case \"BeginsWith\":\n case \"EndsWith\":\n return [\n [\"type\", operator],\n [\"text\", rule.values[0]],\n ];\n case \"IsEmpty\":\n case \"IsNotEmpty\":\n return [[\"type\", operator]];\n case \"Equal\":\n case \"NotEqual\":\n case \"GreaterThan\":\n case \"GreaterThanOrEqual\":\n case \"LessThan\":\n case \"LessThanOrEqual\":\n case \"Between\":\n case \"NotBetween\":\n return [[\"type\", \"cellIs\"]];\n }\n }\n function addColorScaleRule(cf, rule) {\n const ruleAttributes = commonCfAttributes(cf);\n ruleAttributes.push([\"type\", \"colorScale\"]);\n /** mimic our flow:\n * for a given ColorScale CF, each range of the \"ranges set\" has its own behaviour.\n */\n const conditionalFormats = [];\n for (const range of cf.ranges) {\n const cfValueObject = [];\n const colors = [];\n let canExport = true;\n for (let position of [\"minimum\", \"midpoint\", \"maximum\"]) {\n const threshold = rule[position];\n if (!threshold) {\n // pass midpoint if not defined\n continue;\n }\n if (threshold.type === \"formula\") {\n canExport = false;\n continue;\n }\n cfValueObject.push(thresholdAttributes(threshold, position));\n colors.push([[\"rgb\", toXlsxHexColor(colorNumberString(threshold.color))]]);\n }\n if (!canExport) {\n console.warn(\"Conditional formats with formula rules are not supported at the moment. The rule is therefore skipped.\");\n continue;\n }\n const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ ` `);\n const cfColorNodes = colors.map((attrs) => escapeXml /*xml*/ ` `);\n conditionalFormats.push(escapeXml /*xml*/ `\n \n \n \n ${joinXmlNodes(cfValueObjectNodes)}\n ${joinXmlNodes(cfColorNodes)}\n \n \n \n `);\n }\n return joinXmlNodes(conditionalFormats);\n }\n function addIconSetRule(cf, rule) {\n const ruleAttributes = commonCfAttributes(cf);\n ruleAttributes.push([\"type\", \"iconSet\"]);\n /** mimic our flow:\n * for a given IconSet CF, each range of the \"ranges set\" has its own behaviour.\n */\n const conditionalFormats = [];\n for (const range of cf.ranges) {\n const cfValueObject = [\n // It looks like they always want 3 cfvo and they add a dummy entry\n [\n [\"type\", \"percent\"],\n [\"val\", 0],\n ],\n ];\n let canExport = true;\n for (let position of [\"lowerInflectionPoint\", \"upperInflectionPoint\"]) {\n if (rule[position].type === \"formula\") {\n canExport = false;\n continue;\n }\n const threshold = rule[position];\n cfValueObject.push([\n ...thresholdAttributes(threshold, position),\n [\"gte\", threshold.operator === \"ge\" ? \"1\" : \"0\"],\n ]);\n }\n if (!canExport) {\n console.warn(\"Conditional formats with formula rules are not supported at the moment. The rule is therefore skipped.\");\n continue;\n }\n const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ ` `);\n conditionalFormats.push(escapeXml /*xml*/ `\n \n \n \n ${joinXmlNodes(cfValueObjectNodes)}\n \n \n \n `);\n }\n return joinXmlNodes(conditionalFormats);\n }\n // ----------------------\n // MISC\n // ----------------------\n function commonCfAttributes(cf) {\n return [\n [\"priority\", 1],\n [\"stopIfTrue\", cf.stopIfTrue ? 1 : 0],\n ];\n }\n function getIconSet(iconSet) {\n return XLSX_ICONSET_MAP[Object.keys(XLSX_ICONSET_MAP).find((key) => iconSet.upper.toLowerCase().startsWith(key)) ||\n \"dots\"];\n }\n function thresholdAttributes(threshold, position) {\n const type = getExcelThresholdType(threshold.type, position);\n const attrs = [[\"type\", type]];\n if (type !== \"min\" && type !== \"max\") {\n // what if the formula is not correct\n // references cannot be relative :/\n let val = threshold.value;\n if (type === \"formula\") {\n try {\n // Relative references are not supported in formula\n val = adaptFormulaToExcel(threshold.value);\n }\n catch (error) {\n val = threshold.value;\n }\n }\n attrs.push([\"val\", val]); // value is undefined only for type=\"value\")\n }\n return attrs;\n }\n /**\n * This function adapts our Threshold types to their Excel equivalents.\n *\n * if type === \"value\" ,then we must replace it by min or max according to the position\n * if type === \"number\", then it becomes num\n * if type === \"percentage\", it becomes \"percent\"\n * rest of the time, the type is unchanged\n */\n function getExcelThresholdType(type, position) {\n switch (type) {\n case \"value\":\n return position === \"minimum\" ? \"min\" : \"max\";\n case \"number\":\n return \"num\";\n case \"percentage\":\n return \"percent\";\n default:\n return type;\n }\n }\n\n function createDrawing(chartRelIds, sheet, figures) {\n const namespaces = [\n [\"xmlns:xdr\", NAMESPACE.drawing],\n [\"xmlns:r\", RELATIONSHIP_NSR],\n [\"xmlns:a\", DRAWING_NS_A],\n [\"xmlns:c\", DRAWING_NS_C],\n ];\n const figuresNodes = [];\n for (const [figureIndex, figure] of Object.entries(figures)) {\n // position\n const { from, to } = convertFigureData(figure, sheet);\n const chartId = convertChartId(figure.id);\n const cNvPrAttrs = [\n [\"id\", chartId],\n [\"name\", `Chart ${chartId}`],\n [\"title\", \"Chart\"],\n ];\n figuresNodes.push(escapeXml /*xml*/ `\n \n \n ${from.col} \n ${from.colOff} \n ${from.row} \n ${from.rowOff} \n \n \n ${to.col} \n ${to.colOff} \n ${to.row} \n ${to.rowOff} \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n `);\n }\n const xml = escapeXml /*xml*/ `\n \n ${joinXmlNodes(figuresNodes)}\n \n `;\n return parseXML(xml);\n }\n /**\n * Returns the coordinates of topLeft (from) and BottomRight (to) of the chart in English Metric Units (EMU)\n */\n function convertFigureData(figure, sheet) {\n const { x, y, height, width } = figure;\n const cols = Object.values(sheet.cols);\n const rows = Object.values(sheet.rows);\n const { index: colFrom, offset: offsetColFrom } = figureCoordinates(cols, x);\n const { index: colTo, offset: offsetColTo } = figureCoordinates(cols, x + width);\n const { index: rowFrom, offset: offsetRowFrom } = figureCoordinates(rows, y);\n const { index: rowTo, offset: offsetRowTo } = figureCoordinates(rows, y + height);\n return {\n from: {\n col: colFrom,\n colOff: offsetColFrom,\n row: rowFrom,\n rowOff: offsetRowFrom,\n },\n to: {\n col: colTo,\n colOff: offsetColTo,\n row: rowTo,\n rowOff: offsetRowTo,\n },\n };\n }\n /** Returns figure coordinates in EMU for a specific header dimension\n * See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement\n */\n function figureCoordinates(headers, position) {\n let currentPosition = 0;\n for (const [headerIndex, header] of Object.entries(headers)) {\n if (currentPosition <= position && position < currentPosition + header.size) {\n return {\n index: parseInt(headerIndex),\n offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),\n };\n }\n else {\n currentPosition += header.size;\n }\n }\n return {\n index: headers.length - 1,\n offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),\n };\n }\n\n function addNumberFormats(numFmts) {\n const numFmtNodes = [];\n for (let [index, numFmt] of Object.entries(numFmts)) {\n const numFmtAttrs = [\n [\"numFmtId\", parseInt(index) + FIRST_NUMFMT_ID],\n [\"formatCode\", numFmt.format],\n ];\n numFmtNodes.push(escapeXml /*xml*/ `\n \n `);\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(numFmtNodes)}\n \n `;\n }\n function addFont(font) {\n if (isObjectEmptyRecursive(font)) {\n return escapeXml /*xml*/ ``;\n }\n return escapeXml /*xml*/ `\n \n ${font.bold ? escapeXml /*xml*/ ` ` : \"\"}\n ${font.italic ? escapeXml /*xml*/ ` ` : \"\"}\n ${font.underline ? escapeXml /*xml*/ ` ` : \"\"}\n ${font.strike ? escapeXml /*xml*/ ` ` : \"\"}\n ${font.size ? escapeXml /*xml*/ ` ` : \"\"}\n ${font.color && font.color.rgb\n ? escapeXml /*xml*/ ` `\n : \"\"}\n ${font.name ? escapeXml /*xml*/ ` ` : \"\"}\n \n `;\n }\n function addFonts(fonts) {\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(Object.values(fonts).map(addFont))}\n \n `;\n }\n function addFills(fills) {\n const fillNodes = [];\n for (let fill of Object.values(fills)) {\n if (fill.reservedAttribute !== undefined) {\n fillNodes.push(escapeXml /*xml*/ `\n \n \n \n `);\n }\n else {\n fillNodes.push(escapeXml /*xml*/ `\n \n \n \n \n \n \n `);\n }\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(fillNodes)}\n \n `;\n }\n function addBorders(borders) {\n const borderNodes = [];\n for (let border of Object.values(borders)) {\n borderNodes.push(escapeXml /*xml*/ `\n \n \n \n \n \n \n \n `);\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(borderNodes)}\n \n `;\n }\n function formatBorderAttribute(description) {\n if (!description) {\n return escapeXml ``;\n }\n return formatAttributes([\n [\"style\", description.style],\n [\"color\", toXlsxHexColor(description.color.rgb)],\n ]);\n }\n function addStyles(styles) {\n const styleNodes = [];\n for (let style of styles) {\n const attributes = [\n [\"numFmtId\", style.numFmtId],\n [\"fillId\", style.fillId],\n [\"fontId\", style.fontId],\n [\"borderId\", style.borderId],\n ];\n // Note: the apply${substyleName} does not seem to be required\n const alignAttrs = [];\n if (style.alignment && style.alignment.vertical) {\n alignAttrs.push([\"vertical\", style.alignment.vertical]);\n }\n if (style.alignment && style.alignment.horizontal) {\n alignAttrs.push([\"horizontal\", style.alignment.horizontal]);\n }\n styleNodes.push(escapeXml /*xml*/ `\n \n ${alignAttrs ? escapeXml /*xml*/ ` ` : \"\"}\n \n `);\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(styleNodes)}\n \n `;\n }\n /**\n * DXFS : Differential Formatting Records - Conditional formats\n */\n function addCellWiseConditionalFormatting(dxfs // cell-wise CF\n ) {\n const dxfNodes = [];\n for (const dxf of dxfs) {\n let fontNode = escapeXml ``;\n if (dxf.font) {\n fontNode = addFont(dxf.font);\n }\n let fillNode = escapeXml ``;\n if (dxf.fill) {\n fillNode = escapeXml /*xml*/ `\n \n \n \n \n \n `;\n }\n dxfNodes.push(escapeXml /*xml*/ `\n \n ${fontNode}\n ${fillNode}\n \n `);\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(dxfNodes)}\n \n `;\n }\n\n const TABLE_DEFAULT_STYLE = escapeXml /*xml*/ ``;\n function createTable(table, tableId, sheetData) {\n const tableAttributes = [\n [\"id\", tableId],\n [\"name\", `Table${tableId}`],\n [\"displayName\", `Table${tableId}`],\n [\"ref\", table.range],\n [\"xmlns\", NAMESPACE.table],\n [\"xmlns:xr\", NAMESPACE.revision],\n [\"xmlns:xr3\", NAMESPACE.revision3],\n [\"xmlns:mc\", NAMESPACE.markupCompatibility],\n ];\n const xml = escapeXml /*xml*/ `\n \n ${addAutoFilter(table)}\n ${addTableColumns(table, sheetData)}\n ${TABLE_DEFAULT_STYLE}\n
\n `;\n return parseXML(xml);\n }\n function addAutoFilter(table) {\n const autoFilterAttributes = [[\"ref\", table.range]];\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(addFilterColumns(table))}\n \n `;\n }\n function addFilterColumns(table) {\n const tableZone = toZone(table.range);\n const columns = [];\n for (const i of range(0, zoneToDimension(tableZone).width)) {\n const filter = table.filters[i];\n if (!filter || !filter.filteredValues.length) {\n continue;\n }\n const colXml = escapeXml /*xml*/ `\n \n ${addFilter(filter)}\n \n `;\n columns.push(colXml);\n }\n return columns;\n }\n function addFilter(filter) {\n const filterValues = filter.filteredValues.map((val) => escapeXml /*xml*/ ` `);\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(filterValues)}\n \n`;\n }\n function addTableColumns(table, sheetData) {\n var _a;\n const tableZone = toZone(table.range);\n const columns = [];\n for (const i of range(0, zoneToDimension(tableZone).width)) {\n const colHeaderXc = toXC(tableZone.left + i, tableZone.top);\n const colName = ((_a = sheetData.cells[colHeaderXc]) === null || _a === void 0 ? void 0 : _a.content) || `col${i}`;\n const colAttributes = [\n [\"id\", i + 1],\n [\"name\", colName],\n ];\n columns.push(escapeXml /*xml*/ ` `);\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(columns)}\n \n `;\n }\n\n function addColumns(cols) {\n if (!Object.values(cols).length) {\n return escapeXml ``;\n }\n const colNodes = [];\n for (let [id, col] of Object.entries(cols)) {\n // Always force our own col width\n const attributes = [\n [\"min\", parseInt(id) + 1],\n [\"max\", parseInt(id) + 1],\n [\"width\", convertWidthToExcel(col.size || DEFAULT_CELL_WIDTH)],\n [\"customWidth\", 1],\n [\"hidden\", col.isHidden ? 1 : 0],\n ];\n colNodes.push(escapeXml /*xml*/ `\n \n `);\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(colNodes)}\n \n `;\n }\n function addRows(construct, data, sheet) {\n const rowNodes = [];\n for (let r = 0; r < sheet.rowNumber; r++) {\n const rowAttrs = [[\"r\", r + 1]];\n const row = sheet.rows[r] || {};\n // Always force our own row height\n rowAttrs.push([\"ht\", convertHeightToExcel(row.size || DEFAULT_CELL_HEIGHT)], [\"customHeight\", 1], [\"hidden\", row.isHidden ? 1 : 0]);\n const cellNodes = [];\n for (let c = 0; c < sheet.colNumber; c++) {\n const xc = toXC(c, r);\n const cell = sheet.cells[xc];\n if (cell) {\n const attributes = [[\"r\", xc]];\n // style\n const id = normalizeStyle(construct, extractStyle(cell, data));\n attributes.push([\"s\", id]);\n let additionalAttrs = [];\n let cellNode = escapeXml ``;\n // Either formula or static value inside the cell\n if (cell.isFormula) {\n ({ attrs: additionalAttrs, node: cellNode } = addFormula(cell));\n }\n else if (cell.content && isMarkdownLink(cell.content)) {\n const { label } = parseMarkdownLink(cell.content);\n ({ attrs: additionalAttrs, node: cellNode } = addContent(label, construct.sharedStrings));\n }\n else if (cell.content && cell.content !== \"\") {\n const isTableHeader = isCellTableHeader(c, r, sheet);\n ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader));\n }\n attributes.push(...additionalAttrs);\n cellNodes.push(escapeXml /*xml*/ `\n \n ${cellNode}\n \n `);\n }\n }\n if (cellNodes.length || row.size !== DEFAULT_CELL_HEIGHT || row.isHidden) {\n rowNodes.push(escapeXml /*xml*/ `\n \n ${joinXmlNodes(cellNodes)}\n
\n `);\n }\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(rowNodes)}\n \n `;\n }\n function isCellTableHeader(col, row, sheet) {\n return sheet.filterTables.some((table) => {\n const zone = toZone(table.range);\n const headerZone = { ...zone, bottom: zone.top };\n return isInside(col, row, headerZone);\n });\n }\n function addHyperlinks(construct, data, sheetIndex) {\n var _a;\n const sheet = data.sheets[sheetIndex];\n const cells = sheet.cells;\n const linkNodes = [];\n for (const xc in cells) {\n const content = (_a = cells[xc]) === null || _a === void 0 ? void 0 : _a.content;\n if (content && isMarkdownLink(content)) {\n const { label, url } = parseMarkdownLink(content);\n if (isSheetUrl(url)) {\n const sheetId = parseSheetUrl(url);\n const sheet = data.sheets.find((sheet) => sheet.id === sheetId);\n const location = sheet ? `${sheet.name}!A1` : INCORRECT_RANGE_STRING;\n linkNodes.push(escapeXml /*xml*/ `\n \n `);\n }\n else {\n const linkRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {\n target: withHttps(url),\n type: XLSX_RELATION_TYPE.hyperlink,\n targetMode: \"External\",\n });\n linkNodes.push(escapeXml /*xml*/ `\n \n `);\n }\n }\n }\n if (!linkNodes.length) {\n return escapeXml ``;\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(linkNodes)}\n \n `;\n }\n function addMerges(merges) {\n if (merges.length) {\n const mergeNodes = merges.map((merge) => escapeXml /*xml*/ ` `);\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(mergeNodes)}\n \n `;\n }\n else\n return escapeXml ``;\n }\n function addSheetViews(sheet) {\n const panes = sheet.panes;\n let splitPanes = escapeXml /*xml*/ ``;\n if (panes && (panes.xSplit || panes.ySplit)) {\n const xc = toXC(panes.xSplit, panes.ySplit);\n //workbookViewId should be defined in the workbook file but it seems like Excel has a default behaviour.\n const xSplit = panes.xSplit ? escapeXml `xSplit=\"${panes.xSplit}\"` : \"\";\n const ySplit = panes.ySplit ? escapeXml `ySplit=\"${panes.ySplit}\"` : \"\";\n const topRight = panes.xSplit ? escapeXml `` : \"\";\n const bottomLeft = panes.ySplit ? escapeXml `` : \"\";\n const bottomRight = panes.xSplit && panes.ySplit ? escapeXml `` : \"\";\n splitPanes = escapeXml /*xml*/ `\n \n ${topRight}\n ${bottomLeft}\n ${bottomRight}\n `;\n }\n let sheetView = escapeXml /*xml*/ `\n \n \n ${splitPanes}\n \n \n `;\n return sheetView;\n }\n\n /**\n * Return the spreadsheet data in the Office Open XML file format.\n * See ECMA-376 standard.\n * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/\n */\n function getXLSX(data) {\n const files = [];\n const construct = getDefaultXLSXStructure();\n files.push(createWorkbook(data, construct));\n files.push(...createWorksheets(data, construct));\n files.push(createStylesSheet(construct));\n files.push(createSharedStrings(construct.sharedStrings));\n files.push(...createRelsFiles(construct.relsFiles));\n files.push(createContentTypes(files));\n files.push(createRelRoot());\n return {\n name: `my_spreadsheet.xlsx`,\n files,\n };\n }\n function createWorkbook(data, construct) {\n const namespaces = [\n [\"xmlns\", NAMESPACE[\"workbook\"]],\n [\"xmlns:r\", RELATIONSHIP_NSR],\n ];\n const sheetNodes = [];\n for (const [index, sheet] of Object.entries(data.sheets)) {\n const attributes = [\n [\"state\", sheet.isVisible ? \"visible\" : \"hidden\"],\n [\"name\", sheet.name],\n [\"sheetId\", parseInt(index) + 1],\n [\"r:id\", `rId${parseInt(index) + 1}`],\n ];\n sheetNodes.push(escapeXml /*xml*/ `\n \n `);\n addRelsToFile(construct.relsFiles, \"xl/_rels/workbook.xml.rels\", {\n type: XLSX_RELATION_TYPE.sheet,\n target: `worksheets/sheet${index}.xml`,\n });\n }\n const xml = escapeXml /*xml*/ `\n \n \n ${joinXmlNodes(sheetNodes)}\n \n \n `;\n return createXMLFile(parseXML(xml), \"xl/workbook.xml\", \"workbook\");\n }\n function createWorksheets(data, construct) {\n const files = [];\n let currentTableIndex = 1;\n for (const [sheetIndex, sheet] of Object.entries(data.sheets)) {\n const namespaces = [\n [\"xmlns\", NAMESPACE[\"worksheet\"]],\n [\"xmlns:r\", RELATIONSHIP_NSR],\n ];\n const sheetFormatAttributes = [\n [\"defaultRowHeight\", convertHeightToExcel(DEFAULT_CELL_HEIGHT)],\n [\"defaultColWidth\", convertWidthToExcel(DEFAULT_CELL_WIDTH)],\n ];\n const tablesNode = createTablesForSheet(sheet, sheetIndex, currentTableIndex, construct, files);\n currentTableIndex += sheet.filterTables.length;\n // Figures and Charts\n let drawingNode = escapeXml ``;\n const charts = sheet.charts;\n if (charts.length) {\n const chartRelIds = [];\n for (const chart of charts) {\n const xlsxChartId = convertChartId(chart.id);\n const chartRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {\n target: `../charts/chart${xlsxChartId}.xml`,\n type: XLSX_RELATION_TYPE.chart,\n });\n chartRelIds.push(chartRelId);\n files.push(createXMLFile(createChart(chart, sheetIndex, data), `xl/charts/chart${xlsxChartId}.xml`, \"chart\"));\n }\n const drawingRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {\n target: `../drawings/drawing${sheetIndex}.xml`,\n type: XLSX_RELATION_TYPE.drawing,\n });\n files.push(createXMLFile(createDrawing(chartRelIds, sheet, charts), `xl/drawings/drawing${sheetIndex}.xml`, \"drawing\"));\n drawingNode = escapeXml /*xml*/ ` `;\n }\n const sheetXml = escapeXml /*xml*/ `\n \n ${addSheetViews(sheet)}\n \n ${addColumns(sheet.cols)}\n ${addRows(construct, data, sheet)}\n ${addMerges(sheet.merges)}\n ${joinXmlNodes(addConditionalFormatting(construct.dxfs, sheet.conditionalFormats))}\n ${addHyperlinks(construct, data, sheetIndex)}\n ${drawingNode}\n ${tablesNode}\n \n `;\n files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, \"sheet\"));\n }\n addRelsToFile(construct.relsFiles, \"xl/_rels/workbook.xml.rels\", {\n type: XLSX_RELATION_TYPE.sharedStrings,\n target: \"sharedStrings.xml\",\n });\n addRelsToFile(construct.relsFiles, \"xl/_rels/workbook.xml.rels\", {\n type: XLSX_RELATION_TYPE.styles,\n target: \"styles.xml\",\n });\n return files;\n }\n /**\n * Create xlsx files for each tables contained in the given sheet, and add them to the XLSXStructure ans XLSXExportFiles.\n *\n * Return an XML string that should be added in the sheet to link these table to the sheet.\n */\n function createTablesForSheet(sheetData, sheetId, startingTableId, construct, files) {\n let currentTableId = startingTableId;\n if (!sheetData.filterTables.length)\n return new XMLString(\"\");\n const sheetRelFile = `xl/worksheets/_rels/sheet${sheetId}.xml.rels`;\n const tableParts = [];\n for (const table of sheetData.filterTables) {\n const tableRelId = addRelsToFile(construct.relsFiles, sheetRelFile, {\n target: `../tables/table${currentTableId}.xml`,\n type: XLSX_RELATION_TYPE.table,\n });\n files.push(createXMLFile(createTable(table, currentTableId, sheetData), `xl/tables/table${currentTableId}.xml`, \"table\"));\n tableParts.push(escapeXml /*xml*/ ` `);\n currentTableId++;\n }\n return escapeXml /*xml*/ `\n \n ${joinXmlNodes(tableParts)}\n \n`;\n }\n function createStylesSheet(construct) {\n const namespaces = [\n [\"xmlns\", NAMESPACE[\"styleSheet\"]],\n [\"xmlns:r\", RELATIONSHIP_NSR],\n ];\n const styleXml = escapeXml /*xml*/ `\n \n ${addNumberFormats(construct.numFmts)}\n ${addFonts(construct.fonts)}\n ${addFills(construct.fills)}\n ${addBorders(construct.borders)}\n ${addStyles(construct.styles)}\n ${addCellWiseConditionalFormatting(construct.dxfs)}\n \n `;\n return createXMLFile(parseXML(styleXml), \"xl/styles.xml\", \"styles\");\n }\n function createSharedStrings(strings) {\n const namespaces = [\n [\"xmlns\", NAMESPACE[\"sst\"]],\n [\"count\", strings.length],\n [\"uniqueCount\", strings.length],\n ];\n const stringNodes = strings.map((string) => escapeXml /*xml*/ `${string} `);\n const xml = escapeXml /*xml*/ `\n \n ${joinXmlNodes(stringNodes)}\n \n `;\n return createXMLFile(parseXML(xml), \"xl/sharedStrings.xml\", \"sharedStrings\");\n }\n function createRelsFiles(relsFiles) {\n const XMLRelsFiles = [];\n for (const relFile of relsFiles) {\n const relationNodes = [];\n for (const rel of relFile.rels) {\n const attributes = [\n [\"Id\", rel.id],\n [\"Target\", rel.target],\n [\"Type\", rel.type],\n ];\n if (rel.targetMode) {\n attributes.push([\"TargetMode\", rel.targetMode]);\n }\n relationNodes.push(escapeXml /*xml*/ `\n \n `);\n }\n const xml = escapeXml /*xml*/ `\n \n ${joinXmlNodes(relationNodes)}\n \n `;\n XMLRelsFiles.push(createXMLFile(parseXML(xml), relFile.path));\n }\n return XMLRelsFiles;\n }\n function createContentTypes(files) {\n const overrideNodes = [];\n for (const file of files) {\n if (file.contentType) {\n overrideNodes.push(createOverride(\"/\" + file.path, CONTENT_TYPES[file.contentType]));\n }\n }\n const xml = escapeXml /*xml*/ `\n \n \n \n ${joinXmlNodes(overrideNodes)}\n \n `;\n return createXMLFile(parseXML(xml), \"[Content_Types].xml\");\n }\n function createRelRoot() {\n const attributes = [\n [\"Id\", \"rId1\"],\n [\"Type\", XLSX_RELATION_TYPE.document],\n [\"Target\", \"xl/workbook.xml\"],\n ];\n const xml = escapeXml /*xml*/ `\n \n \n \n `;\n return createXMLFile(parseXML(xml), \"_rels/.rels\");\n }\n\n var Status;\n (function (Status) {\n Status[Status[\"Ready\"] = 0] = \"Ready\";\n Status[Status[\"Running\"] = 1] = \"Running\";\n Status[Status[\"RunningCore\"] = 2] = \"RunningCore\";\n Status[Status[\"Finalizing\"] = 3] = \"Finalizing\";\n })(Status || (Status = {}));\n class Model extends EventBus {\n constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = true) {\n super();\n this.corePlugins = [];\n this.featurePlugins = [];\n this.statefulUIPlugins = [];\n this.coreViewsPlugins = [];\n /**\n * In a collaborative context, some commands can be replayed, we have to ensure\n * that these commands are not replayed on the UI plugins.\n */\n this.isReplayingCommand = false;\n /**\n * A plugin can draw some contents on the canvas. But even better: it can do\n * so multiple times. The order of the render calls will determine a list of\n * \"layers\" (i.e., earlier calls will be obviously drawn below later calls).\n * This list simply keeps the renderers+layer information so the drawing code\n * can just iterate on it\n */\n this.renderers = [];\n /**\n * Internal status of the model. Important for command handling coordination\n */\n this.status = 0 /* Status.Ready */;\n /**\n * The dispatch method is the only entry point to manipulate data in the model.\n * This is through this method that commands are dispatched most of the time\n * recursively until no plugin want to react anymore.\n *\n * CoreCommands dispatched from this function are saved in the history.\n *\n * Small technical detail: it is defined as an arrow function. There are two\n * reasons for this:\n * 1. this means that the dispatch method can be \"detached\" from the model,\n * which is done when it is put in the environment (see the Spreadsheet\n * component)\n * 2. This allows us to define its type by using the interface CommandDispatcher\n */\n this.dispatch = (type, payload) => {\n const command = { ...payload, type };\n let status = this.status;\n if (this.getters.isReadonly() && !canExecuteInReadonly(command)) {\n return new DispatchResult(67 /* CommandResult.Readonly */);\n }\n if (!this.session.canApplyOptimisticUpdate()) {\n return new DispatchResult(64 /* CommandResult.WaitingSessionConfirmation */);\n }\n switch (status) {\n case 0 /* Status.Ready */:\n const result = this.checkDispatchAllowed(command);\n if (!result.isSuccessful) {\n return result;\n }\n this.status = 1 /* Status.Running */;\n const { changes, commands } = this.state.recordChanges(() => {\n if (isCoreCommand(command)) {\n this.state.addCommand(command);\n }\n this.dispatchToHandlers(this.handlers, command);\n this.finalize();\n });\n this.session.save(commands, changes);\n this.status = 0 /* Status.Ready */;\n this.trigger(\"update\");\n break;\n case 1 /* Status.Running */:\n if (isCoreCommand(command)) {\n const dispatchResult = this.checkDispatchAllowed(command);\n if (!dispatchResult.isSuccessful) {\n return dispatchResult;\n }\n this.state.addCommand(command);\n }\n this.dispatchToHandlers(this.handlers, command);\n break;\n case 3 /* Status.Finalizing */:\n throw new Error(\"Cannot dispatch commands in the finalize state\");\n case 2 /* Status.RunningCore */:\n if (isCoreCommand(command)) {\n throw new Error(`A UI plugin cannot dispatch ${type} while handling a core command`);\n }\n this.dispatchToHandlers(this.handlers, command);\n }\n return DispatchResult.Success;\n };\n /**\n * Dispatch a command from a Core Plugin (or the History).\n * A command dispatched from this function is not added to the history.\n */\n this.dispatchFromCorePlugin = (type, payload) => {\n const command = { ...payload, type };\n const previousStatus = this.status;\n this.status = 2 /* Status.RunningCore */;\n const handlers = this.isReplayingCommand\n ? [this.range, ...this.corePlugins, ...this.coreViewsPlugins]\n : this.handlers;\n this.dispatchToHandlers(handlers, command);\n this.status = previousStatus;\n return DispatchResult.Success;\n };\n stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);\n const workbookData = load(data, verboseImport);\n this.state = new StateObserver();\n this.uuidGenerator = uuidGenerator;\n this.config = this.setupConfig(config);\n this.session = this.setupSession(workbookData.revisionId);\n this.history = new LocalHistory(this.dispatchFromCorePlugin, this.session);\n this.coreGetters = {};\n this.range = new RangeAdapter(this.coreGetters);\n this.coreGetters.getRangeString = this.range.getRangeString.bind(this.range);\n this.coreGetters.getRangeFromSheetXC = this.range.getRangeFromSheetXC.bind(this.range);\n this.coreGetters.createAdaptedRanges = this.range.createAdaptedRanges.bind(this.range);\n this.coreGetters.getRangeDataFromXc = this.range.getRangeDataFromXc.bind(this.range);\n this.coreGetters.getRangeDataFromZone = this.range.getRangeDataFromZone.bind(this.range);\n this.coreGetters.getRangeFromRangeData = this.range.getRangeFromRangeData.bind(this.range);\n this.coreGetters.getSelectionRangeString = this.range.getSelectionRangeString.bind(this.range);\n this.getters = {\n isReadonly: () => this.config.mode === \"readonly\" || this.config.mode === \"dashboard\",\n isDashboard: () => this.config.mode === \"dashboard\",\n canUndo: this.history.canUndo.bind(this.history),\n canRedo: this.history.canRedo.bind(this.history),\n getClient: this.session.getClient.bind(this.session),\n getConnectedClients: this.session.getConnectedClients.bind(this.session),\n isFullySynchronized: this.session.isFullySynchronized.bind(this.session),\n };\n this.uuidGenerator.setIsFastStrategy(true);\n // Initiate stream processor\n this.selection = new SelectionStreamProcessor(this.getters);\n this.corePluginConfig = this.setupCorePluginConfig();\n this.uiPluginConfig = this.setupUiPluginConfig();\n // registering plugins\n for (let Plugin of corePluginRegistry.getAll()) {\n this.setupCorePlugin(Plugin, workbookData);\n }\n Object.assign(this.getters, this.coreGetters);\n for (let Plugin of statefulUIPluginRegistry.getAll()) {\n this.statefulUIPlugins.push(this.setupUiPlugin(Plugin));\n }\n for (let Plugin of coreViewsPluginRegistry.getAll()) {\n this.coreViewsPlugins.push(this.setupUiPlugin(Plugin));\n }\n for (let Plugin of featurePluginRegistry.getAll()) {\n this.featurePlugins.push(this.setupUiPlugin(Plugin));\n }\n this.uuidGenerator.setIsFastStrategy(false);\n // starting plugins\n this.dispatch(\"START\");\n // Model should be the last permanent subscriber in the list since he should render\n // after all changes have been applied to the other subscribers (plugins)\n this.selection.observe(this, {\n handleEvent: () => this.trigger(\"update\"),\n });\n // This should be done after construction of LocalHistory due to order of\n // events\n this.setupSessionEvents();\n // Load the initial revisions\n this.session.loadInitialMessages(stateUpdateMessages);\n this.joinSession();\n if (config.snapshotRequested) {\n this.session.snapshot(this.exportData());\n this.garbageCollectExternalResources();\n }\n // mark all models as \"raw\", so they will not be turned into reactive objects\n // by owl, since we do not rely on reactivity\n owl.markRaw(this);\n }\n get handlers() {\n return [this.range, ...this.corePlugins, ...this.allUIPlugins, this.history];\n }\n get allUIPlugins() {\n return [...this.statefulUIPlugins, ...this.coreViewsPlugins, ...this.featurePlugins];\n }\n joinSession() {\n this.session.join(this.config.client);\n }\n leaveSession() {\n this.session.leave();\n }\n setupUiPlugin(Plugin) {\n const plugin = new Plugin(this.uiPluginConfig);\n for (let name of Plugin.getters) {\n if (!(name in plugin)) {\n throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);\n }\n if (name in this.getters) {\n throw new Error(`Getter \"${name}\" is already defined.`);\n }\n this.getters[name] = plugin[name].bind(plugin);\n }\n const layers = Plugin.layers.map((l) => [plugin, l]);\n this.renderers.push(...layers);\n this.renderers.sort((p1, p2) => p1[1] - p2[1]);\n return plugin;\n }\n /**\n * Initialize and properly configure a plugin.\n *\n * This method is private for now, but if the need arise, there is no deep\n * reason why the model could not add dynamically a plugin while it is running.\n */\n setupCorePlugin(Plugin, data) {\n const plugin = new Plugin(this.corePluginConfig);\n for (let name of Plugin.getters) {\n if (!(name in plugin)) {\n throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);\n }\n if (name in this.coreGetters) {\n throw new Error(`Getter \"${name}\" is already defined.`);\n }\n this.coreGetters[name] = plugin[name].bind(plugin);\n }\n plugin.import(data);\n this.corePlugins.push(plugin);\n }\n onRemoteRevisionReceived({ commands }) {\n for (let command of commands) {\n const previousStatus = this.status;\n this.status = 2 /* Status.RunningCore */;\n this.dispatchToHandlers(this.statefulUIPlugins, command);\n this.status = previousStatus;\n }\n this.finalize();\n }\n setupSession(revisionId) {\n const session = new Session(buildRevisionLog(revisionId, this.state.recordChanges.bind(this.state), (command) => {\n const result = this.checkDispatchAllowed(command);\n if (!result.isSuccessful) {\n return;\n }\n this.isReplayingCommand = true;\n this.dispatchToHandlers([this.range, ...this.corePlugins, ...this.coreViewsPlugins], command);\n this.isReplayingCommand = false;\n }), this.config.transportService, revisionId);\n return session;\n }\n setupSessionEvents() {\n this.session.on(\"remote-revision-received\", this, this.onRemoteRevisionReceived);\n this.session.on(\"revision-redone\", this, this.finalize);\n this.session.on(\"revision-undone\", this, this.finalize);\n // How could we improve communication between the session and UI?\n // It feels weird to have the model piping specific session events to its own bus.\n this.session.on(\"unexpected-revision-id\", this, () => this.trigger(\"unexpected-revision-id\"));\n this.session.on(\"collaborative-event-received\", this, () => {\n this.trigger(\"update\");\n });\n }\n setupConfig(config) {\n const client = config.client || {\n id: this.uuidGenerator.uuidv4(),\n name: _lt(\"Anonymous\").toString(),\n };\n const transportService = config.transportService || new LocalTransportService();\n return {\n ...config,\n mode: config.mode || \"normal\",\n custom: config.custom || {},\n external: config.external || {},\n transportService,\n client,\n moveClient: () => { },\n snapshotRequested: false,\n notifyUI: (payload) => this.trigger(\"notify-ui\", payload),\n lazyEvaluation: \"lazyEvaluation\" in config ? config.lazyEvaluation : true,\n };\n }\n setupCorePluginConfig() {\n return {\n getters: this.coreGetters,\n stateObserver: this.state,\n range: this.range,\n dispatch: this.dispatchFromCorePlugin,\n uuidGenerator: this.uuidGenerator,\n custom: this.config.custom,\n external: this.config.external,\n };\n }\n setupUiPluginConfig() {\n return {\n getters: this.getters,\n stateObserver: this.state,\n dispatch: this.dispatch,\n selection: this.selection,\n moveClient: this.session.move.bind(this.session),\n custom: this.config.custom,\n uiActions: this.config,\n lazyEvaluation: this.config.lazyEvaluation,\n };\n }\n // ---------------------------------------------------------------------------\n // Command Handling\n // ---------------------------------------------------------------------------\n /**\n * Check if the given command is allowed by all the plugins and the history.\n */\n checkDispatchAllowed(command) {\n const results = this.handlers.map((handler) => handler.allowDispatch(command));\n return new DispatchResult(results.flat());\n }\n finalize() {\n this.status = 3 /* Status.Finalizing */;\n for (const h of this.handlers) {\n h.finalize();\n }\n this.status = 0 /* Status.Ready */;\n }\n /**\n * Dispatch the given command to the given handlers.\n * It will call `beforeHandle` and `handle`\n */\n dispatchToHandlers(handlers, command) {\n command = deepCopy(command);\n for (const handler of handlers) {\n handler.beforeHandle(command);\n }\n for (const handler of handlers) {\n handler.handle(command);\n }\n }\n // ---------------------------------------------------------------------------\n // Grid Rendering\n // ---------------------------------------------------------------------------\n /**\n * When the Grid component is ready (= mounted), it has a reference to its\n * canvas and need to draw the grid on it. This is then done by calling this\n * method, which will dispatch the call to all registered plugins.\n *\n * Note that nothing prevent multiple grid components from calling this method\n * each, or one grid component calling it multiple times with a different\n * context. This is probably the way we should do if we want to be able to\n * freeze a part of the grid (so, we would need to render different zones)\n */\n drawGrid(context) {\n // we make sure here that the viewport is properly positioned: the offsets\n // correspond exactly to a cell\n for (let [renderer, layer] of this.renderers) {\n context.ctx.save();\n renderer.drawGrid(context, layer);\n context.ctx.restore();\n }\n }\n // ---------------------------------------------------------------------------\n // Data Export\n // ---------------------------------------------------------------------------\n /**\n * As the name of this method strongly implies, it is useful when we need to\n * export date out of the model.\n */\n exportData() {\n let data = createEmptyWorkbookData();\n for (let handler of this.handlers) {\n if (handler instanceof CorePlugin) {\n handler.export(data);\n }\n }\n data.revisionId = this.session.getRevisionId() || DEFAULT_REVISION_ID;\n data = JSON.parse(JSON.stringify(data));\n return data;\n }\n updateMode(mode) {\n if (mode !== \"normal\") {\n this.dispatch(\"STOP_EDITION\", { cancel: true });\n }\n // @ts-ignore For testing purposes only\n this.config.mode = mode;\n this.trigger(\"update\");\n }\n /**\n * Exports the current model data into a list of serialized XML files\n * to be zipped together as an *.xlsx file.\n *\n * We need to trigger a cell revaluation on every sheet and ensure that even\n * async functions are evaluated.\n * This prove to be necessary if the client did not trigger that evaluation in the first place\n * (e.g. open a document with several sheet and click on download before visiting each sheet)\n */\n exportXLSX() {\n this.dispatch(\"EVALUATE_CELLS\");\n let data = createEmptyExcelWorkbookData();\n for (let handler of this.handlers) {\n if (handler instanceof BasePlugin) {\n handler.exportForExcel(data);\n }\n }\n data = JSON.parse(JSON.stringify(data));\n return getXLSX(data);\n }\n garbageCollectExternalResources() {\n for (const plugin of this.corePlugins) {\n plugin.garbageCollectExternalResources();\n }\n }\n }\n\n /**\n * We export here all entities that needs to be accessed publicly by Odoo.\n *\n * Note that the __info__ key is actually completed by the build process (see\n * the rollup.config.js file)\n */\n const __info__ = {};\n const SPREADSHEET_DIMENSIONS = {\n MIN_ROW_HEIGHT,\n MIN_COL_WIDTH,\n HEADER_HEIGHT,\n HEADER_WIDTH,\n TOPBAR_HEIGHT,\n BOTTOMBAR_HEIGHT,\n DEFAULT_CELL_WIDTH,\n DEFAULT_CELL_HEIGHT,\n SCROLLBAR_WIDTH: SCROLLBAR_WIDTH$1,\n };\n const registries = {\n autofillModifiersRegistry,\n autofillRulesRegistry,\n cellMenuRegistry,\n colMenuRegistry,\n linkMenuRegistry,\n functionRegistry,\n featurePluginRegistry,\n statefulUIPluginRegistry,\n coreViewsPluginRegistry,\n corePluginRegistry,\n rowMenuRegistry,\n sidePanelRegistry,\n figureRegistry,\n sheetMenuRegistry,\n chartSidePanelComponentRegistry,\n chartComponentRegistry,\n chartRegistry,\n topbarMenuRegistry,\n topbarComponentRegistry,\n clickableCellRegistry,\n otRegistry,\n inverseCommandRegistry,\n urlRegistry,\n cellPopoverRegistry,\n };\n const helpers = {\n args,\n toBoolean,\n toJsDate,\n toNumber,\n toString,\n toXC,\n toZone,\n toCartesian,\n numberToLetters,\n createFullMenuItem,\n UuidGenerator,\n formatValue,\n computeTextWidth,\n createEmptyWorkbookData,\n createEmptySheet,\n createEmptyExcelSheet,\n getDefaultChartJsRuntime,\n chartFontColor,\n getMenuChildren,\n ChartColors,\n EvaluationError,\n CellErrorLevel,\n getFillingMode,\n rgbaToHex,\n colorToRGBA,\n positionToZone,\n isDefined: isDefined$1,\n };\n const links = {\n isMarkdownLink,\n parseMarkdownLink,\n markdownLink,\n openLink,\n urlRepresentation,\n };\n const components = {\n ChartPanel,\n ChartFigure,\n ChartJsComponent,\n Grid,\n GridOverlay,\n ScorecardChart: ScorecardChart$1,\n LineConfigPanel,\n LineBarPieDesignPanel,\n BarConfigPanel,\n LineBarPieConfigPanel,\n GaugeChartConfigPanel,\n GaugeChartDesignPanel,\n ScorecardChartConfigPanel,\n ScorecardChartDesignPanel,\n };\n function addFunction(functionName, functionDescription) {\n functionRegistry.add(functionName, functionDescription);\n }\n\n exports.AbstractChart = AbstractChart;\n exports.CorePlugin = CorePlugin;\n exports.DATETIME_FORMAT = DATETIME_FORMAT;\n exports.DispatchResult = DispatchResult;\n exports.EvaluationError = EvaluationError;\n exports.Model = Model;\n exports.Registry = Registry;\n exports.Revision = Revision;\n exports.SPREADSHEET_DIMENSIONS = SPREADSHEET_DIMENSIONS;\n exports.Spreadsheet = Spreadsheet;\n exports.UIPlugin = UIPlugin;\n exports.__info__ = __info__;\n exports.addFunction = addFunction;\n exports.astToFormula = astToFormula;\n exports.compile = compile;\n exports.components = components;\n exports.convertAstNodes = convertAstNodes;\n exports.coreTypes = coreTypes;\n exports.findCellInNewZone = findCellInNewZone;\n exports.functionCache = functionCache;\n exports.helpers = helpers;\n exports.invalidateEvaluationCommands = invalidateEvaluationCommands;\n exports.links = links;\n exports.load = load;\n exports.parse = parse;\n exports.readonlyAllowedCommands = readonlyAllowedCommands;\n exports.registries = registries;\n exports.setTranslationMethod = setTranslationMethod;\n exports.tokenize = tokenize;\n\n Object.defineProperty(exports, '__esModule', { value: true });\n\n\n __info__.version = '16.1.6';\n __info__.date = '2023-03-23T11:46:03.748Z';\n __info__.hash = '3bad828';\n\n\n})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);\n//# sourceMappingURL=o_spreadsheet.js.map\n", "/** @odoo-module **/\n\nimport { SpreadsheetAction } from \"@documents_spreadsheet/bundle/actions/spreadsheet_action\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { _lt } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nconst { topbarMenuRegistry } = spreadsheet.registries;\nconst { useSubEnv } = owl;\n\ntopbarMenuRegistry.addChild(\"add_document_to_dashboard\", [\"file\"], {\n name: _lt(\"Add to dashboard\"),\n sequence: 200,\n isVisible: (env) => env.canAddDocumentAsDashboard,\n action: (env) => env.createDashboardFromDocument(env.model),\n});\n\n/** @typedef {import(\"@spreadsheet/o_spreadsheet/o_spreadsheet\").Model} Model */\n\npatch(SpreadsheetAction.prototype, \"spreadsheet_dashboard_documents.SpreadsheetAction\", {\n setup() {\n this._super();\n useSubEnv({\n canAddDocumentAsDashboard: true,\n createDashboardFromDocument: this._createDashboardFromDocument.bind(this),\n });\n },\n\n /**\n * @param {Model} model\n * @private\n */\n async _createDashboardFromDocument(model) {\n const resId = this.resId;\n const name = this.state.spreadsheetName;\n await this.env.services.orm.write(\"documents.document\", [resId], {\n raw: JSON.stringify(model.exportData()),\n });\n this.env.services.action.doAction(\n {\n name: this.env._t(\"Name your dashboard and select its section\"),\n type: \"ir.actions.act_window\",\n view_mode: \"form\",\n views: [[false, \"form\"]],\n target: \"new\",\n res_model: \"spreadsheet.document.to.dashboard\",\n },\n {\n additionalContext: {\n default_document_id: resId,\n default_name: name,\n },\n }\n );\n },\n});\n", "/** @odoo-module */\nimport { camelToSnakeObject, toServerDateString } from \"@spreadsheet/helpers/helpers\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nimport { ServerData } from \"@spreadsheet/data_sources/server_data\";\n\n/**\n * @typedef {import(\"./accounting_functions\").DateRange} DateRange\n */\n\nexport class AccountingDataSource {\n constructor(services) {\n this.serverData = new ServerData(services.orm, {\n whenDataIsFetched: () => services.notify(),\n });\n }\n\n /**\n * Gets the total credit for a given account code prefix\n * @param {string[]} codes prefixes of the accounts codes\n * @param {DateRange} dateRange start date of the period to look\n * @param {number} offset end date of the period to look\n * @param {number} companyId specific company to target\n * @param {boolean} includeUnposted wether or not select unposted entries\n * @returns {number | undefined}\n */\n getCredit(codes, dateRange, offset, companyId, includeUnposted) {\n const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);\n return data.credit;\n }\n\n /**\n * Gets the total debit for a given account code prefix\n * @param {string[]} codes prefixes of the accounts codes\n * @param {DateRange} dateRange start date of the period to look\n * @param {number} offset end date of the period to look\n * @param {number} companyId specific company to target\n * @param {boolean} includeUnposted wether or not select unposted entries\n * @returns {number | undefined}\n */\n getDebit(codes, dateRange, offset, companyId, includeUnposted) {\n const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);\n return data.debit;\n }\n\n /**\n * @param {Date} date\n * @param {number | null} companyId\n * @returns {string}\n */\n getFiscalStartDate(date, companyId) {\n return this._fetchCompanyData(date, companyId).start;\n }\n\n /**\n * @param {Date} date\n * @param {number | null} companyId\n * @returns {string}\n */\n getFiscalEndDate(date, companyId) {\n return this._fetchCompanyData(date, companyId).end;\n }\n\n /**\n * @param {string} accountType\n * @returns {string[]}\n */\n getAccountGroupCodes(accountType) {\n return this.serverData.batch.get(\"account.account\", \"get_account_group\", accountType);\n }\n\n /**\n * Fetch the account information (credit/debit) for a given account code\n * @private\n * @param {string[]} codes prefix of the accounts' codes\n * @param {DateRange} dateRange start date of the period to look\n * @param {number} offset end date of the period to look\n * @param {number | null} companyId specific companyId to target\n * @param {boolean} includeUnposted wether or not select unposted entries\n * @returns {{ debit: number, credit: number }}\n */\n _fetchAccountData(codes, dateRange, offset, companyId, includeUnposted) {\n dateRange.year += offset;\n // Excel dates start at 1899-12-30, we should not support date ranges\n // that do not cover dates prior to it.\n // Unfortunately, this check needs to be done right before the server\n // call as a date to low (year <= 1) can raise an error server side.\n if (dateRange.year < 1900) {\n throw new Error(sprintf(_t(\"%s is not a valid year.\"), dateRange.year));\n }\n return this.serverData.batch.get(\n \"account.account\",\n \"spreadsheet_fetch_debit_credit\",\n camelToSnakeObject({ dateRange, codes, companyId, includeUnposted })\n );\n }\n\n /**\n * Fetch the start and end date of the fiscal year enclosing a given date\n * Defaults on the current user company if not provided\n * @private\n * @param {Date} date\n * @param {number | null} companyId\n * @returns {{start: string, end: string}}\n */\n _fetchCompanyData(date, companyId) {\n const result = this.serverData.batch.get(\"res.company\", \"get_fiscal_dates\", {\n date: toServerDateString(date),\n company_id: companyId,\n });\n if (result === false) {\n throw new Error(_t(\"The company fiscal year could not be found.\"));\n }\n return result;\n }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { functionRegistry } = spreadsheet.registries;\nconst { args, toBoolean, toString, toNumber, toJsDate } = spreadsheet.helpers;\n\nconst QuarterRegexp = /^q([1-4])\\/(\\d{4})$/i;\nconst MonthRegexp = /^0?([1-9]|1[0-2])\\/(\\d{4})$/i;\n\n/**\n * @typedef {Object} YearDateRange\n * @property {\"year\"} rangeType\n * @property {number} year\n */\n\n/**\n * @typedef {Object} QuarterDateRange\n * @property {\"quarter\"} rangeType\n * @property {number} year\n * @property {number} quarter\n */\n\n/**\n * @typedef {Object} MonthDateRange\n * @property {\"month\"} rangeType\n * @property {number} year\n * @property {number} month\n */\n\n/**\n * @typedef {Object} DayDateRange\n * @property {\"day\"} rangeType\n * @property {number} year\n * @property {number} month\n * @property {number} day\n */\n\n/**\n * @typedef {YearDateRange | QuarterDateRange | MonthDateRange | DayDateRange} DateRange\n */\n\n/**\n * @param {string} dateRange\n * @returns {QuarterDateRange | undefined}\n */\nfunction parseAccountingQuarter(dateRange) {\n const found = dateRange.match(QuarterRegexp);\n return found\n ? {\n rangeType: \"quarter\",\n year: toNumber(found[2]),\n quarter: toNumber(found[1]),\n }\n : undefined;\n}\n\n/**\n * @param {string} dateRange\n * @returns {MonthDateRange | undefined}\n */\nfunction parseAccountingMonth(dateRange) {\n const found = dateRange.match(MonthRegexp);\n return found\n ? {\n rangeType: \"month\",\n year: toNumber(found[2]),\n month: toNumber(found[1]),\n }\n : undefined;\n}\n\n/**\n * @param {string} dateRange\n * @returns {YearDateRange | undefined}\n */\nfunction parseAccountingYear(dateRange) {\n const dateNumber = toNumber(dateRange);\n // This allows a bit of flexibility for the user if they were to input a\n // numeric value instead of a year.\n // Users won't need to fetch accounting info for year 3000 before a long time\n // And the numeric value 3000 corresponds to 18th march 1908, so it's not an\n //issue to prevent them from fetching accounting data prior to that date.\n if (dateNumber < 3000) {\n return { rangeType: \"year\", year: dateNumber };\n }\n return undefined;\n}\n\n/**\n * @param {string} dateRange\n * @returns {DayDateRange}\n */\nfunction parseAccountingDay(dateRange) {\n const dateNumber = toNumber(dateRange);\n return {\n rangeType: \"day\",\n year: functionRegistry.get(\"YEAR\").compute(dateNumber),\n month: functionRegistry.get(\"MONTH\").compute(dateNumber),\n day: functionRegistry.get(\"DAY\").compute(dateNumber),\n };\n}\n\n/**\n * @param {string | number} dateRange\n * @returns {DateRange}\n */\nexport function parseAccountingDate(dateRange) {\n try {\n dateRange = toString(dateRange).trim();\n return (\n parseAccountingQuarter(dateRange) ||\n parseAccountingMonth(dateRange) ||\n parseAccountingYear(dateRange) ||\n parseAccountingDay(dateRange)\n );\n } catch {\n throw new Error(\n sprintf(\n _t(\n `'%s' is not a valid period. Supported formats are \"21/12/2022\", \"Q1/2022\", \"12/2022\", and \"2022\".`\n ),\n dateRange\n )\n );\n }\n}\n\nconst ODOO_FIN_ARGS = `\n account_codes (string) ${_t(\"The prefix of the accounts.\")}\n date_range (string, date) ${_t(\n `The date range. Supported formats are \"21/12/2022\", \"Q1/2022\", \"12/2022\", and \"2022\".`\n )}\n offset (number, default=0) ${_t(\"Year offset applied to date_range.\")}\n company_id (number, optional) ${_t(\"The company to target (Advanced).\")}\n include_unposted (boolean, default=TRUE) ${_t(\"Set to TRUE to include unposted entries.\")}\n`;\n\nfunctionRegistry.add(\"ODOO.CREDIT\", {\n description: _t(\"Get the total credit for the specified account(s) and period.\"),\n args: args(ODOO_FIN_ARGS),\n returns: [\"NUMBER\"],\n compute: function (\n accountCodes,\n dateRange,\n offset = 0,\n companyId = null,\n includeUnposted = true\n ) {\n accountCodes = toString(accountCodes).split(\",\").sort();\n offset = toNumber(offset);\n dateRange = parseAccountingDate(dateRange);\n includeUnposted = toBoolean(includeUnposted);\n return this.getters.getAccountPrefixCredit(\n accountCodes,\n dateRange,\n offset,\n companyId,\n includeUnposted\n );\n },\n computeFormat: function (\n accountCodes,\n dateRange,\n offset = 0,\n companyId = null,\n includeUnposted = true\n ) {\n return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || \"#,##0.00\";\n },\n});\n\nfunctionRegistry.add(\"ODOO.DEBIT\", {\n description: _t(\"Get the total debit for the specified account(s) and period.\"),\n args: args(ODOO_FIN_ARGS),\n returns: [\"NUMBER\"],\n compute: function (\n accountCodes,\n dateRange,\n offset = 0,\n companyId = null,\n includeUnposted = true\n ) {\n accountCodes = toString(accountCodes).split(\",\").sort();\n offset = toNumber(offset);\n dateRange = parseAccountingDate(dateRange);\n includeUnposted = toBoolean(includeUnposted);\n return this.getters.getAccountPrefixDebit(\n accountCodes,\n dateRange,\n offset,\n companyId,\n includeUnposted\n );\n },\n computeFormat: function (\n accountCodes,\n dateRange,\n offset = 0,\n companyId = null,\n includeUnposted = true\n ) {\n return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || \"#,##0.00\";\n },\n});\n\nfunctionRegistry.add(\"ODOO.BALANCE\", {\n description: _t(\"Get the total balance for the specified account(s) and period.\"),\n args: args(ODOO_FIN_ARGS),\n returns: [\"NUMBER\"],\n compute: function (\n accountCodes,\n dateRange,\n offset = 0,\n companyId = null,\n includeUnposted = true\n ) {\n accountCodes = toString(accountCodes).split(\",\").sort();\n offset = toNumber(offset);\n dateRange = parseAccountingDate(dateRange);\n includeUnposted = toBoolean(includeUnposted);\n return (\n this.getters.getAccountPrefixDebit(\n accountCodes,\n dateRange,\n offset,\n companyId,\n includeUnposted\n ) -\n this.getters.getAccountPrefixCredit(\n accountCodes,\n dateRange,\n offset,\n companyId,\n includeUnposted\n )\n );\n },\n computeFormat: function (\n accountCodes,\n dateRange,\n offset = 0,\n companyId = null,\n includeUnposted = true\n ) {\n return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || \"#,##0.00\";\n },\n});\n\nfunctionRegistry.add(\"ODOO.FISCALYEAR.START\", {\n description: _t(\"Returns the starting date of the fiscal year encompassing the provided date.\"),\n args: args(`\n date (date) ${_t(\"Reference date.\")}\n company_id (number, optional) ${_t(\"The company.\")}\n `),\n returns: [\"NUMBER\"],\n computeFormat: () => \"m/d/yyyy\",\n compute: function (date, companyId = null) {\n const startDate = this.getters.getFiscalStartDate(\n toJsDate(date),\n companyId === null ? null : toNumber(companyId)\n );\n return toNumber(startDate);\n },\n});\n\nfunctionRegistry.add(\"ODOO.FISCALYEAR.END\", {\n description: _t(\"Returns the ending date of the fiscal year encompassing the provided date.\"),\n args: args(`\n date (date) ${_t(\"Reference date.\")}\n company_id (number, optional) ${_t(\"The company.\")}\n `),\n returns: [\"NUMBER\"],\n computeFormat: () => \"m/d/yyyy\",\n compute: function (date, companyId = null) {\n const endDate = this.getters.getFiscalEndDate(\n toJsDate(date),\n companyId === null ? null : toNumber(companyId)\n );\n return toNumber(endDate);\n },\n});\n\nfunctionRegistry.add(\"ODOO.ACCOUNT.GROUP\", {\n description: _t(\"Returns the account ids of a given group.\"),\n args: args(`\n type (string) ${_t(\"Account type.\")}\n `),\n returns: [\"NUMBER\"],\n computeFormat: () => \"m/d/yyyy\",\n compute: function (accountType) {\n const accountTypes = this.getters.getAccountGroupCodes(toString(accountType));\n return accountTypes.join(\",\");\n },\n});\n", "/** @odoo-module */\n\nimport { _lt } from \"@web/core/l10n/translation\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport AccountingPlugin from \"./plugins/accounting_plugin\";\nimport { getFirstAccountFunction, getNumberOfAccountFormulas } from \"./utils\";\nimport { parseAccountingDate } from \"./accounting_functions\";\nimport { camelToSnakeObject } from \"@spreadsheet/helpers/helpers\";\n\nconst { cellMenuRegistry, featurePluginRegistry } = spreadsheet.registries;\nconst { astToFormula } = spreadsheet;\nconst { toString, toBoolean } = spreadsheet.helpers;\n\nfeaturePluginRegistry.add(\"odooAccountingAggregates\", AccountingPlugin);\n\ncellMenuRegistry.add(\"move_lines_see_records\", {\n name: _lt(\"See records\"),\n sequence: 176,\n async action(env) {\n const position = env.model.getters.getActivePosition();\n const cell = env.model.getters.getCell(position);\n const { args } = getFirstAccountFunction(cell.content);\n let [codes, date_range, offset, companyId, includeUnposted] = args\n .map(astToFormula)\n .map((arg) => env.model.getters.evaluateFormula(arg));\n codes = toString(codes).split(\",\");\n const dateRange = parseAccountingDate(date_range);\n dateRange.year += offset || 0;\n companyId = companyId || null;\n includeUnposted = toBoolean(includeUnposted);\n\n const action = await env.services.orm.call(\n \"account.account\",\n \"spreadsheet_move_line_action\",\n [camelToSnakeObject({ dateRange, companyId, codes, includeUnposted })]\n );\n await env.services.action.doAction(action);\n },\n isVisible: (env) => {\n const position = env.model.getters.getActivePosition();\n const evaluatedCell = env.model.getters.getEvaluatedCell(position);\n const cell = env.model.getters.getCell(position);\n return (\n !evaluatedCell.error &&\n evaluatedCell.value !== \"\" &&\n cell &&\n getNumberOfAccountFormulas(cell.content) === 1\n );\n },\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { AccountingDataSource } from \"../accounting_datasource\";\nconst DATA_SOURCE_ID = \"ACCOUNTING_AGGREGATES\";\n\n/**\n * @typedef {import(\"../accounting_functions\").DateRange} DateRange\n */\n\nexport default class AccountingPlugin extends spreadsheet.UIPlugin {\n constructor(config) {\n super(config);\n this.dataSources = config.custom.dataSources;\n if (this.dataSources) {\n this.dataSources.add(DATA_SOURCE_ID, AccountingDataSource);\n }\n }\n\n // -------------------------------------------------------------------------\n // Getters\n // -------------------------------------------------------------------------\n\n /**\n * Gets the total balance for given account code prefix\n * @param {string[]} codes prefixes of the accounts' codes\n * @param {DateRange} dateRange start date of the period to look\n * @param {number} offset end date of the period to look\n * @param {number | null} companyId specific company to target\n * @param {boolean} includeUnposted wether or not select unposted entries\n * @returns {number}\n */\n getAccountPrefixCredit(codes, dateRange, offset, companyId, includeUnposted) {\n return (\n this.dataSources &&\n this.dataSources\n .get(DATA_SOURCE_ID)\n .getCredit(codes, dateRange, offset, companyId, includeUnposted)\n );\n }\n\n /**\n * Gets the total balance for a given account code prefix\n * @param {string[]} codes prefixes of the accounts codes\n * @param {DateRange} dateRange start date of the period to look\n * @param {number} offset end date of the period to look\n * @param {number | null} companyId specific company to target\n * @param {boolean} includeUnposted wether or not select unposted entries\n * @returns {number}\n */\n getAccountPrefixDebit(codes, dateRange, offset, companyId, includeUnposted) {\n return (\n this.dataSources &&\n this.dataSources\n .get(DATA_SOURCE_ID)\n .getDebit(codes, dateRange, offset, companyId, includeUnposted)\n );\n }\n\n /**\n * @param {Date} date Date included in the fiscal year\n * @param {number | null} companyId specific company to target\n * @returns {string | undefined}\n */\n getFiscalStartDate(date, companyId) {\n return (\n this.dataSources &&\n this.dataSources.get(DATA_SOURCE_ID).getFiscalStartDate(date, companyId)\n );\n }\n\n /**\n * @param {Date} date Date included in the fiscal year\n * @param {number | undefined} companyId specific company to target\n * @returns {string | undefined}\n */\n getFiscalEndDate(date, companyId) {\n return (\n this.dataSources &&\n this.dataSources.get(DATA_SOURCE_ID).getFiscalEndDate(date, companyId)\n );\n }\n\n /**\n * @param {string} accountType\n * @returns {string[]}\n */\n getAccountGroupCodes(accountType) {\n return (\n this.dataSources &&\n this.dataSources.get(DATA_SOURCE_ID).getAccountGroupCodes(accountType)\n );\n }\n}\n\nAccountingPlugin.getters = [\n \"getAccountPrefixCredit\",\n \"getAccountPrefixDebit\",\n \"getAccountGroupCodes\",\n \"getFiscalStartDate\",\n \"getFiscalEndDate\",\n];\n", "/** @odoo-module **/\nimport { getOdooFunctions } from \"@spreadsheet/helpers/odoo_functions_helpers\";\n\n/** @typedef {import(\"@spreadsheet/helpers/odoo_functions_helpers\").OdooFunctionDescription} OdooFunctionDescription*/\n\n/**\n * @param {string} formula\n * @returns {number}\n */\nexport function getNumberOfAccountFormulas(formula) {\n return getOdooFunctions(formula, [\"ODOO.BALANCE\", \"ODOO.CREDIT\", \"ODOO.DEBIT\"]).filter(\n (fn) => fn.isMatched\n ).length;\n}\n\n/**\n * Get the first Account function description of the given formula.\n *\n * @param {string} formula\n * @returns {OdooFunctionDescription | undefined}\n */\nexport function getFirstAccountFunction(formula) {\n return getOdooFunctions(formula, [\"ODOO.BALANCE\", \"ODOO.CREDIT\", \"ODOO.DEBIT\"]).find(\n (fn) => fn.isMatched\n );\n}\n", "/** @odoo-module */\n\nimport { DataSources } from \"@spreadsheet/data_sources/data_sources\";\nimport { migrate } from \"@spreadsheet/o_spreadsheet/migration\";\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport spreadsheet from \"../o_spreadsheet/o_spreadsheet_extended\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst { Model } = spreadsheet;\n\nasync function downloadSpreadsheet(env, action) {\n const { orm, name, data, stateUpdateMessages } = action.params;\n const dataSources = new DataSources(orm);\n const model = new Model(migrate(data), { custom: { dataSources } }, stateUpdateMessages);\n await waitForDataLoaded(model);\n const { files } = model.exportXLSX();\n await download({\n url: \"/spreadsheet/xlsx\",\n data: {\n zip_name: `${name}.xlsx`,\n files: JSON.stringify(files),\n },\n });\n}\n\n/**\n * Ensure that the spreadsheet does not contains cells that are in loading state\n * @param {Model} model\n * @returns {Promise}\n */\nasync function waitForDataLoaded(model) {\n const dataSources = model.config.custom.dataSources;\n return new Promise((resolve, reject) => {\n function check() {\n model.dispatch(\"EVALUATE_CELLS\");\n if (isLoaded(model)) {\n dataSources.removeEventListener(\"data-source-updated\", check);\n resolve();\n }\n }\n dataSources.addEventListener(\"data-source-updated\", check);\n check();\n });\n}\n\nfunction isLoaded(model) {\n for (const sheetId of model.getters.getSheetIds()) {\n for (const cell of Object.values(model.getters.getEvaluatedCells(sheetId))) {\n if (cell.type === \"error\" && cell.error.message === _t(\"Data is loading\")) {\n return false;\n }\n }\n }\n return true;\n}\n\nregistry\n .category(\"actions\")\n .add(\"action_download_spreadsheet\", downloadSpreadsheet, { force: true });\n", "/** @odoo-module */\n\nimport { OdooViewsDataSource } from \"@spreadsheet/data_sources/odoo_views_data_source\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { GraphModel as ChartModel} from \"@web/views/graph/graph_model\";\n\nexport default class ChartDataSource extends OdooViewsDataSource {\n /**\n * @override\n * @param {Object} services Services (see DataSource)\n */\n constructor(services, params) {\n super(services, params);\n }\n\n /**\n * @protected\n */\n async _load() {\n await super._load();\n const metaData = {\n fieldAttrs: {},\n ...this._metaData,\n };\n this._model = new ChartModel(\n {\n _t,\n },\n metaData,\n {\n orm: this._orm,\n }\n );\n await this._model.load(this._searchParams);\n }\n\n getData() {\n if (!this.isReady()) {\n this.load();\n return { datasets: [], labels: [] };\n }\n if (!this._isValid) {\n return { datasets: [], labels: [] };\n }\n return this._model.data;\n }\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { chartComponentRegistry } = spreadsheet.registries;\nconst { ChartJsComponent } = spreadsheet.components;\n\nchartComponentRegistry.add(\"odoo_bar\", ChartJsComponent);\nchartComponentRegistry.add(\"odoo_line\", ChartJsComponent);\nchartComponentRegistry.add(\"odoo_pie\", ChartJsComponent);\n\nimport OdooChartCorePlugin from \"./plugins/odoo_chart_core_plugin\";\nimport ChartOdooMenuPlugin from \"./plugins/chart_odoo_menu_plugin\";\nimport OdooChartUIPlugin from \"./plugins/odoo_chart_ui_plugin\";\n\nexport { OdooChartCorePlugin, ChartOdooMenuPlugin, OdooChartUIPlugin };\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooChart } from \"./odoo_chart\";\n\nconst { chartRegistry } = spreadsheet.registries;\n\nconst { getDefaultChartJsRuntime, chartFontColor, ChartColors } = spreadsheet.helpers;\n\nexport class OdooBarChart extends OdooChart {\n constructor(definition, sheetId, getters) {\n super(definition, sheetId, getters);\n this.verticalAxisPosition = definition.verticalAxisPosition;\n this.stacked = definition.stacked;\n }\n\n getDefinition() {\n return {\n ...super.getDefinition(),\n verticalAxisPosition: this.verticalAxisPosition,\n stacked: this.stacked,\n };\n }\n}\n\nchartRegistry.add(\"odoo_bar\", {\n match: (type) => type === \"odoo_bar\",\n createChart: (definition, sheetId, getters) => new OdooBarChart(definition, sheetId, getters),\n getChartRuntime: createOdooChartRuntime,\n validateChartDefinition: (validator, definition) =>\n OdooBarChart.validateChartDefinition(validator, definition),\n transformDefinition: (definition) => OdooBarChart.transformDefinition(definition),\n getChartDefinitionFromContextCreation: () => OdooBarChart.getDefinitionFromContextCreation(),\n name: _t(\"Bar\"),\n});\n\nfunction createOdooChartRuntime(chart, getters) {\n const background = chart.background || \"#FFFFFF\";\n const { datasets, labels } = chart.dataSource.getData();\n const chartJsConfig = getBarConfiguration(chart, labels);\n const colors = new ChartColors();\n for (const { label, data } of datasets) {\n const color = colors.next();\n const dataset = {\n label,\n data,\n borderColor: color,\n backgroundColor: color,\n };\n chartJsConfig.data.datasets.push(dataset);\n }\n\n return { background, chartJsConfig };\n}\n\nfunction getBarConfiguration(chart, labels) {\n const fontColor = chartFontColor(chart.background);\n const config = getDefaultChartJsRuntime(chart, labels, fontColor);\n config.type = chart.type.replace(\"odoo_\", \"\");\n const legend = {\n ...config.options.legend,\n display: chart.legendPosition !== \"none\",\n labels: { fontColor },\n };\n legend.position = chart.legendPosition;\n config.options.legend = legend;\n config.options.layout = {\n padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n };\n config.options.scales = {\n xAxes: [\n {\n ticks: {\n // x axis configuration\n maxRotation: 60,\n minRotation: 15,\n padding: 5,\n labelOffset: 2,\n fontColor,\n },\n },\n ],\n yAxes: [\n {\n position: chart.verticalAxisPosition,\n ticks: {\n fontColor,\n // y axis configuration\n beginAtZero: true, // the origin of the y axis is always zero\n },\n },\n ],\n };\n if (chart.stacked) {\n config.options.scales.xAxes[0].stacked = true;\n config.options.scales.yAxes[0].stacked = true;\n }\n return config;\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport ChartDataSource from \"../data_source/chart_data_source\";\n\nconst { AbstractChart, CommandResult } = spreadsheet;\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n *\n * @typedef MetaData\n * @property {Array} domains\n * @property {Array} groupBy\n * @property {string} measure\n * @property {string} mode\n * @property {string} [order]\n * @property {string} resModel\n * @property {boolean} stacked\n *\n * @typedef OdooChartDefinition\n * @property {string} type\n * @property {MetaData} metaData\n * @property {SearchParams} searchParams\n * @property {string} title\n * @property {string} background\n * @property {string} legendPosition\n *\n * @typedef OdooChartDefinitionDataSource\n * @property {MetaData} metaData\n * @property {SearchParams} searchParams\n *\n */\n\nexport class OdooChart extends AbstractChart {\n /**\n * @param {OdooChartDefinition} definition\n * @param {string} sheetId\n * @param {Object} getters\n */\n constructor(definition, sheetId, getters) {\n super(definition, sheetId, getters);\n this.type = definition.type;\n this.metaData = definition.metaData;\n this.searchParams = definition.searchParams;\n this.legendPosition = definition.legendPosition;\n this.background = definition.background;\n this.dataSource = undefined;\n }\n\n static transformDefinition(definition) {\n return definition;\n }\n\n static validateChartDefinition(validator, definition) {\n return CommandResult.Success;\n }\n\n static getDefinitionFromContextCreation() {\n throw new Error(\"It's not possible to convert an Odoo chart to a native chart\");\n }\n\n /**\n * @returns {OdooChartDefinitionDataSource}\n */\n getDefinitionForDataSource() {\n return {\n metaData: {\n ...this.metaData,\n mode: this.type.replace(\"odoo_\", \"\"),\n },\n searchParams: this.searchParams,\n };\n }\n\n /**\n * @returns {OdooChartDefinition}\n */\n getDefinition() {\n return {\n //@ts-ignore Defined in the parent class\n title: this.title,\n background: this.background,\n legendPosition: this.legendPosition,\n metaData: this.metaData,\n searchParams: this.searchParams,\n type: this.type,\n };\n }\n\n getDefinitionForExcel() {\n // Export not supported\n return undefined;\n }\n\n /**\n * @returns {OdooChart}\n */\n updateRanges() {\n // No range on this graph\n return this;\n }\n\n /**\n * @returns {OdooChart}\n */\n copyForSheetId() {\n return this;\n }\n\n /**\n * @returns {OdooChart}\n */\n copyInSheetId() {\n return this;\n }\n\n getContextCreation() {\n return {};\n }\n\n getSheetIdsUsedInChartRanges() {\n return [];\n }\n\n setDataSource(dataSource) {\n if (dataSource instanceof ChartDataSource) {\n this.dataSource = dataSource;\n }\n else {\n throw new Error(\"Only ChartDataSources can be added.\");\n }\n }\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooChart } from \"./odoo_chart\";\nimport { LINE_FILL_TRANSPARENCY } from \"@web/views/graph/graph_renderer\";\n\nconst { chartRegistry } = spreadsheet.registries;\n\nconst {\n getDefaultChartJsRuntime,\n chartFontColor,\n ChartColors,\n getFillingMode,\n colorToRGBA,\n rgbaToHex,\n} = spreadsheet.helpers;\n\nexport class OdooLineChart extends OdooChart {\n constructor(definition, sheetId, getters) {\n super(definition, sheetId, getters);\n this.verticalAxisPosition = definition.verticalAxisPosition;\n this.stacked = definition.stacked;\n }\n\n getDefinition() {\n return {\n ...super.getDefinition(),\n verticalAxisPosition: this.verticalAxisPosition,\n stacked: this.stacked,\n };\n }\n}\n\nchartRegistry.add(\"odoo_line\", {\n match: (type) => type === \"odoo_line\",\n createChart: (definition, sheetId, getters) => new OdooLineChart(definition, sheetId, getters),\n getChartRuntime: createOdooChartRuntime,\n validateChartDefinition: (validator, definition) =>\n OdooLineChart.validateChartDefinition(validator, definition),\n transformDefinition: (definition) => OdooLineChart.transformDefinition(definition),\n getChartDefinitionFromContextCreation: () => OdooLineChart.getDefinitionFromContextCreation(),\n name: _t(\"Line\"),\n});\n\nfunction createOdooChartRuntime(chart, getters) {\n const background = chart.background || \"#FFFFFF\";\n const { datasets, labels } = chart.dataSource.getData();\n const chartJsConfig = getLineConfiguration(chart, labels);\n const colors = new ChartColors();\n for (const [index, { label, data }] of datasets.entries()) {\n const color = colors.next();\n const backgroundRGBA = colorToRGBA(color);\n if (chart.stacked) {\n // use the transparency of Odoo to keep consistency\n backgroundRGBA.a = LINE_FILL_TRANSPARENCY;\n }\n const backgroundColor = rgbaToHex(backgroundRGBA);\n const dataset = {\n label,\n data,\n lineTension: 0,\n borderColor: color,\n backgroundColor,\n pointBackgroundColor: color,\n fill: chart.stacked ? getFillingMode(index) : false,\n };\n chartJsConfig.data.datasets.push(dataset);\n }\n return { background, chartJsConfig };\n}\n\nfunction getLineConfiguration(chart, labels) {\n const fontColor = chartFontColor(chart.background);\n const config = getDefaultChartJsRuntime(chart, labels, fontColor);\n config.type = chart.type.replace(\"odoo_\", \"\");\n const legend = {\n ...config.options.legend,\n display: chart.legendPosition !== \"none\",\n labels: {\n fontColor,\n generateLabels(chart) {\n const { data } = chart;\n const labels = window.Chart.defaults.global.legend.labels.generateLabels(chart);\n for (const [index, label] of labels.entries()) {\n label.fillStyle = data.datasets[index].borderColor;\n }\n return labels;\n },\n },\n };\n legend.position = chart.legendPosition;\n config.options.legend = legend;\n config.options.layout = {\n padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n };\n config.options.scales = {\n xAxes: [\n {\n ticks: {\n // x axis configuration\n maxRotation: 60,\n minRotation: 15,\n padding: 5,\n labelOffset: 2,\n fontColor,\n },\n },\n ],\n yAxes: [\n {\n position: chart.verticalAxisPosition,\n ticks: {\n fontColor,\n // y axis configuration\n beginAtZero: true, // the origin of the y axis is always zero\n },\n },\n ],\n };\n if (chart.stacked) {\n config.options.scales.yAxes[0].stacked = true;\n }\n return config;\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooChart } from \"./odoo_chart\";\n\nconst { chartRegistry } = spreadsheet.registries;\n\nconst { getDefaultChartJsRuntime, chartFontColor, ChartColors } = spreadsheet.helpers;\n\nchartRegistry.add(\"odoo_pie\", {\n match: (type) => type === \"odoo_pie\",\n createChart: (definition, sheetId, getters) => new OdooChart(definition, sheetId, getters),\n getChartRuntime: createOdooChartRuntime,\n validateChartDefinition: (validator, definition) =>\n OdooChart.validateChartDefinition(validator, definition),\n transformDefinition: (definition) => OdooChart.transformDefinition(definition),\n getChartDefinitionFromContextCreation: () => OdooChart.getDefinitionFromContextCreation(),\n name: _t(\"Pie\"),\n});\n\nfunction createOdooChartRuntime(chart, getters) {\n const background = chart.background || \"#FFFFFF\";\n const { datasets, labels } = chart.dataSource.getData();\n const chartJsConfig = getPieConfiguration(chart, labels);\n const colors = new ChartColors();\n for (const { label, data } of datasets) {\n const backgroundColor = getPieColors(colors, datasets);\n const dataset = {\n label,\n data,\n borderColor: \"#FFFFFF\",\n backgroundColor,\n };\n chartJsConfig.data.datasets.push(dataset);\n }\n return { background, chartJsConfig };\n}\n\nfunction getPieConfiguration(chart, labels) {\n const fontColor = chartFontColor(chart.background);\n const config = getDefaultChartJsRuntime(chart, labels, fontColor);\n config.type = chart.type.replace(\"odoo_\", \"\");\n const legend = {\n ...config.options.legend,\n display: chart.legendPosition !== \"none\",\n labels: { fontColor },\n };\n legend.position = chart.legendPosition;\n config.options.legend = legend;\n config.options.layout = {\n padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n };\n config.options.tooltips = {\n callbacks: {\n title: function (tooltipItems, data) {\n return data.datasets[tooltipItems[0].datasetIndex].label;\n },\n },\n };\n return config;\n}\n\nfunction getPieColors(colors, dataSetsValues) {\n const pieColors = [];\n const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));\n for (let i = 0; i <= maxLength; i++) {\n pieColors.push(colors.next());\n }\n\n return pieColors;\n}\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { useService } from \"@web/core/utils/hooks\";\n\npatch(spreadsheet.components.ChartFigure.prototype, \"spreadsheet.ChartFigure\", {\n setup() {\n this._super();\n this.menuService = useService(\"menu\");\n this.actionService = useService(\"action\");\n },\n async navigateToOdooMenu() {\n const menu = this.env.model.getters.getChartOdooMenu(this.props.figure.id);\n if (!menu) {\n throw new Error(`Cannot find any menu associated with the chart`);\n }\n await this.actionService.doAction(menu.actionID);\n },\n get hasOdooMenu() {\n return this.env.model.getters.getChartOdooMenu(this.props.figure.id) !== undefined;\n },\n async onClick() {\n if (this.env.isDashboard() && this.hasOdooMenu) {\n this.navigateToOdooMenu();\n }\n },\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { coreTypes } = spreadsheet;\n\n/** Plugin that link charts with Odoo menus. It can contain either the Id of the odoo menu, or its xml id. */\nexport default class ChartOdooMenuPlugin extends spreadsheet.CorePlugin {\n constructor(config) {\n super(config);\n this.odooMenuReference = {};\n }\n\n /**\n * Handle a spreadsheet command\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"LINK_ODOO_MENU_TO_CHART\":\n this.history.update(\"odooMenuReference\", cmd.chartId, cmd.odooMenuId);\n break;\n case \"DELETE_FIGURE\":\n this.history.update(\"odooMenuReference\", cmd.id, undefined);\n break;\n }\n }\n\n /**\n * Get odoo menu linked to the chart\n *\n * @param {string} chartId\n * @returns {object | undefined}\n */\n getChartOdooMenu(chartId) {\n const menuId = this.odooMenuReference[chartId];\n return menuId ? this.getters.getIrMenu(menuId) : undefined;\n }\n\n import(data) {\n if (data.chartOdooMenusReferences) {\n this.odooMenuReference = data.chartOdooMenusReferences;\n }\n }\n\n export(data) {\n data.chartOdooMenusReferences = this.odooMenuReference;\n }\n}\nChartOdooMenuPlugin.getters = [\"getChartOdooMenu\"];\n\ncoreTypes.add(\"LINK_ODOO_MENU_TO_CHART\");\n", "/** @odoo-module */\nimport spreadsheet from \"../../o_spreadsheet/o_spreadsheet_extended\";\nimport ChartDataSource from \"../data_source/chart_data_source\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { checkFilterFieldMatching } from \"@spreadsheet/global_filters/helpers\";\nimport CommandResult from \"../../o_spreadsheet/cancelled_reason\";\n\nconst { CorePlugin } = spreadsheet;\n\n/**\n * @typedef {Object} Chart\n * @property {string} dataSourceId\n * @property {Object} fieldMatching\n *\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").FieldMatching} FieldMatching\n */\n\nexport default class OdooChartCorePlugin extends CorePlugin {\n constructor(config) {\n super(config);\n this.dataSources = config.custom.dataSources;\n\n /** @type {Object.} */\n this.charts = {};\n\n globalFiltersFieldMatchers[\"chart\"] = {\n geIds: () => this.getters.getOdooChartIds(),\n getDisplayName: (chartId) => this.getters.getOdooChartDisplayName(chartId),\n getTag: async (chartId) => {\n const model = await this.getChartDataSource(chartId).getModelLabel();\n return sprintf(_t(\"Chart - %s\"), model);\n },\n getFieldMatching: (chartId, filterId) =>\n this.getOdooChartFieldMatching(chartId, filterId),\n waitForReady: () => this.getOdooChartsWaitForReady(),\n getModel: (chartId) =>\n this.getters.getChart(chartId).getDefinitionForDataSource().metaData.resModel,\n getFields: (chartId) => this.getChartDataSource(chartId).getFields(),\n };\n }\n\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n if (cmd.chart) {\n return checkFilterFieldMatching(cmd.chart);\n }\n }\n return CommandResult.Success;\n }\n\n /**\n * Handle a spreadsheet command\n *\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"CREATE_CHART\": {\n switch (cmd.definition.type) {\n case \"odoo_pie\":\n case \"odoo_bar\":\n case \"odoo_line\":\n this._addOdooChart(cmd.id);\n break;\n }\n break;\n }\n case \"UPDATE_CHART\": {\n switch (cmd.definition.type) {\n case \"odoo_pie\":\n case \"odoo_bar\":\n case \"odoo_line\":\n this._setChartDataSource(cmd.id);\n break;\n }\n break;\n }\n case \"DELETE_FIGURE\": {\n const charts = { ...this.charts };\n delete charts[cmd.id];\n this.history.update(\"charts\", charts);\n break;\n }\n case \"REMOVE_GLOBAL_FILTER\":\n this._onFilterDeletion(cmd.id);\n break;\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n if (cmd.chart) {\n this._setOdooChartFieldMatching(cmd.filter.id, cmd.chart);\n }\n break;\n }\n }\n\n // -------------------------------------------------------------------------\n // Getters\n // -------------------------------------------------------------------------\n\n /**\n * Get all the odoo chart ids\n * @returns {Array}\n */\n getOdooChartIds() {\n const ids = [];\n for (const sheetId of this.getters.getSheetIds()) {\n ids.push(\n ...this.getters\n .getChartIds(sheetId)\n .filter((id) => this.getters.getChartType(id).startsWith(\"odoo_\"))\n );\n }\n return ids;\n }\n\n /**\n * @param {string} chartId\n * @returns {string}\n */\n getChartFieldMatch(chartId) {\n return this.charts[chartId].fieldMatching;\n }\n\n /**\n * @param {string} id\n * @returns {ChartDataSource|undefined}\n */\n getChartDataSource(id) {\n const dataSourceId = this.charts[id].dataSourceId;\n return this.dataSources.get(dataSourceId);\n }\n\n /**\n *\n * @param {string} chartId\n * @returns {string}\n */\n getOdooChartDisplayName(chartId) {\n return this.getters.getChart(chartId).title;\n }\n\n /**\n * Import the pivots\n *\n * @param {Object} data\n */\n import(data) {\n for (const sheet of data.sheets) {\n if (sheet.figures) {\n for (const figure of sheet.figures) {\n if (figure.tag === \"chart\" && figure.data.type.startsWith(\"odoo_\")) {\n this._addOdooChart(figure.id, figure.data.fieldMatching);\n }\n }\n }\n }\n }\n /**\n * Export the pivots\n *\n * @param {Object} data\n */\n export(data) {\n for (const sheet of data.sheets) {\n if (sheet.figures) {\n for (const figure of sheet.figures) {\n if (figure.tag === \"chart\" && figure.data.type.startsWith(\"odoo_\")) {\n figure.data.fieldMatching = this.getChartFieldMatch(figure.id);\n }\n }\n }\n }\n }\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n /**\n *\n * @return {Promise[]}\n */\n getOdooChartsWaitForReady() {\n return this.getOdooChartIds().map((chartId) =>\n this.getChartDataSource(chartId).loadMetadata()\n );\n }\n\n /**\n * Get the current pivotFieldMatching of a chart\n *\n * @param {string} chartId\n * @param {string} filterId\n */\n getOdooChartFieldMatching(chartId, filterId) {\n return this.charts[chartId].fieldMatching[filterId];\n }\n\n /**\n * Sets the current pivotFieldMatching of a chart\n *\n * @param {string} filterId\n * @param {Record} chartFieldMatches\n */\n _setOdooChartFieldMatching(filterId, chartFieldMatches) {\n const charts = { ...this.charts };\n for (const [chartId, fieldMatch] of Object.entries(chartFieldMatches)) {\n charts[chartId].fieldMatching[filterId] = fieldMatch;\n }\n this.history.update(\"charts\", charts);\n }\n\n _onFilterDeletion(filterId) {\n const charts = { ...this.charts };\n for (const chartId in charts) {\n this.history.update(\"charts\", chartId, \"fieldMatching\", filterId, undefined);\n }\n }\n\n /**\n * @param {string} chartId\n * @param {string} dataSourceId\n */\n _addOdooChart(chartId, fieldMatching = {}) {\n const dataSourceId = this.uuidGenerator.uuidv4();\n const charts = { ...this.charts };\n charts[chartId] = {\n dataSourceId,\n fieldMatching,\n };\n const definition = this.getters.getChart(chartId).getDefinitionForDataSource();\n if (!this.dataSources.contains(dataSourceId)) {\n this.dataSources.add(dataSourceId, ChartDataSource, definition);\n }\n this.history.update(\"charts\", charts);\n this._setChartDataSource(chartId);\n }\n\n /**\n * Sets the catasource on the corresponding chart\n * @param {string} chartId\n */\n _setChartDataSource(chartId) {\n const chart = this.getters.getChart(chartId);\n chart.setDataSource(this.getters.getChartDataSource(chartId));\n }\n}\n\nOdooChartCorePlugin.getters = [\n \"getChartDataSource\",\n \"getOdooChartIds\",\n \"getChartFieldMatch\",\n \"getOdooChartDisplayName\",\n \"getOdooChartFieldMatching\",\n];\n", "/** @odoo-module */\n\nimport spreadsheet from \"../../o_spreadsheet/o_spreadsheet_extended\";\nimport { Domain } from \"@web/core/domain\";\n\nconst { UIPlugin } = spreadsheet;\n\nexport default class OdooChartUIPlugin extends UIPlugin {\n beforeHandle(cmd) {\n switch (cmd.type) {\n case \"START\":\n // make sure the domains are correctly set before\n // any evaluation\n this._addDomains();\n break;\n }\n }\n\n /**\n * Handle a spreadsheet command\n *\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n case \"REMOVE_GLOBAL_FILTER\":\n case \"SET_GLOBAL_FILTER_VALUE\":\n case \"CLEAR_GLOBAL_FILTER_VALUE\":\n this._addDomains();\n break;\n case \"UNDO\":\n case \"REDO\":\n if (\n cmd.commands.find((command) =>\n [\n \"ADD_GLOBAL_FILTER\",\n \"EDIT_GLOBAL_FILTER\",\n \"REMOVE_GLOBAL_FILTER\",\n ].includes(command.type)\n )\n ) {\n this._addDomains();\n }\n break;\n }\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n /**\n * Add an additional domain to a chart\n *\n * @private\n *\n * @param {string} chartId chart id\n */\n _addDomain(chartId) {\n const domainList = [];\n for (const [filterId, fieldMatch] of Object.entries(\n this.getters.getChartFieldMatch(chartId)\n )) {\n domainList.push(this.getters.getGlobalFilterDomain(filterId, fieldMatch));\n }\n const domain = Domain.combine(domainList, \"AND\").toString();\n this.getters.getChartDataSource(chartId).addDomain(domain);\n }\n\n /**\n * Add an additional domain to all chart\n *\n * @private\n *\n */\n _addDomains() {\n for (const chartId of this.getters.getOdooChartIds()) {\n this._addDomain(chartId);\n }\n }\n}\n\nOdooChartUIPlugin.getters = [];\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { inverseCommandRegistry, otRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n return [cmd];\n}\n\notRegistry.addTransformation(\n \"DELETE_FIGURE\",\n [\"LINK_ODOO_MENU_TO_CHART\"],\n (toTransform, executed) => {\n if (executed.id === toTransform.chartId) {\n return undefined;\n }\n return toTransform;\n }\n);\n\ninverseCommandRegistry.add(\"LINK_ODOO_MENU_TO_CHART\", identity);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ServerData } from \"../data_sources/server_data\";\n\n/**\n * @typedef Currency\n * @property {string} name\n * @property {string} code\n * @property {string} symbol\n * @property {number} decimalPlaces\n * @property {\"before\" | \"after\"} position\n */\nexport class CurrencyDataSource {\n constructor(services) {\n this.serverData = new ServerData(services.orm, {\n whenDataIsFetched: () => services.notify(),\n });\n }\n\n /**\n * Get the currency rate between the two given currencies\n * @param {string} from Currency from\n * @param {string} to Currency to\n * @param {string|undefined} date\n * @returns {number|undefined}\n */\n getCurrencyRate(from, to, date) {\n const data = this.serverData.batch.get(\"res.currency.rate\", \"get_rates_for_spreadsheet\", {\n from,\n to,\n date,\n });\n const rate = data !== undefined ? data.rate : undefined;\n if (rate === false) {\n throw new Error(_t(\"Currency rate unavailable.\"));\n }\n return rate;\n }\n\n /**\n *\n * @param {number|undefined} companyId\n * @returns {Currency}\n */\n getCompanyCurrencyFormat(companyId) {\n const result = this.serverData.get(\"res.currency\", \"get_company_currency_for_spreadsheet\", [\n companyId,\n ]);\n if (result === false) {\n throw new Error(_t(\"Currency not available for this company.\"));\n }\n return result;\n }\n\n /**\n * Get all currencies from the server\n * @param {string} currencyName\n * @returns {Currency}\n */\n getCurrency(currencyName) {\n return this.serverData.batch.get(\n \"res.currency\",\n \"get_currencies_for_spreadsheet\",\n currencyName\n );\n }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport spreadsheet from \"../o_spreadsheet/o_spreadsheet_extended\";\nconst { args, toString, toJSDate } = spreadsheet.helpers;\nconst { functionRegistry } = spreadsheet.registries;\n\n\nfunctionRegistry\n .add(\"ODOO.CURRENCY.RATE\", {\n description: _t(\"This function takes in two currency codes as arguments, and returns the exchange rate from the first currency to the second as float.\"),\n compute: function (currencyFrom, currencyTo, date) {\n const from = toString(currencyFrom);\n const to = toString(currencyTo);\n const _date = date ? toJSDate(date) : undefined;\n return this.getters.getCurrencyRate(from, to, _date);\n },\n args: args(`\n currency_from (string) ${_t(\"First currency code.\")}\n currency_to (string) ${_t(\"Second currency code.\")}\n date (date, optional) ${_t(\"Date of the rate.\")}\n `),\n returns: [\"NUMBER\"],\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"../../o_spreadsheet/o_spreadsheet_extended\";\nimport { CurrencyDataSource } from \"../currency_data_source\";\nconst { featurePluginRegistry } = spreadsheet.registries;\n\nconst DATA_SOURCE_ID = \"CURRENCIES\";\n\n/**\n * @typedef {import(\"../currency_data_source\").Currency} Currency\n */\n\nclass CurrencyPlugin extends spreadsheet.UIPlugin {\n constructor(config) {\n super(config);\n this.dataSources = config.custom.dataSources;\n if (this.dataSources) {\n this.dataSources.add(DATA_SOURCE_ID, CurrencyDataSource);\n }\n }\n\n // -------------------------------------------------------------------------\n // Getters\n // -------------------------------------------------------------------------\n\n /**\n * Get the currency rate between the two given currencies\n * @param {string} from Currency from\n * @param {string} to Currency to\n * @param {string} date\n * @returns {number|string}\n */\n getCurrencyRate(from, to, date) {\n return (\n this.dataSources && this.dataSources.get(DATA_SOURCE_ID).getCurrencyRate(from, to, date)\n );\n }\n\n /**\n *\n * @param {Currency | undefined} currency\n * @private\n *\n * @returns {string | undefined}\n */\n computeFormatFromCurrency(currency) {\n if (!currency) {\n return undefined;\n }\n const decimalFormatPart = currency.decimalPlaces\n ? \".\" + \"0\".repeat(currency.decimalPlaces)\n : \"\";\n const numberFormat = \"#,##0\" + decimalFormatPart;\n const symbolFormatPart = \"[$\" + currency.symbol + \"]\";\n return currency.position === \"after\"\n ? numberFormat + symbolFormatPart\n : symbolFormatPart + numberFormat;\n }\n\n /**\n * Returns the default display format of a given currency\n * @param {string} currencyName\n * @returns {string | undefined}\n */\n getCurrencyFormat(currencyName) {\n const currency =\n currencyName &&\n this.dataSources &&\n this.dataSources.get(DATA_SOURCE_ID).getCurrency(currencyName);\n return this.computeFormatFromCurrency(currency);\n }\n\n /**\n * Returns the default display format of a the company currency\n * @param {number|undefined} companyId\n * @returns {string | undefined}\n */\n getCompanyCurrencyFormat(companyId) {\n const currency =\n this.dataSources &&\n this.dataSources.get(DATA_SOURCE_ID).getCompanyCurrencyFormat(companyId);\n return this.computeFormatFromCurrency(currency);\n }\n}\n\nCurrencyPlugin.getters = [\"getCurrencyRate\", \"getCurrencyFormat\", \"getCompanyCurrencyFormat\"];\n\nfeaturePluginRegistry.add(\"odooCurrency\", CurrencyPlugin);\n", "/** @odoo-module */\n\nimport { LoadingDataError } from \"@spreadsheet/o_spreadsheet/errors\";\nimport { RPCError } from \"@web/core/network/rpc_service\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\n\n/**\n * DataSource is an abstract class that contains the logic of fetching and\n * maintaining access to data that have to be loaded.\n *\n * A class which extends this class have to implement the `_load` method\n * * which should load the data it needs\n *\n * Subclass can implement concrete methods to have access to a\n * particular data.\n */\nexport class LoadableDataSource {\n constructor(services) {\n this._orm = services.orm;\n this._metadataRepository = services.metadataRepository;\n this._notify = services.notify;\n\n /**\n * Last time that this dataSource has been updated\n */\n this._lastUpdate = undefined;\n\n this._concurrency = new KeepLast();\n /**\n * Promise to control the loading of data\n */\n this._loadPromise = undefined;\n this._isFullyLoaded = false;\n this._isValid = true;\n this._loadErrorMessage = \"\";\n }\n\n /**\n * Load data in the model\n * @param {object} [params] Params for fetching data\n * @param {boolean} [params.reload=false] Force the reload of the data\n *\n * @returns {Promise} Resolved when data are fetched.\n */\n async load(params) {\n if (params && params.reload) {\n this._loadPromise = undefined;\n }\n if (!this._loadPromise) {\n this._isFullyLoaded = false;\n this._isValid = true;\n this._loadErrorMessage = \"\";\n this._loadPromise = this._concurrency\n .add(this._load())\n .catch((e) => {\n this._isValid = false;\n this._loadErrorMessage = e instanceof RPCError ? e.data.message : e.message;\n })\n .finally(() => {\n this._lastUpdate = Date.now();\n this._isFullyLoaded = true;\n this._notify();\n });\n }\n return this._loadPromise;\n }\n\n get lastUpdate() {\n return this._lastUpdate;\n }\n\n /**\n * @returns {boolean}\n */\n isReady() {\n return this._isFullyLoaded;\n }\n\n /**\n * @protected\n */\n _assertDataIsLoaded() {\n if (!this._isFullyLoaded) {\n this.load();\n throw new LoadingDataError();\n }\n if (!this._isValid) {\n throw new Error(this._loadErrorMessage);\n }\n }\n\n /**\n * Load the data in the model\n *\n * @abstract\n * @protected\n */\n async _load() {}\n}\n", "/** @odoo-module */\n\nimport { LoadableDataSource } from \"./data_source\";\nimport { MetadataRepository } from \"./metadata_repository\";\n\nconst { EventBus } = owl;\n\n/** *\n * @typedef {object} DataSourceServices\n * @property {MetadataRepository} metadataRepository\n * @property {import(\"@web/core/orm_service\")} orm\n * @property {() => void} notify\n *\n * @typedef {new (services: DataSourceServices, params: object) => any} DataSourceConstructor\n */\n\nexport class DataSources extends EventBus {\n constructor(orm) {\n super();\n this._orm = orm.silent;\n this._metadataRepository = new MetadataRepository(orm);\n this._metadataRepository.addEventListener(\"labels-fetched\", () => this.notify());\n /** @type {Object.} */\n this._dataSources = {};\n }\n\n /**\n * Create a new data source but do not register it.\n *\n * @param {DataSourceConstructor} cls Class to instantiate\n * @param {object} params Params to give to data source\n *\n * @returns {any}\n */\n create(cls, params) {\n return new cls(\n {\n orm: this._orm,\n metadataRepository: this._metadataRepository,\n notify: () => this.notify(),\n },\n params\n );\n }\n\n /**\n * Create a new data source and register it with the following id.\n *\n * @param {string} id\n * @param {DataSourceConstructor} cls Class to instantiate\n * @param {object} params Params to give to data source\n *\n * @returns {any}\n */\n add(id, cls, params) {\n this._dataSources[id] = this.create(cls, params);\n return this._dataSources[id];\n }\n\n async load(id, reload = false) {\n const dataSource = this.get(id);\n if (dataSource instanceof LoadableDataSource) {\n await dataSource.load({ reload });\n }\n }\n\n /**\n * Retrieve the data source with the following id.\n *\n * @param {string} id\n *\n * @returns {any}\n */\n get(id) {\n return this._dataSources[id];\n }\n\n /**\n * Check if the following is correspond to a data source.\n *\n * @param {string} id\n *\n * @returns {boolean}\n */\n contains(id) {\n return id in this._dataSources;\n }\n\n /**\n * Notify that a data source has been updated. Could be useful to\n * request a re-evaluation.\n */\n notify() {\n this.trigger(\"data-source-updated\");\n }\n\n async waitForAllLoaded() {\n await Promise.all(\n Object.values(this._dataSources).map(\n (ds) => ds instanceof LoadableDataSource && ds.load()\n )\n );\n }\n}\n", "/** @odoo-module */\n\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { LoadingDataError } from \"../o_spreadsheet/errors\";\nimport BatchEndpoint, { Request } from \"./server_data\";\n\n/**\n * @typedef PendingDisplayName\n * @property {\"PENDING\"} state\n * @property {Deferred} deferred\n *\n * @typedef ErrorDisplayName\n * @property {\"ERROR\"} state\n * @property {Deferred} deferred\n * @property {Error} error\n *\n * @typedef CompletedDisplayName\n * @property {\"COMPLETED\"} state\n * @property {Deferred} deferred\n * @property {string|undefined} value\n *\n * @typedef {PendingDisplayName | ErrorDisplayName | CompletedDisplayName} DisplayNameResult\n *\n * @typedef {[number, string]} BatchedNameGetRPCResult\n */\n\n/**\n * This class is responsible for fetching the display names of records. It\n * caches the display names of records that have already been fetched.\n * It also provides a way to wait for the display name of a record to be\n * fetched.\n */\nexport class DisplayNameRepository {\n /**\n *\n * @param {import(\"@web/core/orm_service\").ORM} orm\n * @param {Object} params\n * @param {function} params.whenDataIsFetched Callback to call when the\n * display name of a record is fetched.\n */\n constructor(orm, { whenDataIsFetched }) {\n this.dataFetchedCallback = whenDataIsFetched;\n /**\n * Contains the display names of records. It's organized in the following way:\n * {\n * \"res.country\": {\n * 1: {\n * \"value\": \"Belgium\",\n * \"deferred\": Deferred<\"Belgium\">,\n * },\n * }\n */\n /** @type {Object.>}*/\n this._displayNames = {};\n this._orm = orm;\n this._endpoints = {};\n }\n\n /**\n * Get the display name of the given record.\n *\n * @param {string} model\n * @param {number} id\n * @returns {Promise}\n */\n async getDisplayNameAsync(model, id) {\n const displayNameResult = this._displayNames[model] && this._displayNames[model][id];\n if (!displayNameResult) {\n return this._fetchDisplayName(model, id);\n }\n return displayNameResult.deferred;\n }\n\n /**\n * Set the display name of the given record. This will prevent the display name\n * from being fetched in the background.\n *\n * @param {string} model\n * @param {number} id\n * @param {string} displayName\n */\n setDisplayName(model, id, displayName) {\n if (!this._displayNames[model]) {\n this._displayNames[model] = {};\n }\n const deferred = new Deferred();\n deferred.resolve(displayName);\n this._displayNames[model][id] = {\n state: \"COMPLETED\",\n deferred,\n value: displayName,\n };\n }\n\n /**\n * Get the display name of the given record. If the record does not exist,\n * it will throw a LoadingDataError and fetch the display name in the background.\n *\n * @param {string} model\n * @param {number} id\n * @returns {string}\n */\n getDisplayName(model, id) {\n const displayNameResult = this._displayNames[model] && this._displayNames[model][id];\n if (!displayNameResult) {\n // Catch the error to prevent the error from being thrown in the\n // background.\n this._fetchDisplayName(model, id).catch(() => {});\n throw new LoadingDataError();\n }\n switch (displayNameResult.state) {\n case \"ERROR\":\n throw displayNameResult.error;\n case \"COMPLETED\":\n return displayNameResult.value;\n default:\n throw new LoadingDataError();\n }\n }\n\n /**\n * Get the batch endpoint for the given model. If it does not exist, it will\n * be created.\n *\n * @param {string} model\n * @returns {BatchEndpoint}\n */\n _getEndpoint(model) {\n if (!this._endpoints[model]) {\n this._endpoints[model] = new BatchEndpoint(this._orm, model, \"name_get\", {\n whenDataIsFetched: () => this.dataFetchedCallback(),\n successCallback: this._assignResult.bind(this),\n failureCallback: this._assignError.bind(this),\n });\n }\n return this._endpoints[model];\n }\n\n /**\n * This method is called when the display name of a record is successfully\n * fetched. It updates the cache and resolves the deferred of the record.\n *\n * @param {Request} request\n * @param {BatchedNameGetRPCResult} result\n *\n * @private\n */\n _assignResult(request, result) {\n const deferred = this._displayNames[request.resModel][request.args[0]].deferred;\n deferred.resolve(result && result[1]);\n this._displayNames[request.resModel][request.args[0]] = {\n state: \"COMPLETED\",\n deferred,\n value: result && result[1],\n };\n }\n\n /**\n * This method is called when the display name of a record could not be\n * fetched. It updates the cache and rejects the deferred of the record.\n *\n * @param {Request} request\n * @param {Error} error\n *\n * @private\n */\n _assignError(request, error) {\n const deferred = this._displayNames[request.resModel][request.args[0]].deferred;\n deferred.reject(error);\n this._displayNames[request.resModel][request.args[0]] = {\n state: \"ERROR\",\n deferred,\n error,\n };\n }\n\n /**\n * This method is called when the display name of a record is not in the\n * cache. It creates a deferred and fetches the display name in the\n * background.\n *\n * @param {string} model\n * @param {number} id\n *\n * @private\n * @returns {Deferred}\n */\n async _fetchDisplayName(model, id) {\n const deferred = new Deferred();\n if (!this._displayNames[model]) {\n this._displayNames[model] = {};\n }\n this._displayNames[model][id] = {\n state: \"PENDING\",\n deferred,\n };\n const endpoint = this._getEndpoint(model);\n const request = new Request(model, \"name_get\", [id]);\n endpoint.call(request);\n return deferred;\n }\n}\n", "/** @odoo-module */\n\n/**\n * This class is responsible for keeping track of the labels of records. It\n * caches the labels of records that have already been fetched.\n * This class will not fetch the labels of records, it is the responsibility of\n * the caller to fetch the labels and insert them in this repository.\n */\nexport class LabelsRepository {\n constructor() {\n /**\n * Contains the labels of records. It's organized in the following way:\n * {\n * \"crm.lead\": {\n * \"city\": {\n * \"bruxelles\": \"Bruxelles\",\n * }\n * },\n * }\n */\n this._labels = {};\n }\n\n /**\n * Get the label of a record.\n * @param {string} model technical name of the model\n * @param {string} field name of the field\n * @param {any} value value of the field\n *\n * @returns {string|undefined} label of the record\n */\n getLabel(model, field, value) {\n return (\n this._labels[model] && this._labels[model][field] && this._labels[model][field][value]\n );\n }\n\n /**\n * Set the label of a record.\n * @param {string} model\n * @param {string} field\n * @param {string|number} value\n * @param {string|undefined} label\n */\n setLabel(model, field, value, label) {\n if (!this._labels[model]) {\n this._labels[model] = {};\n }\n if (!this._labels[model][field]) {\n this._labels[model][field] = {};\n }\n this._labels[model][field][value] = label;\n }\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { ServerData } from \"../data_sources/server_data\";\n\nimport { LoadingDataError } from \"../o_spreadsheet/errors\";\nimport { DisplayNameRepository } from \"./display_name_repository\";\nimport { LabelsRepository } from \"./labels_repository\";\n\nconst { EventBus } = owl;\n\n/**\n * @typedef {object} Field\n * @property {string} name technical name\n * @property {string} type field type\n * @property {string} string display name\n * @property {string} [relation] related model technical name (only for relational fields)\n * @property {boolean} [searchable] true if a field can be searched in database\n */\n/**\n * This class is used to provide facilities to fetch some common data. It's\n * used in the data sources to obtain the fields (fields_get) and the display\n * name of the models (display_name_for on ir.model).\n *\n * It also manages the labels of all the spreadsheet models (labels of basic\n * fields or display name of relational fields).\n *\n * All the results are cached in order to avoid useless rpc calls, basically\n * for different entities that are defined on the same model.\n *\n * Implementation note:\n * For the labels, when someone is asking for a display name which is not loaded yet,\n * the proxy returns directly (undefined) and a request for a name_get will\n * be triggered. All the requests created are batched and send, with only one\n * request per model, after a clock cycle.\n * At the end of this process, an event is triggered (labels-fetched)\n */\nexport class MetadataRepository extends EventBus {\n constructor(orm) {\n super();\n this.orm = orm;\n\n this.serverData = new ServerData(this.orm, {\n whenDataIsFetched: () => this.trigger(\"labels-fetched\"),\n });\n\n this.labelsRepository = new LabelsRepository();\n\n this.displayNameRepository = new DisplayNameRepository(this.orm, {\n whenDataIsFetched: () => this.trigger(\"labels-fetched\"),\n });\n }\n\n /**\n * Get the display name of the given model\n *\n * @param {string} model Technical name\n * @returns {Promise} Display name of the model\n */\n async modelDisplayName(model) {\n const result = await this.serverData.fetch(\"ir.model\", \"display_name_for\", [[model]]);\n return (result[0] && result[0].display_name) || \"\";\n }\n\n /**\n * Get the list of fields for the given model\n *\n * @param {string} model Technical name\n * @returns {Promise>} List of fields (result of fields_get)\n */\n async fieldsGet(model) {\n return this.serverData.fetch(model, \"fields_get\");\n }\n\n /**\n * Add a label to the cache\n *\n * @param {string} model\n * @param {string} field\n * @param {any} value\n * @param {string} label\n */\n registerLabel(model, field, value, label) {\n this.labelsRepository.setLabel(model, field, value, label);\n }\n\n /**\n * Get the label associated with the given arguments\n *\n * @param {string} model\n * @param {string} field\n * @param {any} value\n * @returns {string}\n */\n getLabel(model, field, value) {\n return this.labelsRepository.getLabel(model, field, value);\n }\n\n /**\n * Save the result of a name_get request in the cache\n */\n setDisplayName(model, id, result) {\n this.displayNameRepository.setDisplayName(model, id, result);\n }\n\n /**\n * Get the display name associated to the given model-id\n * If the name is not yet loaded, a rpc will be triggered in the next clock\n * cycle.\n *\n * @param {string} model\n * @param {number} id\n * @returns {string}\n */\n getRecordDisplayName(model, id) {\n try {\n return this.displayNameRepository.getDisplayName(model, id);\n } catch (e) {\n if (e instanceof LoadingDataError) {\n throw e;\n }\n throw new Error(sprintf(_t(\"Unable to fetch the label of %s of model %s\"), id, model));\n }\n }\n}\n", "/** @odoo-module */\n\nimport { LoadableDataSource } from \"./data_source\";\nimport { Domain } from \"@web/core/domain\";\nimport { LoadingDataError } from \"@spreadsheet/o_spreadsheet/errors\";\nimport { omit } from \"@web/core/utils/objects\";\n\n/**\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n */\n\n/**\n * @typedef {Object} OdooModelMetaData\n * @property {string} resModel\n * @property {Array|undefined} fields\n */\n\nexport class OdooViewsDataSource extends LoadableDataSource {\n /**\n * @override\n * @param {Object} services\n * @param {Object} params\n * @param {OdooModelMetaData} params.metaData\n * @param {Object} params.searchParams\n */\n constructor(services, params) {\n super(services);\n this._metaData = JSON.parse(JSON.stringify(params.metaData));\n /** @protected */\n this._initialSearchParams = JSON.parse(JSON.stringify(params.searchParams));\n this._initialSearchParams.context = omit(\n this._initialSearchParams.context || {},\n ...Object.keys(this._orm.user.context)\n );\n /** @private */\n this._customDomain = this._initialSearchParams.domain;\n }\n\n /**\n * @protected\n */\n get _searchParams() {\n return {\n ...this._initialSearchParams,\n domain: this._customDomain,\n };\n }\n\n async loadMetadata() {\n if (!this._metaData.fields) {\n this._metaData.fields = await this._metadataRepository.fieldsGet(\n this._metaData.resModel\n );\n }\n }\n\n /**\n * @returns {Record} List of fields\n */\n getFields() {\n if (this._metaData.fields === undefined) {\n this.loadMetadata();\n throw new LoadingDataError();\n }\n return this._metaData.fields;\n }\n\n /**\n * @param {string} field Field name\n * @returns {Field | undefined} Field\n */\n getField(field) {\n return this._metaData.fields[field];\n }\n\n /**\n * @protected\n */\n async _load() {\n await this.loadMetadata();\n }\n\n isMetaDataLoaded() {\n return this._metaData.fields !== undefined;\n }\n\n /**\n * Get the computed domain of this source\n * @returns {Array}\n */\n getComputedDomain() {\n return this._customDomain;\n }\n\n addDomain(domain) {\n const newDomain = Domain.and([this._initialSearchParams.domain, domain]);\n if (newDomain.toString() === new Domain(this._customDomain).toString()) {\n return;\n }\n this._customDomain = newDomain.toList();\n if (this._loadPromise === undefined) {\n // if the data source has never been loaded, there's no point\n // at reloading it now.\n return;\n }\n this.load({ reload: true });\n }\n\n /**\n * @returns {Promise} Display name of the model\n */\n getModelLabel() {\n return this._metadataRepository.modelDisplayName(this._metaData.resModel);\n }\n}\n", "/** @odoo-module */\nimport { LoadingDataError } from \"../o_spreadsheet/errors\";\n\n/**\n * @param {T[]} array\n * @returns {T[]}\n * @template T\n */\nfunction removeDuplicates(array) {\n return [...new Set(array.map((el) => JSON.stringify(el)))].map((el) => JSON.parse(el));\n}\n\nexport class Request {\n /**\n * @param {string} resModel\n * @param {string} method\n * @param {unknown[]} args\n */\n constructor(resModel, method, args) {\n this.resModel = resModel;\n this.method = method;\n this.args = args;\n this.key = `${resModel}/${method}(${JSON.stringify(args)})`;\n }\n}\n\n/**\n * A batch request consists of multiple requests which are combined into a single RPC.\n *\n * The batch responsibility is to combine individual requests into a single RPC payload\n * and to split the response back for individual requests.\n *\n * The server method must have the following API:\n * - The input is a list of arguments. Each list item being the arguments of a single request.\n * - The output is a list of results, ordered according to the input list\n *\n * ```\n * [result1, result2] = self.env['my.model'].my_batched_method([request_1_args, request_2_args])\n * ```\n */\nclass ListRequestBatch {\n /**\n * @param {string} resModel\n * @param {string} method\n * @param {Request[]} requests\n */\n constructor(resModel, method, requests = []) {\n this.resModel = resModel;\n this.method = method;\n this.requests = requests;\n }\n\n get payload() {\n const payload = removeDuplicates(this.requests.map((request) => request.args).flat());\n return [payload];\n }\n\n /**\n * @param {Request} request\n */\n add(request) {\n if (request.resModel !== this.resModel || request.method !== this.method) {\n throw new Error(\n `Request ${request.resModel}/${request.method} cannot be added to the batch ${this.resModel}/${this.method}`\n );\n }\n this.requests.push(request);\n }\n\n /**\n * Split the batched RPC response into single request results\n *\n * @param {T[]} results\n * @returns {Map}\n * @template T\n */\n splitResponse(results) {\n const split = new Map();\n for (let i = 0; i < this.requests.length; i++) {\n split.set(this.requests[i], results[i]);\n }\n return split;\n }\n}\n\nexport class ServerData {\n /**\n * @param {any} orm\n * @param {object} params\n * @param {function} params.whenDataIsFetched\n */\n constructor(orm, { whenDataIsFetched }) {\n this.orm = orm;\n this.dataFetchedCallback = whenDataIsFetched;\n /** @type {Record}*/\n this.cache = {};\n /** @type {Record>}*/\n this.asyncCache = {};\n this.batchEndpoints = {};\n }\n\n /**\n * @returns {{get: (resModel:string, method: string, args: unknown) => any}}\n */\n get batch() {\n return { get: (resModel, method, args) => this._getBatchItem(resModel, method, args) };\n }\n\n /**\n * @private\n * @param {string} resModel\n * @param {string} method\n * @param {unknown} args\n * @returns {any}\n */\n _getBatchItem(resModel, method, args) {\n const request = new Request(resModel, method, [args]);\n if (!(request.key in this.cache)) {\n const error = new LoadingDataError();\n this.cache[request.key] = error;\n this._batch(request);\n throw error;\n }\n return this._getOrThrowCachedResponse(request);\n }\n\n /**\n * @param {string} resModel\n * @param {string} method\n * @param {unknown[]} args\n * @returns {any}}\n */\n get(resModel, method, args) {\n const request = new Request(resModel, method, args);\n if (!(request.key in this.cache)) {\n const error = new LoadingDataError();\n this.cache[request.key] = error;\n this.orm\n .call(resModel, method, args)\n .then((result) => (this.cache[request.key] = result))\n .catch((error) => (this.cache[request.key] = error))\n .finally(() => this.dataFetchedCallback());\n throw error;\n }\n return this._getOrThrowCachedResponse(request);\n }\n\n /**\n * Returns the request result if cached or the associated promise\n * @param {string} resModel\n * @param {string} method\n * @param {unknown[]} [args]\n * @returns {Promise}\n */\n async fetch(resModel, method, args) {\n const request = new Request(resModel, method, args);\n if (!(request.key in this.asyncCache)) {\n this.asyncCache[request.key] = this.orm.call(resModel, method, args);\n }\n return this.asyncCache[request.key];\n }\n\n /**\n * @private\n * @param {Request} request\n * @returns {void}\n */\n _batch(request) {\n const endpoint = this._getBatchEndPoint(request.resModel, request.method);\n endpoint.call(request);\n }\n\n /**\n * @private\n * @param {Request} request\n * @return {unknown}\n */\n _getOrThrowCachedResponse(request) {\n const data = this.cache[request.key];\n if (data instanceof Error) {\n throw data;\n }\n return data;\n }\n\n /**\n * @private\n * @param {string} resModel\n * @param {string} method\n */\n _getBatchEndPoint(resModel, method) {\n if (!this.batchEndpoints[resModel] || !this.batchEndpoints[resModel][method]) {\n this.batchEndpoints[resModel] = {\n ...this.batchEndpoints[resModel],\n [method]: this._createBatchEndpoint(resModel, method),\n };\n }\n return this.batchEndpoints[resModel][method];\n }\n\n /**\n * @private\n * @param {string} resModel\n * @param {string} method\n */\n _createBatchEndpoint(resModel, method) {\n return new BatchEndpoint(this.orm, resModel, method, {\n whenDataIsFetched: () => this.dataFetchedCallback(),\n successCallback: (request, result) => (this.cache[request.key] = result),\n failureCallback: (request, error) => (this.cache[request.key] = error),\n });\n }\n}\n\n/**\n * Collect multiple requests into a single batch.\n */\nexport default class BatchEndpoint {\n /**\n * @param {object} orm\n * @param {string} resModel\n * @param {string} method\n * @param {object} callbacks\n * @param {function} callbacks.successCallback\n * @param {function} callbacks.failureCallback\n * @param {function} callbacks.whenDataIsFetched\n */\n constructor(orm, resModel, method, { successCallback, failureCallback, whenDataIsFetched }) {\n this.orm = orm;\n this.resModel = resModel;\n this.method = method;\n this.successCallback = successCallback;\n this.failureCallback = failureCallback;\n this.batchedFetchedCallback = whenDataIsFetched;\n\n this._isScheduled = false;\n this._pendingBatch = new ListRequestBatch(resModel, method);\n }\n\n /**\n * @param {Request} request\n */\n call(request) {\n this._pendingBatch.add(request);\n this._scheduleNextBatch();\n }\n\n /**\n * @param {Map} batchResult\n * @private\n */\n _notifyResults(batchResult) {\n for (const [request, result] of batchResult) {\n if (result instanceof Error) {\n this.failureCallback(request, result);\n } else {\n this.successCallback(request, result);\n }\n }\n }\n\n /**\n * @private\n */\n _scheduleNextBatch() {\n if (this._isScheduled || this._pendingBatch.requests.length === 0) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(async () => {\n try {\n this._isScheduled = false;\n const batch = this._pendingBatch;\n const { resModel, method } = batch;\n this._pendingBatch = new ListRequestBatch(resModel, method);\n await this.orm\n .call(resModel, method, batch.payload)\n .then((result) => batch.splitResponse(result))\n .catch(() => this._retryOneByOne(batch))\n .then((batchResults) => this._notifyResults(batchResults));\n } finally {\n this.batchedFetchedCallback();\n }\n });\n }\n\n /**\n * @private\n * @param {ListRequestBatch} batch\n * @returns {Promise>}\n */\n async _retryOneByOne(batch) {\n const mergedResults = new Map();\n const { resModel, method } = batch;\n const singleRequestBatches = batch.requests.map(\n (request) => new ListRequestBatch(resModel, method, [request])\n );\n const proms = [];\n for (const batch of singleRequestBatches) {\n const request = batch.requests[0];\n const prom = this.orm\n .call(resModel, method, batch.payload)\n .then((result) =>\n mergedResults.set(request, batch.splitResponse(result).get(request))\n )\n .catch((error) => mergedResults.set(request, error));\n proms.push(prom);\n }\n await Promise.allSettled(proms);\n return mergedResults;\n }\n}\n", "/** @odoo-module */\n\nimport { YearPicker } from \"../year_picker\";\nimport { dateOptions } from \"@spreadsheet/global_filters/helpers\";\n\nconst { DateTime } = luxon;\nconst { Component, onWillUpdateProps } = owl;\n\nexport class DateFilterValue extends Component {\n setup() {\n this._setStateFromProps(this.props);\n onWillUpdateProps(this._setStateFromProps);\n }\n _setStateFromProps(props) {\n this.period = props.period;\n /** @type {number|undefined} */\n this.yearOffset = props.yearOffset;\n // date should be undefined if we don't have the yearOffset\n /** @type {DateTime|undefined} */\n this.date =\n this.yearOffset !== undefined\n ? DateTime.local().plus({ year: this.yearOffset })\n : undefined;\n }\n\n dateOptions(type) {\n return type ? dateOptions(type) : [];\n }\n\n isYear() {\n return this.props.type === \"year\";\n }\n\n isSelected(periodId) {\n return this.period === periodId;\n }\n\n /**\n * @param {Event & { target: HTMLSelectElement }} ev\n */\n onPeriodChanged(ev) {\n this.period = ev.target.value;\n this._updateFilter();\n }\n\n onYearChanged(date) {\n this.date = date;\n this.yearOffset = date.year - DateTime.now().year;\n this._updateFilter();\n }\n\n _updateFilter() {\n this.props.onTimeRangeChanged({\n yearOffset: this.yearOffset || 0,\n period: this.period,\n });\n }\n}\nDateFilterValue.template = \"spreadsheet_edition.DateFilterValue\";\nDateFilterValue.components = { YearPicker };\n\nDateFilterValue.props = {\n // See @spreadsheet_edition/bundle/global_filters/filters_plugin.RangeType\n type: { validate: (/**@type {string} */ t) => [\"year\", \"month\", \"quarter\"].includes(t) },\n onTimeRangeChanged: Function,\n yearOffset: { type: Number, optional: true },\n period: { type: String, optional: true },\n};\n", "/** @odoo-module */\n\nimport { RecordsSelector } from \"../records_selector/records_selector\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\nimport { DateFilterValue } from \"../filter_date_value/filter_date_value\";\n\nconst { Component } = owl;\n\nexport class FilterValue extends Component {\n setup() {\n this.getters = this.props.model.getters;\n this.relativeDateRangesTypes = RELATIVE_DATE_RANGE_TYPES;\n }\n onDateInput(id, value) {\n this.props.model.dispatch(\"SET_GLOBAL_FILTER_VALUE\", { id, value });\n }\n\n onTextInput(id, value) {\n this.props.model.dispatch(\"SET_GLOBAL_FILTER_VALUE\", { id, value });\n }\n\n onTagSelected(id, values) {\n this.props.model.dispatch(\"SET_GLOBAL_FILTER_VALUE\", {\n id,\n value: values.map((record) => record.id),\n displayNames: values.map((record) => record.display_name),\n });\n }\n\n onClear(id) {\n this.props.model.dispatch(\"CLEAR_GLOBAL_FILTER_VALUE\", { id });\n }\n}\nFilterValue.template = \"spreadsheet_edition.FilterValue\";\nFilterValue.components = { RecordsSelector, DateFilterValue };\nFilterValue.props = {\n filter: Object,\n model: Object,\n};\n", "/** @odoo-module **/\n\nimport { Domain } from \"@web/core/domain\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { TagsList } from \"@web/views/fields/many2many_tags/tags_list\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\n\nconst { Component, onWillStart, onWillUpdateProps } = owl;\n\nexport class RecordsSelector extends Component {\n setup() {\n /** @type {Record} */\n this.displayNames = {};\n /** @type {import(\"@web/core/orm_service\").ORM}*/\n this.orm = useService(\"orm\");\n onWillStart(() => this.fetchMissingDisplayNames(this.props.resModel, this.props.resIds));\n onWillUpdateProps((nextProps) =>\n this.fetchMissingDisplayNames(nextProps.resModel, nextProps.resIds)\n );\n }\n\n get tags() {\n return this.props.resIds.map((id) => ({\n text: this.displayNames[id],\n onDelete: () => this.removeRecord(id),\n displayBadge: true,\n }));\n }\n\n searchDomain() {\n return Domain.not([[\"id\", \"in\", this.props.resIds]]).toList();\n }\n\n /**\n * @param {number} recordId\n */\n removeRecord(recordId) {\n delete this.displayNames[recordId];\n this.notifyChange(this.props.resIds.filter((id) => id !== recordId));\n }\n\n /**\n * @param {{ id: number; name?: string}[]} records\n */\n update(records) {\n for (const record of records.filter((record) => record.name)) {\n this.displayNames[record.id] = record.name;\n }\n this.notifyChange(this.props.resIds.concat(records.map(({ id }) => id)));\n }\n\n /**\n * @param {number[]} selectedIds\n */\n notifyChange(selectedIds) {\n this.props.onValueChanged(\n selectedIds.map((id) => ({ id, display_name: this.displayNames[id] }))\n );\n }\n\n /**\n * @param {string} resModel\n * @param {number[]} recordIds\n */\n async fetchMissingDisplayNames(resModel, recordIds) {\n const missingNameIds = recordIds.filter((id) => !(id in this.displayNames));\n if (missingNameIds.length === 0) {\n return;\n }\n const results = await this.orm.read(resModel, missingNameIds, [\"display_name\"]);\n for (const { id, display_name } of results) {\n this.displayNames[id] = display_name;\n }\n }\n}\nRecordsSelector.components = { TagsList, Many2XAutocomplete };\nRecordsSelector.template = \"spreadsheet.RecordsSelector\";\nRecordsSelector.props = {\n /**\n * Callback called when a record is selected or removed.\n * (selectedRecords: Array<{ id: number; display_name: string }>) => void\n **/\n onValueChanged: Function,\n resModel: String,\n /**\n * Array of selected record ids\n */\n resIds: {\n optional: true,\n type: Array,\n },\n placeholder: {\n optional: true,\n type: String,\n },\n};\nRecordsSelector.defaultProps = {\n resIds: [],\n};\n", "/** @odoo-module */\n\nimport { DatePicker } from \"@web/core/datepicker/datepicker\";\nconst { DateTime } = luxon;\n\nconst DEFAULT_DATE = DateTime.local();\nexport class YearPicker extends DatePicker {\n /**\n * @override\n */\n initFormat() {\n super.initFormat();\n // moment.js format\n this.defaultFormat = \"yyyy\";\n this.staticFormat = \"yyyy\";\n }\n\n /**\n * @override\n */\n getOptions(useStatic = false) {\n return {\n format:\n !useStatic || this.isValidStaticFormat(this.format) ? this.format : this.staticFormat,\n locale: DEFAULT_DATE.locale,\n };\n }\n\n /**\n * @override\n */\n bootstrapDateTimePicker(commandOrParams) {\n if (typeof commandOrParams === \"object\") {\n const widgetParent = window.$(this.rootRef.el);\n commandOrParams = { ...commandOrParams, widgetParent };\n }\n super.bootstrapDateTimePicker(commandOrParams);\n }\n\n /**\n * @override\n */\n onWillUpdateProps(nextProps) {\n const pickerParams = {};\n let shouldUpdateInput = false;\n for (const prop in nextProps) {\n const prev = this.props[prop];\n const next = nextProps[prop];\n if (\n (prev instanceof DateTime && next instanceof DateTime && !prev.equals(next)) ||\n prev !== next\n ) {\n pickerParams[prop] = nextProps[prop];\n if (prop === \"date\") {\n this.setDateAndFormat(nextProps);\n shouldUpdateInput = true;\n }\n }\n }\n if (shouldUpdateInput) {\n this.updateInput();\n }\n this.bootstrapDateTimePicker(pickerParams);\n }\n /**\n * @override: allow displaying empty dates\n */\n updateInput({ useStatic } = {}) {\n const [formattedValue] = this.formatValue(this.date, this.getOptions(useStatic));\n this.inputRef.el.value = formattedValue || this.props.placeholder;\n }\n\n /**\n * @override\n */\n onDateChange({ useStatic } = {}) {\n const [date] = this.parseValue(this.inputRef.el.value, this.getOptions(useStatic));\n if (!date || (this.date instanceof DateTime && date.equals(this.date))) {\n this.updateInput();\n } else {\n this.state.warning = date > DateTime.local();\n this.props.onDateTimeChanged(date);\n }\n }\n}\n\nconst props = {\n ...DatePicker.props,\n date: { type: DateTime, optional: true },\n};\ndelete props[\"format\"];\n\nYearPicker.props = props;\n\nYearPicker.defaultProps = {\n ...DatePicker.defaultProps,\n};\n", "/** @odoo-module */\n\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { Domain } from \"@web/core/domain\";\n\nimport CommandResult from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\nimport { FILTER_DATE_OPTION, monthsOptions } from \"@spreadsheet/assets_backend/constants\";\nimport { getPeriodOptions } from \"@web/search/utils/dates\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").FieldMatching} FieldMatching\n */\n\nexport function checkFiltersTypeValueCombination(type, value) {\n if (value !== undefined) {\n switch (type) {\n case \"text\":\n if (typeof value !== \"string\") {\n return CommandResult.InvalidValueTypeCombination;\n }\n break;\n case \"date\":\n if (typeof value === \"string\") {\n const expectedValues = RELATIVE_DATE_RANGE_TYPES.map((val) => val.type);\n if (value && !expectedValues.includes(value)) {\n return CommandResult.InvalidValueTypeCombination;\n }\n } else if (typeof value !== \"object\" || Array.isArray(value)) {\n // not a date\n return CommandResult.InvalidValueTypeCombination;\n }\n break;\n case \"relation\":\n if (!Array.isArray(value)) {\n return CommandResult.InvalidValueTypeCombination;\n }\n break;\n }\n }\n return CommandResult.Success;\n}\n\n/**\n *\n * @param {Record} fieldMatchings\n */\nexport function checkFilterFieldMatching(fieldMatchings) {\n for (const fieldMatch of Object.values(fieldMatchings)) {\n if (fieldMatch.offset && (!fieldMatch.chain || !fieldMatch.type)) {\n return CommandResult.InvalidFieldMatch;\n }\n }\n\n return CommandResult.Success;\n}\n\n/**\n * Get a date domain relative to the current date.\n * The domain will span the amount of time specified in rangeType and end the day before the current day.\n *\n *\n * @param {Object} now current time, as luxon time\n * @param {number} offset offset to add to the date\n * @param {\"last_month\" | \"last_week\" | \"last_year\" | \"last_three_years\"} rangeType\n * @param {string} fieldName\n * @param {\"date\" | \"datetime\"} fieldType\n *\n * @returns {Domain|undefined}\n */\nexport function getRelativeDateDomain(now, offset, rangeType, fieldName, fieldType) {\n let endDate = now.minus({ day: 1 }).endOf(\"day\");\n let startDate = endDate;\n switch (rangeType) {\n case \"last_week\": {\n const offsetParam = { day: 7 * offset };\n endDate = endDate.plus(offsetParam);\n startDate = now.minus({ day: 7 }).plus(offsetParam);\n break;\n }\n case \"last_month\": {\n const offsetParam = { day: 30 * offset };\n endDate = endDate.plus(offsetParam);\n startDate = now.minus({ day: 30 }).plus(offsetParam);\n break;\n }\n case \"last_three_months\": {\n const offsetParam = { day: 90 * offset };\n endDate = endDate.plus(offsetParam);\n startDate = now.minus({ day: 90 }).plus(offsetParam);\n break;\n }\n case \"last_six_months\": {\n const offsetParam = { day: 180 * offset };\n endDate = endDate.plus(offsetParam);\n startDate = now.minus({ day: 180 }).plus(offsetParam);\n break;\n }\n case \"last_year\": {\n const offsetParam = { day: 365 * offset };\n endDate = endDate.plus(offsetParam);\n startDate = now.minus({ day: 365 }).plus(offsetParam);\n break;\n }\n case \"last_three_years\": {\n const offsetParam = { day: 3 * 365 * offset };\n endDate = endDate.plus(offsetParam);\n startDate = now.minus({ day: 3 * 365 }).plus(offsetParam);\n break;\n }\n default:\n return undefined;\n }\n startDate = startDate.startOf(\"day\");\n\n let leftBound, rightBound;\n if (fieldType === \"date\") {\n leftBound = serializeDate(startDate);\n rightBound = serializeDate(endDate);\n } else {\n leftBound = serializeDateTime(startDate);\n rightBound = serializeDateTime(endDate);\n }\n\n return new Domain([\"&\", [fieldName, \">=\", leftBound], [fieldName, \"<=\", rightBound]]);\n}\n\n/**\n * Returns a list of time options to choose from according to the requested\n * type. Each option contains its (translated) description.\n * see getPeriodOptions\n *\n *\n * @param {string} type \"month\" | \"quarter\" | \"year\"\n *\n * @returns {Array}\n */\nexport function dateOptions(type) {\n if (type === \"month\") {\n return monthsOptions;\n } else {\n return getPeriodOptions(DateTime.local()).filter(({ id }) =>\n FILTER_DATE_OPTION[type].includes(id)\n );\n }\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport GlobalFiltersUIPlugin from \"./plugins/global_filters_ui_plugin\";\nimport { GlobalFiltersCorePlugin } from \"./plugins/global_filters_core_plugin\";\nconst { inverseCommandRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n return [cmd];\n}\n\nconst { coreTypes, invalidateEvaluationCommands, readonlyAllowedCommands } = spreadsheet;\n\ncoreTypes.add(\"ADD_GLOBAL_FILTER\");\ncoreTypes.add(\"EDIT_GLOBAL_FILTER\");\ncoreTypes.add(\"REMOVE_GLOBAL_FILTER\");\n\ninvalidateEvaluationCommands.add(\"ADD_GLOBAL_FILTER\");\ninvalidateEvaluationCommands.add(\"EDIT_GLOBAL_FILTER\");\ninvalidateEvaluationCommands.add(\"REMOVE_GLOBAL_FILTER\");\ninvalidateEvaluationCommands.add(\"SET_GLOBAL_FILTER_VALUE\");\ninvalidateEvaluationCommands.add(\"CLEAR_GLOBAL_FILTER_VALUE\");\n\nreadonlyAllowedCommands.add(\"SET_GLOBAL_FILTER_VALUE\");\nreadonlyAllowedCommands.add(\"SET_MANY_GLOBAL_FILTER_VALUE\");\nreadonlyAllowedCommands.add(\"CLEAR_GLOBAL_FILTER_VALUE\");\nreadonlyAllowedCommands.add(\"UPDATE_OBJECT_DOMAINS\");\n\ninverseCommandRegistry\n .add(\"EDIT_GLOBAL_FILTER\", identity)\n .add(\"ADD_GLOBAL_FILTER\", (cmd) => {\n return [\n {\n type: \"REMOVE_GLOBAL_FILTER\",\n id: cmd.id,\n },\n ];\n })\n .add(\"REMOVE_GLOBAL_FILTER\", (cmd) => {\n return [\n {\n type: \"ADD_GLOBAL_FILTER\",\n id: cmd.id,\n filter: {},\n },\n ];\n });\n\nexport { GlobalFiltersCorePlugin, GlobalFiltersUIPlugin };\n", "/** @odoo-module */\n\n/**\n * @typedef {\"year\"|\"month\"|\"quarter\"|\"relative\"} RangeType\n *\n/**\n * @typedef {Object} FieldMatching\n * @property {string} chain name of the field\n * @property {string} type type of the field\n * @property {number} [offset] offset to apply to the field (for date filters)\n *\n * @typedef {Object} GlobalFilter\n * @property {string} id\n * @property {string} label\n * @property {string} type \"text\" | \"date\" | \"relation\"\n * @property {RangeType} [rangeType]\n * @property {boolean} [defaultsToCurrentPeriod]\n * @property {boolean} [automaticDefaultValue]\n * @property {string|Array|Object} defaultValue Default Value\n * @property {number} [modelID] ID of the related model\n * @property {string} [modelName] Name of the related model\n */\n\nexport const globalFiltersFieldMatchers = {};\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport CommandResult from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\nimport { checkFiltersTypeValueCombination } from \"@spreadsheet/global_filters/helpers\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\nexport class GlobalFiltersCorePlugin extends spreadsheet.CorePlugin {\n constructor(config) {\n super(config);\n /** @type {Object.} */\n this.globalFilters = {};\n }\n\n /**\n * Check if the given command can be dispatched\n *\n * @param {Object} cmd Command\n */\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"EDIT_GLOBAL_FILTER\":\n if (!this.getGlobalFilter(cmd.id)) {\n return CommandResult.FilterNotFound;\n } else if (this._isDuplicatedLabel(cmd.id, cmd.filter.label)) {\n return CommandResult.DuplicatedFilterLabel;\n }\n return checkFiltersTypeValueCombination(cmd.filter.type, cmd.filter.defaultValue);\n case \"REMOVE_GLOBAL_FILTER\":\n if (!this.getGlobalFilter(cmd.id)) {\n return CommandResult.FilterNotFound;\n }\n break;\n case \"ADD_GLOBAL_FILTER\":\n if (this._isDuplicatedLabel(cmd.id, cmd.filter.label)) {\n return CommandResult.DuplicatedFilterLabel;\n }\n return checkFiltersTypeValueCombination(cmd.filter.type, cmd.filter.defaultValue);\n }\n return CommandResult.Success;\n }\n\n /**\n * Handle a spreadsheet command\n *\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"ADD_GLOBAL_FILTER\":\n this._addGlobalFilter(cmd.filter);\n break;\n case \"EDIT_GLOBAL_FILTER\":\n this._editGlobalFilter(cmd.id, cmd.filter);\n break;\n case \"REMOVE_GLOBAL_FILTER\":\n this._removeGlobalFilter(cmd.id);\n break;\n }\n }\n\n // ---------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------\n\n /**\n * Retrieve the global filter with the given id\n *\n * @param {string} id\n * @returns {GlobalFilter|undefined} Global filter\n */\n getGlobalFilter(id) {\n return this.globalFilters[id];\n }\n\n /**\n * Get the global filter with the given name\n *\n * @param {string} label Label\n *\n * @returns {GlobalFilter|undefined}\n */\n getGlobalFilterLabel(label) {\n return this.getGlobalFilters().find((filter) => _t(filter.label) === _t(label));\n }\n\n /**\n * Retrieve all the global filters\n *\n * @returns {Array} Array of Global filters\n */\n getGlobalFilters() {\n return Object.values(this.globalFilters);\n }\n\n /**\n * Get the default value of a global filter\n *\n * @param {string} id Id of the filter\n *\n * @returns {string|Array|Object}\n */\n getGlobalFilterDefaultValue(id) {\n return this.getGlobalFilter(id).defaultValue;\n }\n\n // ---------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------\n\n /**\n * Add a global filter\n *\n * @param {GlobalFilter} filter\n */\n _addGlobalFilter(filter) {\n const globalFilters = { ...this.globalFilters };\n globalFilters[filter.id] = filter;\n this.history.update(\"globalFilters\", globalFilters);\n }\n /**\n * Remove a global filter\n *\n * @param {string} id Id of the filter to remove\n */\n _removeGlobalFilter(id) {\n const globalFilters = { ...this.globalFilters };\n delete globalFilters[id];\n this.history.update(\"globalFilters\", globalFilters);\n }\n /**\n * Edit a global filter\n *\n * @param {string} id Id of the filter to update\n * @param {GlobalFilter} newFilter\n */\n _editGlobalFilter(id, newFilter) {\n const currentLabel = this.getGlobalFilter(id).label;\n const globalFilters = { ...this.globalFilters };\n newFilter.id = id;\n globalFilters[id] = newFilter;\n this.history.update(\"globalFilters\", globalFilters);\n const newLabel = this.getGlobalFilter(id).label;\n if (currentLabel !== newLabel) {\n this._updateFilterLabelInFormulas(currentLabel, newLabel);\n }\n }\n\n // ---------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------\n\n /**\n * Import the filters\n *\n * @param {Object} data\n */\n import(data) {\n for (const globalFilter of data.globalFilters || []) {\n this.globalFilters[globalFilter.id] = globalFilter;\n }\n }\n /**\n * Export the filters\n *\n * @param {Object} data\n */\n export(data) {\n data.globalFilters = this.getGlobalFilters().map((filter) => ({\n ...filter,\n }));\n }\n\n // ---------------------------------------------------------------------\n // Global filters\n // ---------------------------------------------------------------------\n\n /**\n * Update all ODOO.FILTER.VALUE formulas to reference a filter\n * by its new label.\n *\n * @param {string} currentLabel\n * @param {string} newLabel\n */\n _updateFilterLabelInFormulas(currentLabel, newLabel) {\n const sheetIds = this.getters.getSheetIds();\n currentLabel = escapeRegExp(currentLabel);\n for (const sheetId of sheetIds) {\n for (const cell of Object.values(this.getters.getCells(sheetId))) {\n if (cell.isFormula) {\n const newContent = cell.content.replace(\n new RegExp(`FILTER\\\\.VALUE\\\\(\\\\s*\"${currentLabel}\"\\\\s*\\\\)`, \"g\"),\n `FILTER.VALUE(\"${newLabel}\")`\n );\n if (newContent !== cell.content) {\n const { col, row } = this.getters.getCellPosition(cell.id);\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n content: newContent,\n col,\n row,\n });\n }\n }\n }\n }\n }\n\n /**\n * Return true if the label is duplicated\n *\n * @param {string | undefined} filterId\n * @param {string} label\n * @returns {boolean}\n */\n _isDuplicatedLabel(filterId, label) {\n return (\n this.getGlobalFilters().findIndex(\n (filter) => (!filterId || filter.id !== filterId) && filter.label === label\n ) > -1\n );\n }\n}\n\nGlobalFiltersCorePlugin.getters = [\n \"getGlobalFilter\",\n \"getGlobalFilters\",\n \"getGlobalFilterDefaultValue\",\n \"getGlobalFilterLabel\",\n];\n", "/** @odoo-module */\n\n/**\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n * @typedef {import(\"./global_filters_core_plugin\").GlobalFilter} GlobalFilter\n * @typedef {import(\"./global_filters_core_plugin\").FieldMatching} FieldMatching\n \n */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { Domain } from \"@web/core/domain\";\nimport { constructDateRange, getPeriodOptions, QUARTER_OPTIONS } from \"@web/search/utils/dates\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport CommandResult from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\n\nimport { isEmpty } from \"@spreadsheet/helpers/helpers\";\nimport { FILTER_DATE_OPTION } from \"@spreadsheet/assets_backend/constants\";\nimport {\n checkFiltersTypeValueCombination,\n getRelativeDateDomain,\n} from \"@spreadsheet/global_filters/helpers\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\n\nconst { DateTime } = luxon;\n\nconst MONTHS = {\n january: { value: 1, granularity: \"month\" },\n february: { value: 2, granularity: \"month\" },\n march: { value: 3, granularity: \"month\" },\n april: { value: 4, granularity: \"month\" },\n may: { value: 5, granularity: \"month\" },\n june: { value: 6, granularity: \"month\" },\n july: { value: 7, granularity: \"month\" },\n august: { value: 8, granularity: \"month\" },\n september: { value: 9, granularity: \"month\" },\n october: { value: 10, granularity: \"month\" },\n november: { value: 11, granularity: \"month\" },\n december: { value: 12, granularity: \"month\" },\n};\n\nconst { UuidGenerator, createEmptyExcelSheet } = spreadsheet.helpers;\nconst uuidGenerator = new UuidGenerator();\n\nexport default class GlobalFiltersUIPlugin extends spreadsheet.UIPlugin {\n constructor(config) {\n super(config);\n this.orm = config.custom.env ? config.custom.env.services.orm : undefined;\n /**\n * Cache record display names for relation filters.\n * For each filter, contains a promise resolving to\n * the list of display names.\n */\n this.recordsDisplayName = {};\n /** @type {Object.|Object>} */\n this.values = {};\n }\n\n /**\n * Check if the given command can be dispatched\n *\n * @param {Object} cmd Command\n */\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"SET_GLOBAL_FILTER_VALUE\": {\n const filter = this.getters.getGlobalFilter(cmd.id);\n if (!filter) {\n return CommandResult.FilterNotFound;\n }\n return checkFiltersTypeValueCombination(filter.type, cmd.value);\n }\n }\n return CommandResult.Success;\n }\n\n /**\n * Handle a spreadsheet command\n *\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"ADD_GLOBAL_FILTER\":\n this.recordsDisplayName[cmd.filter.id] = cmd.filter.defaultValueDisplayNames;\n break;\n case \"EDIT_GLOBAL_FILTER\":\n if (this.values[cmd.id] && this.values[cmd.id].rangeType !== cmd.filter.rangeType) {\n delete this.values[cmd.id];\n }\n this.recordsDisplayName[cmd.filter.id] = cmd.filter.defaultValueDisplayNames;\n break;\n case \"SET_GLOBAL_FILTER_VALUE\":\n this.recordsDisplayName[cmd.id] = cmd.displayNames;\n this._setGlobalFilterValue(cmd.id, cmd.value);\n break;\n case \"SET_MANY_GLOBAL_FILTER_VALUE\":\n for (const filter of cmd.filters) {\n if (filter.value !== undefined) {\n this.dispatch(\"SET_GLOBAL_FILTER_VALUE\", {\n id: filter.filterId,\n value: filter.value,\n });\n } else {\n this.dispatch(\"CLEAR_GLOBAL_FILTER_VALUE\", { id: filter.filterId });\n }\n }\n break;\n case \"REMOVE_GLOBAL_FILTER\":\n delete this.recordsDisplayName[cmd.id];\n delete this.values[cmd.id];\n break;\n case \"CLEAR_GLOBAL_FILTER_VALUE\":\n this.recordsDisplayName[cmd.id] = [];\n this._clearGlobalFilterValue(cmd.id);\n break;\n }\n }\n\n // -------------------------------------------------------------------------\n // Getters\n // -------------------------------------------------------------------------\n\n /**\n * @param {string} filterId\n * @param {FieldMatching} fieldMatching\n *\n * @return {Domain}\n */\n getGlobalFilterDomain(filterId, fieldMatching) {\n /** @type {GlobalFilter} */\n const filter = this.getters.getGlobalFilter(filterId);\n if (!filter) {\n return new Domain();\n }\n switch (filter.type) {\n case \"text\":\n return this._getTextDomain(filter, fieldMatching);\n case \"date\":\n return this._getDateDomain(filter, fieldMatching);\n case \"relation\":\n return this._getRelationDomain(filter, fieldMatching);\n }\n }\n\n /**\n * Get the current value of a global filter\n *\n * @param {string} filterId Id of the filter\n *\n * @returns {string|Array|Object} value Current value to set\n */\n getGlobalFilterValue(filterId) {\n const filter = this.getters.getGlobalFilter(filterId);\n\n const value = filterId in this.values ? this.values[filterId].value : filter.defaultValue;\n\n const preventAutomaticValue =\n this.values[filterId] &&\n this.values[filterId].value &&\n this.values[filterId].value.preventAutomaticValue;\n const defaultsToCurrentPeriod = !preventAutomaticValue && filter.defaultsToCurrentPeriod;\n\n if (filter.type === \"date\" && isEmpty(value) && defaultsToCurrentPeriod) {\n return this._getValueOfCurrentPeriod(filterId);\n }\n\n return value;\n }\n\n /**\n * @param {string} id Id of the filter\n *\n * @returns { boolean } true if the given filter is active\n */\n isGlobalFilterActive(id) {\n const { type } = this.getters.getGlobalFilter(id);\n const value = this.getGlobalFilterValue(id);\n switch (type) {\n case \"text\":\n return value;\n case \"date\":\n return (\n value &&\n (typeof value === \"string\" || value.yearOffset !== undefined || value.period)\n );\n case \"relation\":\n return value && value.length;\n }\n }\n\n /**\n * Get the number of active global filters\n *\n * @returns {number}\n */\n getActiveFilterCount() {\n return this.getters\n .getGlobalFilters()\n .filter((filter) => this.isGlobalFilterActive(filter.id)).length;\n }\n\n getFilterDisplayValue(filterName) {\n const filter = this.getters.getGlobalFilterLabel(filterName);\n if (!filter) {\n throw new Error(sprintf(_t(`Filter \"%s\" not found`), filterName));\n }\n const value = this.getGlobalFilterValue(filter.id);\n switch (filter.type) {\n case \"text\":\n return value || \"\";\n case \"date\": {\n if (value && typeof value === \"string\") {\n const type = RELATIVE_DATE_RANGE_TYPES.find((type) => type.type === value);\n if (!type) {\n return \"\";\n }\n return type.description.toString();\n }\n if (!value || value.yearOffset === undefined) {\n return \"\";\n }\n const periodOptions = getPeriodOptions(DateTime.local());\n const year = String(DateTime.local().year + value.yearOffset);\n const period = periodOptions.find(({ id }) => value.period === id);\n let periodStr = period && period.description;\n // Named months aren't in getPeriodOptions\n if (!period) {\n periodStr =\n MONTHS[value.period] && String(MONTHS[value.period].value).padStart(2, \"0\");\n }\n return periodStr ? periodStr + \"/\" + year : year;\n }\n case \"relation\":\n if (!value || !this.orm) {\n return \"\";\n }\n if (!this.recordsDisplayName[filter.id]) {\n this.orm.call(filter.modelName, \"name_get\", [value]).then((result) => {\n const names = result.map(([, name]) => name);\n this.recordsDisplayName[filter.id] = names;\n this.dispatch(\"EVALUATE_CELLS\", {\n sheetId: this.getters.getActiveSheetId(),\n });\n });\n return \"\";\n }\n return this.recordsDisplayName[filter.id].join(\", \");\n }\n }\n\n // -------------------------------------------------------------------------\n // Handlers\n // -------------------------------------------------------------------------\n\n /**\n * Set the current value of a global filter\n *\n * @param {string} id Id of the filter\n * @param {string|Array|Object} value Current value to set\n */\n _setGlobalFilterValue(id, value) {\n this.values[id] = { value: value, rangeType: this.getters.getGlobalFilter(id).rangeType };\n }\n\n /**\n * Get the filter value corresponding to the current period, depending of the type of range of the filter.\n * For example if rangeType === \"month\", the value will be the current month of the current year.\n *\n * @param {string} filterId a global filter\n * @return {Object} filter value\n */\n _getValueOfCurrentPeriod(filterId) {\n const filter = this.getters.getGlobalFilter(filterId);\n const rangeType = filter.rangeType;\n switch (rangeType) {\n case \"year\":\n return { yearOffset: 0 };\n case \"month\": {\n const month = new Date().getMonth() + 1;\n const period = Object.entries(MONTHS).find((item) => item[1].value === month)[0];\n return { yearOffset: 0, period };\n }\n case \"quarter\": {\n const quarter = Math.floor(new Date().getMonth() / 3);\n const period = FILTER_DATE_OPTION.quarter[quarter];\n return { yearOffset: 0, period };\n }\n }\n return {};\n }\n\n /**\n * Set the current value to empty values which functionally deactivate the filter\n *\n * @param {string} id Id of the filter\n */\n _clearGlobalFilterValue(id) {\n const { type, rangeType } = this.getters.getGlobalFilter(id);\n let value;\n switch (type) {\n case \"text\":\n value = \"\";\n break;\n case \"date\":\n value = { yearOffset: undefined, preventAutomaticValue: true };\n break;\n case \"relation\":\n value = [];\n break;\n }\n this.values[id] = { value, rangeType };\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n /**\n * Get the domain relative to a date field\n *\n * @private\n *\n * @param {GlobalFilter} filter\n * @param {FieldMatching} fieldMatching\n *\n * @returns {Domain}\n */\n _getDateDomain(filter, fieldMatching) {\n let granularity;\n const value = this.getGlobalFilterValue(filter.id);\n if (!value || !fieldMatching.chain) {\n return new Domain();\n }\n const field = fieldMatching.chain;\n const type = fieldMatching.type;\n const offset = fieldMatching.offset || 0;\n const now = DateTime.local();\n\n if (filter.rangeType === \"relative\") {\n return getRelativeDateDomain(now, offset, value, field, type);\n }\n\n const setParam = { year: now.year };\n const yearOffset = value.yearOffset || 0;\n const plusParam = {\n years: filter.rangeType === \"year\" ? yearOffset + offset : yearOffset,\n };\n if (!value.period || value.period === \"empty\") {\n granularity = \"year\";\n } else {\n switch (filter.rangeType) {\n case \"month\":\n granularity = \"month\";\n setParam.month = MONTHS[value.period].value;\n plusParam.month = offset;\n break;\n case \"quarter\":\n granularity = \"quarter\";\n setParam.quarter = QUARTER_OPTIONS[value.period].setParam.quarter;\n plusParam.quarter = offset;\n break;\n }\n }\n return constructDateRange({\n referenceMoment: now,\n fieldName: field,\n fieldType: type,\n granularity,\n setParam,\n plusParam,\n }).domain;\n }\n\n /**\n * Get the domain relative to a text field\n *\n * @private\n *\n * @param {GlobalFilter} filter\n * @param {FieldMatching} fieldMatching\n *\n * @returns {Domain}\n */\n _getTextDomain(filter, fieldMatching) {\n const value = this.getGlobalFilterValue(filter.id);\n if (!value || !fieldMatching.chain) {\n return new Domain();\n }\n const field = fieldMatching.chain;\n return new Domain([[field, \"ilike\", value]]);\n }\n\n /**\n * Get the domain relative to a relation field\n *\n * @private\n *\n * @param {GlobalFilter} filter\n * @param {FieldMatching} fieldMatching\n *\n * @returns {Domain}\n */\n _getRelationDomain(filter, fieldMatching) {\n const values = this.getGlobalFilterValue(filter.id);\n if (!values || values.length === 0 || !fieldMatching.chain) {\n return new Domain();\n }\n const field = fieldMatching.chain;\n return new Domain([[field, \"in\", values]]);\n }\n\n /**\n * Adds all active filters (and their values) at the time of export in a dedicated sheet\n *\n * @param {Object} data\n */\n exportForExcel(data) {\n if (this.getters.getGlobalFilters().length === 0) {\n return;\n }\n const styles = Object.entries(data.styles);\n let titleStyleId =\n styles.findIndex((el) => JSON.stringify(el[1]) === JSON.stringify({ bold: true })) + 1;\n\n if (titleStyleId <= 0) {\n titleStyleId = styles.length + 1;\n data.styles[styles.length + 1] = { bold: true };\n }\n\n const cells = {};\n cells[\"A1\"] = { content: \"Filter\", style: titleStyleId };\n cells[\"B1\"] = { content: \"Value\", style: titleStyleId };\n let row = 2;\n for (const filter of this.getters.getGlobalFilters()) {\n const content = this.getFilterDisplayValue(filter.label);\n cells[`A${row}`] = { content: filter.label };\n cells[`B${row}`] = { content };\n row++;\n }\n data.sheets.push({\n ...createEmptyExcelSheet(uuidGenerator.uuidv4(), _t(\"Active Filters\")),\n cells,\n colNumber: 2,\n rowNumber: this.getters.getGlobalFilters().length + 1,\n cols: {},\n rows: {},\n merges: [],\n figures: [],\n conditionalFormats: [],\n charts: [],\n });\n }\n}\n\nGlobalFiltersUIPlugin.getters = [\n \"getFilterDisplayValue\",\n \"getGlobalFilterDomain\",\n \"getGlobalFilterValue\",\n \"getActiveFilterCount\",\n \"isGlobalFilterActive\",\n];\n", "/** @odoo-module */\n\nimport { _lt } from \"@web/core/l10n/translation\";\n\nexport const DEFAULT_LINES_NUMBER = 20;\n\nexport const FORMATS = {\n day: { out: \"MM/DD/YYYY\", display: \"DD MMM YYYY\", interval: \"d\" },\n week: { out: \"WW/YYYY\", display: \"[W]W YYYY\", interval: \"w\" },\n month: { out: \"MM/YYYY\", display: \"MMMM YYYY\", interval: \"M\" },\n quarter: { out: \"Q/YYYY\", display: \"[Q]Q YYYY\", interval: \"Q\" },\n year: { out: \"YYYY\", display: \"YYYY\", interval: \"y\" },\n};\n\nexport const HEADER_STYLE = { fillColor: \"#f2f2f2\" };\nexport const TOP_LEVEL_STYLE = { bold: true, fillColor: \"#f2f2f2\" };\nexport const MEASURE_STYLE = { fillColor: \"#f2f2f2\", textColor: \"#756f6f\" };\n\nexport const UNTITLED_SPREADSHEET_NAME = _lt(\"Untitled spreadsheet\");\n\nexport const RELATIVE_DATE_RANGE_TYPES = [\n { type: \"last_week\", description: _lt(\"Last 7 Days\") },\n { type: \"last_month\", description: _lt(\"Last 30 Days\") },\n { type: \"last_three_months\", description: _lt(\"Last 90 Days\") },\n { type: \"last_six_months\", description: _lt(\"Last 180 Days\") },\n { type: \"last_year\", description: _lt(\"Last 365 Days\") },\n { type: \"last_three_years\", description: _lt(\"Last 3 Years\") },\n];\n", "/** @odoo-module */\n\nimport { serializeDate } from \"@web/core/l10n/dates\";\nimport { loadJS } from \"@web/core/assets\";\n\nconst { DateTime } = luxon;\n\n/**\n * Get the intersection of two arrays\n *\n * @param {Array} a\n * @param {Array} b\n *\n * @private\n * @returns {Array} intersection between a and b\n */\nexport function intersect(a, b) {\n return a.filter((x) => b.includes(x));\n}\n\n/**\n * Given an object of form {\"1\": {...}, \"2\": {...}, ...} get the maximum ID used\n * in this object\n * If the object has no keys, return 0\n *\n * @param {Object} o an object for which the keys are an ID\n *\n * @returns {number}\n */\nexport function getMaxObjectId(o) {\n const keys = Object.keys(o);\n if (!keys.length) {\n return 0;\n }\n const nums = keys.map((id) => parseInt(id, 10));\n const max = Math.max(...nums);\n return max;\n}\n\n/** converts and orderBy Object to a string equivalent that can be processed by orm.call */\nexport function orderByToString(orderBy) {\n return orderBy.map((o) => `${o.name} ${o.asc ? \"ASC\" : \"DESC\"}`).join(\", \");\n}\n\n/**\n * Convert a spreadsheet date representation to an odoo\n * server formatted date\n *\n * @param {Date} value\n * @returns {string}\n */\nexport function toServerDateString(value) {\n const date = DateTime.fromJSDate(value);\n return serializeDate(date);\n}\n\n/**\n * @param {number[]} array\n * @returns {number}\n */\nexport function sum(array) {\n return array.reduce((acc, n) => acc + n, 0);\n}\n\nfunction camelToSnakeKey(word) {\n const result = word.replace(/(.){1}([A-Z])/g, \"$1 $2\");\n return result.split(\" \").join(\"_\").toLowerCase();\n}\n\n/**\n * Recursively convert camel case object keys to snake case keys\n * @param {object} obj\n * @returns {object}\n */\nexport function camelToSnakeObject(obj) {\n const result = {};\n for (const [key, value] of Object.entries(obj)) {\n const isPojo = typeof value === \"object\" && value !== null && value.constructor === Object;\n result[camelToSnakeKey(key)] = isPojo ? camelToSnakeObject(value) : value;\n }\n return result;\n}\n\n/**\n * Check if the argument is falsy or is an empty object/array\n *\n * TODO : remove this and replace it by the one in o_spreadsheet xlsx import when its merged\n */\nexport function isEmpty(item) {\n if (!item) {\n return true;\n }\n if (typeof item === \"object\") {\n if (\n Object.values(item).length === 0 ||\n Object.values(item).every((val) => val === undefined)\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Load external libraries required for o-spreadsheet\n * @returns {Promise}\n */\nexport async function loadSpreadsheetDependencies() {\n await loadJS(\"/web/static/lib/Chart/Chart.js\");\n // chartjs-gauge should only be loaded when Chart.js is fully loaded !\n await loadJS(\"/spreadsheet/static/lib/chartjs-gauge/chartjs-gauge.js\");\n}\n", "/** @odoo-module **/\n\nimport spreadsheet from \"../o_spreadsheet/o_spreadsheet_extended\";\n\nconst { parse } = spreadsheet;\n\n/**\n * @typedef {Object} OdooFunctionDescription\n * @property {string} functionName Name of the function\n * @property {Array} args Arguments of the function\n * @property {boolean} isMatched True if the function is matched by the matcher function\n */\n\n/**\n * This function is used to search for the functions which match the given matcher\n * from the given formula\n *\n * @param {string} formula\n * @param {string[]} functionNames e.g. [\"ODOO.LIST\", \"ODOO.LIST.HEADER\"]\n * @private\n * @returns {Array}\n */\nexport function getOdooFunctions(formula, functionNames) {\n const formulaUpperCased = formula.toUpperCase();\n // Parsing is an expensive operation, so we first check if the\n // formula contains one of the function names\n if (!functionNames.some((fn) => formulaUpperCased.includes(fn.toUpperCase()))) {\n return [];\n }\n let ast;\n try {\n ast = parse(formula);\n } catch {\n return [];\n }\n return _getOdooFunctionsFromAST(ast, functionNames);\n}\n\n/**\n * This function is used to search for the functions which match the given matcher\n * from the given AST\n *\n * @param {Object} ast (see o-spreadsheet)\n * @param {string[]} functionNames e.g. [\"ODOO.LIST\", \"ODOO.LIST.HEADER\"]\n *\n * @private\n * @returns {Array}\n */\nfunction _getOdooFunctionsFromAST(ast, functionNames) {\n switch (ast.type) {\n case \"UNARY_OPERATION\":\n return _getOdooFunctionsFromAST(ast.operand, functionNames);\n case \"BIN_OPERATION\": {\n return _getOdooFunctionsFromAST(ast.left, functionNames).concat(\n _getOdooFunctionsFromAST(ast.right, functionNames)\n );\n }\n case \"FUNCALL\": {\n const functionName = ast.value.toUpperCase();\n\n if (functionNames.includes(functionName)) {\n return [{ functionName, args: ast.args, isMatched: true }];\n } else {\n return ast.args.map((arg) => _getOdooFunctionsFromAST(arg, functionNames)).flat();\n }\n }\n default:\n return [];\n }\n}\n", "/** @odoo-module */\n\n/**\n * This file is meant to load the different subparts of the module\n * to guarantee their plugins are loaded in the right order\n *\n * dependency:\n * other plugins\n * |\n * ...\n * |\n * filters\n * /\\ \\\n * / \\ \\\n * pivot list Odoo chart\n */\n\n/** TODO: Introduce a position parameter to the plugin registry in order to load them in a specific order */\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { corePluginRegistry, coreViewsPluginRegistry } = spreadsheet.registries;\n\nimport { GlobalFiltersCorePlugin, GlobalFiltersUIPlugin } from \"@spreadsheet/global_filters/index\";\nimport { PivotCorePlugin, PivotUIPlugin } from \"@spreadsheet/pivot/index\"; // list depends on filter for its getters\nimport { ListCorePlugin, ListUIPlugin } from \"@spreadsheet/list/index\"; // pivot depends on filter for its getters\nimport {\n ChartOdooMenuPlugin,\n OdooChartCorePlugin,\n OdooChartUIPlugin,\n} from \"@spreadsheet/chart/index\"; // Odoochart depends on filter for its getters\n\ncorePluginRegistry.add(\"OdooGlobalFiltersCorePlugin\", GlobalFiltersCorePlugin);\ncorePluginRegistry.add(\"OdooPivotCorePlugin\", PivotCorePlugin);\ncorePluginRegistry.add(\"OdooListCorePlugin\", ListCorePlugin);\ncorePluginRegistry.add(\"odooChartCorePlugin\", OdooChartCorePlugin);\ncorePluginRegistry.add(\"chartOdooMenuPlugin\", ChartOdooMenuPlugin);\n\ncoreViewsPluginRegistry.add(\"OdooGlobalFiltersUIPlugin\", GlobalFiltersUIPlugin);\ncoreViewsPluginRegistry.add(\"OdooPivotUIPlugin\", PivotUIPlugin);\ncoreViewsPluginRegistry.add(\"OdooListUIPlugin\", ListUIPlugin);\ncoreViewsPluginRegistry.add(\"odooChartUIPlugin\", OdooChartUIPlugin);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport IrMenuPlugin from \"./ir_ui_menu_plugin\";\n\nimport {\n isMarkdownIrMenuIdUrl,\n isIrMenuXmlUrl,\n isMarkdownViewUrl,\n parseIrMenuXmlUrl,\n parseViewLink,\n parseIrMenuIdLink,\n} from \"./odoo_menu_link_cell\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nconst { urlRegistry, corePluginRegistry } = spreadsheet.registries;\nconst { EvaluationError } = spreadsheet;\n\ncorePluginRegistry.add(\"ir_ui_menu_plugin\", IrMenuPlugin);\n\nclass BadOdooLinkError extends EvaluationError {\n constructor(menuId) {\n super(\n _t(\"#LINK\"),\n sprintf(_t(\"Menu %s not found. You may not have the required access rights.\"), menuId)\n );\n }\n}\n\nexport const spreadsheetLinkMenuCellService = {\n dependencies: [\"menu\"],\n start(env) {\n function _getIrMenuByXmlId(xmlId) {\n const menu = env.services.menu.getAll().find((menu) => menu.xmlid === xmlId);\n if (!menu) {\n throw new BadOdooLinkError(xmlId);\n }\n return menu;\n }\n\n urlRegistry\n .add(\"OdooMenuIdLink\", {\n sequence: 65,\n match: isMarkdownIrMenuIdUrl,\n createLink(url, label) {\n const menuId = parseIrMenuIdLink(url);\n const menu = env.services.menu.getMenu(menuId);\n if (!menu) {\n throw new BadOdooLinkError(menuId);\n }\n return {\n url,\n label,\n isExternal: false,\n isUrlEditable: false,\n };\n },\n urlRepresentation(url) {\n const menuId = parseIrMenuIdLink(url);\n return env.services.menu.getMenu(menuId).name;\n },\n open(url) {\n const menuId = parseIrMenuIdLink(url);\n const menu = env.services.menu.getMenu(menuId);\n env.services.action.doAction(menu.actionID);\n },\n // createCell: (id, content, properties, sheetId, getters) => {\n // const { url } = parseMarkdownLink(content);\n // const menuId = parseIrMenuIdLink(url);\n // const menuName = env.services.menu.getMenu(menuId).name;\n // return new OdooMenuLinkCell(id, content, menuId, menuName, properties);\n // },\n })\n .add(\"OdooMenuXmlLink\", {\n sequence: 66,\n match: isIrMenuXmlUrl,\n createLink(url, label) {\n const xmlId = parseIrMenuXmlUrl(url);\n _getIrMenuByXmlId(xmlId);\n return {\n url,\n label,\n isExternal: false,\n isUrlEditable: false,\n };\n },\n urlRepresentation(url) {\n const xmlId = parseIrMenuXmlUrl(url);\n const menuId = _getIrMenuByXmlId(xmlId).id;\n return env.services.menu.getMenu(menuId).name;\n },\n open(url) {\n const xmlId = parseIrMenuXmlUrl(url);\n const menuId = _getIrMenuByXmlId(xmlId).id;\n const menu = env.services.menu.getMenu(menuId);\n env.services.action.doAction(menu.actionID);\n },\n })\n .add(\"OdooViewLink\", {\n sequence: 67,\n match: isMarkdownViewUrl,\n createLink(url, label) {\n return {\n url,\n label: label,\n isExternal: false,\n isUrlEditable: false,\n };\n },\n urlRepresentation(url) {\n const actionDescription = parseViewLink(url);\n return actionDescription.name;\n },\n open(url) {\n const { viewType, action, name } = parseViewLink(url);\n env.services.action.doAction(\n {\n type: \"ir.actions.act_window\",\n name: name,\n res_model: action.modelName,\n views: action.views,\n target: \"current\",\n domain: action.domain,\n context: action.context,\n },\n { viewType }\n );\n },\n });\n\n return true;\n },\n};\n\nregistry.category(\"services\").add(\"spreadsheetLinkMenuCell\", spreadsheetLinkMenuCellService);\n", "/** @odoo-module */\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { CorePlugin } = spreadsheet;\n\nexport default class IrMenuPlugin extends CorePlugin {\n constructor(config) {\n super(config);\n this.env = config.custom.env;\n }\n\n /**\n * Get an ir menu from an id or an xml id\n * @param {number | string} menuId\n * @returns {object | undefined}\n */\n getIrMenu(menuId) {\n let menu = this.env.services.menu.getMenu(menuId);\n if (!menu) {\n menu = this.env.services.menu.getAll().find((menu) => menu.xmlid === menuId);\n }\n return menu;\n }\n}\nIrMenuPlugin.getters = [\"getIrMenu\"];\n", "/** @odoo-module */\n\nconst VIEW_PREFIX = \"odoo://view/\";\nconst IR_MENU_ID_PREFIX = \"odoo://ir_menu_id/\";\nconst IR_MENU_XML_ID_PREFIX = \"odoo://ir_menu_xml_id/\";\n\n/**\n * @typedef Action\n * @property {Array} domain\n * @property {Object} context\n * @property {string} modelName\n * @property {string} orderBy\n * @property {Array<[boolean, string]>} views\n *\n * @typedef ViewLinkDescription\n * @property {string} name Action name\n * @property {Action} action\n * @property {string} viewType Type of view (list, pivot, ...)\n */\n\n/**\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport function isMarkdownViewUrl(url) {\n return url.startsWith(VIEW_PREFIX);\n}\n\n/**\n *\n * @param {string} viewLink\n * @returns {ViewLinkDescription}\n */\nexport function parseViewLink(viewLink) {\n if (viewLink.startsWith(VIEW_PREFIX)) {\n return JSON.parse(viewLink.substr(VIEW_PREFIX.length));\n }\n throw new Error(`${viewLink} is not a valid view link`);\n}\n\n/**\n * @param {ViewLinkDescription} viewDescription Id of the ir.filter\n * @returns {string}\n */\nexport function buildViewLink(viewDescription) {\n return `${VIEW_PREFIX}${JSON.stringify(viewDescription)}`;\n}\n\n/**\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport function isMarkdownIrMenuIdUrl(url) {\n return url.startsWith(IR_MENU_ID_PREFIX);\n}\n\n/**\n *\n * @param {string} irMenuLink\n * @returns ir.ui.menu record id\n */\nexport function parseIrMenuIdLink(irMenuLink) {\n if (irMenuLink.startsWith(IR_MENU_ID_PREFIX)) {\n return parseInt(irMenuLink.substr(IR_MENU_ID_PREFIX.length), 10);\n }\n throw new Error(`${irMenuLink} is not a valid menu id link`);\n}\n\n/**\n * @param {number} menuId\n * @returns\n */\nexport function buildIrMenuIdLink(menuId) {\n return `${IR_MENU_ID_PREFIX}${menuId}`;\n}\n\n/**\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport function isIrMenuXmlUrl(url) {\n return url.startsWith(IR_MENU_XML_ID_PREFIX);\n}\n\n/**\n *\n * @param {string} irMenuUrl\n * @returns {string} ir.ui.menu record id\n */\nexport function parseIrMenuXmlUrl(irMenuUrl) {\n if (irMenuUrl.startsWith(IR_MENU_XML_ID_PREFIX)) {\n return irMenuUrl.substr(IR_MENU_XML_ID_PREFIX.length);\n }\n throw new Error(`${irMenuUrl} is not a valid menu xml link`);\n}\n/**\n * @param {number} menuXmlId\n * @returns\n */\nexport function buildIrMenuXmlLink(menuXmlId) {\n return `${IR_MENU_XML_ID_PREFIX}${menuXmlId}`;\n}\n", "/** @odoo-module */\nimport { _lt } from \"@web/core/l10n/translation\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport \"./list_functions\";\n\nimport ListCorePlugin from \"@spreadsheet/list/plugins/list_core_plugin\";\nimport ListUIPlugin from \"@spreadsheet/list/plugins/list_ui_plugin\";\n\nimport { SEE_RECORD_LIST, SEE_RECORD_LIST_VISIBLE } from \"./list_actions\";\nconst { inverseCommandRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n return [cmd];\n}\n\nconst { coreTypes, invalidateEvaluationCommands } = spreadsheet;\nconst { cellMenuRegistry } = spreadsheet.registries;\n\ncoreTypes.add(\"INSERT_ODOO_LIST\");\ncoreTypes.add(\"RENAME_ODOO_LIST\");\ncoreTypes.add(\"REMOVE_ODOO_LIST\");\ncoreTypes.add(\"RE_INSERT_ODOO_LIST\");\ncoreTypes.add(\"UPDATE_ODOO_LIST_DOMAIN\");\ncoreTypes.add(\"ADD_LIST_DOMAIN\");\n\ninvalidateEvaluationCommands.add(\"UPDATE_ODOO_LIST_DOMAIN\");\ninvalidateEvaluationCommands.add(\"REMOVE_ODOO_LIST\");\n\ncellMenuRegistry.add(\"list_see_record\", {\n name: _lt(\"See record\"),\n sequence: 200,\n action: async (env) => {\n const position = env.model.getters.getActivePosition();\n await SEE_RECORD_LIST(position, env);\n },\n isVisible: (env) => {\n const position = env.model.getters.getActivePosition();\n return SEE_RECORD_LIST_VISIBLE(position, env);\n },\n});\n\ninverseCommandRegistry\n .add(\"INSERT_ODOO_LIST\", identity)\n .add(\"UPDATE_ODOO_LIST_DOMAIN\", identity)\n .add(\"RE_INSERT_ODOO_LIST\", identity)\n .add(\"RENAME_ODOO_LIST\", identity)\n .add(\"REMOVE_ODOO_LIST\", identity);\n\nexport { ListCorePlugin, ListUIPlugin };\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { getFirstListFunction, getNumberOfListFormulas } from \"./list_helpers\";\n\nconst { astToFormula } = spreadsheet;\n\nexport const SEE_RECORD_LIST = async (position, env) => {\n const cell = env.model.getters.getCell(position);\n if (!cell) {\n return;\n }\n const { args } = getFirstListFunction(cell.content);\n const evaluatedArgs = args\n .map(astToFormula)\n .map((arg) => env.model.getters.evaluateFormula(arg));\n const listId = env.model.getters.getListIdFromPosition(position);\n const { model } = env.model.getters.getListDefinition(listId);\n const dataSource = await env.model.getters.getAsyncListDataSource(listId);\n const recordId = dataSource.getIdFromPosition(evaluatedArgs[1] - 1);\n if (!recordId) {\n return;\n }\n await env.services.action.doAction({\n type: \"ir.actions.act_window\",\n res_model: model,\n res_id: recordId,\n views: [[false, \"form\"]],\n view_mode: \"form\",\n });\n};\n\nexport const SEE_RECORD_LIST_VISIBLE = (position, env) => {\n const evaluatedCell = env.model.getters.getEvaluatedCell(position);\n const cell = env.model.getters.getCell(position);\n return (\n evaluatedCell.type !== \"empty\" &&\n evaluatedCell.type !== \"error\" &&\n getNumberOfListFormulas(cell.content) === 1 &&\n cell &&\n getFirstListFunction(cell.content).functionName === \"ODOO.LIST\"\n );\n};\n", "/** @odoo-module */\n\nimport { OdooViewsDataSource } from \"@spreadsheet/data_sources/odoo_views_data_source\";\nimport { orderByToString } from \"@spreadsheet/helpers/helpers\";\nimport { LoadingDataError } from \"@spreadsheet/o_spreadsheet/errors\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nimport spreadsheet from \"../o_spreadsheet/o_spreadsheet_extended\";\n\nconst { toNumber } = spreadsheet.helpers;\n\n/**\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n *\n * @typedef {Object} ListMetaData\n * @property {Array} columns\n * @property {string} resModel\n * @property {Record} fields\n *\n * @typedef {Object} ListSearchParams\n * @property {Array} orderBy\n * @property {Object} domain\n * @property {Object} context\n */\n\nexport default class ListDataSource extends OdooViewsDataSource {\n /**\n * @override\n * @param {Object} services Services (see DataSource)\n * @param {Object} params\n * @param {ListMetaData} params.metaData\n * @param {ListSearchParams} params.searchParams\n * @param {number} params.limit\n */\n constructor(services, params) {\n super(services, params);\n this.maxPosition = params.limit;\n this.maxPositionFetched = 0;\n this.data = [];\n }\n\n /**\n * Increase the max position of the list\n * @param {number} position\n */\n increaseMaxPosition(position) {\n this.maxPosition = Math.max(this.maxPosition, position);\n }\n\n async _load() {\n await super._load();\n if (this.maxPosition === 0) {\n this.data = [];\n return;\n }\n const { domain, orderBy, context } = this._searchParams;\n this.data = await this._orm.searchRead(\n this._metaData.resModel,\n domain,\n this._getFieldsToFetch(),\n {\n order: orderByToString(orderBy),\n limit: this.maxPosition,\n context,\n }\n );\n this.maxPositionFetched = this.maxPosition;\n }\n\n /**\n * Get the fields to fetch from the server.\n * Automatically add the currency field if the field is a monetary field.\n */\n _getFieldsToFetch() {\n const fields = this._metaData.columns.filter((f) => this.getField(f));\n for (const field of fields) {\n if (this.getField(field).type === \"monetary\") {\n fields.push(this.getField(field).currency_field);\n }\n }\n return fields;\n }\n\n /**\n * @param {number} position\n * @returns {number}\n */\n getIdFromPosition(position) {\n this._assertDataIsLoaded();\n const record = this.data[position];\n return record ? record.id : undefined;\n }\n\n /**\n * @param {string} fieldName\n * @returns {string}\n */\n getListHeaderValue(fieldName) {\n this._assertDataIsLoaded();\n const field = this.getField(fieldName);\n return field ? field.string : fieldName;\n }\n\n /**\n * @param {number} position\n * @param {string} fieldName\n * @returns {string|number|undefined}\n */\n getListCellValue(position, fieldName) {\n this._assertDataIsLoaded();\n if (position >= this.maxPositionFetched) {\n this.increaseMaxPosition(position + 1);\n // A reload is needed because the asked position is not already loaded.\n this._triggerFetching();\n throw new LoadingDataError();\n }\n const record = this.data[position];\n if (!record) {\n return \"\";\n }\n const field = this.getField(fieldName);\n if (!field) {\n throw new Error(\n sprintf(\n _t(\"The field %s does not exist or you do not have access to that field\"),\n fieldName\n )\n );\n }\n if (!(fieldName in record)) {\n this._metaData.columns.push(fieldName);\n this._metaData.columns = [...new Set(this._metaData.columns)]; //Remove duplicates\n this._triggerFetching();\n throw new LoadingDataError();\n }\n switch (field.type) {\n case \"many2one\":\n return record[fieldName].length === 2 ? record[fieldName][1] : \"\";\n case \"one2many\":\n case \"many2many\": {\n const labels = record[fieldName]\n .map((id) => this._metadataRepository.getRecordDisplayName(field.relation, id))\n .filter((value) => value !== undefined);\n return labels.join(\", \");\n }\n case \"selection\": {\n const key = record[fieldName];\n const value = field.selection.find((array) => array[0] === key);\n return value ? value[1] : \"\";\n }\n case \"boolean\":\n return record[fieldName] ? \"TRUE\" : \"FALSE\";\n case \"date\":\n case \"datetime\":\n return record[fieldName] ? toNumber(record[fieldName]) : \"\";\n default:\n return record[fieldName] || \"\";\n }\n }\n\n //--------------------------------------------------------------------------\n // Private\n //--------------------------------------------------------------------------\n\n /**\n * Ask the parent data source to force a reload of this data source in the\n * next clock cycle. It's necessary when this.limit was updated and new\n * records have to be fetched.\n */\n _triggerFetching() {\n if (this._fetchingPromise) {\n return;\n }\n this._fetchingPromise = Promise.resolve().then(() => {\n new Promise((resolve) => {\n this.load({ reload: true });\n this._fetchingPromise = undefined;\n resolve();\n });\n });\n }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"web.core\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { args, toString, toNumber } = spreadsheet.helpers;\nconst { functionRegistry } = spreadsheet.registries;\n\n//--------------------------------------------------------------------------\n// Spreadsheet functions\n//--------------------------------------------------------------------------\n\nfunction assertListsExists(listId, getters) {\n if (!getters.isExistingList(listId)) {\n throw new Error(_.str.sprintf(_t('There is no list with id \"%s\"'), listId));\n }\n}\n\nfunctionRegistry.add(\"ODOO.LIST\", {\n description: _t(\"Get the value from a list.\"),\n args: args(`\n list_id (string) ${_t(\"ID of the list.\")}\n index (string) ${_t(\"Position of the record in the list.\")}\n field_name (string) ${_t(\"Name of the field.\")}\n `),\n compute: function (listId, index, fieldName) {\n const id = toString(listId);\n const position = toNumber(index) - 1;\n const field = toString(fieldName);\n assertListsExists(id, this.getters);\n return this.getters.getListCellValue(id, position, field);\n },\n computeFormat: function (listId, index, fieldName) {\n const id = toString(listId.value);\n const position = toNumber(index.value) - 1;\n const field = this.getters.getListDataSource(id).getField(toString(fieldName.value));\n switch (field.type) {\n case \"integer\":\n return \"0\";\n case \"float\":\n return \"#,##0.00\";\n case \"monetary\": {\n const currencyName = this.getters.getListCellValue(\n id,\n position,\n field.currency_field\n );\n return this.getters.getCurrencyFormat(currencyName);\n }\n case \"date\":\n return \"m/d/yyyy\";\n case \"datetime\":\n return \"m/d/yyyy hh:mm:ss\";\n default:\n return undefined;\n }\n },\n returns: [\"NUMBER\", \"STRING\"],\n});\n\nfunctionRegistry.add(\"ODOO.LIST.HEADER\", {\n description: _t(\"Get the header of a list.\"),\n args: args(`\n list_id (string) ${_t(\"ID of the list.\")}\n field_name (string) ${_t(\"Name of the field.\")}\n `),\n compute: function (listId, fieldName) {\n const id = toString(listId);\n const field = toString(fieldName);\n assertListsExists(id, this.getters);\n return this.getters.getListHeaderValue(id, field);\n },\n returns: [\"NUMBER\", \"STRING\"],\n});\n", "/** @odoo-module */\n\nimport { getOdooFunctions } from \"../helpers/odoo_functions_helpers\";\n\n/**\n * Parse a spreadsheet formula and detect the number of LIST functions that are\n * present in the given formula.\n *\n * @param {string} formula\n *\n * @returns {number}\n */\nexport function getNumberOfListFormulas(formula) {\n return getOdooFunctions(formula, [\"ODOO.LIST\", \"ODOO.LIST.HEADER\"]).filter((fn) => fn.isMatched)\n .length;\n}\n\n/**\n * Get the first List function description of the given formula.\n *\n * @param {string} formula\n *\n * @returns {import(\"../helpers/odoo_functions_helpers\").OdooFunctionDescription|undefined}\n */\nexport function getFirstListFunction(formula) {\n return getOdooFunctions(formula, [\"ODOO.LIST\", \"ODOO.LIST.HEADER\"]).find((fn) => fn.isMatched);\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"../../o_spreadsheet/o_spreadsheet_extended\";\nimport CommandResult from \"../../o_spreadsheet/cancelled_reason\";\nimport { getMaxObjectId } from \"../../helpers/helpers\";\nimport ListDataSource from \"../list_data_source\";\nimport { TOP_LEVEL_STYLE } from \"../../helpers/constants\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { checkFilterFieldMatching } from \"@spreadsheet/global_filters/helpers\";\nimport { getFirstListFunction, getNumberOfListFormulas } from \"../list_helpers\";\n\n/**\n * @typedef {Object} ListDefinition\n * @property {Array} columns\n * @property {Object} context\n * @property {Array>} domain\n * @property {string} id The id of the list\n * @property {string} model The technical name of the model we are listing\n * @property {string} name Name of the list\n * @property {Array} orderBy\n *\n * @typedef {Object} List\n * @property {string} id\n * @property {string} dataSourceId\n * @property {ListDefinition} definition\n * @property {Object} fieldMatching\n *\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").FieldMatching} FieldMatching\n */\n\nconst { CorePlugin } = spreadsheet;\n\nexport default class ListCorePlugin extends CorePlugin {\n constructor(config) {\n super(config);\n this.dataSources = config.custom.dataSources;\n\n this.nextId = 1;\n /** @type {Object.} */\n this.lists = {};\n\n globalFiltersFieldMatchers[\"list\"] = {\n geIds: () => this.getters.getListIds(),\n getDisplayName: (listId) => this.getters.getListName(listId),\n getTag: (listId) => sprintf(_t(\"List #%s\"), listId),\n getFieldMatching: (listId, filterId) => this.getListFieldMatching(listId, filterId),\n waitForReady: () => this.getListsWaitForReady(),\n getModel: (listId) => this.getListDefinition(listId).model,\n getFields: (listId) => this.getListDataSource(listId).getFields(),\n };\n }\n\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"INSERT_ODOO_LIST\":\n if (cmd.id !== this.nextId.toString()) {\n return CommandResult.InvalidNextId;\n }\n if (this.lists[cmd.id]) {\n return CommandResult.ListIdDuplicated;\n }\n break;\n case \"RENAME_ODOO_LIST\":\n if (!(cmd.listId in this.lists)) {\n return CommandResult.ListIdNotFound;\n }\n if (cmd.name === \"\") {\n return CommandResult.EmptyName;\n }\n break;\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n if (cmd.list) {\n return checkFilterFieldMatching(cmd.list);\n }\n }\n return CommandResult.Success;\n }\n\n /**\n * Handle a spreadsheet command\n *\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"INSERT_ODOO_LIST\": {\n const { sheetId, col, row, id, definition, dataSourceId, linesNumber, columns } =\n cmd;\n const anchor = [col, row];\n this._addList(id, definition, dataSourceId, linesNumber);\n this._insertList(sheetId, anchor, id, linesNumber, columns);\n this.history.update(\"nextId\", parseInt(id, 10) + 1);\n break;\n }\n case \"RE_INSERT_ODOO_LIST\": {\n const { sheetId, col, row, id, linesNumber, columns } = cmd;\n const anchor = [col, row];\n this._insertList(sheetId, anchor, id, linesNumber, columns);\n break;\n }\n case \"RENAME_ODOO_LIST\": {\n this.history.update(\"lists\", cmd.listId, \"definition\", \"name\", cmd.name);\n break;\n }\n case \"REMOVE_ODOO_LIST\": {\n const lists = { ...this.lists };\n delete lists[cmd.listId];\n this.history.update(\"lists\", lists);\n break;\n }\n case \"UPDATE_ODOO_LIST_DOMAIN\": {\n this.history.update(\n \"lists\",\n cmd.listId,\n \"definition\",\n \"searchParams\",\n \"domain\",\n cmd.domain\n );\n const list = this.lists[cmd.listId];\n this.dataSources.add(list.dataSourceId, ListDataSource, list.definition);\n break;\n }\n case \"UNDO\":\n case \"REDO\": {\n const domainEditionCommands = cmd.commands.filter(\n (cmd) => cmd.type === \"UPDATE_ODOO_LIST_DOMAIN\"\n );\n for (const cmd of domainEditionCommands) {\n const list = this.lists[cmd.listId];\n this.dataSources.add(list.dataSourceId, ListDataSource, list.definition);\n }\n break;\n }\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n if (cmd.list) {\n this._setListFieldMatching(cmd.filter.id, cmd.list);\n }\n break;\n case \"REMOVE_GLOBAL_FILTER\":\n this._onFilterDeletion(cmd.id);\n break;\n\n case \"START\":\n for (const sheetId of this.getters.getSheetIds()) {\n const cells = this.getters.getCells(sheetId);\n for (const cell of Object.values(cells)) {\n if (cell.isFormula) {\n this._addListPositionToDataSource(cell.content);\n }\n }\n }\n break;\n case \"UPDATE_CELL\":\n if (cmd.content) {\n this._addListPositionToDataSource(cmd.content);\n }\n break;\n }\n }\n\n /**\n * Extract the position of the records asked in the given formula and\n * increase the max position of the corresponding data source.\n *\n * @param {string} content Odoo list formula\n */\n _addListPositionToDataSource(content) {\n if (getNumberOfListFormulas(content) !== 1) {\n return;\n }\n const { functionName, args } = getFirstListFunction(content);\n if (functionName !== \"ODOO.LIST\") {\n return;\n }\n const [listId, positionArg] = args.map((arg) => arg.value.toString());\n if (!(listId in this.lists)) {\n return;\n }\n const position = parseInt(positionArg, 10);\n if (isNaN(position)) {\n return;\n }\n const dataSourceId = this.lists[listId].dataSourceId;\n this.dataSources.get(dataSourceId).increaseMaxPosition(position);\n }\n\n // -------------------------------------------------------------------------\n // Getters\n // -------------------------------------------------------------------------\n\n /**\n * @param {string} id\n * @returns {import(\"@spreadsheet/list/list_data_source\").default|undefined}\n */\n getListDataSource(id) {\n const dataSourceId = this.lists[id].dataSourceId;\n return this.dataSources.get(dataSourceId);\n }\n\n /**\n * @param {string} id\n * @returns {string}\n */\n getListDisplayName(id) {\n return `(#${id}) ${this.getListName(id)}`;\n }\n\n /**\n * @param {string} id\n * @returns {string}\n */\n getListName(id) {\n return _t(this.lists[id].definition.name);\n }\n\n /**\n * @param {string} id\n * @returns {string}\n */\n getListFieldMatch(id) {\n return this.lists[id].fieldMatching;\n }\n\n /**\n * @param {string} id\n * @returns {Promise}\n */\n async getAsyncListDataSource(id) {\n const dataSourceId = this.lists[id].dataSourceId;\n await this.dataSources.load(dataSourceId);\n return this.getListDataSource(id);\n }\n\n /**\n * Retrieve all the list ids\n *\n * @returns {Array} list ids\n */\n getListIds() {\n return Object.keys(this.lists);\n }\n\n /**\n * Retrieve the next available id for a new list\n *\n * @returns {string} id\n */\n getNextListId() {\n return this.nextId.toString();\n }\n\n /**\n * @param {string} id\n * @returns {ListDefinition}\n */\n getListDefinition(id) {\n const def = this.lists[id].definition;\n return {\n columns: [...def.metaData.columns],\n domain: [...def.searchParams.domain],\n model: def.metaData.resModel,\n context: { ...def.searchParams.context },\n orderBy: [...def.searchParams.orderBy],\n id,\n name: def.name,\n };\n }\n\n /**\n * Check if an id is an id of an existing list\n *\n * @param {string} id Id of the list\n *\n * @returns {boolean}\n */\n isExistingList(id) {\n return id in this.lists;\n }\n\n // ---------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------\n\n /**\n *\n * @return {Promise[]}\n */\n getListsWaitForReady() {\n return this.getListIds().map((ListId) => this.getListDataSource(ListId).loadMetadata());\n }\n\n /**\n * Get the current FieldMatching on a list\n *\n * @param {string} listId\n * @param {string} filterId\n */\n getListFieldMatching(listId, filterId) {\n return this.lists[listId].fieldMatching[filterId];\n }\n\n /**\n * Sets the current FieldMatching on a list\n *\n * @param {string} filterId\n * @param {Record} listFieldMatches\n */\n _setListFieldMatching(filterId, listFieldMatches) {\n const lists = { ...this.lists };\n for (const [listId, fieldMatch] of Object.entries(listFieldMatches)) {\n lists[listId].fieldMatching[filterId] = fieldMatch;\n }\n this.history.update(\"lists\", lists);\n }\n\n _onFilterDeletion(filterId) {\n const lists = { ...this.lists };\n for (const listId in lists) {\n this.history.update(\"lists\", listId, \"fieldMatching\", filterId, undefined);\n }\n }\n\n _addList(id, definition, dataSourceId, limit, fieldMatching = {}) {\n const lists = { ...this.lists };\n lists[id] = {\n id,\n definition,\n dataSourceId,\n fieldMatching,\n };\n\n if (!this.dataSources.contains(dataSourceId)) {\n this.dataSources.add(dataSourceId, ListDataSource, {\n ...definition,\n limit,\n });\n }\n this.history.update(\"lists\", lists);\n }\n\n /**\n * Build an Odoo List\n * @param {string} sheetId Id of the sheet\n * @param {[number,number]} anchor Top-left cell in which the list should be inserted\n * @param {string} id Id of the list\n * @param {number} linesNumber Number of records to insert\n * @param {Array} columns Columns ({name, type})\n */\n _insertList(sheetId, anchor, id, linesNumber, columns) {\n this._resizeSheet(sheetId, anchor, columns.length, linesNumber + 1);\n this._insertHeaders(sheetId, anchor, id, columns);\n this._insertValues(sheetId, anchor, id, columns, linesNumber);\n }\n\n _insertHeaders(sheetId, anchor, id, columns) {\n let [col, row] = anchor;\n for (const column of columns) {\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n content: `=ODOO.LIST.HEADER(${id},\"${column.name}\")`,\n });\n col++;\n }\n this.dispatch(\"SET_FORMATTING\", {\n sheetId,\n style: TOP_LEVEL_STYLE,\n target: [\n {\n top: anchor[1],\n bottom: anchor[1],\n left: anchor[0],\n right: anchor[0] + columns.length - 1,\n },\n ],\n });\n }\n\n _insertValues(sheetId, anchor, id, columns, linesNumber) {\n let col = anchor[0];\n let row = anchor[1] + 1;\n for (let i = 1; i <= linesNumber; i++) {\n col = anchor[0];\n for (const column of columns) {\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n content: `=ODOO.LIST(${id},${i},\"${column.name}\")`,\n });\n col++;\n }\n row++;\n }\n }\n\n /**\n * Resize the sheet to match the size of the listing. Columns and/or rows\n * could be added to be sure to insert the entire sheet.\n *\n * @param {string} sheetId Id of the sheet\n * @param {[number,number]} anchor Anchor of the list [col,row]\n * @param {number} columns Number of columns of the list\n * @param {number} rows Number of rows of the list\n */\n _resizeSheet(sheetId, anchor, columns, rows) {\n const numberCols = this.getters.getNumberCols(sheetId);\n const deltaCol = numberCols - anchor[0];\n if (deltaCol < columns) {\n this.dispatch(\"ADD_COLUMNS_ROWS\", {\n dimension: \"COL\",\n base: numberCols - 1,\n sheetId: sheetId,\n quantity: columns - deltaCol,\n position: \"after\",\n });\n }\n const numberRows = this.getters.getNumberRows(sheetId);\n const deltaRow = numberRows - anchor[1];\n if (deltaRow < rows) {\n this.dispatch(\"ADD_COLUMNS_ROWS\", {\n dimension: \"ROW\",\n base: numberRows - 1,\n sheetId: sheetId,\n quantity: rows - deltaRow,\n position: \"after\",\n });\n }\n }\n\n // ---------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------\n\n /**\n * Import the lists\n *\n * @param {Object} data\n */\n import(data) {\n if (data.lists) {\n for (const [id, list] of Object.entries(data.lists)) {\n const definition = {\n metaData: {\n resModel: list.model,\n columns: list.columns,\n },\n searchParams: {\n domain: list.domain,\n context: list.context,\n orderBy: list.orderBy,\n },\n name: list.name,\n };\n this._addList(id, definition, this.uuidGenerator.uuidv4(), 0, list.fieldMatching);\n }\n }\n this.nextId = data.listNextId || getMaxObjectId(this.lists) + 1;\n }\n /**\n * Export the lists\n *\n * @param {Object} data\n */\n export(data) {\n data.lists = {};\n for (const id in this.lists) {\n data.lists[id] = JSON.parse(JSON.stringify(this.getListDefinition(id)));\n data.lists[id].fieldMatching = this.lists[id].fieldMatching;\n }\n data.listNextId = this.nextId;\n }\n}\n\nListCorePlugin.getters = [\n \"getListDataSource\",\n \"getListDisplayName\",\n \"getAsyncListDataSource\",\n \"getListDefinition\",\n \"getListIds\",\n \"getListName\",\n \"getNextListId\",\n \"isExistingList\",\n \"getListFieldMatch\",\n \"getListFieldMatching\",\n];\n", "/** @odoo-module */\n\nimport spreadsheet from \"../../o_spreadsheet/o_spreadsheet_extended\";\nimport { getFirstListFunction } from \"../list_helpers\";\nimport { Domain } from \"@web/core/domain\";\n\nconst { astToFormula } = spreadsheet;\n\n/**\n * @typedef {import(\"./list_core_plugin\").SpreadsheetList} SpreadsheetList\n */\n\nexport default class ListUIPlugin extends spreadsheet.UIPlugin {\n constructor(config) {\n super(config);\n /** @type {string} */\n this.selectedListId = undefined;\n this.env = config.custom.env;\n }\n\n beforeHandle(cmd) {\n switch (cmd.type) {\n case \"START\":\n // make sure the domains are correctly set before\n // any evaluation\n this._addDomains();\n break;\n }\n }\n\n /**\n * Handle a spreadsheet command\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"SELECT_ODOO_LIST\":\n this._selectList(cmd.listId);\n break;\n case \"REFRESH_ODOO_LIST\":\n this._refreshOdooList(cmd.listId);\n break;\n case \"REFRESH_ALL_DATA_SOURCES\":\n this._refreshOdooLists();\n break;\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n case \"REMOVE_GLOBAL_FILTER\":\n case \"SET_GLOBAL_FILTER_VALUE\":\n case \"CLEAR_GLOBAL_FILTER_VALUE\":\n this._addDomains();\n break;\n case \"UNDO\":\n case \"REDO\":\n if (\n cmd.commands.find((command) =>\n [\n \"ADD_GLOBAL_FILTER\",\n \"EDIT_GLOBAL_FILTER\",\n \"REMOVE_GLOBAL_FILTER\",\n ].includes(command.type)\n )\n ) {\n this._addDomains();\n }\n break;\n }\n }\n\n // -------------------------------------------------------------------------\n // Handlers\n // -------------------------------------------------------------------------\n\n /**\n * Add an additional domain to a list\n *\n * @private\n *\n * @param {string} listId list id\n *\n */\n _addDomain(listId) {\n const domainList = [];\n for (const [filterId, fieldMatch] of Object.entries(\n this.getters.getListFieldMatch(listId)\n )) {\n domainList.push(this.getters.getGlobalFilterDomain(filterId, fieldMatch));\n }\n const domain = Domain.combine(domainList, \"AND\").toString();\n this.getters.getListDataSource(listId).addDomain(domain);\n }\n\n /**\n * Add an additional domain to all lists\n *\n * @private\n *\n */\n _addDomains() {\n for (const listId of this.getters.getListIds()) {\n this._addDomain(listId);\n }\n }\n\n /**\n * Refresh the cache of a list\n * @param {string} listId Id of the list\n */\n _refreshOdooList(listId) {\n this.getters.getListDataSource(listId).load({ reload: true });\n }\n\n /**\n * Refresh the cache of all the lists\n */\n _refreshOdooLists() {\n for (const listId of this.getters.getListIds()) {\n this._refreshOdooList(listId);\n }\n }\n\n /**\n * Select the given list id. If the id is undefined, it unselect the list.\n * @param {number|undefined} listId Id of the list, or undefined to remove\n * the selected list\n */\n _selectList(listId) {\n this.selectedListId = listId;\n }\n\n // -------------------------------------------------------------------------\n // Getters\n // -------------------------------------------------------------------------\n\n /**\n * Get the computed domain of a list\n *\n * @param {string} listId Id of the list\n * @returns {Array}\n */\n getListComputedDomain(listId) {\n return this.getters.getListDataSource(listId).getComputedDomain();\n }\n\n /**\n * Get the id of the list at the given position. Returns undefined if there\n * is no list at this position\n *\n * @param {{ sheetId: string; col: number; row: number}} position\n *\n * @returns {string|undefined}\n */\n getListIdFromPosition(position) {\n const cell = this.getters.getCell(position);\n if (cell && cell.isFormula) {\n const listFunction = getFirstListFunction(cell.content);\n if (listFunction) {\n const content = astToFormula(listFunction.args[0]);\n return this.getters.evaluateFormula(content).toString();\n }\n }\n return undefined;\n }\n\n /**\n * Get the value of a list header\n *\n * @param {string} listId Id of a list\n * @param {string} fieldName\n */\n getListHeaderValue(listId, fieldName) {\n return this.getters.getListDataSource(listId).getListHeaderValue(fieldName);\n }\n\n /**\n * Get the value for a field of a record in the list\n * @param {string} listId Id of the list\n * @param {number} position Position of the record in the list\n * @param {string} fieldName Field Name\n *\n * @returns {string|undefined}\n */\n getListCellValue(listId, position, fieldName) {\n return this.getters.getListDataSource(listId).getListCellValue(position, fieldName);\n }\n\n /**\n * Get the currently selected list id\n * @returns {number|undefined} Id of the list, undefined if no one is selected\n */\n getSelectedListId() {\n return this.selectedListId;\n }\n}\n\nListUIPlugin.getters = [\n \"getListComputedDomain\",\n \"getListHeaderValue\",\n \"getListIdFromPosition\",\n \"getListCellValue\",\n \"getSelectedListId\",\n];\n", "/** @odoo-module */\n\nexport default {\n Success: 0, // should be imported from o-spreadsheet instead of redefined here\n FilterNotFound: 1000,\n DuplicatedFilterLabel: 1001,\n PivotCacheNotLoaded: 1002,\n InvalidValueTypeCombination: 1003,\n ListIdDuplicated: 1004,\n InvalidNextId: 1005,\n ListIdNotFound: 1006,\n EmptyName: 1007,\n PivotIdNotFound: 1008,\n InvalidFieldMatch: 1009,\n};\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport spreadsheet from \"./o_spreadsheet_extended\";\n\nconst { EvaluationError, CellErrorLevel } = spreadsheet.helpers;\n\nexport class LoadingDataError extends EvaluationError {\n constructor() {\n super(_t(\"Loading...\"), _t(\"Data is loading\"), CellErrorLevel.silent);\n }\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"./o_spreadsheet_extended\";\nconst { load, CorePlugin, tokenize, parse, convertAstNodes, astToFormula } = spreadsheet;\nconst { corePluginRegistry } = spreadsheet.registries;\n\nexport const ODOO_VERSION = 5;\n\nconst MAP = {\n PIVOT: \"ODOO.PIVOT\",\n \"PIVOT.HEADER\": \"ODOO.PIVOT.HEADER\",\n \"PIVOT.POSITION\": \"ODOO.PIVOT.POSITION\",\n \"FILTER.VALUE\": \"ODOO.FILTER.VALUE\",\n LIST: \"ODOO.LIST\",\n \"LIST.HEADER\": \"ODOO.LIST.HEADER\",\n};\n\nconst dmyRegex = /^([0|1|2|3][1-9])\\/(0[1-9]|1[0-2])\\/(\\d{4})$/i;\n\nexport function migrate(data) {\n let _data = load(data, !!odoo.debug);\n const version = _data.odooVersion || 0;\n if (version < 1) {\n _data = migrate0to1(_data);\n }\n if (version < 2) {\n _data = migrate1to2(_data);\n }\n if (version < 3) {\n _data = migrate2to3(_data);\n }\n if (version < 4) {\n _data = migrate3to4(_data);\n }\n if (version < 5) {\n _data = migrate4to5(_data);\n }\n return _data;\n}\n\nfunction tokensToString(tokens) {\n return tokens.reduce((acc, token) => acc + token.value, \"\");\n}\n\nfunction migrate0to1(data) {\n for (const sheet of data.sheets) {\n for (const xc in sheet.cells || []) {\n const cell = sheet.cells[xc];\n if (cell.content && cell.content.startsWith(\"=\")) {\n const tokens = tokenize(cell.content);\n for (const token of tokens) {\n if (token.type === \"SYMBOL\" && token.value.toUpperCase() in MAP) {\n token.value = MAP[token.value.toUpperCase()];\n }\n }\n cell.content = tokensToString(tokens);\n }\n }\n }\n return data;\n}\n\nfunction migrate1to2(data) {\n for (const sheet of data.sheets) {\n for (const xc in sheet.cells || []) {\n const cell = sheet.cells[xc];\n if (cell.content && cell.content.startsWith(\"=\")) {\n try {\n cell.content = migratePivotDaysParameters(cell.content);\n } catch {\n continue;\n }\n }\n }\n }\n return data;\n}\n\n/**\n * Migration of global filters\n */\nfunction migrate2to3(data) {\n if (data.globalFilters) {\n for (const gf of data.globalFilters) {\n if (gf.fields) {\n gf.pivotFields = gf.fields;\n delete gf.fields;\n }\n if (\n gf.type === \"date\" &&\n typeof gf.defaultValue === \"object\" &&\n \"year\" in gf.defaultValue\n ) {\n switch (gf.defaultValue.year) {\n case \"last_year\":\n gf.defaultValue.yearOffset = -1;\n break;\n case \"antepenultimate_year\":\n gf.defaultValue.yearOffset = -2;\n break;\n case \"this_year\":\n case undefined:\n gf.defaultValue.yearOffset = 0;\n break;\n }\n delete gf.defaultValue.year;\n }\n if (!gf.listFields) {\n gf.listFields = {};\n }\n if (!gf.graphFields) {\n gf.graphFields = {};\n }\n }\n }\n return data;\n}\n\n/**\n * Migration of list/pivot names\n */\nfunction migrate3to4(data) {\n if (data.lists) {\n for (const list of Object.values(data.lists)) {\n list.name = list.name || list.model;\n }\n }\n if (data.pivots) {\n for (const pivot of Object.values(data.pivots)) {\n pivot.name = pivot.name || pivot.model;\n }\n }\n return data;\n}\n\nfunction migrate4to5(data) {\n for (const filter of data.globalFilters || []) {\n for (const [id, fm] of Object.entries(filter.pivotFields || {})) {\n if (!(data.pivots && id in data.pivots)) {\n delete filter.pivotFields[id];\n continue;\n }\n if (!data.pivots[id].fieldMatching) {\n data.pivots[id].fieldMatching = {};\n }\n data.pivots[id].fieldMatching[filter.id] = { chain: fm.field, type: fm.type };\n if (\"offset\" in fm) {\n data.pivots[id].fieldMatching[filter.id].offset = fm.offset;\n }\n }\n delete filter.pivotFields;\n\n for (const [id, fm] of Object.entries(filter.listFields || {})) {\n if (!(data.lists && id in data.lists)) {\n delete filter.listFields[id];\n continue;\n }\n if (!data.lists[id].fieldMatching) {\n data.lists[id].fieldMatching = {};\n }\n data.lists[id].fieldMatching[filter.id] = { chain: fm.field, type: fm.type };\n if (\"offset\" in fm) {\n data.lists[id].fieldMatching[filter.id].offset = fm.offset;\n }\n }\n delete filter.listFields;\n\n const findFigureFromId = (id) => {\n for (const sheet of data.sheets) {\n const fig = sheet.figures.find((f) => f.id === id);\n if (fig) {\n return fig;\n }\n }\n return undefined;\n };\n for (const [id, fm] of Object.entries(filter.graphFields || {})) {\n const figure = findFigureFromId(id);\n if (!figure) {\n delete filter.graphFields[id];\n continue;\n }\n if (!figure.data.fieldMatching) {\n figure.data.fieldMatching = {};\n }\n figure.data.fieldMatching[filter.id] = { chain: fm.field, type: fm.type };\n if (\"offset\" in fm) {\n figure.data.fieldMatching[filter.id].offset = fm.offset;\n }\n }\n delete filter.graphFields;\n }\n return data;\n}\n\n/**\n * Convert pivot formulas days parameters from day/month/year\n * format to the standard spreadsheet month/day/year format.\n * e.g. =PIVOT.HEADER(1,\"create_date:day\",\"30/07/2022\") becomes =PIVOT.HEADER(1,\"create_date:day\",\"07/30/2022\")\n * @param {string} formulaString\n * @returns {string}\n */\nfunction migratePivotDaysParameters(formulaString) {\n const ast = parse(formulaString);\n const convertedAst = convertAstNodes(ast, \"FUNCALL\", (ast) => {\n if ([\"ODOO.PIVOT\", \"ODOO.PIVOT.HEADER\"].includes(ast.value.toUpperCase())) {\n for (const subAst of ast.args) {\n if (subAst.type === \"STRING\") {\n const date = subAst.value.match(dmyRegex);\n if (date) {\n subAst.value = `${[date[2], date[1], date[3]].join(\"/\")}`;\n }\n }\n }\n }\n return ast;\n });\n return \"=\" + astToFormula(convertedAst);\n}\n\nexport default class OdooVersion extends CorePlugin {\n export(data) {\n data.odooVersion = ODOO_VERSION;\n }\n}\n\nOdooVersion.getters = [];\n\ncorePluginRegistry.add(\"odooMigration\", OdooVersion);\n", "/** @odoo-module */\n\n/**\n * Requires to have the file o_spreadsheet.d.ts included in the file tsconfig.json\n * See https://github.com/odoo/odoo/blob/16.0/addons/web/tooling/types/readme.md\n */\n/** @type {o_spreadsheet} */\nconst spreadsheet = window.o_spreadsheet;\nexport const initCallbackRegistry = new spreadsheet.Registry();\n\nimport { _t } from \"@web/core/l10n/translation\";\nspreadsheet.setTranslationMethod(_t);\n\n// export * from spreadsheet ?\nexport default spreadsheet;\n", "/** @odoo-module **/\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst { args, toString } = spreadsheet.helpers;\nconst { functionRegistry } = spreadsheet.registries;\n\nfunctionRegistry.add(\"_t\", {\n description: _t(\"Get the translated value of the given string\"),\n args: args(`\n value (string) ${_t(\"Value to translate.\")}\n `),\n compute: function (value) {\n return _t(toString(value));\n },\n returns: [\"STRING\"],\n});\n", "/** @odoo-module */\nimport { _lt } from \"@web/core/l10n/translation\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport PivotCorePlugin from \"./plugins/pivot_core_plugin\";\nimport PivotUIPlugin from \"./plugins/pivot_ui_plugin\";\n\nimport { SEE_RECORDS_PIVOT, SEE_RECORDS_PIVOT_VISIBLE } from \"./pivot_actions\";\n\nconst { coreTypes, invalidateEvaluationCommands } = spreadsheet;\nconst { cellMenuRegistry } = spreadsheet.registries;\n\nconst { inverseCommandRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n return [cmd];\n}\n\ncoreTypes.add(\"INSERT_PIVOT\");\ncoreTypes.add(\"RENAME_ODOO_PIVOT\");\ncoreTypes.add(\"REMOVE_PIVOT\");\ncoreTypes.add(\"RE_INSERT_PIVOT\");\ncoreTypes.add(\"UPDATE_ODOO_PIVOT_DOMAIN\");\n\ninvalidateEvaluationCommands.add(\"UPDATE_ODOO_PIVOT_DOMAIN\");\ninvalidateEvaluationCommands.add(\"REMOVE_PIVOT\");\ninvalidateEvaluationCommands.add(\"INSERT_PIVOT\");\n\ncellMenuRegistry.add(\"pivot_see_records\", {\n name: _lt(\"See records\"),\n sequence: 175,\n action: async (env) => {\n const position = env.model.getters.getActivePosition();\n await SEE_RECORDS_PIVOT(position, env);\n },\n isVisible: (env) => {\n const position = env.model.getters.getActivePosition();\n return SEE_RECORDS_PIVOT_VISIBLE(position, env);\n },\n});\n\ninverseCommandRegistry\n .add(\"INSERT_PIVOT\", identity)\n .add(\"RENAME_ODOO_PIVOT\", identity)\n .add(\"REMOVE_PIVOT\", identity)\n .add(\"UPDATE_ODOO_PIVOT_DOMAIN\", identity)\n .add(\"RE_INSERT_PIVOT\", identity);\n\nexport { PivotCorePlugin, PivotUIPlugin };\n", "/** @odoo-module */\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { getFirstPivotFunction, getNumberOfPivotFormulas } from \"./pivot_helpers\";\n\nconst { astToFormula } = spreadsheet;\n\nexport const SEE_RECORDS_PIVOT = async (position, env) => {\n const cell = env.model.getters.getCell(position);\n if (!cell) {\n return;\n }\n const { args, functionName } = getFirstPivotFunction(cell.content);\n const evaluatedArgs = args\n .map(astToFormula)\n .map((arg) => env.model.getters.evaluateFormula(arg));\n const pivotId = env.model.getters.getPivotIdFromPosition(position);\n const { model } = env.model.getters.getPivotDefinition(pivotId);\n const dataSource = await env.model.getters.getAsyncPivotDataSource(pivotId);\n const slice = functionName === \"ODOO.PIVOT.HEADER\" ? 1 : 2;\n let argsDomain = evaluatedArgs.slice(slice);\n if (argsDomain[argsDomain.length - 2] === \"measure\") {\n // We have to remove the measure from the domain\n argsDomain = argsDomain.slice(0, argsDomain.length - 2);\n }\n const domain = dataSource.getPivotCellDomain(argsDomain);\n const name = await dataSource.getModelLabel();\n await env.services.action.doAction({\n type: \"ir.actions.act_window\",\n name,\n res_model: model,\n view_mode: \"list\",\n views: [\n [false, \"list\"],\n [false, \"form\"],\n ],\n target: \"current\",\n domain,\n });\n};\n\nexport const SEE_RECORDS_PIVOT_VISIBLE = (position, env) => {\n const evaluatedCell = env.model.getters.getEvaluatedCell(position);\n const cell = env.model.getters.getCell(position);\n return (\n evaluatedCell.type !== \"empty\" &&\n evaluatedCell.type !== \"error\" &&\n cell &&\n getNumberOfPivotFormulas(cell.content) === 1\n );\n};\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooViewsDataSource } from \"../data_sources/odoo_views_data_source\";\nimport { SpreadsheetPivotModel } from \"./pivot_model\";\n\nexport default class PivotDataSource extends OdooViewsDataSource {\n /**\n *\n * @override\n * @param {Object} services Services (see DataSource)\n * @param {Object} params\n * @param {import(\"./pivot_model\").PivotMetaData} params.metaData\n * @param {import(\"./pivot_model\").PivotSearchParams} params.searchParams\n */\n constructor(services, params) {\n super(services, params);\n }\n\n async _load() {\n await super._load();\n /** @type {SpreadsheetPivotModel} */\n this._model = new SpreadsheetPivotModel(\n { _t },\n {\n metaData: this._metaData,\n searchParams: this._searchParams,\n },\n {\n orm: this._orm,\n metadataRepository: this._metadataRepository,\n }\n );\n await this._model.load(this._searchParams);\n }\n\n async copyModelWithOriginalDomain() {\n await this.loadMetadata();\n const model = new SpreadsheetPivotModel(\n { _t },\n {\n metaData: this._metaData,\n searchParams: this._initialSearchParams,\n },\n {\n orm: this._orm,\n metadataRepository: this._metadataRepository,\n }\n );\n await model.load(this._initialSearchParams);\n return model;\n }\n\n getReportMeasures() {\n this._assertDataIsLoaded();\n return this._model.getReportMeasures();\n }\n\n /**\n * @param {string[]} domain\n */\n getDisplayedPivotHeaderValue(domain) {\n this._assertDataIsLoaded();\n return this._model.getDisplayedPivotHeaderValue(domain);\n }\n\n /**\n * @param {string[]} domain\n */\n getPivotHeaderValue(domain) {\n this._assertDataIsLoaded();\n return this._model.getPivotHeaderValue(domain);\n }\n\n /**\n * @param {string} measure Field name of the measures\n * @param {string[]} domain\n */\n markAsValueUsed(measure, domain) {\n if (this._model) {\n this._model.markAsValueUsed(measure, domain);\n }\n }\n\n /**\n * @param {string[]} domain\n */\n markAsHeaderUsed(domain) {\n if (this._model) {\n this._model.markAsHeaderUsed(domain);\n }\n }\n\n /**\n * @param {string} measure Field name of the measures\n * @param {string[]} domain\n * @returns {boolean}\n */\n isUsedValue(measure, domain) {\n this._assertDataIsLoaded();\n return this._model.isUsedValue(measure, domain);\n }\n\n /**\n * @param {string[]} domain\n * @returns {boolean}\n */\n isUsedHeader(domain) {\n this._assertDataIsLoaded();\n return this._model.isUsedHeader(domain);\n }\n\n clearUsedValues() {\n if (this._model) {\n this._model.clearUsedValues();\n }\n }\n\n getTableStructure() {\n this._assertDataIsLoaded();\n return this._model.getTableStructure();\n }\n\n /**\n * @param {string} measure Field name of the measures\n * @param {string[]} domain\n */\n getPivotCellValue(measure, domain) {\n this._assertDataIsLoaded();\n return this._model.getPivotCellValue(measure, domain);\n }\n\n /**\n * @param {string[]}\n */\n getPivotCellDomain(domain) {\n this._assertDataIsLoaded();\n return this._model.getPivotCellDomain(domain);\n }\n\n /**\n * @param {string} fieldName\n * @param {string} value raw string value\n * @returns {string}\n */\n getGroupByDisplayLabel(fieldName, value) {\n this._assertDataIsLoaded();\n return this._model.getGroupByDisplayLabel(fieldName, value);\n }\n\n /**\n * @param {string} fieldName\n * @returns {string}\n */\n getFormattedGroupBy(fieldName) {\n this._assertDataIsLoaded();\n return this._model.getFormattedGroupBy(fieldName);\n }\n\n /**\n * @param {string} groupFieldString\n */\n parseGroupField(groupFieldString) {\n this._assertDataIsLoaded();\n return this._model.parseGroupField(groupFieldString);\n }\n\n /**\n * @param {\"COLUMN\" | \"ROW\"} dimension\n * @returns {boolean}\n */\n isGroupedOnlyByOneDate(dimension) {\n this._assertDataIsLoaded();\n return this._model.isGroupedOnlyByOneDate(dimension);\n }\n\n /**\n * @param {\"COLUMN\" | \"ROW\"} dimension\n * @returns {string}\n */\n getGroupOfFirstDate(dimension) {\n this._assertDataIsLoaded();\n return this._model.getGroupOfFirstDate(dimension);\n }\n\n /**\n * @param {\"COLUMN\" | \"ROW\"} dimension\n * @param {number} index\n * @returns {string}\n */\n getGroupByAtIndex(dimension, index) {\n this._assertDataIsLoaded();\n return this._model.getGroupByAtIndex(dimension, index);\n }\n\n /**\n * @param {string} fieldName\n * @returns {boolean}\n */\n isColumnGroupBy(fieldName) {\n this._assertDataIsLoaded();\n return this._model.isColumnGroupBy(fieldName);\n }\n\n /**\n * @param {string} fieldName\n * @returns {boolean}\n */\n isRowGroupBy(fieldName) {\n this._assertDataIsLoaded();\n return this._model.isRowGroupBy(fieldName);\n }\n\n /**\n * @returns {number}\n */\n getNumberOfColGroupBys() {\n this._assertDataIsLoaded();\n return this._model.getNumberOfColGroupBys();\n }\n\n async prepareForTemplateGeneration() {\n this._assertDataIsLoaded();\n await this._model.prepareForTemplateGeneration();\n }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { args, toString } = spreadsheet.helpers;\nconst { functionRegistry } = spreadsheet.registries;\n\n//--------------------------------------------------------------------------\n// Spreadsheet functions\n//--------------------------------------------------------------------------\n\nfunction assertPivotsExists(pivotId, getters) {\n if (!getters.isExistingPivot(pivotId)) {\n throw new Error(sprintf(_t('There is no pivot with id \"%s\"'), pivotId));\n }\n}\n\nfunction assertMeasureExist(pivotId, measure, getters) {\n const { measures } = getters.getPivotDefinition(pivotId);\n if (!measures.includes(measure)) {\n const validMeasures = `(${measures})`;\n throw new Error(\n sprintf(\n _t(\"The argument %s is not a valid measure. Here are the measures: %s\"),\n measure,\n validMeasures\n )\n );\n }\n}\n\nfunction assertDomainLength(domain) {\n if (domain.length % 2 !== 0) {\n throw new Error(_t(\"Function PIVOT takes an even number of arguments.\"));\n }\n}\n\nfunctionRegistry\n .add(\"ODOO.FILTER.VALUE\", {\n description: _t(\"Return the current value of a spreadsheet filter.\"),\n args: args(`\n filter_name (string) ${_t(\"The label of the filter whose value to return.\")}\n `),\n compute: function (filterName) {\n return this.getters.getFilterDisplayValue(filterName);\n },\n returns: [\"STRING\"],\n })\n .add(\"ODOO.PIVOT\", {\n description: _t(\"Get the value from a pivot.\"),\n args: args(`\n pivot_id (string) ${_t(\"ID of the pivot.\")}\n measure_name (string) ${_t(\"Name of the measure.\")}\n domain_field_name (string,optional,repeating) ${_t(\"Field name.\")}\n domain_value (string,optional,repeating) ${_t(\"Value.\")}\n `),\n compute: function (pivotId, measureName, ...domain) {\n pivotId = toString(pivotId);\n const measure = toString(measureName);\n const args = domain.map(toString);\n assertPivotsExists(pivotId, this.getters);\n assertMeasureExist(pivotId, measure, this.getters);\n assertDomainLength(args);\n return this.getters.getPivotCellValue(pivotId, measure, args);\n },\n computeFormat: function (pivotId, measureName, ...domain) {\n pivotId = toString(pivotId.value);\n const measure = toString(measureName.value);\n const field = this.getters.getPivotDataSource(pivotId).getReportMeasures()[measure];\n if (!field) {\n return undefined;\n }\n switch (field.type) {\n case \"integer\":\n return \"0\";\n case \"float\":\n return \"#,##0.00\";\n case \"monetary\":\n return this.getters.getCompanyCurrencyFormat() || \"#,##0.00\";\n default:\n return undefined;\n }\n },\n returns: [\"NUMBER\", \"STRING\"],\n })\n .add(\"ODOO.PIVOT.HEADER\", {\n description: _t(\"Get the header of a pivot.\"),\n args: args(`\n pivot_id (string) ${_t(\"ID of the pivot.\")}\n domain_field_name (string,optional,repeating) ${_t(\"Field name.\")}\n domain_value (string,optional,repeating) ${_t(\"Value.\")}\n `),\n compute: function (pivotId, ...domain) {\n pivotId = toString(pivotId);\n const args = domain.map(toString);\n assertPivotsExists(pivotId, this.getters);\n assertDomainLength(args);\n return this.getters.getDisplayedPivotHeaderValue(pivotId, args);\n },\n computeFormat: function (pivotId, ...domain) {\n pivotId = toString(pivotId.value);\n const pivot = this.getters.getPivotDataSource(pivotId);\n const len = domain.length;\n if (!len) {\n return undefined;\n }\n const fieldName = toString(domain[len - 2].value);\n const value = toString(domain[len - 1].value);\n if (fieldName === \"measure\" || value === \"false\") {\n return undefined;\n }\n const { aggregateOperator, field } = pivot.parseGroupField(fieldName);\n switch (field.type) {\n case \"integer\":\n return \"0\";\n case \"float\":\n case \"monetary\":\n return \"#,##0.00\";\n case \"date\":\n case \"datetime\":\n if (aggregateOperator === \"day\") {\n return \"mm/dd/yyyy\";\n }\n return undefined;\n default:\n return undefined;\n }\n },\n returns: [\"NUMBER\", \"STRING\"],\n })\n .add(\"ODOO.PIVOT.POSITION\", {\n description: _t(\"Get the absolute ID of an element in the pivot\"),\n args: args(`\n pivot_id (string) ${_t(\"ID of the pivot.\")}\n field_name (string) ${_t(\"Name of the field.\")}\n position (number) ${_t(\"Position in the pivot\")}\n `),\n compute: function () {\n throw new Error(_t(`[[FUNCTION_NAME]] cannot be called from the spreadsheet.`));\n },\n returns: [\"STRING\"],\n });\n", "/** @odoo-module **/\n\nimport { _t } from \"web.core\";\nimport { FORMATS } from \"../helpers/constants\";\nimport { getOdooFunctions } from \"../helpers/odoo_functions_helpers\";\n\nexport const pivotFormulaRegex = /^=.*PIVOT/;\n\n//--------------------------------------------------------------------------\n// Public\n//--------------------------------------------------------------------------\n\n/**\n * Format a data\n *\n * @param {string} interval aggregate interval i.e. month, week, quarter, ...\n * @param {string} value\n */\nexport function formatDate(interval, value) {\n const output = FORMATS[interval].display;\n const input = FORMATS[interval].out;\n const date = moment(value, input);\n return date.isValid() ? date.format(output) : _t(\"None\");\n}\n\n/**\n * Parse a spreadsheet formula and detect the number of PIVOT functions that are\n * present in the given formula.\n *\n * @param {string} formula\n *\n * @returns {number}\n */\nexport function getNumberOfPivotFormulas(formula) {\n return getOdooFunctions(formula, [\n \"ODOO.PIVOT\",\n \"ODOO.PIVOT.HEADER\",\n \"ODOO.PIVOT.POSITION\",\n ]).filter((fn) => fn.isMatched).length;\n}\n\n/**\n * Get the first Pivot function description of the given formula.\n *\n * @param {string} formula\n *\n * @returns {import(\"../helpers/odoo_functions_helpers\").OdooFunctionDescription|undefined}\n */\nexport function getFirstPivotFunction(formula) {\n return getOdooFunctions(formula, [\n \"ODOO.PIVOT\",\n \"ODOO.PIVOT.HEADER\",\n \"ODOO.PIVOT.POSITION\",\n ]).find((fn) => fn.isMatched);\n}\n\n/**\n * Build a pivot formula expression\n *\n * @param {string} formula formula to be used (PIVOT or PIVOT.HEADER)\n * @param {*} args arguments of the formula\n *\n * @returns {string}\n */\nexport function makePivotFormula(formula, args) {\n return `=${formula}(${args\n .map((arg) =>\n typeof arg == \"number\" || (typeof arg == \"string\" && !isNaN(arg))\n ? `${arg}`\n : `\"${arg.toString().replace(/\"/g, '\\\\\"')}\"`\n )\n .join(\",\")})`;\n}\n\nexport const PERIODS = {\n day: _t(\"Day\"),\n week: _t(\"Week\"),\n month: _t(\"Month\"),\n quarter: _t(\"Quarter\"),\n year: _t(\"Year\"),\n};\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { PivotModel } from \"@web/views/pivot/pivot_model\";\nimport { computeReportMeasures } from \"@web/views/utils\";\nimport { session } from \"@web/session\";\n\nimport { FORMATS } from \"../helpers/constants\";\n\nimport spreadsheet from \"../o_spreadsheet/o_spreadsheet_extended\";\nimport { formatDate } from \"./pivot_helpers\";\nimport { PERIODS } from \"@spreadsheet/pivot/pivot_helpers\";\nimport { SpreadsheetPivotTable } from \"@spreadsheet/pivot/pivot_table\";\n\nconst { toString, toNumber, toBoolean } = spreadsheet.helpers;\n\n/**\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n * @typedef {import(\"@spreadsheet/pivot/pivot_table\").Row} Row\n * @typedef {import(\"@spreadsheet/pivot/pivot_table\").Column} Column\n *\n * @typedef {Object} PivotMetaData\n * @property {Array} colGroupBys\n * @property {Array} rowGroupBys\n * @property {Array} activeMeasures\n * @property {string} resModel\n * @property {Record} fields\n * @property {string|undefined} modelLabel\n *\n * @typedef {Object} PivotSearchParams\n * @property {Array} groupBy\n * @property {Array} orderBy\n * @property {Object} domain\n * @property {Object} context\n */\n\n/**\n * Parses the positional char (#), the field and operator string of pivot group.\n * e.g. \"create_date:month\"\n * @param {Record} allFields\n * @param {string} groupFieldString\n * @returns {{field: Field, aggregateOperator: string, isPositional: boolean}}\n */\nfunction parseGroupField(allFields, groupFieldString) {\n let [fieldName, aggregateOperator] = groupFieldString.split(\":\");\n const isPositional = fieldName.startsWith(\"#\");\n fieldName = isPositional ? fieldName.substring(1) : fieldName;\n const field = allFields[fieldName];\n if (field === undefined) {\n throw new Error(sprintf(_t(\"Field %s does not exist\"), fieldName));\n }\n if ([\"date\", \"datetime\"].includes(field.type)) {\n aggregateOperator = aggregateOperator || \"month\";\n }\n return {\n isPositional,\n field,\n aggregateOperator,\n };\n}\n\nconst UNSUPPORTED_FIELD_TYPES = [\"one2many\", \"binary\", \"html\"];\nexport const NO_RECORD_AT_THIS_POSITION = Symbol(\"NO_RECORD_AT_THIS_POSITION\");\n\nfunction isNotSupported(fieldType) {\n return UNSUPPORTED_FIELD_TYPES.includes(fieldType);\n}\n\nfunction throwUnsupportedFieldError(field) {\n throw new Error(\n sprintf(_t(\"Field %s is not supported because of its type (%s)\"), field.string, field.type)\n );\n}\n\n/**\n * Parses the value defining a pivot group in a PIVOT formula\n * e.g. given the following formula PIVOT(\"1\", \"stage_id\", \"42\", \"status\", \"won\"),\n * the two group values are \"42\" and \"won\".\n * @param {object} field\n * @param {number | boolean | string} groupValue\n * @returns {number | boolean | string}\n */\nexport function parsePivotFormulaFieldValue(field, groupValue) {\n const groupValueString =\n typeof groupValue === \"boolean\"\n ? toString(groupValue).toLocaleLowerCase()\n : toString(groupValue);\n if (isNotSupported(field.type)) {\n throwUnsupportedFieldError(field);\n }\n // represents a field which is not set (=False server side)\n if (groupValueString === \"false\") {\n return false;\n }\n switch (field.type) {\n case \"datetime\":\n case \"date\":\n return toString(groupValueString);\n case \"selection\":\n case \"char\":\n case \"text\":\n return toString(groupValueString);\n case \"boolean\":\n return toBoolean(groupValueString);\n case \"float\":\n case \"integer\":\n case \"monetary\":\n case \"many2one\":\n case \"many2many\":\n return toNumber(groupValueString);\n default:\n throwUnsupportedFieldError(field);\n }\n}\n\n/**\n * This class is an extension of PivotModel with some additional information\n * that we need in spreadsheet (name_get, isUsedInSheet, ...)\n */\nexport class SpreadsheetPivotModel extends PivotModel {\n /**\n * @param {Object} params\n * @param {PivotMetaData} params.metaData\n * @param {PivotSearchParams} params.searchParams\n * @param {Object} services\n * @param {import(\"../data_sources/metadata_repository\").MetadataRepository} services.metadataRepository\n */\n setup(params, services) {\n // fieldAttrs is required, but not needed in Spreadsheet, so we define it as empty\n (params.metaData.fieldAttrs = {}), super.setup(params);\n\n this.metadataRepository = services.metadataRepository;\n\n /**\n * Contains the domain of the values used during the evaluation of the formula =Pivot(...)\n * Is used to know if a pivot cell is missing or not\n * */\n\n this._usedValueDomains = new Set();\n /**\n * Contains the domain of the headers used during the evaluation of the formula =Pivot.header(...)\n * Is used to know if a pivot cell is missing or not\n * */\n this._usedHeaderDomains = new Set();\n\n /**\n * Display name of the model\n */\n this._modelLabel = params.metaData.modelLabel;\n }\n\n //--------------------------------------------------------------------------\n // Metadata getters\n //--------------------------------------------------------------------------\n\n /**\n * Return true if the given field name is part of the col group bys\n * @param {string} fieldName\n * @returns {boolean}\n */\n isColumnGroupBy(fieldName) {\n try {\n const { field } = this.parseGroupField(fieldName);\n return this._isCol(field);\n } catch {\n return false;\n }\n }\n\n /**\n * Return true if the given field name is part of the row group bys\n * @param {string} fieldName\n * @returns {boolean}\n */\n isRowGroupBy(fieldName) {\n try {\n const { field } = this.parseGroupField(fieldName);\n return this._isRow(field);\n } catch {\n return false;\n }\n }\n\n /**\n * Get the display name of a group by\n * @param {string} fieldName\n * @returns {string}\n */\n getFormattedGroupBy(fieldName) {\n const { field, aggregateOperator } = this.parseGroupField(fieldName);\n return field.string + (aggregateOperator ? ` (${PERIODS[aggregateOperator]})` : \"\");\n }\n\n getReportMeasures() {\n return computeReportMeasures(this.metaData.fields, this.metaData.fieldAttrs, []);\n }\n\n //--------------------------------------------------------------------------\n // Cell missing\n //--------------------------------------------------------------------------\n\n /**\n * Reset the used values and headers\n */\n clearUsedValues() {\n this._usedHeaderDomains.clear();\n this._usedValueDomains.clear();\n }\n\n /**\n * Check if the given domain with the given measure has been used\n */\n isUsedValue(domain, measure) {\n const tag = [measure, ...domain];\n return this._usedValueDomains.has(tag.join());\n }\n\n /**\n * Check if the given domain has been used\n */\n isUsedHeader(domain) {\n return this._usedHeaderDomains.has(domain.join());\n }\n\n /**\n * Indicate that the given domain has been used with the given measure\n */\n markAsValueUsed(domain, measure) {\n const toTag = [measure, ...domain];\n this._usedValueDomains.add(toTag.join());\n }\n\n /**\n * Indicate that the given domain has been used\n */\n markAsHeaderUsed(domain) {\n this._usedHeaderDomains.add(domain.join());\n }\n\n //--------------------------------------------------------------------------\n // Autofill\n //--------------------------------------------------------------------------\n\n /**\n * @param {string} dimension COLUMN | ROW\n */\n isGroupedOnlyByOneDate(dimension) {\n const groupBys =\n dimension === \"COLUMN\" ? this.metaData.fullColGroupBys : this.metaData.fullRowGroupBys;\n return groupBys.length === 1 && this._isDateField(this.parseGroupField(groupBys[0]).field);\n }\n /**\n * @param {string} dimension COLUMN | ROW\n */\n getGroupOfFirstDate(dimension) {\n if (!this.isGroupedOnlyByOneDate(dimension)) {\n return undefined;\n }\n const groupBys =\n dimension === \"COLUMN\" ? this.metaData.fullColGroupBys : this.metaData.fullRowGroupBys;\n return this.parseGroupField(groupBys[0]).aggregateOperator;\n }\n\n /**\n * @param {string} dimension COLUMN | ROW\n * @param {number} index\n */\n getGroupByAtIndex(dimension, index) {\n const groupBys =\n dimension === \"COLUMN\" ? this.metaData.fullColGroupBys : this.metaData.fullRowGroupBys;\n return groupBys[index];\n }\n\n getNumberOfColGroupBys() {\n return this.metaData.fullColGroupBys.length;\n }\n\n //--------------------------------------------------------------------------\n // Evaluation\n //--------------------------------------------------------------------------\n\n /**\n * Get the value of the given domain for the given measure\n */\n getPivotCellValue(measure, domain) {\n const { cols, rows } = this._getColsRowsValuesFromDomain(domain);\n const group = JSON.stringify([rows, cols]);\n const values = this.data.measurements[group];\n return (values && values[0][measure]) || \"\";\n }\n\n /**\n * Get the label the given field-value\n *\n * @param {string} groupFieldString Name of the field\n * @param {string} groupValueString Value of the group by\n * @returns {string}\n */\n getGroupByDisplayLabel(groupFieldString, groupValueString) {\n if (groupValueString === NO_RECORD_AT_THIS_POSITION) {\n return \"\";\n }\n if (groupFieldString === \"measure\") {\n if (groupValueString === \"__count\") {\n return _t(\"Count\");\n }\n // the value is actually the measure field name\n return this.parseGroupField(groupValueString).field.string;\n }\n const { field, aggregateOperator } = this.parseGroupField(groupFieldString);\n const value = parsePivotFormulaFieldValue(field, groupValueString);\n const undef = _t(\"None\");\n if (this._isDateField(field)) {\n if (value && aggregateOperator === \"day\") {\n return toNumber(value);\n }\n return formatDate(aggregateOperator, value);\n }\n if (field.relation) {\n const label = this.metadataRepository.getRecordDisplayName(field.relation, value);\n if (!label) {\n return undef;\n }\n return label;\n }\n const label = this.metadataRepository.getLabel(this.metaData.resModel, field.name, value);\n if (!label) {\n return undef;\n }\n return label;\n }\n\n /**\n * Get the label of the last group by of the domain\n *\n * @param {string[]} domain Domain of the formula\n */\n getPivotHeaderValue(domain) {\n const groupFieldString = domain[domain.length - 2];\n if (groupFieldString.startsWith(\"#\")) {\n const { field } = this.parseGroupField(groupFieldString);\n const { cols, rows } = this._getColsRowsValuesFromDomain(domain);\n return this._isCol(field) ? cols[cols.length - 1] : rows[rows.length - 1];\n }\n const groupValueString = domain[domain.length - 1];\n return groupValueString;\n }\n\n /**\n * Get the displayed label of the last group by of the domain\n *\n * @param {string[]} domain Domain of the formula\n * @returns {string}\n */\n getDisplayedPivotHeaderValue(domain) {\n const groupFieldString = domain[domain.length - 2];\n return this.getGroupByDisplayLabel(groupFieldString, this.getPivotHeaderValue(domain));\n }\n\n //--------------------------------------------------------------------------\n // Misc\n //--------------------------------------------------------------------------\n\n /**\n * Get the Odoo domain corresponding to the given domain\n */\n getPivotCellDomain(domain) {\n const { cols, rows } = this._getColsRowsValuesFromDomain(domain);\n const key = JSON.stringify([rows, cols]);\n const domains = this.data.groupDomains[key];\n return domains ? domains[0] : Domain.FALSE.toList();\n }\n\n /**\n * @returns {SpreadsheetPivotTable}\n */\n getTableStructure() {\n const cols = this._getSpreadsheetCols();\n const rows = this._getSpreadsheetRows(this.data.rowGroupTree);\n rows.push(rows.shift()); //Put the Total row at the end.\n const measures = this.metaData.activeMeasures;\n return new SpreadsheetPivotTable(cols, rows, measures);\n }\n\n //--------------------------------------------------------------------------\n // Private\n //--------------------------------------------------------------------------\n\n /**\n * @override\n */\n async _loadData(config) {\n /** @type {(groupFieldString: string) => ReturnType} */\n this.parseGroupField = parseGroupField.bind(null, this.metaData.fields);\n /*\n * prune is manually set to false in order to expand all the groups\n * automatically\n */\n const prune = false;\n await super._loadData(config, prune);\n\n const metadataRepository = this.metadataRepository;\n\n const registerLabels = (tree, groupBys) => {\n const group = tree.root;\n if (!tree.directSubTrees.size) {\n for (let i = 0; i < group.values.length; i++) {\n const { field } = this.parseGroupField(groupBys[i]);\n if (!field.relation) {\n metadataRepository.registerLabel(\n config.metaData.resModel,\n field.name,\n group.values[i],\n group.labels[i]\n );\n } else {\n metadataRepository.setDisplayName(\n field.relation,\n group.values[i],\n group.labels[i]\n );\n }\n }\n }\n [...tree.directSubTrees.values()].forEach((subTree) => {\n registerLabels(subTree, groupBys);\n });\n };\n\n registerLabels(this.data.colGroupTree, this.metaData.fullColGroupBys);\n registerLabels(this.data.rowGroupTree, this.metaData.fullRowGroupBys);\n }\n\n /**\n * Determines if the given field is a date or datetime field.\n *\n * @param {Field} field Field description\n * @private\n * @returns {boolean} True if the type of the field is date or datetime\n */\n _isDateField(field) {\n return [\"date\", \"datetime\"].includes(field.type);\n }\n\n /**\n * @override\n */\n _getGroupValues(group, groupBys) {\n return groupBys.map((groupBy) => {\n const { field, aggregateOperator } = this.parseGroupField(groupBy);\n if (this._isDateField(field)) {\n const value = this._getGroupStartingDay(groupBy, group);\n if (!value) {\n return false;\n }\n const fOut = FORMATS[aggregateOperator][\"out\"];\n // eslint-disable-next-line no-undef\n const date = moment(value);\n return date.isValid() ? date.format(fOut) : false;\n }\n return this._sanitizeValue(group[groupBy]);\n });\n }\n\n /**\n * When grouping by a time field, return\n * the group starting day (local to the timezone)\n * @param {string} groupBy\n * @param {object} readGroup\n * @returns {string | undefined}\n */\n _getGroupStartingDay(groupBy, readGroup) {\n if (!readGroup[\"__range\"] || !readGroup[\"__range\"][groupBy]) {\n return undefined;\n }\n const { field } = this.parseGroupField(groupBy);\n const sqlValue = readGroup[\"__range\"][groupBy].from;\n if (this.metaData.fields[field.name].type === \"date\") {\n return sqlValue;\n }\n const userTz = session.user_context.tz || luxon.Settings.defaultZoneName;\n return luxon.DateTime.fromSQL(sqlValue, { zone: \"utc\" }).setZone(userTz).toISODate();\n }\n\n /**\n * Check if the given field is used as col group by\n */\n _isCol(field) {\n return this.metaData.fullColGroupBys\n .map(this.parseGroupField)\n .map(({ field }) => field.name)\n .includes(field.name);\n }\n\n /**\n * Check if the given field is used as row group by\n */\n _isRow(field) {\n return this.metaData.fullRowGroupBys\n .map(this.parseGroupField)\n .map(({ field }) => field.name)\n .includes(field.name);\n }\n\n /**\n * Get the value of a field-value for a positional group by\n *\n * @param {object} field Field of the group by\n * @param {unknown} groupValueString Value of the group by\n * @param {(number | boolean | string)[]} rows Values for the previous row group bys\n * @param {(number | boolean | string)[]} cols Values for the previous col group bys\n *\n * @private\n * @returns {number | boolean | string}\n */\n _parsePivotFormulaWithPosition(field, groupValueString, cols, rows) {\n const position = toNumber(groupValueString) - 1;\n let tree;\n if (this._isCol(field)) {\n tree = this.data.colGroupTree;\n for (const col of cols) {\n tree = tree && tree.directSubTrees.get(col);\n }\n } else {\n tree = this.data.rowGroupTree;\n for (const row of rows) {\n tree = tree && tree.directSubTrees.get(row);\n }\n }\n if (tree) {\n const treeKeys = tree.sortedKeys || [...tree.directSubTrees.keys()];\n const sortedKey = treeKeys[position];\n return sortedKey !== undefined ? sortedKey : NO_RECORD_AT_THIS_POSITION;\n }\n return NO_RECORD_AT_THIS_POSITION;\n }\n\n /**\n * Transform the given domain in the structure used in this class\n *\n * @param {(number | boolean | string)[]} domain Domain\n *\n * @private\n */\n _getColsRowsValuesFromDomain(domain) {\n const rows = [];\n const cols = [];\n let i = 0;\n while (i < domain.length) {\n const groupFieldString = domain[i];\n const groupValue = domain[i + 1];\n const { field, isPositional } = this.parseGroupField(groupFieldString);\n let value;\n if (isPositional) {\n value = this._parsePivotFormulaWithPosition(field, groupValue, cols, rows);\n } else {\n value = parsePivotFormulaFieldValue(field, groupValue);\n }\n if (this._isCol(field)) {\n cols.push(value);\n } else if (this._isRow(field)) {\n rows.push(value);\n }\n i += 2;\n }\n return { rows, cols };\n }\n\n /**\n * Get the row structure\n * @returns {Row[]}\n */\n _getSpreadsheetRows(tree) {\n /**@type {Row[]}*/\n let rows = [];\n const group = tree.root;\n const indent = group.labels.length;\n const rowGroupBys = this.metaData.fullRowGroupBys;\n\n rows.push({\n fields: rowGroupBys.slice(0, indent),\n values: group.values.map((val) => val.toString()),\n indent,\n });\n\n const subTreeKeys = tree.sortedKeys || [...tree.directSubTrees.keys()];\n subTreeKeys.forEach((subTreeKey) => {\n const subTree = tree.directSubTrees.get(subTreeKey);\n rows = rows.concat(this._getSpreadsheetRows(subTree));\n });\n return rows;\n }\n\n /**\n * Get the col structure\n * @returns {Column[][]}\n */\n _getSpreadsheetCols() {\n const colGroupBys = this.metaData.fullColGroupBys;\n const height = colGroupBys.length;\n const measureCount = this.metaData.activeMeasures.length;\n const leafCounts = this._getLeafCounts(this.data.colGroupTree);\n\n const headers = new Array(height).fill(0).map(() => []);\n\n function generateTreeHeaders(tree, fields) {\n const group = tree.root;\n const rowIndex = group.values.length;\n if (rowIndex !== 0) {\n const row = headers[rowIndex - 1];\n const leafCount = leafCounts[JSON.stringify(tree.root.values)];\n const cell = {\n fields: colGroupBys.slice(0, rowIndex),\n values: group.values.map((val) => val.toString()),\n width: leafCount * measureCount,\n };\n row.push(cell);\n }\n\n [...tree.directSubTrees.values()].forEach((subTree) => {\n generateTreeHeaders(subTree, fields);\n });\n }\n\n generateTreeHeaders(this.data.colGroupTree, this.metaData.fields);\n const hasColGroupBys = this.metaData.colGroupBys.length;\n\n // 2) generate measures row\n const measureRow = [];\n\n if (hasColGroupBys) {\n headers[headers.length - 1].forEach((cell) => {\n this.metaData.activeMeasures.forEach((measureName) => {\n const measureCell = {\n fields: [...cell.fields, \"measure\"],\n values: [...cell.values, measureName],\n width: 1,\n };\n measureRow.push(measureCell);\n });\n });\n }\n this.metaData.activeMeasures.forEach((measureName) => {\n const measureCell = {\n fields: [\"measure\"],\n values: [measureName],\n width: 1,\n };\n measureRow.push(measureCell);\n });\n headers.push(measureRow);\n // 3) Add the total cell\n if (headers.length === 1) {\n headers.unshift([]); // Will add the total there\n }\n headers[headers.length - 2].push({\n fields: [],\n values: [],\n width: this.metaData.activeMeasures.length,\n });\n\n return headers;\n }\n}\n", "/** @odoo-module */\n\n/**\n * @typedef {Object} Column\n * @property {string[]} fields\n * @property {string[]} values\n * @property {number} width\n *\n * @typedef {Object} Row\n * @property {string[]} fields\n * @property {string[]} values\n * @property {number} intend\n *\n * @typedef {Object} SpreadsheetTableData\n * @property {Column[][]} cols\n * @property {Row[]} rows\n * @property {string[]} measures\n */\n\n/**\n * Class used to ease the construction of a pivot table.\n * Let's consider the following example, with:\n * - columns groupBy: [sales_team, create_date]\n * - rows groupBy: [continent, city]\n * - measures: [revenues]\n * _____________________________________________________________________________________| ----|\n * | | Sale Team 1 | Sale Team 2 | | |\n * | |___________________________|_________________________|_____________| |\n * | | May 2020 | June 2020 | May 2020 | June 2020 | Total | |<---- `cols`\n * | |______________|____________|____________|____________|_____________| | ----|\n * | | Revenues | Revenues | Revenues | Revenues | Revenues | | |<--- `measureRow`\n * |________________|______________|____________|____________|____________|_____________| ----| ----|\n * |Europe | 25 | 35 | 40 | 30 | 65 | ----|\n * | Brussels | 0 | 15 | 30 | 30 | 30 | |\n * | Paris | 25 | 20 | 10 | 0 | 35 | |\n * |North America | 60 | 75 | | | 60 | |<---- `body`\n * | Washington | 60 | 75 | | | 60 | |\n * |Total | 85 | 110 | 40 | 30 | 125 | |\n * |________________|______________|____________|____________|____________|_____________| ----|\n *\n * | |\n * |----------------|\n * |\n * |\n * `rows`\n *\n * `rows` is an array of cells, each cells contains the indent level, the fields used for the group by and the values for theses fields.\n * For example:\n * `Europe`: { indent: 1, fields: [\"continent\"], values: [\"id_of_Europe\"]}\n * `Brussels`: { indent: 2, fields: [\"continent\", \"city\"], values: [\"id_of_Europe\", \"id_of_Brussels\"]}\n * `Total`: { indent: 0, fields: [], values: []}\n *\n * `columns` is an double array, first by row and then by cell. So, in this example, it looks like:\n * [[row1], [row2], [measureRow]]\n * Each cell of a column's row contains the width (span) of the cells, the fields used for the group by and the values for theses fields.\n * For example:\n * `Sale Team 1`: { width: 2, fields: [\"sales_team\"], values: [\"id_of_SaleTeam1\"]}\n * `May 2020` (the one under Sale Team 2): { width: 1, fields: [\"sales_team\", \"create_date\"], values: [\"id_of_SaleTeam2\", \"May 2020\"]}\n * `Revenues` (the one under Total): { width: 1, fields: [\"measure\"], values: [\"revenues\"]}\n *\n */\nexport class SpreadsheetPivotTable {\n /**\n * @param {Column[][]} cols\n * @param {Row[]} rows\n * @param {string[]} measures\n */\n constructor(cols, rows, measures) {\n this._cols = cols;\n this._rows = rows;\n this._measures = measures;\n }\n\n /**\n * @returns {number}\n */\n getNumberOfMeasures() {\n return this._measures.length;\n }\n\n /**\n * @returns {Column[][]}\n */\n getColHeaders() {\n return this._cols;\n }\n\n /**\n * Get the last row of the columns (i.e. the one with the measures)\n * @returns {Column[]}\n */\n getMeasureHeaders() {\n return this._cols[this._cols.length - 1];\n }\n\n /**\n * Get the number of columns leafs (i.e. the number of the last row of columns)\n * @returns {number}\n */\n getColWidth() {\n return this._cols[this._cols.length - 1].length;\n }\n\n /**\n * Get the number of row in each columns\n * @return {number}\n */\n getColHeight() {\n return this._cols.length;\n }\n\n /**\n * @returns {Row[]}\n */\n getRowHeaders() {\n return this._rows;\n }\n\n /**\n * Get the number of rows\n *\n * @returns {number}\n */\n getRowHeight() {\n return this._rows.length;\n }\n\n /**\n * Get the index of the cell in the measure row (i.e. the last one) which\n * correspond to the given values\n *\n * @returns {number}\n */\n getColMeasureIndex(values) {\n const vals = JSON.stringify(values);\n const maxLength = Math.max(...this._cols.map((col) => col.length));\n for (let i = 0; i < maxLength; i++) {\n const cellValues = this._cols.map((col) => JSON.stringify((col[i] || {}).values));\n if (cellValues.includes(vals)) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n *\n * @param {number} colIndex\n * @param {number} rowIndex\n * @returns {Column}\n */\n getNextColCell(colIndex, rowIndex) {\n return this._cols[rowIndex][colIndex];\n }\n\n getRowIndex(values) {\n const vals = JSON.stringify(values);\n return this._rows.findIndex(\n (cell) => JSON.stringify(cell.values.map((val) => val.toString())) === vals\n );\n }\n\n getCellFromMeasureRowAtIndex(index) {\n return this.getMeasureHeaders()[index];\n }\n\n getCellsFromRowAtIndex(index) {\n return this._rows[index];\n }\n\n /**\n * @returns {SpreadsheetTableData}\n */\n export() {\n return {\n cols: this._cols,\n rows: this._rows,\n measures: this._measures,\n };\n }\n}\n", "/** @odoo-module */\n\n/**\n *\n * @typedef {Object} PivotDefinition\n * @property {Array} colGroupBys\n * @property {Array} rowGroupBys\n * @property {Array} measures\n * @property {string} model\n * @property {Array} domain\n * @property {Object} context\n * @property {string} name\n * @property {string} id\n * @property {Object | null} sortedColumn\n *\n * @typedef {Object} Pivot\n * @property {string} id\n * @property {string} dataSourceId\n * @property {PivotDefinition} definition\n * @property {Object} fieldMatching\n *\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").FieldMatching} FieldMatching\n */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { makePivotFormula } from \"../pivot_helpers\";\nimport { getMaxObjectId } from \"@spreadsheet/helpers/helpers\";\nimport { HEADER_STYLE, TOP_LEVEL_STYLE, MEASURE_STYLE } from \"@spreadsheet/helpers/constants\";\nimport PivotDataSource from \"../pivot_data_source\";\nimport { SpreadsheetPivotTable } from \"../pivot_table\";\nimport CommandResult from \"../../o_spreadsheet/cancelled_reason\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { checkFilterFieldMatching } from \"@spreadsheet/global_filters/helpers\";\n\nconst { CorePlugin } = spreadsheet;\n\nexport default class PivotCorePlugin extends CorePlugin {\n constructor(config) {\n super(config);\n this.dataSources = config.custom.dataSources;\n\n this.nextId = 1;\n /** @type {Object.} */\n this.pivots = {};\n globalFiltersFieldMatchers[\"pivot\"] = {\n geIds: () => this.getters.getPivotIds(),\n getDisplayName: (pivotId) => this.getters.getPivotName(pivotId),\n getTag: (pivotId) => sprintf(_t(\"Pivot #%s\"), pivotId),\n getFieldMatching: (pivotId, filterId) => this.getPivotFieldMatching(pivotId, filterId),\n waitForReady: () => this.getPivotsWaitForReady(),\n getModel: (pivotId) => this.getPivotDefinition(pivotId).model,\n getFields: (pivotId) => this.getPivotDataSource(pivotId).getFields(),\n };\n }\n\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"RENAME_ODOO_PIVOT\":\n if (!(cmd.pivotId in this.pivots)) {\n return CommandResult.PivotIdNotFound;\n }\n if (cmd.name === \"\") {\n return CommandResult.EmptyName;\n }\n break;\n case \"INSERT_PIVOT\":\n if (cmd.id !== this.nextId.toString()) {\n return CommandResult.InvalidNextId;\n }\n break;\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n if (cmd.pivot) {\n return checkFilterFieldMatching(cmd.pivot);\n }\n }\n return CommandResult.Success;\n }\n\n /**\n * Handle a spreadsheet command\n *\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"INSERT_PIVOT\": {\n const { sheetId, col, row, id, definition, dataSourceId } = cmd;\n /** @type [number,number] */\n const anchor = [col, row];\n const { cols, rows, measures } = cmd.table;\n const table = new SpreadsheetPivotTable(cols, rows, measures);\n this._addPivot(id, definition, dataSourceId);\n this._insertPivot(sheetId, anchor, id, table);\n this.history.update(\"nextId\", parseInt(id, 10) + 1);\n break;\n }\n case \"RE_INSERT_PIVOT\": {\n const { sheetId, col, row, id } = cmd;\n /** @type [number,number] */\n const anchor = [col, row];\n const { cols, rows, measures } = cmd.table;\n const table = new SpreadsheetPivotTable(cols, rows, measures);\n this._insertPivot(sheetId, anchor, id, table);\n break;\n }\n case \"RENAME_ODOO_PIVOT\": {\n this.history.update(\"pivots\", cmd.pivotId, \"definition\", \"name\", cmd.name);\n break;\n }\n case \"REMOVE_PIVOT\": {\n const pivots = { ...this.pivots };\n delete pivots[cmd.pivotId];\n this.history.update(\"pivots\", pivots);\n break;\n }\n case \"UPDATE_ODOO_PIVOT_DOMAIN\": {\n this.history.update(\n \"pivots\",\n cmd.pivotId,\n \"definition\",\n \"searchParams\",\n \"domain\",\n cmd.domain\n );\n const pivot = this.pivots[cmd.pivotId];\n this.dataSources.add(pivot.dataSourceId, PivotDataSource, pivot.definition);\n break;\n }\n case \"UNDO\":\n case \"REDO\": {\n const domainEditionCommands = cmd.commands.filter(\n (cmd) => cmd.type === \"UPDATE_ODOO_PIVOT_DOMAIN\"\n );\n for (const cmd of domainEditionCommands) {\n const pivot = this.pivots[cmd.pivotId];\n this.dataSources.add(pivot.dataSourceId, PivotDataSource, pivot.definition);\n }\n break;\n }\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n if (cmd.pivot) {\n this._setPivotFieldMatching(cmd.filter.id, cmd.pivot);\n }\n break;\n case \"REMOVE_GLOBAL_FILTER\":\n this._onFilterDeletion(cmd.id);\n break;\n }\n }\n\n // -------------------------------------------------------------------------\n // Getters\n // -------------------------------------------------------------------------\n\n /**\n * @param {string} id\n * @returns {PivotDataSource|undefined}\n */\n getPivotDataSource(id) {\n const dataSourceId = this.pivots[id].dataSourceId;\n return this.dataSources.get(dataSourceId);\n }\n\n /**\n * @param {string} id\n * @returns {string}\n */\n getPivotDisplayName(id) {\n return `(#${id}) ${this.getPivotName(id)}`;\n }\n\n /**\n * @param {string} id\n * @returns {string}\n */\n getPivotName(id) {\n return _t(this.pivots[id].definition.name);\n }\n\n /**\n * @param {string} id\n * @returns {string}\n */\n getPivotFieldMatch(id) {\n return this.pivots[id].fieldMatching;\n }\n\n /**\n * @param {string} id\n * @returns {Promise}\n */\n async getAsyncPivotDataSource(id) {\n const dataSourceId = this.pivots[id].dataSourceId;\n await this.dataSources.load(dataSourceId);\n return this.getPivotDataSource(id);\n }\n\n /**\n * Retrieve the next available id for a new pivot\n *\n * @returns {string} id\n */\n getNextPivotId() {\n return this.nextId.toString();\n }\n\n /**\n * @param {string} id Id of the pivot\n *\n * @returns {PivotDefinition}\n */\n getPivotDefinition(id) {\n const def = this.pivots[id].definition;\n return {\n colGroupBys: [...def.metaData.colGroupBys],\n context: { ...def.searchParams.context },\n domain: [...def.searchParams.domain],\n id,\n measures: [...def.metaData.activeMeasures],\n model: def.metaData.resModel,\n rowGroupBys: [...def.metaData.rowGroupBys],\n name: def.name,\n sortedColumn: def.metaData.sortedColumn ? { ...def.metaData.sortedColumn } : null,\n };\n }\n\n /**\n * Retrieve all the pivot ids\n *\n * @returns {Array}\n */\n getPivotIds() {\n return Object.keys(this.pivots);\n }\n\n /**\n * Check if an id is an id of an existing pivot\n *\n * @param {number} pivotId Id of the pivot\n *\n * @returns {boolean}\n */\n isExistingPivot(pivotId) {\n return pivotId in this.pivots;\n }\n\n /**\n * Get the current pivotFieldMatching on a pivot\n *\n * @param {string} pivotId\n * @param {string} filterId\n */\n getPivotFieldMatching(pivotId, filterId) {\n return this.pivots[pivotId].fieldMatching[filterId];\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n /**\n *\n * @return {Promise[]}\n */\n getPivotsWaitForReady() {\n return this.getPivotIds().map((pivotId) => this.getPivotDataSource(pivotId).loadMetadata());\n }\n\n /**\n * Sets the current pivotFieldMatching on a pivot\n *\n * @param {string} filterId\n * @param {Record} pivotFieldMatches\n */\n _setPivotFieldMatching(filterId, pivotFieldMatches) {\n const pivots = { ...this.pivots };\n for (const [pivotId, fieldMatch] of Object.entries(pivotFieldMatches)) {\n pivots[pivotId].fieldMatching[filterId] = fieldMatch;\n }\n this.history.update(\"pivots\", pivots);\n }\n\n _onFilterDeletion(filterId) {\n const pivots = { ...this.pivots };\n for (const pivotId in pivots) {\n this.history.update(\"pivots\", pivotId, \"fieldMatching\", filterId, undefined);\n }\n }\n\n /**\n * @param {string} id\n * @param {PivotDefinition} definition\n * @param {string} dataSourceId\n */\n _addPivot(id, definition, dataSourceId, fieldMatching = {}) {\n const pivots = { ...this.pivots };\n pivots[id] = {\n id,\n definition,\n dataSourceId,\n fieldMatching,\n };\n\n if (!this.dataSources.contains(dataSourceId)) {\n this.dataSources.add(dataSourceId, PivotDataSource, definition);\n }\n this.history.update(\"pivots\", pivots);\n }\n\n /**\n * @param {string} sheetId\n * @param {[number, number]} anchor\n * @param {string} id\n * @param {SpreadsheetPivotTable} table\n */\n _insertPivot(sheetId, anchor, id, table) {\n this._resizeSheet(sheetId, anchor, table);\n this._insertColumns(sheetId, anchor, id, table);\n this._insertRows(sheetId, anchor, id, table);\n this._insertBody(sheetId, anchor, id, table);\n }\n\n /**\n * @param {string} sheetId\n * @param {[number, number]} anchor\n * @param {string} id\n * @param {SpreadsheetPivotTable} table\n */\n _insertColumns(sheetId, anchor, id, table) {\n let anchorLeft = anchor[0] + 1;\n let anchorTop = anchor[1];\n for (const _row of table.getColHeaders()) {\n anchorLeft = anchor[0] + 1;\n for (const cell of _row) {\n const args = [id];\n for (let i = 0; i < cell.fields.length; i++) {\n args.push(cell.fields[i]);\n args.push(cell.values[i]);\n }\n if (cell.width > 1) {\n this._merge(sheetId, {\n top: anchorTop,\n bottom: anchorTop,\n left: anchorLeft,\n right: anchorLeft + cell.width - 1,\n });\n }\n this._addPivotFormula(sheetId, anchorLeft, anchorTop, \"ODOO.PIVOT.HEADER\", args);\n anchorLeft += cell.width;\n }\n anchorTop++;\n }\n const colHeight = table.getColHeight();\n const colWidth = table.getColWidth();\n const lastRowBeforeMeasureRow = anchor[1] + colHeight - 2;\n const right = anchor[0] + colWidth;\n const left = right - table.getNumberOfMeasures() + 1;\n for (let anchorTop = anchor[1]; anchorTop < lastRowBeforeMeasureRow; anchorTop++) {\n this._merge(sheetId, { top: anchorTop, bottom: anchorTop, left, right });\n }\n const headersZone = {\n top: anchor[1],\n bottom: lastRowBeforeMeasureRow,\n left: anchor[0],\n right: anchor[0] + colWidth,\n };\n const measuresZone = {\n top: anchor[1] + colHeight - 1,\n bottom: anchor[1] + colHeight - 1,\n left: anchor[0],\n right: anchor[0] + colWidth,\n };\n this.dispatch(\"SET_FORMATTING\", { sheetId, target: [headersZone], style: TOP_LEVEL_STYLE });\n this.dispatch(\"SET_FORMATTING\", { sheetId, target: [measuresZone], style: MEASURE_STYLE });\n }\n\n /**\n * Merge a zone\n *\n * @param {string} sheetId\n * @param {Object} zone\n *\n * @private\n */\n _merge(sheetId, zone) {\n this.dispatch(\"ADD_MERGE\", { sheetId, target: [zone] });\n }\n\n /**\n * @param {string} sheetId\n * @param {[number,number]} anchor\n * @param {SpreadsheetPivotTable} table\n */\n _resizeSheet(sheetId, anchor, table) {\n const colLimit = table.getColWidth() + 1; // +1 for the Top-Left\n const numberCols = this.getters.getNumberCols(sheetId);\n const deltaCol = numberCols - anchor[0];\n if (deltaCol < colLimit) {\n this.dispatch(\"ADD_COLUMNS_ROWS\", {\n dimension: \"COL\",\n base: numberCols - 1,\n sheetId: sheetId,\n quantity: colLimit - deltaCol,\n position: \"after\",\n });\n }\n const rowLimit = table.getColHeight() + table.getRowHeight();\n const numberRows = this.getters.getNumberRows(sheetId);\n const deltaRow = numberRows - anchor[1];\n if (deltaRow < rowLimit) {\n this.dispatch(\"ADD_COLUMNS_ROWS\", {\n dimension: \"ROW\",\n base: numberRows - 1,\n sheetId: sheetId,\n quantity: rowLimit - deltaRow,\n position: \"after\",\n });\n }\n }\n\n /**\n * @param {string} sheetId\n * @param {[number, number]} anchor\n * @param {string} id\n * @param {SpreadsheetPivotTable} table\n */\n _insertRows(sheetId, anchor, id, table) {\n let y = anchor[1] + table.getColHeight();\n const x = anchor[0];\n for (const row of table.getRowHeaders()) {\n const args = [id];\n for (let i = 0; i < row.fields.length; i++) {\n args.push(row.fields[i]);\n args.push(row.values[i]);\n }\n this._addPivotFormula(sheetId, x, y, \"ODOO.PIVOT.HEADER\", args);\n if (row.indent <= 2) {\n const target = [{ top: y, bottom: y, left: x, right: x }];\n const style = row.indent === 2 ? HEADER_STYLE : TOP_LEVEL_STYLE;\n this.dispatch(\"SET_FORMATTING\", { sheetId, target, style });\n }\n y++;\n }\n }\n\n /**\n * @param {string} sheetId\n * @param {[number, number]} anchor\n * @param {string} id\n * @param {SpreadsheetPivotTable} table\n */\n _insertBody(sheetId, anchor, id, table) {\n let x = anchor[0] + 1;\n for (const col of table.getMeasureHeaders()) {\n let y = anchor[1] + table.getColHeight();\n const measure = col.values[col.values.length - 1];\n for (const row of table.getRowHeaders()) {\n const args = [id, measure];\n for (let i = 0; i < row.fields.length; i++) {\n args.push(row.fields[i]);\n args.push(row.values[i]);\n }\n for (let i = 0; i < col.fields.length - 1; i++) {\n args.push(col.fields[i]);\n args.push(col.values[i]);\n }\n this._addPivotFormula(sheetId, x, y, \"ODOO.PIVOT\", args);\n y++;\n }\n x++;\n }\n }\n\n /**\n * @param {string} sheetId\n * @param {number} col\n * @param {number} row\n * @param {string} formula\n * @param {Array} args\n */\n _addPivotFormula(sheetId, col, row, formula, args) {\n this.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n content: makePivotFormula(formula, args),\n });\n }\n\n // ---------------------------------------------------------------------\n // Import/Export\n // ---------------------------------------------------------------------\n\n /**\n * Import the pivots\n *\n * @param {Object} data\n */\n import(data) {\n if (data.pivots) {\n for (const [id, pivot] of Object.entries(data.pivots)) {\n const definition = {\n metaData: {\n colGroupBys: pivot.colGroupBys,\n rowGroupBys: pivot.rowGroupBys,\n activeMeasures: pivot.measures.map((elt) => elt.field),\n resModel: pivot.model,\n sortedColumn: !pivot.sortedColumn\n ? undefined\n : {\n groupId: pivot.sortedColumn.groupId,\n measure: pivot.sortedColumn.measure,\n order: pivot.sortedColumn.order,\n },\n },\n searchParams: {\n groupBy: [],\n orderBy: [],\n domain: pivot.domain,\n context: pivot.context,\n },\n name: pivot.name,\n };\n this._addPivot(id, definition, this.uuidGenerator.uuidv4(), pivot.fieldMatching);\n }\n }\n this.nextId = data.pivotNextId || getMaxObjectId(this.pivots) + 1;\n }\n /**\n * Export the pivots\n *\n * @param {Object} data\n */\n export(data) {\n data.pivots = {};\n for (const id in this.pivots) {\n data.pivots[id] = JSON.parse(JSON.stringify(this.getPivotDefinition(id)));\n data.pivots[id].measures = data.pivots[id].measures.map((elt) => ({ field: elt }));\n data.pivots[id].fieldMatching = this.pivots[id].fieldMatching;\n }\n data.pivotNextId = this.nextId;\n }\n}\n\nPivotCorePlugin.getters = [\n \"getNextPivotId\",\n \"getPivotDefinition\",\n \"getPivotDisplayName\",\n \"getPivotIds\",\n \"getPivotName\",\n \"getAsyncPivotDataSource\",\n \"isExistingPivot\",\n \"getPivotDataSource\",\n \"getPivotFieldMatch\",\n \"getPivotFieldMatching\",\n];\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { getFirstPivotFunction } from \"../pivot_helpers\";\nimport { FILTER_DATE_OPTION, monthsOptions } from \"@spreadsheet/assets_backend/constants\";\nimport { Domain } from \"@web/core/domain\";\nimport { NO_RECORD_AT_THIS_POSITION } from \"../pivot_model\";\n\nconst { astToFormula } = spreadsheet;\nconst { DateTime } = luxon;\n\n/**\n * Convert pivot period to the related filter value\n *\n * @param {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").RangeType} timeRange\n * @param {string} value\n * @returns {object}\n */\nfunction pivotPeriodToFilterValue(timeRange, value) {\n // reuse the same logic as in `parseAccountingDate`?\n const yearOffset = (value.split(\"/\").pop() | 0) - DateTime.now().year;\n switch (timeRange) {\n case \"year\":\n return {\n yearOffset,\n };\n case \"month\": {\n const month = value.split(\"/\")[0] | 0;\n return {\n yearOffset,\n period: monthsOptions[month - 1].id,\n };\n }\n case \"quarter\": {\n const quarter = value.split(\"/\")[0] | 0;\n return {\n yearOffset,\n period: FILTER_DATE_OPTION.quarter[quarter - 1],\n };\n }\n }\n}\n\nexport default class PivotUIPlugin extends spreadsheet.UIPlugin {\n constructor(config) {\n super(config);\n /** @type {string} */\n this.selectedPivotId = undefined;\n this.selection.observe(this, {\n handleEvent: this.handleEvent.bind(this),\n });\n }\n\n handleEvent(event) {\n if (!this.getters.isDashboard()) {\n return;\n }\n switch (event.type) {\n case \"ZonesSelected\": {\n const sheetId = this.getters.getActiveSheetId();\n const { col, row } = event.anchor.cell;\n const cell = this.getters.getCell({ sheetId, col, row });\n if (cell !== undefined && cell.content.startsWith(\"=ODOO.PIVOT.HEADER(\")) {\n const filters = this.getFiltersMatchingPivot(cell.content);\n this.dispatch(\"SET_MANY_GLOBAL_FILTER_VALUE\", { filters });\n }\n break;\n }\n }\n }\n\n beforeHandle(cmd) {\n switch (cmd.type) {\n case \"START\":\n // make sure the domains are correctly set before\n // any evaluation\n this._addDomains();\n break;\n }\n }\n\n /**\n * Handle a spreadsheet command\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"SELECT_PIVOT\":\n this.selectedPivotId = cmd.pivotId;\n break;\n case \"REFRESH_PIVOT\":\n this._refreshOdooPivot(cmd.id);\n break;\n case \"REFRESH_ALL_DATA_SOURCES\":\n this._refreshOdooPivots();\n break;\n case \"ADD_GLOBAL_FILTER\":\n case \"EDIT_GLOBAL_FILTER\":\n case \"REMOVE_GLOBAL_FILTER\":\n case \"SET_GLOBAL_FILTER_VALUE\":\n case \"CLEAR_GLOBAL_FILTER_VALUE\":\n this._addDomains();\n break;\n case \"UNDO\":\n case \"REDO\":\n if (\n cmd.commands.find((command) =>\n [\n \"ADD_GLOBAL_FILTER\",\n \"EDIT_GLOBAL_FILTER\",\n \"REMOVE_GLOBAL_FILTER\",\n ].includes(command.type)\n )\n ) {\n this._addDomains();\n }\n break;\n }\n }\n\n // ---------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------\n\n /**\n * Retrieve the pivotId of the current selected cell\n *\n * @returns {string}\n */\n getSelectedPivotId() {\n return this.selectedPivotId;\n }\n\n /**\n * Get the id of the pivot at the given position. Returns undefined if there\n * is no pivot at this position\n *\n * @param {{ sheetId: string; col: number; row: number}} position\n *\n * @returns {string|undefined}\n */\n getPivotIdFromPosition(position) {\n const cell = this.getters.getCell(position);\n if (cell && cell.isFormula) {\n const pivotFunction = getFirstPivotFunction(cell.content);\n if (pivotFunction) {\n const content = astToFormula(pivotFunction.args[0]);\n return this.getters.evaluateFormula(content).toString();\n }\n }\n return undefined;\n }\n\n /**\n * Get the computed domain of a pivot\n * CLEAN ME not used outside of tests\n * @param {string} pivotId Id of the pivot\n * @returns {Array}\n */\n getPivotComputedDomain(pivotId) {\n return this.getters.getPivotDataSource(pivotId).getComputedDomain();\n }\n\n /**\n * Return all possible values in the pivot for a given field.\n *\n * @param {string} pivotId Id of the pivot\n * @param {string} fieldName\n * @returns {Array}\n */\n getPivotGroupByValues(pivotId, fieldName) {\n return this.getters.getPivotDataSource(pivotId).getPossibleValuesForGroupBy(fieldName);\n }\n\n /**\n * Get the value of a pivot header\n *\n * @param {string} pivotId Id of a pivot\n * @param {Array} domain Domain\n */\n getDisplayedPivotHeaderValue(pivotId, domain) {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n dataSource.markAsHeaderUsed(domain);\n const len = domain.length;\n if (len === 0) {\n return _t(\"Total\");\n }\n return dataSource.getDisplayedPivotHeaderValue(domain);\n }\n\n /**\n * Get the value for a pivot cell\n *\n * @param {string} pivotId Id of a pivot\n * @param {string} measure Field name of the measures\n * @param {Array} domain Domain\n *\n * @returns {string|number|undefined}\n */\n getPivotCellValue(pivotId, measure, domain) {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n dataSource.markAsValueUsed(domain, measure);\n return dataSource.getPivotCellValue(measure, domain);\n }\n\n /**\n * Get the filter impacted by a pivot formula's argument\n *\n * @param {string} formula Formula of the pivot cell\n *\n * @returns {Array}\n */\n getFiltersMatchingPivot(formula) {\n const functionDescription = getFirstPivotFunction(formula);\n if (!functionDescription) {\n return [];\n }\n const { args } = functionDescription;\n const evaluatedArgs = args\n .map(astToFormula)\n .map((arg) => this.getters.evaluateFormula(arg));\n if (evaluatedArgs.length <= 2) {\n return [];\n }\n const pivotId = evaluatedArgs[0];\n const argField = evaluatedArgs[evaluatedArgs.length - 2];\n if (argField === \"measure\") {\n return [];\n }\n const filters = this.getters.getGlobalFilters();\n const matchingFilters = [];\n\n for (const filter of filters) {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n const { field, aggregateOperator: time } = dataSource.parseGroupField(argField);\n const pivotFieldMatching = this.getters.getPivotFieldMatching(pivotId, filter.id);\n if (pivotFieldMatching && pivotFieldMatching.chain === field.name) {\n let value = dataSource.getPivotHeaderValue(evaluatedArgs.slice(-2));\n if (value === NO_RECORD_AT_THIS_POSITION) {\n continue;\n }\n let transformedValue;\n const currentValue = this.getters.getGlobalFilterValue(filter.id);\n switch (filter.type) {\n case \"date\":\n if (time === filter.rangeType) {\n transformedValue = pivotPeriodToFilterValue(time, value);\n if (JSON.stringify(transformedValue) === JSON.stringify(currentValue)) {\n transformedValue = undefined;\n }\n } else {\n continue;\n }\n break;\n case \"relation\":\n if (typeof value == \"string\") {\n value = Number(value);\n if (Number.isNaN(value)) {\n break;\n }\n }\n if (JSON.stringify(currentValue) !== `[${value}]`) {\n transformedValue = [value];\n }\n break;\n case \"text\":\n if (currentValue !== value) {\n transformedValue = value;\n }\n break;\n }\n matchingFilters.push({ filterId: filter.id, value: transformedValue });\n }\n }\n return matchingFilters;\n }\n\n // ---------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------\n\n /**\n * Refresh the cache of a pivot\n *\n * @param {string} pivotId Id of the pivot\n */\n _refreshOdooPivot(pivotId) {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n dataSource.clearUsedValues();\n dataSource.load({ reload: true });\n }\n\n /**\n * Refresh the cache of all the pivots\n */\n _refreshOdooPivots() {\n for (const pivotId of this.getters.getPivotIds()) {\n this._refreshOdooPivot(pivotId, false);\n }\n }\n\n /**\n * Add an additional domain to a pivot\n *\n * @private\n *\n * @param {string} pivotId pivot id\n */\n _addDomain(pivotId) {\n const domainList = [];\n for (const [filterId, fieldMatch] of Object.entries(\n this.getters.getPivotFieldMatch(pivotId)\n )) {\n domainList.push(this.getters.getGlobalFilterDomain(filterId, fieldMatch));\n }\n const domain = Domain.combine(domainList, \"AND\").toString();\n this.getters.getPivotDataSource(pivotId).addDomain(domain);\n }\n\n /**\n * Add an additional domain to all pivots\n *\n * @private\n *\n */\n _addDomains() {\n for (const pivotId of this.getters.getPivotIds()) {\n this._addDomain(pivotId);\n }\n }\n}\n\nPivotUIPlugin.getters = [\n \"getSelectedPivotId\",\n \"getPivotComputedDomain\",\n \"getDisplayedPivotHeaderValue\",\n \"getPivotIdFromPosition\",\n \"getPivotCellValue\",\n \"getPivotGroupByValues\",\n \"getFiltersMatchingPivot\",\n];\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/webclient/actions/action_hook\";\n\nimport { UNTITLED_SPREADSHEET_NAME } from \"@spreadsheet/helpers/constants\";\nimport { initCallbackRegistry } from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport { LegacyComponent } from \"@web/legacy/legacy_component\";\nimport { loadSpreadsheetDependencies } from \"@spreadsheet/helpers/helpers\";\n\n/**\n * @typedef SpreadsheetRecord\n * @property {number} id\n * @property {string} name\n * @property {string} raw\n * @property {Object[]} revisions\n * @property {boolean} snapshot_requested\n * @property {Boolean} isReadonly\n */\n\nconst { onMounted, onWillStart, useState } = owl;\nexport class AbstractSpreadsheetAction extends LegacyComponent {\n setup() {\n if (!this.props.action.params) {\n // the action is coming from a this.trigger(\"do-action\", ... ) of owl (not wowl and not legacy)\n this.params = this.props.action.context;\n } else {\n // the action is coming from wowl\n this.params = this.props.action.params;\n }\n this.isEmptySpreadsheet = this.params.is_new_spreadsheet || false;\n this.resId =\n this.params.spreadsheet_id ||\n this.params.active_id || // backward compatibility. spreadsheet_id used to be active_id\n (this.props.state && this.props.state.resId); // used when going back to a spreadsheet via breadcrumb\n this.router = useService(\"router\");\n this.actionService = useService(\"action\");\n this.notifications = useService(\"notification\");\n this.orm = useService(\"orm\");\n this.http = useService(\"http\");\n useSetupAction({\n getLocalState: () => {\n return {\n resId: this.resId,\n };\n },\n });\n this.state = useState({\n spreadsheetName: UNTITLED_SPREADSHEET_NAME,\n });\n\n onWillStart(() => this.onWillStart());\n onMounted(() => this.onMounted());\n }\n\n async onWillStart() {\n // if we are returning to the spreadsheet via the breadcrumb, we don't want\n // to do all the \"creation\" options of the actions\n if (!this.props.state) {\n await Promise.all([this._setupPreProcessingCallbacks(), loadSpreadsheetDependencies()]);\n }\n const [record] = await Promise.all([this._fetchData(), loadSpreadsheetDependencies()]);\n this._initializeWith(record);\n }\n\n async _setupPreProcessingCallbacks() {\n if (this.params.preProcessingAction) {\n const initCallbackGenerator = initCallbackRegistry\n .get(this.params.preProcessingAction)\n .bind(this);\n this.initCallback = await initCallbackGenerator(this.params.preProcessingActionData);\n }\n if (this.params.preProcessingAsyncAction) {\n const initCallbackGenerator = initCallbackRegistry\n .get(this.params.preProcessingAsyncAction)\n .bind(this);\n this.asyncInitCallback = await initCallbackGenerator(\n this.params.preProcessingAsyncActionData\n );\n }\n }\n\n onMounted() {\n this.router.pushState({ spreadsheet_id: this.resId });\n this.env.config.setDisplayName(this.state.spreadsheetName);\n }\n\n /**\n * @protected\n * @abstract\n * @param {SpreadsheetRecord} record\n */\n _initializeWith(record) {\n throw new Error(\"not implemented by children\");\n }\n\n async _onMakeCopy() {\n throw new Error(\"not implemented by children\");\n }\n async _onNewSpreadsheet() {\n throw new Error(\"not implemented by children\");\n }\n async _onSpreadsheetSaved() {\n throw new Error(\"not implemented by children\");\n }\n async _onSpreadSheetNameChanged() {\n throw new Error(\"not implemented by children\");\n }\n\n /**\n * @returns {Promise}\n */\n async _fetchData() {\n throw new Error(\"not implemented by children\");\n }\n\n /**\n * @protected\n */\n _notifyCreation() {\n this.notifications.add(this.notificationMessage, {\n type: \"info\",\n sticky: false,\n });\n }\n\n /**\n * Open a spreadsheet\n * @private\n */\n _openSpreadsheet(spreadsheetId) {\n this._notifyCreation();\n this.actionService.doAction(\n {\n type: \"ir.actions.client\",\n tag: this.props.action.tag,\n params: { spreadsheet_id: spreadsheetId },\n },\n { clear_breadcrumbs: true }\n );\n }\n}\n", "/** @odoo-module **/\n\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SpreadsheetName } from \"./spreadsheet_name\";\n\nconst { Component, useState } = owl;\n\nexport class SpreadsheetControlPanel extends Component {\n setup() {\n this.controlPanelDisplay = {\n \"bottom-left\": false,\n \"bottom-right\": false,\n };\n this.actionService = useService(\"action\");\n this.breadcrumbs = useState(this.env.config.breadcrumbs);\n }\n\n /**\n * Called when an element of the breadcrumbs is clicked.\n *\n * @param {string} jsId\n */\n onBreadcrumbClicked(jsId) {\n this.actionService.restore(jsId);\n }\n}\n\nSpreadsheetControlPanel.template = \"spreadsheet_edition.SpreadsheetControlPanel\";\nSpreadsheetControlPanel.components = {\n ControlPanel,\n SpreadsheetName,\n};\nSpreadsheetControlPanel.props = {\n spreadsheetName: String,\n isSpreadsheetSynced: {\n type: Boolean,\n optional: true,\n },\n numberOfConnectedUsers: {\n type: Number,\n optional: true,\n },\n isReadonly: {\n type: Boolean,\n optional: true,\n },\n onSpreadsheetNameChanged: {\n type: Function,\n optional: true,\n },\n};\n", "/** @odoo-module **/\n\nimport { UNTITLED_SPREADSHEET_NAME } from \"@spreadsheet/helpers/constants\";\n\nconst { Component, onMounted, useState, useRef, onWillUpdateProps } = owl;\n\nconst WIDTH_MARGIN = 3;\nconst PADDING_RIGHT = 5;\nconst PADDING_LEFT = PADDING_RIGHT - WIDTH_MARGIN;\n\nexport class SpreadsheetName extends Component {\n setup() {\n this.placeholder = UNTITLED_SPREADSHEET_NAME;\n this.state = useState({\n inputSize: 1,\n isUntitled: this._isUntitled(this.props.name),\n name: this.props.name,\n });\n this.input = useRef(\"speadsheetNameInput\");\n\n onMounted(() => {\n this._setInputSize(this.state.name);\n });\n onWillUpdateProps(nextProps => {\n if (nextProps.name !== this.props.name) {\n this.state.name = nextProps.name;\n this.state.isUntitled = this._isUntitled(nextProps.name);\n }\n });\n }\n\n /**\n * @private\n * @param {string} text in the input element\n */\n _setInputSize(text) {\n const { fontFamily, fontSize } = window.getComputedStyle(this.input.el);\n const font = `${fontSize} ${fontFamily}`;\n this.state.inputSize =\n this._computeTextWidth(text || this.placeholder, font) +\n PADDING_RIGHT +\n PADDING_LEFT;\n }\n\n /**\n * Return the width in pixels of a text with the given font.\n * @private\n * @param {string} text\n * @param {string} font css font attribute value\n * @returns {number} width in pixels\n */\n _computeTextWidth(text, font) {\n const canvas = document.createElement(\"canvas\");\n const context = canvas.getContext(\"2d\");\n context.font = font;\n const width = context.measureText(text).width;\n // add a small extra margin, otherwise the text jitters in\n // the input because it overflows very slightly for some\n // letters (?).\n return Math.ceil(width) + WIDTH_MARGIN;\n }\n\n /**\n * Check if the name is empty or is the generic name\n * for untitled spreadsheets.\n * @param {string} name\n * @returns {boolean}\n */\n _isUntitled(name) {\n name = name.trim();\n return !name || name === UNTITLED_SPREADSHEET_NAME.toString();\n }\n\n /**\n * @private\n * @param {InputEvent} ev\n */\n _onFocus(ev) {\n if (this._isUntitled(ev.target.value)) {\n ev.target.value = this.placeholder;\n ev.target.select();\n }\n }\n\n /**\n * @private\n * @param {InputEvent} ev\n */\n _onInput(ev) {\n const value = ev.target.value;\n this.state.isUntitled = this._isUntitled(value);\n this.state.name = value\n this._setInputSize(value);\n }\n\n /**\n * @private\n * @param {InputEvent} ev\n */\n _onNameChanged(ev) {\n const value = ev.target.value.trim();\n ev.target.value = value;\n this._setInputSize(value);\n this.props.onSpreadsheetNameChanged({\n name: value,\n });\n ev.target.blur();\n }\n}\n\nSpreadsheetName.template = \"spreadsheet_edition.SpreadsheetName\";\nSpreadsheetName.props = {\n name: String,\n isReadonly: Boolean,\n onSpreadsheetNameChanged: { type: Function, optional: true },\n};\nSpreadsheetName.defaultProps = {\n onSpreadsheetNameChanged: () => {},\n};\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport Dialog from \"web.OwlDialog\";\nimport { useSetupAction } from \"@web/webclient/actions/action_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { DEFAULT_LINES_NUMBER } from \"@spreadsheet/helpers/constants\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { LegacyComponent } from \"@web/legacy/legacy_component\";\nimport { DataSources } from \"@spreadsheet/data_sources/data_sources\";\nimport { migrate } from \"@spreadsheet/o_spreadsheet/migration\";\n\nconst { onMounted, onWillUnmount, useExternalListener, useState, useSubEnv, onWillStart } = owl;\nconst uuidGenerator = new spreadsheet.helpers.UuidGenerator();\n\nconst { Spreadsheet, Model } = spreadsheet;\n\nconst tags = new Set();\n\nexport default class SpreadsheetComponent extends LegacyComponent {\n setup() {\n this.orm = useService(\"orm\");\n const user = useService(\"user\");\n this.ui = useService(\"ui\");\n this.action = useService(\"action\");\n this.notifications = useService(\"notification\");\n\n useSubEnv({\n newSpreadsheet: this.newSpreadsheet.bind(this),\n makeCopy: this.makeCopy.bind(this),\n download: this._download.bind(this),\n getLinesNumber: this._getLinesNumber.bind(this),\n notifyUser: this.notifyUser.bind(this),\n raiseError: this.raiseError.bind(this),\n editText: this.editText.bind(this),\n askConfirmation: this.askConfirmation.bind(this),\n });\n\n useSetupAction({\n beforeLeave: this._onLeave.bind(this),\n });\n\n useExternalListener(window, \"beforeunload\", this._onLeave.bind(this));\n\n this.state = useState({\n dialog: {\n isDisplayed: false,\n title: undefined,\n isEditText: false,\n errorText: undefined,\n inputContent: undefined,\n isEditInteger: false,\n inputIntegerContent: undefined,\n },\n });\n\n const dataSources = new DataSources(this.orm);\n\n this.model = new Model(\n migrate(this.props.data),\n {\n custom: { env: this.env, orm: this.orm, dataSources },\n external: {\n fileStore: this.props.fileStore,\n loadCurrencies: this.loadCurrencies.bind(this),\n },\n transportService: this.props.transportService,\n client: {\n id: uuidGenerator.uuidv4(),\n name: user.name,\n userId: user.userId,\n },\n mode: this.props.isReadonly ? \"readonly\" : \"normal\",\n snapshotRequested: this.props.snapshotRequested,\n },\n this.props.stateUpdateMessages\n );\n\n if (this.env.debug) {\n spreadsheet.__DEBUG__ = spreadsheet.__DEBUG__ || {};\n spreadsheet.__DEBUG__.model = this.model;\n }\n\n this.model.on(\"unexpected-revision-id\", this, () => {\n if (this.props.onUnexpectedRevisionId) {\n this.props.onUnexpectedRevisionId();\n }\n });\n dataSources.addEventListener(\"data-source-updated\", () => {\n const sheetId = this.model.getters.getActiveSheetId();\n this.model.dispatch(\"EVALUATE_CELLS\", { sheetId });\n });\n if (this.props.showFormulas) {\n this.model.dispatch(\"SET_FORMULA_VISIBILITY\", { show: true });\n }\n\n this.dialogContent = undefined;\n this.pivot = undefined;\n this.confirmDialog = () => true;\n\n onWillStart(async () => {\n if (this.props.asyncInitCallback) {\n await this.props.asyncInitCallback(this.model);\n }\n });\n\n onMounted(() => {\n if (this.props.initCallback) {\n this.props.initCallback(this.model);\n }\n this.model.on(\"update\", this, () => {\n if (this.props.spreadsheetSyncStatus) {\n this.props.spreadsheetSyncStatus({\n synced: this.model.getters.isFullySynchronized(),\n numberOfConnectedUsers: this.getConnectedUsers(),\n });\n }\n });\n });\n\n onWillUnmount(() => this._onLeave());\n }\n\n /**\n * Return the number of connected users. If one user has more than\n * one open tab, it's only counted once.\n * @return {number}\n */\n getConnectedUsers() {\n return new Set(\n [...this.model.getters.getConnectedClients().values()].map((client) => client.userId)\n ).size;\n }\n\n /**\n * Open a dialog to ask a confirmation to the user.\n *\n * @param {string} content Content to display\n * @param {Function} confirm Callback if the user press 'Confirm'\n */\n askConfirmation(content, confirm) {\n this.dialogContent = content;\n this.confirmDialog = () => {\n confirm();\n this.closeDialog();\n };\n this.state.dialog.isDisplayed = true;\n }\n\n /**\n * Ask the user to edit a text\n *\n * @param {string} title Title of the popup\n * @param {Function} callback Callback to call with the entered text\n * @param {Object} options Options of the dialog. Can contain a placeholder and an error message.\n */\n editText(title, callback, options = {}) {\n this.dialogContent = undefined;\n this.state.dialog.title = title && title.toString();\n this.state.dialog.errorText = options.error && options.error.toString();\n this.state.dialog.isEditText = true;\n this.state.inputContent = options.placeholder;\n this.confirmDialog = () => {\n this.closeDialog();\n callback(this.state.inputContent);\n };\n this.state.dialog.isDisplayed = true;\n }\n\n _getLinesNumber(callback) {\n this.dialogContent = _t(\"Select the number of records to insert\");\n this.state.dialog.title = _t(\"Re-insert list\");\n this.state.dialog.isEditInteger = true;\n this.state.dialog.inputIntegerContent = DEFAULT_LINES_NUMBER;\n this.confirmDialog = () => {\n this.closeDialog();\n callback(this.state.dialog.inputIntegerContent);\n };\n this.state.dialog.isDisplayed = true;\n }\n\n /**\n * Close the dialog.\n */\n closeDialog() {\n this.dialogContent = undefined;\n this.confirmDialog = () => true;\n this.state.dialog.title = undefined;\n this.state.dialog.errorText = undefined;\n this.state.dialog.isDisplayed = false;\n this.state.dialog.isEditText = false;\n this.state.dialog.isEditInteger = false;\n document.querySelector(\".o-grid>input\").focus();\n }\n\n /**\n * Load currencies from database\n */\n async loadCurrencies() {\n const odooCurrencies = await this.orm.searchRead(\n \"res.currency\", // model\n [], // domain\n [\"symbol\", \"full_name\", \"position\", \"name\", \"decimal_places\"], // fields\n {\n // opts\n order: \"active DESC, full_name ASC\",\n context: { active_test: false },\n }\n );\n return odooCurrencies.map((currency) => {\n return {\n code: currency.name,\n symbol: currency.symbol,\n position: currency.position || \"after\",\n name: currency.full_name || _t(\"Currency\"),\n decimalPlaces: currency.decimal_places || 2,\n };\n });\n }\n\n /**\n * Retrieve the spreadsheet_data and the thumbnail associated to the\n * current spreadsheet\n */\n getSaveData() {\n const data = this.model.exportData();\n return {\n data,\n revisionId: data.revisionId,\n thumbnail: this.getThumbnail(),\n };\n }\n\n getThumbnail() {\n const dimensions = spreadsheet.SPREADSHEET_DIMENSIONS;\n const canvas = document.querySelector(\".o-grid canvas:not(.o-figure-canvas)\");\n const canvasResizer = document.createElement(\"canvas\");\n const size = this.props.thumbnailSize;\n canvasResizer.width = size;\n canvasResizer.height = size;\n const canvasCtx = canvasResizer.getContext(\"2d\");\n // use only 25 first rows in thumbnail\n const sourceSize = Math.min(\n 25 * dimensions.DEFAULT_CELL_HEIGHT,\n canvas.width,\n canvas.height\n );\n canvasCtx.drawImage(\n canvas,\n dimensions.HEADER_WIDTH - 1,\n dimensions.HEADER_HEIGHT - 1,\n sourceSize,\n sourceSize,\n 0,\n 0,\n size,\n size\n );\n return canvasResizer.toDataURL().replace(\"data:image/png;base64,\", \"\");\n }\n /**\n * Make a copy of the current document\n */\n makeCopy() {\n const { data, thumbnail } = this.getSaveData();\n this.props.onMakeCopy({ data, thumbnail });\n }\n /**\n * Create a new spreadsheet\n */\n newSpreadsheet() {\n this.props.onNewSpreadsheet();\n }\n\n /**\n * Downloads the spreadsheet in xlsx format\n */\n async _download() {\n this.ui.block();\n try {\n await this.action.doAction({\n type: \"ir.actions.client\",\n tag: \"action_download_spreadsheet\",\n params: {\n orm: this.orm,\n name: this.props.name,\n data: this.model.exportData(),\n stateUpdateMessages: [],\n },\n });\n } finally {\n this.ui.unblock();\n }\n }\n\n /**\n * Adds a notification to display to the user\n * @param {{text: string, tag: string}} notification\n */\n notifyUser(notification) {\n if (tags.has(notification.tag)) {\n return;\n }\n this.notifications.add(notification.text, {\n type: \"warning\",\n sticky: true,\n onClose: () => tags.delete(notification.tag),\n });\n tags.add(notification.tag);\n }\n\n /**\n * Open a dialog to display an error message to the user.\n *\n * @param {string} content Content to display\n */\n raiseError(content) {\n this.dialogContent = content;\n this.confirmDialog = this.closeDialog;\n this.state.dialog.isDisplayed = true;\n }\n\n _onLeave() {\n if (this.alreadyLeft) {\n return;\n }\n this.alreadyLeft = true;\n this.model.leaveSession();\n this.model.off(\"update\", this);\n if (!this.props.isReadonly) {\n this.props.onSpreadsheetSaved(this.getSaveData());\n }\n }\n}\n\nSpreadsheetComponent.template = \"spreadsheet_edition.SpreadsheetComponent\";\nSpreadsheetComponent.components = { Spreadsheet, Dialog };\nSpreadsheet._t = _t;\nSpreadsheetComponent.props = {\n name: String,\n data: Object,\n thumbnailSize: Number,\n isReadonly: { type: Boolean, optional: true },\n snapshotRequested: { type: Boolean, optional: true },\n showFormulas: { type: Boolean, optional: true },\n stateUpdateMessages: { type: Array, optional: true },\n asyncInitCallback: {\n optional: true,\n type: Function,\n },\n initCallback: {\n optional: true,\n type: Function,\n },\n transportService: {\n optional: true,\n type: Object,\n },\n fileStore: {\n optional: true,\n type: Object,\n },\n spreadsheetSyncStatus: {\n optional: true,\n type: Function,\n },\n onDownload: {\n optional: true,\n type: Function,\n },\n onUnexpectedRevisionId: {\n optional: true,\n type: Function,\n },\n onMakeCopy: {\n type: Function,\n },\n onSpreadsheetSaved: {\n type: Function,\n },\n onNewSpreadsheet: {\n type: Function,\n },\n};\nSpreadsheetComponent.defaultProps = {\n isReadonly: false,\n snapshotRequested: false,\n showFormulas: false,\n stateUpdateMessages: [],\n onDownload: () => {},\n};\n", "/** @odoo-module **/\n\nimport spreadsheet, {\n initCallbackRegistry,\n} from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst uuidGenerator = new spreadsheet.helpers.UuidGenerator();\n\nexport function insertChart(chartData) {\n const definition = {\n metaData: {\n groupBy: chartData.metaData.groupBy,\n measure: chartData.metaData.measure,\n order: chartData.metaData.order,\n resModel: chartData.metaData.resModel,\n },\n searchParams: { ...chartData.searchParams },\n stacked: chartData.metaData.stacked,\n title: chartData.name,\n background: \"#FFFFFF\",\n legendPosition: \"top\",\n verticalAxisPosition: \"left\",\n type: `odoo_${chartData.metaData.mode}`,\n dataSourceId: uuidGenerator.uuidv4(),\n id: uuidGenerator.uuidv4(),\n };\n return (model) => {\n model.dispatch(\"CREATE_CHART\", {\n sheetId: model.getters.getActiveSheetId(),\n id: definition.id,\n position: {\n x: 10,\n y: 10,\n },\n definition,\n });\n if (chartData.menuXMLId) {\n model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n chartId: definition.id,\n odooMenuId: chartData.menuXMLId,\n });\n }\n };\n}\n\ninitCallbackRegistry.add(\"insertChart\", insertChart);\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { IrMenuSelector } from \"@spreadsheet_edition/assets/components/ir_menu_selector/ir_menu_selector\";\n\nconst { LineBarPieConfigPanel, ScorecardChartConfigPanel, GaugeChartConfigPanel } =\n spreadsheet.components;\n\n/**\n * Patch the chart configuration panel to add an input to\n * link the chart to an Odoo menu.\n */\nfunction patchChartPanelWithMenu(PanelComponent, patchName) {\n patch(PanelComponent.prototype, patchName, {\n get odooMenuId() {\n const menu = this.env.model.getters.getChartOdooMenu(this.props.figureId);\n return menu ? menu.id : undefined;\n },\n /**\n * @param {number | undefined} odooMenuId\n */\n updateOdooLink(odooMenuId) {\n if (!odooMenuId) {\n this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n chartId: this.props.figureId,\n odooMenuId: undefined,\n });\n return;\n }\n const menu = this.env.model.getters.getIrMenu(odooMenuId);\n this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n chartId: this.props.figureId,\n odooMenuId: menu.xmlid || menu.id,\n });\n },\n });\n PanelComponent.components = {\n ...PanelComponent.components,\n IrMenuSelector,\n };\n}\npatchChartPanelWithMenu(LineBarPieConfigPanel, \"document_spreadsheet.LineBarPieConfigPanel\");\npatchChartPanelWithMenu(GaugeChartConfigPanel, \"document_spreadsheet.GaugeChartConfigPanel\");\npatchChartPanelWithMenu(\n ScorecardChartConfigPanel,\n \"document_spreadsheet.ScorecardChartConfigPanel\"\n);\n", "/** @odoo-module */\n\nimport { IrMenuSelector } from \"@spreadsheet_edition/assets/components/ir_menu_selector/ir_menu_selector\";\n\nconst { Component } = owl;\n\nexport class CommonOdooChartConfigPanel extends Component {\n get odooMenuId() {\n const menu = this.env.model.getters.getChartOdooMenu(this.props.figureId);\n return menu ? menu.id : undefined;\n }\n /**\n * @param {number | undefined} odooMenuId\n */\n updateOdooLink(odooMenuId) {\n if (!odooMenuId) {\n this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n chartId: this.props.figureId,\n odooMenuId: undefined,\n });\n return;\n }\n const menu = this.env.model.getters.getIrMenu(odooMenuId);\n this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n chartId: this.props.figureId,\n odooMenuId: menu.xmlid || menu.id,\n });\n }\n}\n\nCommonOdooChartConfigPanel.template = \"spreadsheet_edition.CommonOdooChartConfigPanel\";\nCommonOdooChartConfigPanel.components = { IrMenuSelector };\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { CommonOdooChartConfigPanel } from \"./common/config_panel\";\nimport { OdooBarChartConfigPanel } from \"./odoo_bar/odoo_bar_config_panel\";\nimport { OdooLineChartConfigPanel } from \"./odoo_line/odoo_line_config_panel\";\n\nconst { chartSidePanelComponentRegistry } = spreadsheet.registries;\nconst { LineBarPieDesignPanel } = spreadsheet.components;\n\nchartSidePanelComponentRegistry\n .add(\"odoo_line\", {\n configuration: OdooLineChartConfigPanel,\n design: LineBarPieDesignPanel,\n })\n .add(\"odoo_bar\", {\n configuration: OdooBarChartConfigPanel,\n design: LineBarPieDesignPanel,\n })\n .add(\"odoo_pie\", {\n configuration: CommonOdooChartConfigPanel,\n design: LineBarPieDesignPanel,\n });\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { chartRegistry } = spreadsheet.registries;\nconst { ChartPanel } = spreadsheet.components;\n\n/**\n * This patch is necessary to ensure that the chart type cannot be changed\n * between odoo charts and spreadsheet charts.\n */\n\npatch(ChartPanel.prototype, \"spreadsheet.ChartPanel\", {\n get chartTypes() {\n const definition = this.getChartDefinition();\n const isOdoo = definition.type.startsWith(\"odoo_\");\n return this.getChartTypes(isOdoo);\n },\n\n /**\n * @param {boolean} isOdoo\n */\n getChartTypes(isOdoo) {\n const result = {};\n for (const key of chartRegistry.getKeys()) {\n if ((isOdoo && key.startsWith(\"odoo_\")) || (!isOdoo && !key.startsWith(\"odoo_\"))) {\n result[key] = chartRegistry.get(key).name;\n }\n }\n return result;\n },\n\n onTypeChange(type) {\n if (this.getChartDefinition().type.startsWith(\"odoo_\")) {\n const definition = {\n stacked: false,\n verticalAxisPosition: \"left\",\n ...this.env.model.getters.getChartDefinition(this.figureId),\n type,\n };\n this.env.model.dispatch(\"UPDATE_CHART\", {\n definition,\n id: this.figureId,\n sheetId: this.env.model.getters.getActiveSheetId(),\n });\n } else {\n this._super(type);\n }\n },\n});\n", "/** @odoo-module */\n\nimport { IrMenuSelector } from \"@spreadsheet_edition/assets/components/ir_menu_selector/ir_menu_selector\";\nimport { CommonOdooChartConfigPanel } from \"../common/config_panel\";\n\nexport class OdooBarChartConfigPanel extends CommonOdooChartConfigPanel {\n onUpdateStacked(ev) {\n this.props.updateChart({\n stacked: ev.target.checked,\n });\n }\n}\n\nOdooBarChartConfigPanel.template = \"spreadsheet_edition.OdooBarChartConfigPanel\";\nOdooBarChartConfigPanel.components = { IrMenuSelector };\n", "/** @odoo-module */\n\nimport { IrMenuSelector } from \"@spreadsheet_edition/assets/components/ir_menu_selector/ir_menu_selector\";\nimport { CommonOdooChartConfigPanel } from \"../common/config_panel\";\n\nexport class OdooLineChartConfigPanel extends CommonOdooChartConfigPanel {\n onUpdateStacked(ev) {\n this.props.updateChart({\n stacked: ev.target.checked,\n });\n }\n}\n\nOdooLineChartConfigPanel.template = \"spreadsheet_edition.OdooLineChartConfigPanel\";\nOdooLineChartConfigPanel.components = { IrMenuSelector };\n", "/** @odoo-module */\n\nimport { _lt } from \"@web/core/l10n/translation\";\nimport { FilterFieldOffset } from \"../filter_field_offset\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\nimport AbstractFilterEditorSidePanel from \"./filter_editor_side_panel\";\nimport FilterEditorFieldMatching from \"./filter_editor_field_matching\";\nimport FilterEditorLabel from \"./filter_editor_label\";\n\nconst { useState } = owl;\n\nconst RANGE_TYPES = [\n { type: \"year\", description: _lt(\"Year\") },\n { type: \"quarter\", description: _lt(\"Quarter\") },\n { type: \"month\", description: _lt(\"Month\") },\n { type: \"relative\", description: _lt(\"Relative Period\") },\n];\n\n/**\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").GlobalFilter} GlobalFilter\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").FieldMatching} FieldMatching\n *\n * @typedef DateState\n * @property {Object} defaultValue\n * @property {boolean} defaultsToCurrentPeriod\n * @property {boolean} automaticDefaultValue\n * @property {\"year\" | \"month\" | \"quarter\" | \"relative\"} type type of the filter\n */\n\nclass DateFilterEditorFieldMatching extends FilterEditorFieldMatching {}\n\nDateFilterEditorFieldMatching.components = {\n ...FilterEditorFieldMatching.components,\n FilterFieldOffset,\n};\n\nDateFilterEditorFieldMatching.template = \"spreadsheet_edition.DateFilterEditorFieldMatching\";\n\nDateFilterEditorFieldMatching.props = {\n ...FilterEditorFieldMatching.props,\n onOffsetSelected: Function,\n};\n\n/**\n * This is the side panel to define/edit a global filter of type \"date\".\n */\nexport default class DateFilterEditorSidePanel extends AbstractFilterEditorSidePanel {\n /**\n * @constructor\n */\n setup() {\n super.setup();\n\n this.type = \"date\";\n /** @type {DateState} */\n this.dateState = useState({\n defaultValue: {},\n defaultsToCurrentPeriod: false,\n automaticDefaultValue: false,\n type: \"year\",\n options: [],\n });\n\n this.relativeDateRangesTypes = RELATIVE_DATE_RANGE_TYPES;\n this.dateRangeTypes = RANGE_TYPES;\n\n this.ALLOWED_FIELD_TYPES = [\"datetime\", \"date\"];\n }\n\n /**\n * @override\n */\n get filterValues() {\n const values = super.filterValues;\n return {\n ...values,\n defaultValue: this.dateState.defaultValue,\n rangeType: this.dateState.type,\n defaultsToCurrentPeriod: this.dateState.defaultsToCurrentPeriod,\n };\n }\n\n shouldDisplayFieldMatching() {\n return this.fieldMatchings.length;\n }\n\n isDateTypeSelected(dateType) {\n return dateType === this.dateState.type;\n }\n\n /**\n * @override\n * @param {GlobalFilter} globalFilter\n */\n loadSpecificFilterValues(globalFilter) {\n this.dateState.type = globalFilter.rangeType;\n this.dateState.defaultValue = globalFilter.defaultValue;\n this.dateState.automaticDefaultValue = globalFilter.automaticDefaultValue;\n this.dateState.defaultsToCurrentPeriod = globalFilter.defaultsToCurrentPeriod;\n }\n\n /**\n * @override\n * @param {string} index\n * @param {string|undefined} chain\n * @param {Field|undefined} field\n */\n onSelectedField(index, chain, field) {\n super.onSelectedField(index, chain, field);\n this.fieldMatchings[index].fieldMatch.offset = 0;\n }\n\n /**\n * @param {string} index\n * @param {number} offset\n */\n onOffsetSelected(index, offset) {\n this.fieldMatchings[index].fieldMatch.offset = offset;\n }\n\n onTimeRangeChanged(defaultValue) {\n this.dateState.defaultValue = defaultValue;\n }\n\n onDateOptionChange(ev) {\n // TODO t-model does not work ?\n this.dateState.type = ev.target.value;\n this.dateState.defaultValue = this.dateState.type !== \"relative\" ? {} : \"\";\n }\n\n toggleDefaultsToCurrentPeriod(ev) {\n this.dateState.defaultsToCurrentPeriod = ev.target.checked;\n }\n}\n\nDateFilterEditorSidePanel.template = \"spreadsheet_edition.DateFilterEditorSidePanel\";\nDateFilterEditorSidePanel.components = {\n DateFilterEditorFieldMatching,\n FilterEditorLabel,\n};\n", "/** @odoo-module */\n\nimport { SpreadsheetModelFieldSelector } from \"../model_field_selector/spreadsheet_model_field_selector\";\n\nconst { Component } = owl;\n\n/**\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").FieldMatching} FieldMatching\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n */\n\nexport default class FilterEditorFieldMatching extends Component {\n /**\n *\n * @param {FieldMatching} fieldMatch\n * @returns {string}\n */\n getModelField(fieldMatch) {\n if (!fieldMatch || !fieldMatch.chain) {\n return \"\";\n }\n return fieldMatch.chain;\n }\n\n /**\n * @param {{resModel:string, field: Field}[]} [fieldChain]\n * @return {Field | undefined}\n */\n extractField(fieldChain) {\n if (!fieldChain) {\n return undefined;\n }\n const candidate = [...fieldChain].reverse().find((chain) => chain.field);\n return candidate ? candidate.field : undefined;\n }\n}\nFilterEditorFieldMatching.template = \"spreadsheet_edition.FilterEditorFieldMatching\";\n\nFilterEditorFieldMatching.components = {\n SpreadsheetModelFieldSelector,\n};\n\nFilterEditorFieldMatching.props = {\n // See AbstractFilterEditorSidePanel fieldMatchings\n fieldMatchings: Array,\n wrongFieldMatchings: Array,\n selectField: Function,\n filterModelFieldSelectorField: Function,\n};\n", "/** @odoo-module */\n\nconst { Component, onMounted, useRef } = owl;\n\nexport default class FilterEditorLabel extends Component {\n setup() {\n this.labelInput = useRef(\"labelInput\");\n onMounted(this.onMounted);\n }\n\n onMounted() {\n this.labelInput.el.focus();\n }\n}\nFilterEditorLabel.template = \"spreadsheet_edition.FilterEditorLabel\";\n\nFilterEditorLabel.props = {\n label: { type: String, optional: true },\n placeholder: { type: String, optional: true },\n inputClass: { type: String, optional: true },\n setLabel: Function,\n};\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport CommandResult from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\n\nconst { onWillStart, Component, useRef, useState, toRaw } = owl;\nconst uuidGenerator = new spreadsheet.helpers.UuidGenerator();\n\n/**\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").FieldMatching} FieldMatching\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").GlobalFilter} GlobalFilter\n *\n * @typedef State\n * @property {boolean} saved\n * @property {string} label label of the filter\n */\n\n/**\n * This is the side panel to define/edit a global filter.\n * It can be of 3 different type: text, date and relation.\n */\nexport default class AbstractFilterEditorSidePanel extends Component {\n setup() {\n this.id = undefined;\n this.type = \"\";\n /** @type {State} */\n this.genericState = useState({\n saved: false,\n label: undefined,\n });\n this.fieldMatchings = useState([]);\n this._wrongFieldMatchingsSet = new Set();\n this.getters = this.env.model.getters;\n this.orm = useService(\"orm\");\n this.notification = useService(\"notification\");\n this.labelInput = useRef(\"labelInput\");\n\n /** @type {string[]} */\n this.ALLOWED_FIELD_TYPES = [];\n\n onWillStart(this.onWillStart);\n }\n\n /**\n * Retrieve the placeholder of the label\n */\n get placeholder() {\n return sprintf(_t(\"New %s filter\"), this.type);\n }\n\n get missingLabel() {\n return this.genericState.saved && !this.genericState.label;\n }\n\n get wrongFieldMatchings() {\n return this.genericState.saved ? [...this._wrongFieldMatchingsSet] : [];\n }\n\n get missingRequired() {\n return !!this.missingLabel || this.wrongFieldMatchings.length !== 0;\n }\n\n get filterValues() {\n const id = this.id || uuidGenerator.uuidv4();\n return {\n id,\n type: this.type,\n label: this.genericState.label,\n };\n }\n\n /**\n * @param {Event & { target: HTMLInputElement }} ev\n */\n setLabel(ev) {\n this.genericState.label = ev.target.value;\n }\n\n shouldDisplayFieldMatching() {\n throw new Error(\"Not implemented by children\");\n }\n\n loadValues() {\n this.id = this.props.id;\n const globalFilter = this.id && this.getters.getGlobalFilter(this.id);\n if (globalFilter) {\n this.genericState.label = _t(globalFilter.label);\n this.loadSpecificFilterValues(globalFilter);\n }\n }\n\n /**\n * @param {GlobalFilter} globalFilter\n */\n loadSpecificFilterValues(globalFilter) {\n return;\n }\n\n /**\n * @private\n */\n async _loadFieldMatchings() {\n for (const [type, el] of Object.entries(globalFiltersFieldMatchers)) {\n for (const objectId of el.geIds()) {\n const tag = await el.getTag(objectId);\n this.fieldMatchings.push({\n name: el.getDisplayName(objectId),\n tag,\n fieldMatch: el.getFieldMatching(objectId, this.id) || {},\n fields: () => el.getFields(objectId),\n model: () => el.getModel(objectId),\n payload: () => ({ id: objectId, type }),\n });\n }\n }\n }\n\n async onWillStart() {\n this.loadValues();\n const proms = [];\n proms.push(\n ...Object.values(globalFiltersFieldMatchers)\n .map((el) => el.waitForReady())\n .flat()\n );\n await this._loadFieldMatchings();\n await Promise.all(proms);\n }\n\n /**\n * @param {Field} field\n * @returns {boolean}\n */\n isFieldValid(field) {\n return !!field.searchable;\n }\n\n /**\n * Function that will be called by ModelFieldSelector on each fields, to\n * filter the ones that should be displayed\n * @param {Field} field\n * @returns {boolean}\n */\n filterModelFieldSelectorField(field) {\n if (this.env.debug) {\n // Debug users are allowed to go through relational fields a target multi-depth\n // relations e.g. product_id.categ_id.name\n return this.ALLOWED_FIELD_TYPES.includes(field.type) || !!field.relation;\n }\n if (this.ALLOWED_FIELD_TYPES.includes(field.type)) {\n if (this.isFieldValid(field)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * @param {{resModel:string, field: Object}[] | undefined} fieldChain\n * @return {Object | undefined}\n */\n extractField(fieldChain) {\n if (!fieldChain) {\n return undefined;\n }\n const candidate = [...fieldChain].reverse().find((chain) => chain.field);\n return candidate ? candidate.field : candidate;\n }\n\n /**\n *\n * @param {Object} field\n * @returns {boolean}\n */\n matchingRelation(field) {\n return !field.relation;\n }\n\n /**\n * @param {string} index\n * @param {string|undefined} chain\n * @param {Field|undefined} field\n */\n onSelectedField(index, chain, field) {\n if (!chain || !field) {\n this.fieldMatchings[index].fieldMatch = {};\n return;\n }\n const fieldName = chain;\n this.fieldMatchings[index].fieldMatch = {\n chain: fieldName,\n type: field.type,\n };\n if (!this.matchingRelation(field)) {\n this._wrongFieldMatchingsSet.add(index);\n } else {\n this._wrongFieldMatchingsSet.delete(index);\n }\n }\n\n /**\n *\n * @param {FieldMatching} fieldMatch\n * @returns\n */\n getModelField(fieldMatch) {\n if (!fieldMatch || !fieldMatch.chain) {\n return \"\";\n }\n return fieldMatch.chain;\n }\n\n onSave() {\n this.genericState.saved = true;\n if (this.missingRequired) {\n this.notification.add(this.env._t(\"Some required fields are not valid\"), {\n type: \"danger\",\n sticky: false,\n });\n return;\n }\n const cmd = this.id ? \"EDIT_GLOBAL_FILTER\" : \"ADD_GLOBAL_FILTER\";\n const filter = this.filterValues;\n // Populate the command a bit more with a key chart, pivot or list\n const additionalPayload = {};\n this.fieldMatchings.forEach((fm) => {\n const { type, id } = fm.payload();\n additionalPayload[type] = additionalPayload[type] || {};\n //remove reactivity\n additionalPayload[type][id] = toRaw(fm.fieldMatch);\n });\n const result = this.env.model.dispatch(cmd, {\n id: filter.id,\n filter,\n ...additionalPayload,\n });\n if (result.isCancelledBecause(CommandResult.DuplicatedFilterLabel)) {\n this.notification.add(this.env._t(\"Duplicated Label\"), {\n type: \"danger\",\n sticky: false,\n });\n return;\n }\n this.env.openSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\", {});\n }\n\n onCancel() {\n this.env.openSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\", {});\n }\n\n onDelete() {\n if (this.id) {\n this.env.model.dispatch(\"REMOVE_GLOBAL_FILTER\", { id: this.id });\n }\n this.env.openSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\", {});\n }\n}\n\nAbstractFilterEditorSidePanel.props = {\n id: { type: String, optional: true },\n onCloseSidePanel: { type: Function, optional: true },\n};\n", "/** @odoo-module */\n\nimport { RecordsSelector } from \"@spreadsheet/global_filters/components/records_selector/records_selector\";\nimport { ModelSelector } from \"@web/core/model_selector/model_selector\";\nimport AbstractFilterEditorSidePanel from \"./filter_editor_side_panel\";\nimport FilterEditorLabel from \"./filter_editor_label\";\nimport FilterEditorFieldMatching from \"./filter_editor_field_matching\";\n\nconst { useState } = owl;\n\n/**\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").GlobalFilter} GlobalFilter\n\n *\n * @typedef RelationState\n * @property {Array} defaultValue\n * @property {Array} displayNames\n * @property {{label?: string, technical?: string}} relatedModel\n */\n\n/**\n * This is the side panel to define/edit a global filter of type \"relation\".\n */\nexport default class RelationFilterEditorSidePanel extends AbstractFilterEditorSidePanel {\n setup() {\n super.setup();\n\n this.type = \"relation\";\n /** @type {RelationState} */\n this.relationState = useState({\n defaultValue: [],\n displayNames: [],\n relatedModel: {\n label: undefined,\n technical: undefined,\n },\n });\n\n this.ALLOWED_FIELD_TYPES = [\"many2one\", \"many2many\", \"one2many\"];\n }\n\n get missingModel() {\n return this.genericState.saved && !this.relationState.relatedModel.technical;\n }\n\n get missingRequired() {\n return super.missingRequired || this.missingModel;\n }\n\n /**\n * @override\n */\n get filterValues() {\n const values = super.filterValues;\n return {\n ...values,\n defaultValue: this.relationState.defaultValue,\n defaultValueDisplayNames: this.relationState.displayNames,\n modelName: this.relationState.relatedModel.technical,\n };\n }\n\n shouldDisplayFieldMatching() {\n return this.fieldMatchings.length && this.relationState.relatedModel.technical;\n }\n\n /**\n * List of model names of all related models of all pivots\n * @returns {Array}\n */\n get relatedModels() {\n const all = this.fieldMatchings.map((object) => Object.values(object.fields()));\n return [\n ...new Set(\n all\n .flat()\n .filter((field) => field.relation)\n .map((field) => field.relation)\n ),\n ];\n }\n\n /**\n * @override\n * @param {GlobalFilter} globalFilter\n */\n loadSpecificFilterValues(globalFilter) {\n this.relationState.defaultValue = globalFilter.defaultValue;\n this.relationState.relatedModel.technical = globalFilter.modelName;\n }\n\n async onWillStart() {\n await super.onWillStart();\n await this.fetchModelFromName();\n }\n\n /**\n * Get the first field which could be a relation of the current related\n * model\n *\n * @param {Object.} fields Fields to look in\n * @returns {field|undefined}\n */\n _findRelation(fields) {\n const field = Object.values(fields).find(\n (field) =>\n field.searchable && field.relation === this.relationState.relatedModel.technical\n );\n return field;\n }\n\n async onModelSelected({ technical, label }) {\n if (!this.genericState.label) {\n this.genericState.label = label;\n }\n if (this.relationState.relatedModel.technical !== technical) {\n this.relationState.defaultValue = [];\n }\n this.relationState.relatedModel.technical = technical;\n this.relationState.relatedModel.label = label;\n\n for (const [index, object] of Object.entries(this.fieldMatchings)) {\n const field = this._findRelation(object.fields());\n this.onSelectedField(index, field ? field.name : undefined, field);\n }\n }\n\n async fetchModelFromName() {\n if (!this.relationState.relatedModel.technical) {\n return;\n }\n const result = await this.orm.call(\"ir.model\", \"display_name_for\", [\n [this.relationState.relatedModel.technical],\n ]);\n this.relationState.relatedModel.label = result[0] && result[0].display_name;\n if (!this.genericState.label) {\n this.genericState.label = this.relationState.relatedModel.label;\n }\n }\n\n /**\n * @param {Field} field\n * @returns {boolean}\n */\n isFieldValid(field) {\n const relatedModel = this.relationState.relatedModel.technical;\n return super.isFieldValid(field) && (!relatedModel || field.relation === relatedModel);\n }\n\n /**\n * @override\n * @param {Field} field\n * @returns {boolean}\n */\n matchingRelation(field) {\n return field.relation === this.relationState.relatedModel.technical;\n }\n\n /**\n * @param {{id: number, display_name: string}[]} value\n */\n onValuesSelected(value) {\n this.relationState.defaultValue = value.map((record) => record.id);\n this.relationState.displayNames = value.map((record) => record.display_name);\n }\n}\n\nRelationFilterEditorSidePanel.template = \"spreadsheet_edition.RelationFilterEditorSidePanel\";\nRelationFilterEditorSidePanel.components = {\n ModelSelector,\n RecordsSelector,\n FilterEditorLabel,\n FilterEditorFieldMatching,\n};\n", "/** @odoo-module */\n\nimport AbstractFilterEditorSidePanel from \"./filter_editor_side_panel\";\nimport FilterEditorLabel from \"./filter_editor_label\";\nimport FilterEditorFieldMatching from \"./filter_editor_field_matching\";\n\nconst { useState } = owl;\n\n/**\n * @typedef {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").GlobalFilter} GlobalFilter\n *\n * @typedef TextState\n * @property {string} defaultValue\n\n */\n\n/**\n * This is the side panel to define/edit a global filter of type \"text\".\n */\nexport default class TextFilterEditorSidePanel extends AbstractFilterEditorSidePanel {\n setup() {\n super.setup();\n\n this.type = \"text\";\n /** @type {TextState} */\n this.textState = useState({\n defaultValue: \"\",\n });\n this.ALLOWED_FIELD_TYPES = [\"many2one\", \"text\", \"char\"];\n }\n\n /**\n * @override\n */\n shouldDisplayFieldMatching() {\n return this.fieldMatchings.length;\n }\n\n /**\n * @override\n */\n get filterValues() {\n const values = super.filterValues;\n return {\n ...values,\n defaultValue: this.textState.defaultValue,\n };\n }\n\n /**\n * @override\n * @param {GlobalFilter} globalFilter\n */\n loadSpecificFilterValues(globalFilter) {\n this.textState.defaultValue = globalFilter.defaultValue;\n }\n}\n\nTextFilterEditorSidePanel.template = \"spreadsheet_edition.TextFilterEditorSidePanel\";\nTextFilterEditorSidePanel.components = {\n FilterEditorLabel,\n FilterEditorFieldMatching,\n};\n", "/** @odoo-module */\n\nconst { Component } = owl;\nimport { _t, _lt } from \"@web/core/l10n/translation\";\n\nconst FIELD_OFFSETS = [\n { value: 0, description: \"\" },\n { value: -1, description: _lt(\"Previous\") },\n { value: -2, description: _lt(\"Before previous\") },\n { value: 1, description: _lt(\"Next\") },\n { value: 2, description: _lt(\"After next\") },\n];\n\nexport class FilterFieldOffset extends Component {\n setup() {\n this.fieldsOffsets = FIELD_OFFSETS;\n }\n\n /**\n * @param {Event & { target: HTMLSelectElement }} ev\n */\n onOffsetSelected(ev) {\n this.props.onOffsetSelected(parseInt(ev.target.value));\n }\n\n get title() {\n return this.props.active\n ? _t(\"Period offset applied to this source\")\n : _t(\"Requires a selected field\");\n }\n}\n\nFilterFieldOffset.template = \"spreadsheet_edition.FilterFieldOffset\";\nFilterFieldOffset.props = {\n onOffsetSelected: Function,\n selectedOffset: Number,\n active: Boolean,\n};\n", "/** @odoo-module */\n\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { SpreadsheetModelFieldSelectorPopover } from \"./spreadsheet_model_field_selector_popover\";\n\n/**\n * @typedef {import(\"@spreadsheet/data_sources/metadata_repository\").Field} Field\n */\nexport class SpreadsheetModelFieldSelector extends ModelFieldSelector {\n /**\n * @override\n *\n * @param {string[]} fieldNameChain\n * @param {Object[]} chain\n */\n update(fieldNameChain, chain) {\n this.props.update(fieldNameChain.join(\".\"), chain);\n }\n}\nSpreadsheetModelFieldSelector.components = {\n Popover: SpreadsheetModelFieldSelectorPopover,\n};\n", "/** @odoo-module */\n\nimport { ModelFieldSelectorPopover } from \"@web/core/model_field_selector/model_field_selector_popover\";\n\nexport class SpreadsheetModelFieldSelectorPopover extends ModelFieldSelectorPopover {\n async update() {\n const fieldNameChain = this.fieldNameChain;\n this.fullFieldName = fieldNameChain.join(\".\");\n await this.props.update(fieldNameChain, [...this.chain]);\n await this.loadFields();\n this.render();\n }\n}\n", "/** @odoo-module */\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { Component } = owl;\nconst { Menu } = spreadsheet;\n\nexport class FilterComponent extends Component {\n get activeFilter() {\n return this.env.model.getters.getActiveFilterCount();\n }\n\n toggleDropdown() {\n this.env.toggleSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\");\n }\n}\n\nFilterComponent.template = \"spreadsheet_edition.FilterComponent\";\n\nFilterComponent.components = { Menu };\n\nFilterComponent.props = {};\n", "/** @odoo-module */\n\nimport { FilterValue } from \"@spreadsheet/global_filters/components/filter_value/filter_value\";\n\nconst { Component } = owl;\n/**\n * This is the side panel to define/edit a global filter.\n * It can be of 3 different type: text, date and relation.\n */\nexport default class GlobalFiltersSidePanel extends Component {\n setup() {\n this.getters = this.env.model.getters;\n }\n\n get isReadonly() {\n return this.env.model.getters.isReadonly();\n }\n\n get filters() {\n return this.env.model.getters.getGlobalFilters();\n }\n\n hasDataSources() {\n return (\n this.env.model.getters.getPivotIds().length +\n this.env.model.getters.getListIds().length +\n this.env.model.getters.getOdooChartIds().length\n );\n }\n\n newText() {\n this.env.openSidePanel(\"TEXT_FILTER_SIDE_PANEL\");\n }\n\n newDate() {\n this.env.openSidePanel(\"DATE_FILTER_SIDE_PANEL\");\n }\n\n newRelation() {\n this.env.openSidePanel(\"RELATION_FILTER_SIDE_PANEL\");\n }\n\n /**\n * @param {string} id\n */\n onEdit(id) {\n const filter = this.env.model.getters.getGlobalFilter(id);\n if (!filter) {\n return;\n }\n switch (filter.type) {\n case \"text\":\n this.env.openSidePanel(\"TEXT_FILTER_SIDE_PANEL\", { id });\n break;\n case \"date\":\n this.env.openSidePanel(\"DATE_FILTER_SIDE_PANEL\", { id });\n break;\n case \"relation\":\n this.env.openSidePanel(\"RELATION_FILTER_SIDE_PANEL\", { id });\n break;\n }\n }\n}\n\nGlobalFiltersSidePanel.template = \"spreadsheet_edition.GlobalFiltersSidePanel\";\nGlobalFiltersSidePanel.components = { FilterValue };\nGlobalFiltersSidePanel.props = {\n onCloseSidePanel: { type: Function, optional: true },\n};\n", "/** @odoo-module */\n\nimport { _t, _lt } from \"@web/core/l10n/translation\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport GlobalFiltersSidePanel from \"./global_filters_side_panel\";\nimport { FilterComponent } from \"./filter_component\";\n\nimport \"./operational_transform\";\nimport DateFilterEditorSidePanel from \"./components/filter_editor/date_filter_editor_side_panel\";\nimport TextFilterEditorSidePanel from \"./components/filter_editor/text_filter_editor_side_panel\";\nimport RelationFilterEditorSidePanel from \"./components/filter_editor/relation_filter_editor_side_panel\";\n\nconst { sidePanelRegistry, topbarComponentRegistry, cellMenuRegistry } = spreadsheet.registries;\n\nsidePanelRegistry.add(\"DATE_FILTER_SIDE_PANEL\", {\n title: _t(\"Filter properties\"),\n Body: DateFilterEditorSidePanel,\n});\n\nsidePanelRegistry.add(\"TEXT_FILTER_SIDE_PANEL\", {\n title: _t(\"Filter properties\"),\n Body: TextFilterEditorSidePanel,\n});\n\nsidePanelRegistry.add(\"RELATION_FILTER_SIDE_PANEL\", {\n title: _t(\"Filter properties\"),\n Body: RelationFilterEditorSidePanel,\n});\n\nsidePanelRegistry.add(\"GLOBAL_FILTERS_SIDE_PANEL\", {\n title: _t(\"Filters\"),\n Body: GlobalFiltersSidePanel,\n});\n\ntopbarComponentRegistry.add(\"filter_component\", {\n component: FilterComponent,\n isVisible: (env) => {\n return !env.model.getters.isReadonly() || env.model.getters.getGlobalFilters().length;\n },\n});\n\ncellMenuRegistry.add(\"use_global_filter\", {\n name: _lt(\"Set as filter\"),\n sequence: 175,\n action(env) {\n const position = env.model.getters.getActivePosition();\n const cell = env.model.getters.getCell(position);\n const filters = env.model.getters.getFiltersMatchingPivot(cell.content);\n env.model.dispatch(\"SET_MANY_GLOBAL_FILTER_VALUE\", { filters });\n },\n isVisible: (env) => {\n const position = env.model.getters.getActivePosition();\n const cell = env.model.getters.getCell(position);\n if (!cell) {\n return false;\n }\n const filters = env.model.getters.getFiltersMatchingPivot(cell.content);\n return filters.length > 0;\n },\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { otRegistry } = spreadsheet.registries;\n\notRegistry.addTransformation(\n \"REMOVE_GLOBAL_FILTER\",\n [\"EDIT_GLOBAL_FILTER\"],\n (toTransform, executed) =>\n toTransform.id === executed.id ? undefined : toTransform\n);\n", "/** @odoo-module */\n\n/**\n * see https://stackoverflow.com/a/30106551\n * @param {string} string\n * @returns {string}\n */\nfunction base64ToUtf8(str) {\n // Going backwards: from bytestream, to percent-encoding, to original string.\n return decodeURIComponent(\n atob(str)\n .split(\"\")\n .map((c) => \"%\" + (\"00\" + c.charCodeAt(0).toString(16)).slice(-2))\n .join(\"\")\n );\n}\n\n/**\n * see https://stackoverflow.com/a/30106551\n * @param {string} string\n * @returns {string}\n */\nfunction utf8ToBase64(str) {\n // first we use encodeURIComponent to get percent-encoded UTF-8,\n // then we convert the percent encodings into raw bytes which\n // can be fed into btoa.\n return btoa(\n encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function toSolidBytes(match, p1) {\n return String.fromCharCode(\"0x\" + p1);\n })\n );\n}\n\n/**\n * Encode a json to a base64 string\n * @param {object} json\n */\nexport function jsonToBase64(json) {\n return utf8ToBase64(JSON.stringify(json));\n}\n\n/**\n * Decode a base64 encoded json\n * @param {string} string\n */\nexport function base64ToJson(string) {\n return JSON.parse(base64ToUtf8(string));\n}\n", "/** @odoo-module **/\n\n/**\n * @typedef {import(\"@web/core/orm_service\").ORM} ORM\n */\n\n/**\n *\n * Upload files on the server and link the files to a record as attachment.\n *\n * Implements the `FileStore` interface defined by o-spreadsheet.\n * https://github.com/odoo/o-spreadsheet/blob/300da461b23b5f3db017270192893d4a972bacf0/src/types/files.ts#L4\n *\n */\nexport class RecordFileStore {\n /**\n *\n * @param {string} resModel\n * @param {number} resId\n * @param {*} http\n * @param {ORM} orm\n */\n constructor(resModel, resId, http, orm) {\n this.resModel = resModel;\n this.resId = resId;\n this.http = http;\n this.orm = orm;\n }\n\n /**\n * Upload a file on the server and returns the path to the file.\n */\n async upload(file) {\n const route = \"/web/binary/upload_attachment\";\n const params = {\n ufile: [file],\n csrf_token: odoo.csrf_token,\n model: this.resModel,\n id: this.resId,\n };\n const fileData = JSON.parse(await this.http.post(route, params, \"text\"))[0];\n return \"/web/image/\" + fileData.id;\n }\n\n /**\n * @param {string} path\n * @returns {Promise}\n */\n async delete(path) {\n const attachmentId = path.split(\"/\").pop();\n if (Number.isNaN(attachmentId)) {\n throw new Error(\"Invalid path: \" + path);\n }\n await this.orm.unlink(\"ir.attachment\", [parseInt(attachmentId)]);\n }\n}\n", "/** @odoo-module */\n\nimport { _lt } from \"@web/core/l10n/translation\";\nimport spreadsheet, {\n initCallbackRegistry,\n} from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport {\n buildIrMenuIdLink,\n buildViewLink,\n buildIrMenuXmlLink,\n} from \"@spreadsheet/ir_ui_menu/odoo_menu_link_cell\";\nimport { IrMenuSelectorDialog } from \"@spreadsheet_edition/assets/components/ir_menu_selector/ir_menu_selector\";\n\nconst { markdownLink } = spreadsheet.links;\nconst { linkMenuRegistry } = spreadsheet.registries;\n\n/**\n * Helper to get the function to be called when the spreadsheet is opened\n * in order to insert the link.\n * @param {import(\"@spreadsheet/ir_ui_menu/odoo_menu_link_cell\").ViewLinkDescription} actionToLink\n * @returns Function to call\n */\nfunction insertLink(actionToLink) {\n return (model) => {\n if (!this.isEmptySpreadsheet) {\n const sheetId = model.uuidGenerator.uuidv4();\n const sheetIdFrom = model.getters.getActiveSheetId();\n model.dispatch(\"CREATE_SHEET\", {\n sheetId,\n position: model.getters.getSheetIds().length,\n });\n model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom, sheetIdTo: sheetId });\n }\n const viewLink = buildViewLink(actionToLink);\n model.dispatch(\"UPDATE_CELL\", {\n sheetId: model.getters.getActiveSheetId(),\n content: markdownLink(actionToLink.name, viewLink),\n col: 0,\n row: 0,\n });\n };\n}\n\ninitCallbackRegistry.add(\"insertLink\", insertLink);\n\nlinkMenuRegistry.add(\"odooMenu\", {\n name: _lt(\"Link an Odoo menu\"),\n sequence: 20,\n action: async (env) => {\n return new Promise((resolve) => {\n const closeDialog = env.services.dialog.add(IrMenuSelectorDialog, {\n onMenuSelected: (menuId) => {\n closeDialog();\n const menu = env.services.menu.getMenu(menuId);\n const xmlId = menu && menu.xmlid;\n const url = xmlId ? buildIrMenuXmlLink(xmlId) : buildIrMenuIdLink(menuId);\n const label = menu.name;\n resolve(markdownLink(label, url));\n },\n });\n });\n },\n});\n", "/** @odoo-module */\n\nimport { getNumberOfListFormulas } from \"@spreadsheet/list/list_helpers\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { autofillModifiersRegistry, autofillRulesRegistry } = spreadsheet.registries;\n\n//--------------------------------------------------------------------------\n// Autofill Rules\n//--------------------------------------------------------------------------\n\nautofillRulesRegistry.add(\"autofill_list\", {\n condition: (cell) => cell && cell.isFormula && getNumberOfListFormulas(cell.content) === 1,\n generateRule: (cell, cells) => {\n const increment = cells.filter(\n (cell) => cell && cell.isFormula && getNumberOfListFormulas(cell.content) === 1\n ).length;\n return { type: \"LIST_UPDATER\", increment, current: 0 };\n },\n sequence: 3,\n});\n\n//--------------------------------------------------------------------------\n// Autofill Modifier\n//--------------------------------------------------------------------------\n\nautofillModifiersRegistry.add(\"LIST_UPDATER\", {\n apply: (rule, data, getters, direction) => {\n rule.current += rule.increment;\n let isColumn;\n let steps;\n switch (direction) {\n case \"up\":\n isColumn = false;\n steps = -rule.current;\n break;\n case \"down\":\n isColumn = false;\n steps = rule.current;\n break;\n case \"left\":\n isColumn = true;\n steps = -rule.current;\n break;\n case \"right\":\n isColumn = true;\n steps = rule.current;\n }\n const content = getters.getNextListValue(\n getters.getFormulaCellContent(data.sheetId, data.cell),\n isColumn,\n steps\n );\n let tooltip = {\n props: {\n content,\n },\n };\n if (content && content !== data.content) {\n tooltip = {\n props: {\n content: getters.getTooltipListFormula(content, isColumn),\n },\n };\n }\n return {\n cellData: {\n style: undefined,\n format: undefined,\n border: undefined,\n content,\n },\n tooltip,\n };\n },\n});\n", "/** @odoo-module */\n\nimport { _t, _lt } from \"@web/core/l10n/translation\";\n\nimport spreadsheet, {\n initCallbackRegistry,\n} from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport \"./autofill\";\nimport \"./operational_transform\";\n\nimport ListingAllSidePanel from \"./side_panels/listing_all_side_panel\";\nimport ListAutofillPlugin from \"./plugins/list_autofill_plugin\";\n\nimport { insertList } from \"./list_init_callback\";\n\nconst { featurePluginRegistry, sidePanelRegistry, cellMenuRegistry } = spreadsheet.registries;\n\nfeaturePluginRegistry.add(\"odooListAutofillPlugin\", ListAutofillPlugin);\n\nsidePanelRegistry.add(\"LIST_PROPERTIES_PANEL\", {\n title: () => _t(\"List properties\"),\n Body: ListingAllSidePanel,\n});\n\ninitCallbackRegistry.add(\"insertList\", insertList);\n\ncellMenuRegistry.add(\"listing_properties\", {\n name: _lt(\"See list properties\"),\n sequence: 190,\n action(env) {\n const position = env.model.getters.getActivePosition();\n const listId = env.model.getters.getListIdFromPosition(position);\n env.model.dispatch(\"SELECT_ODOO_LIST\", { listId });\n env.openSidePanel(\"LIST_PROPERTIES_PANEL\", {});\n },\n isVisible: (env) => {\n const position = env.model.getters.getActivePosition();\n return env.model.getters.getListIdFromPosition(position) !== undefined;\n },\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { createFullMenuItem } = spreadsheet.helpers;\n\nexport const REINSERT_LIST_CHILDREN = (env) =>\n env.model.getters.getListIds().map((listId, index) => {\n return createFullMenuItem(`reinsert_list_${listId}`, {\n name: env.model.getters.getListDisplayName(listId),\n sequence: index,\n action: async (env) => {\n const zone = env.model.getters.getSelectedZone();\n const dataSource = await env.model.getters.getAsyncListDataSource(listId);\n const list = env.model.getters.getListDefinition(listId);\n const columns = list.columns.map((name) => ({\n name,\n type: dataSource.getField(name).type,\n }));\n env.getLinesNumber((linesNumber) => {\n env.model.dispatch(\"RE_INSERT_ODOO_LIST\", {\n sheetId: env.model.getters.getActiveSheetId(),\n col: zone.left,\n row: zone.top,\n id: listId,\n linesNumber,\n columns: columns,\n });\n });\n },\n });\n });\n", "/** @odoo-module **/\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport ListDataSource from \"@spreadsheet/list/list_data_source\";\n\nconst uuidGenerator = new spreadsheet.helpers.UuidGenerator();\n\n/**\n * Get the function that have to be executed to insert the given list in the\n * given spreadsheet. The returned function has to be called with the model\n * of the spreadsheet and the dataSource of this list\n *\n * @private\n *\n * @param {import(\"@spreadsheet/list/plugins/list_core_plugin\").SpreadsheetList} list\n * @param {object} param\n * @param {number} param.threshold\n * @param {object} param.fields fields coming from list_model\n * @param {string} param.name Name of the list\n *\n * @returns {function}\n */\nexport function insertList({ list, threshold, fields, name }) {\n const definition = {\n metaData: {\n resModel: list.model,\n columns: list.columns.map((column) => column.name),\n fields,\n },\n searchParams: {\n domain: list.domain,\n context: list.context,\n orderBy: list.orderBy,\n },\n name,\n };\n return async (model) => {\n const dataSourceId = uuidGenerator.uuidv4();\n model.config.custom.dataSources.add(dataSourceId, ListDataSource, {\n ...definition,\n limit: threshold,\n });\n await model.config.custom.dataSources.load(dataSourceId);\n if (!this.isEmptySpreadsheet) {\n const sheetId = uuidGenerator.uuidv4();\n const sheetIdFrom = model.getters.getActiveSheetId();\n model.dispatch(\"CREATE_SHEET\", {\n sheetId,\n position: model.getters.getSheetIds().length,\n });\n model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom, sheetIdTo: sheetId });\n }\n const defWithoutFields = JSON.parse(JSON.stringify(definition));\n defWithoutFields.metaData.fields = undefined;\n const sheetId = model.getters.getActiveSheetId();\n model.dispatch(\"INSERT_ODOO_LIST\", {\n sheetId,\n col: 0,\n row: 0,\n id: model.getters.getNextListId(),\n definition: defWithoutFields,\n dataSourceId,\n linesNumber: threshold,\n columns: list.columns,\n });\n const columns = [];\n for (let col = 0; col < list.columns.length; col++) {\n columns.push(col);\n }\n model.dispatch(\"AUTORESIZE_COLUMNS\", { sheetId, cols: columns });\n };\n}\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { otRegistry } = spreadsheet.registries;\n\notRegistry\n\n .addTransformation(\"INSERT_ODOO_LIST\", [\"INSERT_ODOO_LIST\"], (toTransform) => ({\n ...toTransform,\n id: (parseInt(toTransform.id, 10) + 1).toString(),\n }))\n .addTransformation(\"REMOVE_ODOO_LIST\", [\"RENAME_ODOO_LIST\"], (toTransform, executed) => {\n if (toTransform.listId === executed.listId) {\n return undefined;\n }\n return toTransform;\n })\n .addTransformation(\"REMOVE_ODOO_LIST\", [\"RE_INSERT_ODOO_LIST\"], (toTransform, executed) => {\n if (toTransform.id === executed.listId) {\n return undefined;\n }\n return toTransform;\n });\n", "/** @odoo-module */\n\nimport { _t } from \"web.core\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { getFirstListFunction, getNumberOfListFormulas } from \"@spreadsheet/list/list_helpers\";\n\nconst { astToFormula } = spreadsheet;\n\nexport default class ListAutofillPlugin extends spreadsheet.UIPlugin {\n // ---------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------\n\n /**\n * Get the next value to autofill of a list function\n *\n * @param {string} formula List formula\n * @param {boolean} isColumn True if autofill is LEFT/RIGHT, false otherwise\n * @param {number} increment number of steps\n *\n * @returns Autofilled value\n */\n getNextListValue(formula, isColumn, increment) {\n if (getNumberOfListFormulas(formula) !== 1) {\n return formula;\n }\n const { functionName, args } = getFirstListFunction(formula);\n const evaluatedArgs = args\n .map(astToFormula)\n .map((arg) => this.getters.evaluateFormula(arg));\n const listId = evaluatedArgs[0];\n const columns = this.getters.getListDefinition(listId).columns;\n if (functionName === \"ODOO.LIST\") {\n const position = parseInt(evaluatedArgs[1], 10);\n const field = evaluatedArgs[2];\n if (isColumn) {\n /** Change the field */\n const index = columns.findIndex((col) => col === field) + increment;\n if (index < 0 || index >= columns.length) {\n return \"\";\n }\n return this._getListFunction(listId, position, columns[index]);\n } else {\n /** Change the position */\n const nextPosition = position + increment;\n if (nextPosition === 0) {\n return this._getListHeaderFunction(listId, field);\n }\n if (nextPosition < 0) {\n return \"\";\n }\n return this._getListFunction(listId, nextPosition, field);\n }\n }\n if (functionName === \"ODOO.LIST.HEADER\") {\n const field = evaluatedArgs[1];\n if (isColumn) {\n /** Change the field */\n const index = columns.findIndex((col) => col === field) + increment;\n if (index < 0 || index >= columns.length) {\n return \"\";\n }\n return this._getListHeaderFunction(listId, columns[index]);\n } else {\n /** If down, set position */\n if (increment > 0) {\n return this._getListFunction(listId, increment, field);\n }\n return \"\";\n }\n }\n return formula;\n }\n\n /**\n * Compute the tooltip to display from a Pivot formula\n *\n * @param {string} formula Pivot formula\n * @param {boolean} isColumn True if the direction is left/right, false\n * otherwise\n */\n getTooltipListFormula(formula, isColumn) {\n if (!formula) {\n return [];\n }\n const { functionName, args } = getFirstListFunction(formula);\n const evaluatedArgs = args\n .map(astToFormula)\n .map((arg) => this.getters.evaluateFormula(arg));\n if (isColumn || functionName === \"ODOO.LIST.HEADER\") {\n const fieldName = functionName === \"ODOO.LIST\" ? evaluatedArgs[2] : evaluatedArgs[1];\n return this.getters.getListDataSource(evaluatedArgs[0]).getListHeaderValue(fieldName);\n }\n return _t(\"Record #\") + evaluatedArgs[1];\n }\n\n _getListFunction(listId, position, field) {\n return `=ODOO.LIST(${listId},${position},\"${field}\")`;\n }\n\n _getListHeaderFunction(listId, field) {\n return `=ODOO.LIST.HEADER(${listId},\"${field}\")`;\n }\n}\n\nListAutofillPlugin.getters = [\"getNextListValue\", \"getTooltipListFormula\"];\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ListingDetailsSidePanel } from \"./listing_details_side_panel\";\n\nconst { Component } = owl;\n\nexport default class ListingAllSidePanel extends Component {\n constructor() {\n super(...arguments);\n this.getters = this.env.model.getters;\n }\n\n selectListing(listId) {\n this.env.model.dispatch(\"SELECT_ODOO_LIST\", { listId });\n }\n\n resetListingSelection() {\n this.env.model.dispatch(\"SELECT_ODOO_LIST\");\n }\n\n delete(listId) {\n this.env.askConfirmation(_t(\"Are you sure you want to delete this list ?\"), () => {\n this.env.model.dispatch(\"REMOVE_ODOO_LIST\", { listId });\n this.props.onCloseSidePanel();\n });\n }\n}\nListingAllSidePanel.template = \"spreadsheet_edition.ListingAllSidePanel\";\nListingAllSidePanel.components = { ListingDetailsSidePanel };\n", "/** @odoo-module */\n\nimport { Domain } from \"@web/core/domain\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"web.core\";\nimport { time_to_str } from \"web.time\";\n\nimport EditableName from \"../../o_spreadsheet/editable_name/editable_name\";\n\nconst { Component, onWillStart, onWillUpdateProps } = owl;\n\nexport class ListingDetailsSidePanel extends Component {\n setup() {\n this.getters = this.env.model.getters;\n this.dialog = useService(\"dialog\");\n const loadData = async () => {\n this.dataSource = await this.env.model.getters.getAsyncListDataSource(\n this.props.listId\n );\n this.modelDisplayName = await this.dataSource.getModelLabel();\n };\n onWillStart(loadData);\n onWillUpdateProps(loadData);\n }\n\n get listDefinition() {\n const listId = this.props.listId;\n const def = this.getters.getListDefinition(listId);\n return {\n model: def.model,\n modelDisplayName: this.modelDisplayName,\n domain: new Domain(def.domain).toString(),\n orderBy: def.orderBy,\n };\n }\n\n formatSort(sort) {\n return `${this.dataSource.getListHeaderValue(sort.name)} (${\n sort.asc ? _t(\"ascending\") : _t(\"descending\")\n })`;\n }\n\n getLastUpdate() {\n const lastUpdate = this.dataSource.lastUpdate;\n if (lastUpdate) {\n return time_to_str(new Date(lastUpdate));\n }\n return _t(\"never\");\n }\n\n onNameChanged(name) {\n this.env.model.dispatch(\"RENAME_ODOO_LIST\", {\n listId: this.props.listId,\n name,\n });\n }\n\n async refresh() {\n this.env.model.dispatch(\"REFRESH_ODOO_LIST\", { listId: this.props.listId });\n this.env.model.dispatch(\"EVALUATE_CELLS\", { sheetId: this.getters.getActiveSheetId() });\n }\n\n openDomainEdition() {\n this.dialog.add(DomainSelectorDialog, {\n resModel: this.listDefinition.model,\n initialValue: this.listDefinition.domain,\n readonly: false,\n isDebugMode: !!this.env.debug,\n onSelected: (domain) =>\n this.env.model.dispatch(\"UPDATE_ODOO_LIST_DOMAIN\", {\n listId: this.props.listId,\n domain: new Domain(domain).toList(),\n }),\n });\n }\n}\nListingDetailsSidePanel.template = \"spreadsheet_edition.ListingDetailsSidePanel\";\nListingDetailsSidePanel.components = { DomainSelector, EditableName };\nListingDetailsSidePanel.props = {\n listId: {\n type: String,\n optional: true,\n },\n};\n", "/** @odoo-module **/\n\n/**\n * This class implements the `TransportService` interface defined\n * by o-spreadsheet. Its purpose is to communicate with other clients\n * by sending and receiving spreadsheet messages through the server.\n * @see https://github.com/odoo/o-spreadsheet\n *\n * It listens messages on the long polling bus and forwards spreadsheet messages\n * to the handler. (note: it is assumed there is only one handler)\n *\n * It uses the RPC protocol to send messages to the server which\n * push them in the long polling bus for other clients.\n */\nexport default class SpreadsheetCollaborativeChannel {\n /**\n * @param {Env} env\n * @param {string} resModel model linked to the spreadsheet\n * @param {number} resId Id of the spreadsheet\n */\n constructor(env, resModel, resId) {\n this.env = env;\n this.resId = resId;\n this.resModel = resModel;\n /**\n * A callback function called to handle messages when they are received.\n */\n this._listener;\n /**\n * Messages are queued while there is no listener. They are forwarded\n * once it registers.\n */\n this._queue = [];\n // Listening this channel tells the server the spreadsheet is active\n // but the server will actually push to channel [{dbname}, {resModel}, {resId}]\n // The user can listen to this channel only if he has the required read access.\n this._channel = `spreadsheet_collaborative_session:${this.resModel}:${this.resId}`;\n this.env.services.bus_service.addChannel(this._channel);\n this.env.services.bus_service.addEventListener('notification', ({ detail: notifs }) =>\n this._handleNotifications(this._filterSpreadsheetNotifs(notifs))\n );\n }\n\n /**\n * Register a function that is called whenever a new spreadsheet revision\n * message notification is received by server.\n *\n * @param {any} id\n * @param {Function} callback\n */\n onNewMessage(id, callback) {\n this._listener = callback;\n for (let message of this._queue) {\n callback(message);\n }\n this._queue = [];\n }\n\n /**\n * Send a message to the server\n *\n * @param {Object} message\n */\n sendMessage(message) {\n return this.env.services.rpc({\n model: this.resModel,\n method: \"dispatch_spreadsheet_message\",\n args: [this.resId, message],\n }, { shadow: true });\n }\n\n /**\n * Stop listening new messages\n */\n leave() {\n this._listener = undefined;\n }\n\n /**\n * Filters the received messages to only handle the messages related to\n * spreadsheet\n *\n * @private\n * @param {Array} notifs\n *\n * @returns {Array} notifs which are related to spreadsheet\n */\n _filterSpreadsheetNotifs(notifs) {\n return notifs.filter((notification) => {\n const { payload, type } = notification;\n return type === 'spreadsheet' && payload.id === this.resId;\n });\n }\n\n /**\n * Either forward the message to the listener if it's already registered,\n * or put it in a queue.\n *\n * @private\n * @param {Array} notifs\n */\n _handleNotifications(notifs) {\n for (const { payload } of notifs) {\n if (!this._listener) {\n this._queue.push(payload);\n } else {\n this._listener(payload);\n }\n }\n }\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport SpreadsheetCollaborativeChannel from \"./spreadsheet_collaborative_channel\";\n\nexport class SpreadsheetCollaborativeService {\n /**\n * Get a new collaborative channel for the given spreadsheet id\n * @param {Env} env Env of owl (Component.env)\n * @param {string} resModel model linked to the spreadsheet\n * @param {number} resId id of the spreadsheet\n */\n getCollaborativeChannel(env, resModel, resId) {\n return new SpreadsheetCollaborativeChannel(env, resModel, resId);\n }\n}\n\n/**\n * This service exposes a single instance of the above class.\n */\nexport const spreadsheetCollaborativeService = {\n dependencies: ['bus_service'],\n start(env, dependencies) {\n return new SpreadsheetCollaborativeService(env, dependencies);\n },\n};\n\nregistry.category(\"services\").add(\"spreadsheet_collaborative\", spreadsheetCollaborativeService);\n", "/** @odoo-module */\n\nconst { Component, useState } = owl;\n\nexport default class EditableName extends Component {\n setup() {\n super.setup();\n this.state = useState({\n isEditing: false,\n name: \"\",\n });\n }\n\n rename() {\n this.state.isEditing = true;\n this.state.name = this.props.name;\n }\n\n save() {\n this.props.onChanged(this.state.name.trim());\n this.state.isEditing = false;\n }\n}\n\nEditableName.template = \"spreadsheet_edition.EditableName\";\nEditableName.props = {\n name: String,\n displayName: String,\n onChanged: Function,\n};\n", "/** @odoo-module */\n\nimport { _t, _lt } from \"web.core\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { REINSERT_LIST_CHILDREN } from \"../list/list_actions\";\nimport { INSERT_PIVOT_CELL_CHILDREN, REINSERT_PIVOT_CHILDREN } from \"../pivot/pivot_actions\";\nconst { topbarMenuRegistry } = spreadsheet.registries;\nconst { createFullMenuItem } = spreadsheet.helpers;\n\n//--------------------------------------------------------------------------\n// Spreadsheet context menu items\n//--------------------------------------------------------------------------\n\ntopbarMenuRegistry.add(\"file\", { name: _t(\"File\"), sequence: 10 });\ntopbarMenuRegistry.addChild(\"new_sheet\", [\"file\"], {\n name: _lt(\"New\"),\n sequence: 10,\n isVisible: (env) => !env.isDashboardSpreadsheet,\n action: (env) => env.newSpreadsheet(),\n});\ntopbarMenuRegistry.addChild(\"make_copy\", [\"file\"], {\n name: _lt(\"Make a copy\"),\n sequence: 20,\n isVisible: (env) => !env.isDashboardSpreadsheet,\n action: (env) => env.makeCopy(),\n});\ntopbarMenuRegistry.addChild(\"save_as_template\", [\"file\"], {\n name: _lt(\"Save as template\"),\n sequence: 40,\n isVisible: (env) => !env.isDashboardSpreadsheet,\n action: (env) => env.saveAsTemplate(),\n});\ntopbarMenuRegistry.addChild(\"download\", [\"file\"], {\n name: _lt(\"Download\"),\n sequence: 50,\n action: (env) => env.download(),\n isReadonlyAllowed: true,\n});\n\ntopbarMenuRegistry.addChild(\"clear_history\", [\"file\"], {\n name: _lt(\"Clear history\"),\n sequence: 60,\n isVisible: (env) => env.debug,\n action: (env) => {\n env.model.session.snapshot(env.model.exportData());\n env.model.garbageCollectExternalResources();\n window.location.reload();\n },\n});\n\ntopbarMenuRegistry.addChild(\"data_sources_data\", [\"data\"], (env) => {\n const pivots = env.model.getters.getPivotIds();\n const children = pivots.map((pivotId, index) =>\n createFullMenuItem(`item_pivot_${pivotId}`, {\n name: env.model.getters.getPivotDisplayName(pivotId),\n sequence: 100 + index,\n action: (env) => {\n env.model.dispatch(\"SELECT_PIVOT\", { pivotId: pivotId });\n env.openSidePanel(\"PIVOT_PROPERTIES_PANEL\", {});\n },\n icon: \"fa fa-table\",\n separator: index === env.model.getters.getPivotIds().length - 1,\n })\n );\n const lists = env.model.getters.getListIds().map((listId, index) => {\n return createFullMenuItem(`item_list_${listId}`, {\n name: env.model.getters.getListDisplayName(listId),\n sequence: 100 + index + pivots.length,\n action: (env) => {\n env.model.dispatch(\"SELECT_ODOO_LIST\", { listId: listId });\n env.openSidePanel(\"LIST_PROPERTIES_PANEL\", {});\n },\n icon: \"fa fa-list\",\n separator: index === env.model.getters.getListIds().length - 1,\n });\n });\n return children.concat(lists).concat([\n createFullMenuItem(`refresh_all_data`, {\n name: _t(\"Refresh all data\"),\n sequence: 1000,\n action: (env) => {\n env.model.dispatch(\"REFRESH_ALL_DATA_SOURCES\");\n },\n separator: true,\n }),\n createFullMenuItem(`reinsert_pivot`, {\n name: _t(\"Re-insert pivot\"),\n sequence: 1010,\n children: [REINSERT_PIVOT_CHILDREN],\n isVisible: (env) => env.model.getters.getPivotIds().length,\n }),\n createFullMenuItem(`insert_pivot_cell`, {\n name: _t(\"Insert pivot cell\"),\n sequence: 1020,\n children: [INSERT_PIVOT_CELL_CHILDREN],\n isVisible: (env) => env.model.getters.getPivotIds().length,\n }),\n createFullMenuItem(`reinsert_list`, {\n name: _t(\"Re-insert list\"),\n sequence: 1021,\n children: [REINSERT_LIST_CHILDREN],\n isVisible: (env) => env.model.getters.getListIds().length,\n }),\n ]);\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { Component } = owl;\nconst { autofillModifiersRegistry, autofillRulesRegistry } = spreadsheet.registries;\n\n//--------------------------------------------------------------------------\n// Autofill Component\n//--------------------------------------------------------------------------\nexport class AutofillTooltip extends Component {}\nAutofillTooltip.template = \"spreadsheet_edition.AutofillTooltip\";\n\n//--------------------------------------------------------------------------\n// Autofill Rules\n//--------------------------------------------------------------------------\n\nautofillRulesRegistry\n .add(\"autofill_pivot\", {\n condition: (cell) => cell && cell.isFormula && cell.content.match(/=\\s*ODOO\\.PIVOT/),\n generateRule: (cell, cells) => {\n const increment = cells.filter(\n (cell) => cell && cell.isFormula && cell.content.match(/=\\s*ODOO\\.PIVOT/)\n ).length;\n return { type: \"PIVOT_UPDATER\", increment, current: 0 };\n },\n sequence: 2,\n })\n .add(\"autofill_pivot_position\", {\n condition: (cell) =>\n cell && cell.isFormula && cell.content.match(/=.*ODOO\\.PIVOT.*ODOO\\.PIVOT\\.POSITION/),\n generateRule: () => ({ type: \"PIVOT_POSITION_UPDATER\", current: 0 }),\n sequence: 1,\n });\n\n//--------------------------------------------------------------------------\n// Autofill Modifier\n//--------------------------------------------------------------------------\n\nautofillModifiersRegistry\n .add(\"PIVOT_UPDATER\", {\n apply: (rule, data, getters, direction) => {\n rule.current += rule.increment;\n let isColumn;\n let steps;\n switch (direction) {\n case \"up\":\n isColumn = false;\n steps = -rule.current;\n break;\n case \"down\":\n isColumn = false;\n steps = rule.current;\n break;\n case \"left\":\n isColumn = true;\n steps = -rule.current;\n break;\n case \"right\":\n isColumn = true;\n steps = rule.current;\n }\n const content = getters.getPivotNextAutofillValue(\n getters.getFormulaCellContent(data.sheetId, data.cell),\n isColumn,\n steps\n );\n let tooltip = {\n props: {\n content: data.content,\n },\n };\n if (content && content !== data.content) {\n tooltip = {\n props: {\n content: getters.getTooltipFormula(content, isColumn),\n },\n component: AutofillTooltip,\n };\n }\n if (!content) {\n tooltip = undefined;\n }\n return {\n cellData: {\n style: undefined,\n format: undefined,\n border: undefined,\n content,\n },\n tooltip,\n };\n },\n })\n .add(\"PIVOT_POSITION_UPDATER\", {\n /**\n * Increment (or decrement) positions in template pivot formulas.\n * Autofilling vertically increments the field of the deepest row\n * group of the formula. Autofilling horizontally does the same for\n * column groups.\n */\n apply: (rule, data, getters, direction) => {\n const formulaString = data.cell.content;\n const pivotId = formulaString.match(/ODOO\\.PIVOT\\.POSITION\\(\\s*\"(\\w+)\"\\s*,/)[1];\n if (!getters.isExistingPivot(pivotId)) {\n return { cellData: { ...data.cell, content: formulaString } };\n }\n const pivotDefinition = getters.getPivotDefinition(pivotId);\n const fields = [\"up\", \"down\"].includes(direction)\n ? pivotDefinition.rowGroupBys\n : pivotDefinition.colGroupBys;\n const step = [\"right\", \"down\"].includes(direction) ? 1 : -1;\n\n const field = fields\n .reverse()\n .find((field) =>\n new RegExp(`ODOO\\\\.PIVOT\\\\.POSITION.*${field}.*\\\\)`).test(formulaString)\n );\n const content = formulaString.replace(\n new RegExp(\n `(.*ODOO\\\\.PIVOT\\\\.POSITION\\\\(\\\\s*\"\\\\w\"\\\\s*,\\\\s*\"${field}\"\\\\s*,\\\\s*\"?)(\\\\d+)(.*)`\n ),\n (match, before, position, after) => {\n rule.current += step;\n return before + Math.max(parseInt(position) + rule.current, 1) + after;\n }\n );\n return {\n cellData: { ...data.cell, content },\n tooltip: content\n ? {\n props: { content },\n }\n : undefined,\n };\n },\n });\n", "/** @odoo-module */\n\nimport { _t, _lt } from \"@web/core/l10n/translation\";\n\nimport spreadsheet, {\n initCallbackRegistry,\n} from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport PivotAutofillPlugin from \"./plugins/pivot_autofill_plugin\";\nimport PivotSidePanel from \"./side_panels/pivot_list_side_panel\";\n\nimport \"./autofill\";\nimport \"./operational_transform\";\nimport { insertPivot } from \"./pivot_init_callback\";\nimport { pivotFormulaRegex } from \"@spreadsheet/pivot/pivot_helpers\";\n\nconst { featurePluginRegistry, sidePanelRegistry, cellMenuRegistry } = spreadsheet.registries;\n\nfeaturePluginRegistry.add(\"odooPivotAutofillPlugin\", PivotAutofillPlugin);\n\nsidePanelRegistry.add(\"PIVOT_PROPERTIES_PANEL\", {\n title: () => _t(\"Pivot properties\"),\n Body: PivotSidePanel,\n});\n\ninitCallbackRegistry.add(\"insertPivot\", insertPivot);\n\ncellMenuRegistry.add(\"pivot_properties\", {\n name: _lt(\"See pivot properties\"),\n sequence: 170,\n action(env) {\n const position = env.model.getters.getActivePosition();\n const pivotId = env.model.getters.getPivotIdFromPosition(position);\n env.model.dispatch(\"SELECT_PIVOT\", { pivotId });\n env.openSidePanel(\"PIVOT_PROPERTIES_PANEL\", {});\n },\n isVisible: (env) => {\n const cell = env.model.getters.getActiveCell();\n return cell && cell.isFormula && cell.content.match(pivotFormulaRegex);\n },\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nconst { otRegistry } = spreadsheet.registries;\n\notRegistry\n .addTransformation(\"INSERT_PIVOT\", [\"INSERT_PIVOT\"], (toTransform) => ({\n ...toTransform,\n id: (parseInt(toTransform.id, 10) + 1).toString(),\n }))\n .addTransformation(\"REMOVE_PIVOT\", [\"RENAME_ODOO_PIVOT\"], (toTransform, executed) => {\n if (toTransform.pivotId === executed.pivotId) {\n return undefined;\n }\n return toTransform;\n })\n .addTransformation(\"REMOVE_PIVOT\", [\"RE_INSERT_PIVOT\"], (toTransform, executed) => {\n if (toTransform.id === executed.pivotId) {\n return undefined;\n }\n return toTransform;\n });\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { PivotDialog } from \"./spreadsheet_pivot_dialog\";\n\nconst { createFullMenuItem } = spreadsheet.helpers;\n\nexport const REINSERT_PIVOT_CHILDREN = (env) =>\n env.model.getters.getPivotIds().map((pivotId, index) =>\n createFullMenuItem(`reinsert_pivot_${pivotId}`, {\n name: env.model.getters.getPivotDisplayName(pivotId),\n sequence: index,\n action: async (env) => {\n const dataSource = env.model.getters.getPivotDataSource(pivotId);\n const model = await dataSource.copyModelWithOriginalDomain();\n const table = model.getTableStructure().export();\n const zone = env.model.getters.getSelectedZone();\n env.model.dispatch(\"RE_INSERT_PIVOT\", {\n id: pivotId,\n col: zone.left,\n row: zone.top,\n sheetId: env.model.getters.getActiveSheetId(),\n table,\n });\n env.model.dispatch(\"REFRESH_PIVOT\", { id: pivotId });\n },\n })\n );\n\nexport const INSERT_PIVOT_CELL_CHILDREN = (env) =>\n env.model.getters.getPivotIds().map((pivotId, index) =>\n createFullMenuItem(`insert_pivot_cell_${pivotId}`, {\n name: env.model.getters.getPivotDisplayName(pivotId),\n sequence: index,\n action: async (env) => {\n env.model.dispatch(\"REFRESH_PIVOT\", { id: pivotId });\n let { sheetId, col, row } = env.model.getters.getActivePosition();\n await env.model.getters.getAsyncPivotDataSource(pivotId);\n // make sure all cells are evaluated\n for (const sheetId of env.model.getters.getSheetIds()) {\n env.model.getters.getEvaluatedCells(sheetId);\n }\n const insertPivotValueCallback = (formula) => {\n env.model.dispatch(\"UPDATE_CELL\", {\n sheetId,\n col,\n row,\n content: formula,\n });\n };\n\n const getMissingValueDialogTitle = () => {\n const title = _t(\"Insert pivot cell\");\n const pivotTitle = getPivotTitle();\n if (pivotTitle) {\n return `${title} - ${pivotTitle}`;\n }\n return title;\n };\n\n const getPivotTitle = () => {\n if (pivotId) {\n return env.model.getters.getPivotDisplayName(pivotId);\n }\n return \"\";\n };\n\n env.services.dialog.add(PivotDialog, {\n title: getMissingValueDialogTitle(),\n pivotId,\n insertPivotValueCallback,\n getters: env.model.getters,\n });\n },\n })\n );\n", "/** @odoo-module **/\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport PivotDataSource from \"@spreadsheet/pivot/pivot_data_source\";\n\nconst uuidGenerator = new spreadsheet.helpers.UuidGenerator();\n\nexport function insertPivot(pivotData) {\n const definition = {\n metaData: {\n colGroupBys: [...pivotData.metaData.fullColGroupBys],\n rowGroupBys: [...pivotData.metaData.fullRowGroupBys],\n activeMeasures: [...pivotData.metaData.activeMeasures],\n resModel: pivotData.metaData.resModel,\n fields: pivotData.metaData.fields,\n sortedColumn: pivotData.metaData.sortedColumn,\n },\n searchParams: {\n ...pivotData.searchParams,\n // groups from the search bar are included in `fullRowGroupBys` and `fullColGroupBys`\n // but takes precedence if they are defined\n groupBy: [],\n },\n name: pivotData.name,\n };\n return async (model) => {\n const dataSourceId = uuidGenerator.uuidv4();\n model.config.custom.dataSources.add(dataSourceId, PivotDataSource, definition);\n await model.config.custom.dataSources.load(dataSourceId);\n const pivotDataSource = model.config.custom.dataSources.get(dataSourceId);\n // Add an empty sheet in the case of an existing spreadsheet.\n if (!this.isEmptySpreadsheet) {\n const sheetId = uuidGenerator.uuidv4();\n const sheetIdFrom = model.getters.getActiveSheetId();\n model.dispatch(\"CREATE_SHEET\", {\n sheetId,\n position: model.getters.getSheetIds().length,\n });\n model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom, sheetIdTo: sheetId });\n }\n const structure = pivotDataSource.getTableStructure();\n const table = structure.export();\n const sheetId = model.getters.getActiveSheetId();\n\n const defWithoutFields = JSON.parse(JSON.stringify(definition));\n defWithoutFields.metaData.fields = undefined;\n model.dispatch(\"INSERT_PIVOT\", {\n sheetId,\n col: 0,\n row: 0,\n table,\n id: model.getters.getNextPivotId(),\n dataSourceId,\n definition: defWithoutFields,\n });\n const columns = [];\n for (let col = 0; col <= table.cols[table.cols.length - 1].length; col++) {\n columns.push(col);\n }\n model.dispatch(\"AUTORESIZE_COLUMNS\", { sheetId, cols: columns });\n };\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { FORMATS } from \"@spreadsheet/helpers/constants\";\nimport {\n getFirstPivotFunction,\n getNumberOfPivotFormulas,\n makePivotFormula,\n} from \"@spreadsheet/pivot/pivot_helpers\";\n\n/**\n * @typedef {import(\"@spreadsheet/pivot/pivot_table\").SpreadsheetPivotTable} SpreadsheetPivotTable\n */\n\nconst { astToFormula } = spreadsheet;\n\n/**\n * @typedef CurrentElement\n * @property {Array} cols\n * @property {Array} rows\n *\n * @typedef TooltipFormula\n * @property {string} value\n *\n * @typedef GroupByDate\n * @property {boolean} isDate\n * @property {string|undefined} group\n */\n\nexport default class PivotAutofillPlugin extends spreadsheet.UIPlugin {\n // ---------------------------------------------------------------------\n // Getters\n // ---------------------------------------------------------------------\n\n /**\n * Get the next value to autofill of a pivot function\n *\n * @param {string} formula Pivot formula\n * @param {boolean} isColumn True if autofill is LEFT/RIGHT, false otherwise\n * @param {number} increment number of steps\n *\n * @returns {string}\n */\n getPivotNextAutofillValue(formula, isColumn, increment) {\n if (getNumberOfPivotFormulas(formula) !== 1) {\n return formula;\n }\n const { functionName, args } = getFirstPivotFunction(formula);\n const evaluatedArgs = args\n .map(astToFormula)\n .map((arg) => this.getters.evaluateFormula(arg).toString());\n const pivotId = evaluatedArgs[0];\n const dataSource = this.getters.getPivotDataSource(pivotId);\n for (let i = evaluatedArgs.length - 1; i > 0; i--) {\n const fieldName = evaluatedArgs[i];\n if (\n fieldName.startsWith(\"#\") &&\n ((isColumn && dataSource.isColumnGroupBy(fieldName)) ||\n (!isColumn && dataSource.isRowGroupBy(fieldName)))\n ) {\n evaluatedArgs[i + 1] = parseInt(evaluatedArgs[i + 1], 10) + increment;\n if (evaluatedArgs[i + 1] < 0) {\n return formula;\n }\n if (functionName === \"ODOO.PIVOT\") {\n return makePivotFormula(\"ODOO.PIVOT\", evaluatedArgs);\n } else if (functionName === \"ODOO.PIVOT.HEADER\") {\n return makePivotFormula(\"ODOO.PIVOT.HEADER\", evaluatedArgs);\n }\n return formula;\n }\n }\n let builder;\n if (functionName === \"ODOO.PIVOT\") {\n builder = this._autofillPivotValue.bind(this);\n } else if (functionName === \"ODOO.PIVOT.HEADER\") {\n if (evaluatedArgs.length === 1) {\n // Total\n if (isColumn) {\n // LEFT-RIGHT\n builder = this._autofillPivotRowHeader.bind(this);\n } else {\n // UP-DOWN\n builder = this._autofillPivotColHeader.bind(this);\n }\n } else if (\n this.getters.getPivotDefinition(pivotId).rowGroupBys.includes(evaluatedArgs[1])\n ) {\n builder = this._autofillPivotRowHeader.bind(this);\n } else {\n builder = this._autofillPivotColHeader.bind(this);\n }\n }\n if (builder) {\n return builder(pivotId, evaluatedArgs, isColumn, increment);\n }\n return formula;\n }\n\n /**\n * Compute the tooltip to display from a Pivot formula\n *\n * @param {string} formula Pivot formula\n * @param {boolean} isColumn True if the direction is left/right, false\n * otherwise\n *\n * @returns {Array}\n */\n getTooltipFormula(formula, isColumn) {\n if (getNumberOfPivotFormulas(formula) !== 1) {\n return [];\n }\n const { functionName, args } = getFirstPivotFunction(formula);\n const evaluatedArgs = args\n .map(astToFormula)\n .map((arg) => this.getters.evaluateFormula(arg));\n const pivotId = evaluatedArgs[0];\n if (functionName === \"ODOO.PIVOT\") {\n return this._tooltipFormatPivot(pivotId, evaluatedArgs, isColumn);\n } else if (functionName === \"ODOO.PIVOT.HEADER\") {\n return this._tooltipFormatPivotHeader(pivotId, evaluatedArgs);\n }\n return [];\n }\n\n // ---------------------------------------------------------------------\n // Autofill\n // ---------------------------------------------------------------------\n\n /**\n * Get the next value to autofill from a pivot value (\"=PIVOT()\")\n *\n * Here are the possibilities:\n * 1) LEFT-RIGHT\n * - Working on a date value, with one level of group by in the header\n * => Autofill the date, without taking care of headers\n * - Targeting a row-header\n * => Creation of a PIVOT.HEADER with the value of the current rows\n * - Targeting outside the pivot (before the row header and after the\n * last col)\n * => Return empty string\n * - Targeting a value cell\n * => Autofill by changing the cols\n * 2) UP-DOWN\n * - Working on a date value, with one level of group by in the header\n * => Autofill the date, without taking care of headers\n * - Targeting a col-header\n * => Creation of a PIVOT.HEADER with the value of the current cols,\n * with the given increment\n * - Targeting outside the pivot (after the last row)\n * => Return empty string\n * - Targeting a value cell\n * => Autofill by changing the rows\n *\n * @param {string} pivotId Id of the pivot\n * @param {Array} args args of the pivot formula\n * @param {boolean} isColumn True if the direction is left/right, false\n * otherwise\n * @param {number} increment Increment of the autofill\n *\n * @private\n *\n * @returns {string}\n */\n _autofillPivotValue(pivotId, args, isColumn, increment) {\n const currentElement = this._getCurrentValueElement(pivotId, args);\n const dataSource = this.getters.getPivotDataSource(pivotId);\n const table = dataSource.getTableStructure();\n const isDate = dataSource.isGroupedOnlyByOneDate(isColumn ? \"COLUMN\" : \"ROW\");\n let cols = [];\n let rows = [];\n let measure;\n if (isColumn) {\n // LEFT-RIGHT\n rows = currentElement.rows;\n if (isDate) {\n // Date\n const group = dataSource.getGroupOfFirstDate(\"COLUMN\");\n cols = currentElement.cols;\n cols[0] = this._incrementDate(cols[0], group, increment);\n measure = cols.pop();\n } else {\n const currentColIndex = table.getColMeasureIndex(currentElement.cols);\n if (currentColIndex === -1) {\n return \"\";\n }\n const nextColIndex = currentColIndex + increment;\n if (nextColIndex === -1) {\n // Targeting row-header\n return this._autofillRowFromValue(pivotId, currentElement);\n }\n if (nextColIndex < -1 || nextColIndex >= table.getColWidth()) {\n // Outside the pivot\n return \"\";\n }\n // Targeting value\n const measureCell = table.getCellFromMeasureRowAtIndex(nextColIndex);\n cols = [...measureCell.values];\n measure = cols.pop();\n }\n } else {\n // UP-DOWN\n cols = currentElement.cols;\n if (isDate) {\n // Date\n if (currentElement.rows.length === 0) {\n return \"\";\n }\n const group = dataSource.getGroupOfFirstDate(\"ROW\");\n rows = currentElement.rows;\n rows[0] = this._incrementDate(rows[0], group, increment);\n } else {\n const currentRowIndex = table.getRowIndex(currentElement.rows);\n if (currentRowIndex === -1) {\n return \"\";\n }\n const nextRowIndex = currentRowIndex + increment;\n if (nextRowIndex < 0) {\n // Targeting col-header\n return this._autofillColFromValue(pivotId, nextRowIndex, currentElement);\n }\n if (nextRowIndex >= table.getRowHeight()) {\n // Outside the pivot\n return \"\";\n }\n // Targeting value\n rows = [...table.getCellsFromRowAtIndex(nextRowIndex).values];\n }\n measure = cols.pop();\n }\n return makePivotFormula(\"ODOO.PIVOT\", this._buildArgs(pivotId, measure, rows, cols));\n }\n /**\n * Get the next value to autofill from a pivot header (\"=PIVOT.HEADER()\")\n * which is a col.\n *\n * Here are the possibilities:\n * 1) LEFT-RIGHT\n * - Working on a date value, with one level of group by in the header\n * => Autofill the date, without taking care of headers\n * - Targeting outside (before the first col after the last col)\n * => Return empty string\n * - Targeting a col-header\n * => Creation of a PIVOT.HEADER with the value of the new cols\n * 2) UP-DOWN\n * - Working on a date value, with one level of group by in the header\n * => Replace the date in the headers and autocomplete as usual\n * - Targeting a cell (after the last col and before the last row)\n * => Autofill by adding the corresponding rows\n * - Targeting a col-header (after the first col and before the last\n * col)\n * => Creation of a PIVOT.HEADER with the value of the new cols\n * - Targeting outside the pivot (before the first col of after the\n * last row)\n * => Return empty string\n *\n * @param {string} pivotId Id of the pivot\n * @param {Array} args args of the pivot.header formula\n * @param {boolean} isColumn True if the direction is left/right, false\n * otherwise\n * @param {number} increment Increment of the autofill\n *\n * @private\n *\n * @returns {string}\n */\n _autofillPivotColHeader(pivotId, args, isColumn, increment) {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n /** @type {SpreadsheetPivotTable} */\n const table = dataSource.getTableStructure();\n const currentElement = this._getCurrentHeaderElement(pivotId, args);\n const currentColIndex = table.getColMeasureIndex(currentElement.cols);\n const isDate = dataSource.isGroupedOnlyByOneDate(\"COLUMN\");\n if (isColumn) {\n // LEFT-RIGHT\n let groupValues;\n if (isDate) {\n // Date\n const group = dataSource.getGroupOfFirstDate(\"COLUMN\");\n groupValues = currentElement.cols;\n groupValues[0] = this._incrementDate(groupValues[0], group, increment);\n } else {\n const rowIndex = currentElement.cols.length - 1;\n const nextColIndex = currentColIndex + increment;\n const nextGroup = table.getNextColCell(nextColIndex, rowIndex);\n if (\n currentColIndex === -1 ||\n nextColIndex < 0 ||\n nextColIndex >= table.getColWidth() ||\n !nextGroup\n ) {\n // Outside the pivot\n return \"\";\n }\n // Targeting a col.header\n groupValues = nextGroup.values;\n }\n return makePivotFormula(\n \"ODOO.PIVOT.HEADER\",\n this._buildArgs(pivotId, undefined, [], groupValues)\n );\n } else {\n // UP-DOWN\n const rowIndex =\n currentColIndex === table.getColWidth() - 1\n ? table.getColHeight() - 2 + currentElement.cols.length\n : currentElement.cols.length - 1;\n const nextRowIndex = rowIndex + increment;\n const groupLevels = dataSource.getNumberOfColGroupBys();\n if (nextRowIndex < 0 || nextRowIndex >= groupLevels + 1 + table.getRowHeight()) {\n // Outside the pivot\n return \"\";\n }\n if (nextRowIndex >= groupLevels + 1) {\n // Targeting a value\n const rowIndex = nextRowIndex - groupLevels - 1;\n const measureCell = table.getCellFromMeasureRowAtIndex(currentColIndex);\n const cols = [...measureCell.values];\n const measure = cols.pop();\n const rows = [...table.getCellsFromRowAtIndex(rowIndex).values];\n return makePivotFormula(\n \"ODOO.PIVOT\",\n this._buildArgs(pivotId, measure, rows, cols)\n );\n } else {\n // Targeting a col.header\n const groupValues = table.getNextColCell(currentColIndex, nextRowIndex).values;\n return makePivotFormula(\n \"ODOO.PIVOT.HEADER\",\n this._buildArgs(pivotId, undefined, [], groupValues)\n );\n }\n }\n }\n /**\n * Get the next value to autofill from a pivot header (\"=PIVOT.HEADER()\")\n * which is a row.\n *\n * Here are the possibilities:\n * 1) LEFT-RIGHT\n * - Targeting outside (LEFT or after the last col)\n * => Return empty string\n * - Targeting a cell\n * => Autofill by adding the corresponding cols\n * 2) UP-DOWN\n * - Working on a date value, with one level of group by in the header\n * => Autofill the date, without taking care of headers\n * - Targeting a row-header\n * => Creation of a PIVOT.HEADER with the value of the new rows\n * - Targeting outside the pivot (before the first row of after the\n * last row)\n * => Return empty string\n *\n * @param {string} pivotId Id of the pivot\n * @param {Array} args args of the pivot.header formula\n * @param {boolean} isColumn True if the direction is left/right, false\n * otherwise\n * @param {number} increment Increment of the autofill\n *\n * @private\n *\n * @returns {string}\n */\n _autofillPivotRowHeader(pivotId, args, isColumn, increment) {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n const table = dataSource.getTableStructure();\n const currentElement = this._getCurrentHeaderElement(pivotId, args);\n const currentIndex = table.getRowIndex(currentElement.rows);\n const isDate = dataSource.isGroupedOnlyByOneDate(\"ROW\");\n if (isColumn) {\n const colIndex = increment - 1;\n // LEFT-RIGHT\n if (colIndex < 0 || colIndex >= table.getColWidth()) {\n // Outside the pivot\n return \"\";\n }\n const measureCell = table.getCellFromMeasureRowAtIndex(colIndex);\n const values = [...measureCell.values];\n const measure = values.pop();\n return makePivotFormula(\n \"ODOO.PIVOT\",\n this._buildArgs(pivotId, measure, currentElement.rows, values)\n );\n } else {\n // UP-DOWN\n let rows;\n if (isDate) {\n // Date\n const group = dataSource.getGroupOfFirstDate(\"ROW\");\n rows = currentElement.rows;\n rows[0] = this._incrementDate(rows[0], group, increment);\n } else {\n const nextIndex = currentIndex + increment;\n if (currentIndex === -1 || nextIndex < 0 || nextIndex >= table.getRowHeight()) {\n return \"\";\n }\n rows = [...table.getCellsFromRowAtIndex(nextIndex).values];\n }\n return makePivotFormula(\n \"ODOO.PIVOT.HEADER\",\n this._buildArgs(pivotId, undefined, rows, [])\n );\n }\n }\n /**\n * Create a col header from a value\n *\n * @param {string} pivotId Id of the pivot\n * @param {number} nextIndex Index of the target column\n * @param {CurrentElement} currentElement Current element (rows and cols)\n *\n * @private\n *\n * @returns {string}\n */\n _autofillColFromValue(pivotId, nextIndex, currentElement) {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n const table = dataSource.getTableStructure();\n const groupIndex = table.getColMeasureIndex(currentElement.cols);\n if (groupIndex < 0) {\n return \"\";\n }\n const levels = dataSource.getNumberOfColGroupBys();\n const index = levels + 1 + nextIndex;\n if (index < 0 || index >= levels + 1) {\n return \"\";\n }\n const cols = [];\n for (let i = 0; i <= index; i++) {\n cols.push(currentElement.cols[i]);\n }\n return makePivotFormula(\"ODOO.PIVOT.HEADER\", this._buildArgs(pivotId, undefined, [], cols));\n }\n /**\n * Create a row header from a value\n *\n * @param {string} pivotId Id of the pivot\n * @param {CurrentElement} currentElement Current element (rows and cols)\n *\n * @private\n *\n * @returns {string}\n */\n _autofillRowFromValue(pivotId, currentElement) {\n const rows = currentElement.rows;\n if (!rows) {\n return \"\";\n }\n return makePivotFormula(\"ODOO.PIVOT.HEADER\", this._buildArgs(pivotId, undefined, rows, []));\n }\n /**\n * Parse the arguments of a pivot function to find the col values and\n * the row values of a PIVOT.HEADER function\n *\n * @param {string} pivotId Id of the pivot\n * @param {Array} args Args of the pivot.header formula\n *\n * @private\n *\n * @returns {CurrentElement}\n */\n _getCurrentHeaderElement(pivotId, args) {\n const definition = this.getters.getPivotDefinition(pivotId);\n const values = this._parseArgs(args.slice(1));\n const cols = this._getFieldValues([...definition.colGroupBys, \"measure\"], values);\n const rows = this._getFieldValues(definition.rowGroupBys, values);\n return { cols, rows };\n }\n /**\n * Parse the arguments of a pivot function to find the col values and\n * the row values of a PIVOT function\n *\n * @param {string} pivotId Id of the pivot\n * @param {Array} args Args of the pivot formula\n *\n * @private\n *\n * @returns {CurrentElement}\n */\n _getCurrentValueElement(pivotId, args) {\n const definition = this.getters.getPivotDefinition(pivotId);\n const values = this._parseArgs(args.slice(2));\n const cols = this._getFieldValues(definition.colGroupBys, values);\n cols.push(args[1]); // measure\n const rows = this._getFieldValues(definition.rowGroupBys, values);\n return { cols, rows };\n }\n /**\n * Return the values for the fields which are present in the list of\n * fields\n *\n * ex: fields: [\"create_date\"]\n * values: { create_date: \"01/01\", stage_id: 1 }\n * => [\"01/01\"]\n *\n * @param {Array} fields List of fields\n * @param {Object} values Association field-values\n *\n * @private\n * @returns {Array}\n */\n _getFieldValues(fields, values) {\n return fields.filter((field) => field in values).map((field) => values[field]);\n }\n /**\n * Increment a date with a given increment and interval (group)\n *\n * @param {string} date\n * @param {string} group (day, week, month, ...)\n * @param {number} increment\n *\n * @private\n * @returns {string}\n */\n _incrementDate(date, group, increment) {\n const format = FORMATS[group].out;\n const interval = FORMATS[group].interval;\n const dateMoment = moment(date, format);\n return dateMoment.isValid() ? dateMoment.add(increment, interval).format(format) : date;\n }\n /**\n * Create a structure { field: value } from the arguments of a pivot\n * function\n *\n * @param {Array} args\n *\n * @private\n * @returns {Object}\n */\n _parseArgs(args) {\n const values = {};\n for (let i = 0; i < args.length; i += 2) {\n values[args[i]] = args[i + 1];\n }\n return values;\n }\n\n // ---------------------------------------------------------------------\n // Tooltips\n // ---------------------------------------------------------------------\n\n /**\n * Get the tooltip for a pivot formula\n *\n * @param {string} pivotId Id of the pivot\n * @param {Array} args\n * @param {boolean} isColumn True if the direction is left/right, false\n * otherwise\n * @private\n *\n * @returns {Array}\n */\n _tooltipFormatPivot(pivotId, args, isColumn) {\n const tooltips = [];\n const definition = this.getters.getPivotDefinition(pivotId);\n const dataSource = this.getters.getPivotDataSource(pivotId);\n const values = this._parseArgs(args.slice(2));\n for (const [fieldName, value] of Object.entries(values)) {\n if (\n (isColumn && dataSource.isColumnGroupBy(fieldName)) ||\n (!isColumn && dataSource.isRowGroupBy(fieldName))\n ) {\n tooltips.push({\n value: dataSource.getDisplayedPivotHeaderValue([fieldName, value]),\n });\n }\n }\n if (definition.measures.length !== 1 && isColumn) {\n const measure = args[1];\n tooltips.push({\n value: dataSource.getGroupByDisplayLabel(\"measure\", measure),\n });\n }\n if (!tooltips.length) {\n tooltips.push({\n value: _t(\"Total\"),\n });\n }\n return tooltips;\n }\n /**\n * Get the tooltip for a pivot header formula\n *\n * @param {string} pivotId Id of the pivot\n * @param {Array} args\n *\n * @private\n *\n * @returns {Array}\n */\n _tooltipFormatPivotHeader(pivotId, args) {\n const tooltips = [];\n const values = this._parseArgs(args.slice(1));\n const dataSource = this.getters.getPivotDataSource(pivotId);\n if (Object.keys(values).length === 0) {\n return [{ value: _t(\"Total\") }];\n }\n for (const [fieldName, value] of Object.entries(values)) {\n tooltips.push({ value: dataSource.getDisplayedPivotHeaderValue([fieldName, value]) });\n }\n return tooltips;\n }\n\n // ---------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------\n\n /**\n * Create the args from pivot, measure, rows and cols\n * if measure is undefined, it's not added\n *\n * @param {string} pivotId Id of the pivot\n * @param {string} measure\n * @param {Object} rows\n * @param {Object} cols\n *\n * @private\n * @returns {Array}\n */\n _buildArgs(pivotId, measure, rows, cols) {\n const { rowGroupBys, measures } = this.getters.getPivotDefinition(pivotId);\n const args = [pivotId];\n if (measure) {\n args.push(measure);\n }\n for (const index in rows) {\n args.push(rowGroupBys[index]);\n args.push(rows[index]);\n }\n if (cols.length === 1 && measures.includes(cols[0])) {\n args.push(\"measure\");\n args.push(cols[0]);\n } else {\n const dataSource = this.getters.getPivotDataSource(pivotId);\n for (const index in cols) {\n args.push(dataSource.getGroupByAtIndex(\"COLUMN\", index) || \"measure\");\n args.push(cols[index]);\n }\n }\n return args;\n }\n}\n\nPivotAutofillPlugin.getters = [\"getPivotNextAutofillValue\", \"getTooltipFormula\"];\n", "/** @odoo-module */\n\nimport { Domain } from \"@web/core/domain\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"web.core\";\nimport { time_to_str } from \"web.time\";\nimport EditableName from \"../../o_spreadsheet/editable_name/editable_name\";\n\nconst { Component, onWillStart, onWillUpdateProps } = owl;\n\nexport default class PivotDetailsSidePanel extends Component {\n setup() {\n this.dialog = useService(\"dialog\");\n /** @type {import(\"@spreadsheet/pivot/pivot_data_source\").default} */\n this.dataSource = undefined;\n const loadData = async () => {\n this.dataSource = await this.env.model.getters.getAsyncPivotDataSource(\n this.props.pivotId\n );\n this.modelDisplayName = await this.dataSource.getModelLabel();\n };\n onWillStart(loadData);\n onWillUpdateProps(loadData);\n }\n\n get pivotDefinition() {\n const definition = this.env.model.getters.getPivotDefinition(this.props.pivotId);\n return {\n model: definition.model,\n modelDisplayName: this.modelDisplayName,\n domain: new Domain(definition.domain).toString(),\n dimensions: [...definition.rowGroupBys, ...definition.colGroupBys].map((fieldName) =>\n this.dataSource.getFormattedGroupBy(fieldName)\n ),\n measures: definition.measures.map((measure) =>\n this.dataSource.getGroupByDisplayLabel(\"measure\", measure)\n ),\n sortedColumn: definition.sortedColumn,\n };\n }\n\n onNameChanged(name) {\n this.env.model.dispatch(\"RENAME_ODOO_PIVOT\", {\n pivotId: this.props.pivotId,\n name,\n });\n }\n\n formatSort() {\n const sortedColumn = this.pivotDefinition.sortedColumn;\n const order = sortedColumn.order === \"asc\" ? _t(\"ascending\") : _t(\"descending\");\n const measureDisplayName = this.dataSource.getGroupByDisplayLabel(\n \"measure\",\n sortedColumn.measure\n );\n return `${measureDisplayName} (${order})`;\n }\n\n /**\n * Get the last update date, formatted\n *\n * @returns {string} date formatted\n */\n getLastUpdate() {\n const lastUpdate = this.dataSource.lastUpdate;\n if (lastUpdate) {\n return time_to_str(new Date(lastUpdate));\n }\n return _t(\"never\");\n }\n\n /**\n * Refresh the cache of the current pivot\n *\n */\n refresh() {\n this.env.model.dispatch(\"REFRESH_PIVOT\", { id: this.props.pivotId });\n }\n\n openDomainEdition() {\n const definition = this.env.model.getters.getPivotDefinition(this.props.pivotId);\n this.dialog.add(DomainSelectorDialog, {\n resModel: definition.model,\n initialValue: new Domain(definition.domain).toString(),\n readonly: false,\n isDebugMode: !!this.env.debug,\n onSelected: (domain) =>\n this.env.model.dispatch(\"UPDATE_ODOO_PIVOT_DOMAIN\", {\n pivotId: this.props.pivotId,\n domain: new Domain(domain).toList(),\n }),\n });\n }\n}\nPivotDetailsSidePanel.template = \"spreadsheet_edition.PivotDetailsSidePanel\";\nPivotDetailsSidePanel.components = { DomainSelector, EditableName };\nPivotDetailsSidePanel.props = {\n pivotId: {\n type: String,\n optional: true,\n },\n};\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport PivotDetailsSidePanel from \"./pivot_details_side_panel\";\n\nconst { Component } = owl;\n\nexport default class PivotSidePanel extends Component {\n selectPivot(pivotId) {\n this.env.model.dispatch(\"SELECT_PIVOT\", { pivotId });\n }\n\n resetSelectedPivot() {\n this.env.model.dispatch(\"SELECT_PIVOT\");\n }\n\n delete(pivotId) {\n this.env.askConfirmation(_t(\"Are you sure you want to delete this pivot ?\"), () => {\n this.env.model.dispatch(\"REMOVE_PIVOT\", { pivotId });\n this.props.onCloseSidePanel();\n });\n }\n}\nPivotSidePanel.template = \"spreadsheet_edition.PivotSidePanel\";\nPivotSidePanel.components = { PivotDetailsSidePanel };\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { PivotDialogTable } from \"./spreadsheet_pivot_dialog_table\";\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nimport { makePivotFormula } from \"@spreadsheet/pivot/pivot_helpers\";\n\nconst { Component, useState } = owl;\nconst formatValue = spreadsheet.helpers.formatValue;\n\n/**\n * @typedef {Object} PivotDialogColumn\n * @property {string} formula Pivot formula\n * @property {string} value Pivot value of the formula\n * @property {number} span Size of col-span\n * @property {boolean} isMissing True if the value is missing from the sheet\n * @property {string} style Style of the column\n */\n\n/**\n * @typedef {Object} PivotDialogRow\n * @property {Array} args Args of the pivot formula\n * @property {string} formula Pivot formula\n * @property {string} value Pivot value of the formula\n * @property {boolean} isMissing True if the value is missing from the sheet\n * @property {string} style Style of the column\n */\n\n/**\n * @typedef {Object} PivotDialogValue\n * @property {Object} args\n * @property {string} args.formula Pivot formula\n * @property {string} args.value Pivot value of the formula\n * @property {boolean} isMissing True if the value is missing from the sheet\n */\n\nexport class PivotDialog extends Component {\n setup() {\n this.state = useState({\n showMissingValuesOnly: false,\n });\n this.dataSource = this.props.getters.getPivotDataSource(this.props.pivotId);\n\n const table = this.dataSource.getTableStructure();\n const id = this.props.pivotId;\n this.data = {\n columns: this._buildColHeaders(id, table),\n rows: this._buildRowHeaders(id, table),\n values: this._buildValues(id, table),\n };\n }\n\n _onCellClicked(detail) {\n this.props.insertPivotValueCallback(detail.formula);\n this.props.close();\n }\n\n // ---------------------------------------------------------------------\n // Missing values building\n // ---------------------------------------------------------------------\n\n /**\n * Retrieve the data to display in the Pivot Table\n * In the case when showMissingValuesOnly is false, the returned value\n * is the complete data\n * In the case when showMissingValuesOnly is true, the returned value is\n * the data which contains only missing values in the rows and cols. In\n * the rows, we also return the parent rows of rows which contains missing\n * values, to give context to the user.\n *\n * @returns {Object} { columns, rows, values }\n */\n getTableData() {\n if (!this.state.showMissingValuesOnly) {\n return this.data;\n }\n const colIndexes = this._getColumnsIndexes();\n const rowIndexes = this._getRowsIndexes();\n const columns = this._buildColumnsMissing(colIndexes);\n const rows = this._buildRowsMissing(rowIndexes);\n const values = this._buildValuesMissing(colIndexes, rowIndexes);\n return { columns, rows, values };\n }\n\n getRowIndex(groupValues) {\n const stringifiedValues = JSON.stringify(groupValues);\n return this._rows.findIndex((values) => JSON.stringify(values) === stringifiedValues);\n }\n\n /**\n * Retrieve the parents of the given row\n * ex:\n * Australia\n * January\n * February\n * The parent of \"January\" is \"Australia\"\n *\n * @private\n * @param {number} index Index of the row\n * @returns {Array}\n */\n _addRecursiveRow(index) {\n const rows = this.dataSource.getTableStructure().getRowHeaders();\n const row = [...rows[index].values];\n if (row.length <= 1) {\n return [index];\n }\n row.pop();\n const parentRowIndex = rows.findIndex(\n (r) => JSON.stringify(r.values) === JSON.stringify(row)\n );\n return [index].concat(this._addRecursiveRow(parentRowIndex));\n }\n /**\n * Create the columns to be used, based on the indexes of the columns in\n * which a missing value is present\n *\n * @private\n * @param {Array} indexes Indexes of columns with a missing value\n * @returns {Array>}\n */\n _buildColumnsMissing(indexes) {\n // columnsMap explode the columns in an array of array of the same\n // size with the index of each column, repeated 'span' times.\n // ex:\n // | A | B |\n // | 1 | 2 | 3 |\n // => [\n // [0, 0, 1]\n // [0, 1, 2]\n // ]\n const columnsMap = [];\n for (const column of this.data.columns) {\n const columnMap = [];\n for (const index in column) {\n for (let i = 0; i < column[index].span; i++) {\n columnMap.push(index);\n }\n }\n columnsMap.push(columnMap);\n }\n // Remove the columns that are not present in indexes\n for (let i = columnsMap[columnsMap.length - 1].length; i >= 0; i--) {\n if (!indexes.includes(i)) {\n for (const columnMap of columnsMap) {\n columnMap.splice(i, 1);\n }\n }\n }\n // Build the columns\n const columns = [];\n for (const mapIndex in columnsMap) {\n const column = [];\n let index = undefined;\n let span = 1;\n for (let i = 0; i < columnsMap[mapIndex].length; i++) {\n if (index !== columnsMap[mapIndex][i]) {\n if (index) {\n column.push(\n Object.assign({}, this.data.columns[mapIndex][index], { span })\n );\n }\n index = columnsMap[mapIndex][i];\n span = 1;\n } else {\n span++;\n }\n }\n if (index) {\n column.push(Object.assign({}, this.data.columns[mapIndex][index], { span }));\n }\n columns.push(column);\n }\n return columns;\n }\n /**\n * Create the rows to be used, based on the indexes of the rows in\n * which a missing value is present.\n *\n * @private\n * @param {Array} indexes Indexes of rows with a missing value\n * @returns {Array}\n */\n _buildRowsMissing(indexes) {\n return indexes.map((index) => this.data.rows[index]);\n }\n /**\n * Create the value to be used, based on the indexes of the columns and\n * rows in which a missing value is present.\n *\n * @private\n * @param {Array} colIndexes Indexes of columns with a missing value\n * @param {Array} rowIndexes Indexes of rows with a missing value\n * @returns {Array}\n */\n _buildValuesMissing(colIndexes, rowIndexes) {\n const values = colIndexes.map(() => []);\n for (const row of rowIndexes) {\n for (const col in colIndexes) {\n values[col].push(this.data.values[colIndexes[col]][row]);\n }\n }\n return values;\n }\n /**\n * Get the indexes of the columns in which a missing value is present\n * @private\n * @returns {Array}\n */\n _getColumnsIndexes() {\n const indexes = new Set();\n for (let i = 0; i < this.data.columns.length; i++) {\n const exploded = [];\n for (let y = 0; y < this.data.columns[i].length; y++) {\n for (let x = 0; x < this.data.columns[i][y].span; x++) {\n exploded.push(this.data.columns[i][y]);\n }\n }\n for (let y = 0; y < exploded.length; y++) {\n if (exploded[y].isMissing) {\n indexes.add(y);\n }\n }\n }\n for (let i = 0; i < this.data.columns[this.data.columns.length - 1].length; i++) {\n const values = this.data.values[i];\n if (values.find((x) => x.isMissing)) {\n indexes.add(i);\n }\n }\n return Array.from(indexes).sort((a, b) => a - b);\n }\n /**\n * Get the indexes of the rows in which a missing value is present\n * @private\n * @returns {Array}\n */\n _getRowsIndexes() {\n const rowIndexes = new Set();\n for (let i = 0; i < this.data.rows.length; i++) {\n if (this.data.rows[i].isMissing) {\n rowIndexes.add(i);\n }\n for (const col of this.data.values) {\n if (col[i].isMissing) {\n this._addRecursiveRow(i).forEach((x) => rowIndexes.add(x));\n }\n }\n }\n return Array.from(rowIndexes).sort((a, b) => a - b);\n }\n\n _getDisplayedPivotHeaderValue(domain) {\n const len = domain.length;\n if (len === 0) {\n return _t(\"Total\");\n }\n const field = domain[len - 2];\n const value = domain[len - 1];\n return this.dataSource.getGroupByDisplayLabel(field, value);\n }\n\n // ---------------------------------------------------------------------\n // Data table creation\n // ---------------------------------------------------------------------\n\n /**\n * Create the columns headers of the Pivot\n *\n * @param {string} id Pivot Id\n * @param {SpreadsheetPivotTable} table\n *\n * @private\n * @returns {Array>}\n */\n _buildColHeaders(id, table) {\n const headers = [];\n for (const row of table.getColHeaders()) {\n const current = [];\n for (const cell of row) {\n const domain = [];\n for (let i = 0; i < cell.fields.length; i++) {\n domain.push(cell.fields[i]);\n domain.push(cell.values[i]);\n }\n current.push({\n formula: makePivotFormula(\"ODOO.PIVOT.HEADER\", [id, ...domain]),\n value: this._getDisplayedPivotHeaderValue(domain),\n span: cell.width,\n isMissing: !this.dataSource.isUsedHeader(domain),\n });\n }\n headers.push(current);\n }\n const last = headers[headers.length - 1];\n headers[headers.length - 1] = last.map((cell) => {\n if (!cell.isMissing) {\n cell.style = \"color: #756f6f;\";\n }\n return cell;\n });\n return headers;\n }\n /**\n * Create the row of the pivot table\n *\n * @param {string} id Pivot Id\n * @param {SpreadsheetPivotTable} table\n *\n * @private\n * @returns {Array}\n */\n _buildRowHeaders(id, table) {\n const headers = [];\n for (const row of table.getRowHeaders()) {\n const domain = [];\n for (let i = 0; i < row.fields.length; i++) {\n domain.push(row.fields[i]);\n domain.push(row.values[i]);\n }\n const cell = {\n args: domain,\n formula: makePivotFormula(\"ODOO.PIVOT.HEADER\", [id, ...domain]),\n value: this._getDisplayedPivotHeaderValue(domain),\n isMissing: !this.dataSource.isUsedHeader(domain),\n };\n if (row.indent > 1) {\n cell.style = `padding-left: ${row.indent - 1 * 10}px`;\n }\n headers.push(cell);\n }\n return headers;\n }\n /**\n * Build the values of the pivot table\n *\n * @param {string} id Pivot Id\n * @param {SpreadsheetPivotTable} table\n *\n * @private\n * @returns {Array}\n */\n _buildValues(id, table) {\n const values = [];\n for (const col of table.getMeasureHeaders()) {\n const current = [];\n const measure = col.values[col.values.length - 1];\n for (const row of table.getRowHeaders()) {\n const domain = [];\n for (let i = 0; i < row.fields.length; i++) {\n domain.push(row.fields[i]);\n domain.push(row.values[i]);\n }\n for (let i = 0; i < col.fields.length - 1; i++) {\n domain.push(col.fields[i]);\n domain.push(col.values[i]);\n }\n const value = this.dataSource.getPivotCellValue(measure, domain);\n current.push({\n args: {\n formula: makePivotFormula(\"ODOO.PIVOT\", [id, measure, ...domain]),\n value: !value ? \"\" : formatValue(value),\n },\n isMissing: !this.dataSource.isUsedValue(domain, measure),\n });\n }\n values.push(current);\n }\n return values;\n }\n}\n\nPivotDialog.template = \"spreadsheet_edition.PivotDialog\";\nPivotDialog.components = { Dialog, PivotDialogTable };\n", "/** @odoo-module */\nconst { Component } = owl;\n\nexport class PivotDialogTable extends Component {\n _onCellClicked(formula) {\n this.props.onCellSelected({ formula });\n }\n}\nPivotDialogTable.template = \"spreadsheet_edition.PivotDialogTable\";\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { download } from \"@web/core/network/download\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport SpreadsheetComponent from \"@spreadsheet_edition/bundle/actions/spreadsheet_component\";\nimport { SpreadsheetName } from \"@spreadsheet_edition/bundle/actions/control_panel/spreadsheet_name\";\n\nimport { UNTITLED_SPREADSHEET_NAME } from \"@spreadsheet/helpers/constants\";\nimport { convertFromSpreadsheetTemplate } from \"@documents_spreadsheet/bundle/helpers\";\nimport { AbstractSpreadsheetAction } from \"@spreadsheet_edition/bundle/actions/abstract_spreadsheet_action\";\nimport { DocumentsSpreadsheetControlPanel } from \"../components/control_panel/spreadsheet_control_panel\";\nimport { RecordFileStore } from \"@spreadsheet_edition/bundle/image/record_file_store\";\n\nconst { Component, useState } = owl;\n\nexport class SpreadsheetAction extends AbstractSpreadsheetAction {\n setup() {\n super.setup();\n this.orm = useService(\"orm\");\n this.actionService = useService(\"action\");\n this.notificationMessage = this.env._t(\"New spreadsheet created in Documents\");\n\n this.state = useState({\n numberOfConnectedUsers: 1,\n isSynced: true,\n isFavorited: false,\n spreadsheetName: UNTITLED_SPREADSHEET_NAME,\n });\n\n this.spreadsheetCollaborative = useService(\"spreadsheet_collaborative\");\n this.fileStore = new RecordFileStore(\"documents.document\", this.resId, this.http, this.orm);\n }\n\n async onWillStart() {\n await super.onWillStart();\n this.transportService = this.spreadsheetCollaborative.getCollaborativeChannel(\n Component.env,\n \"documents.document\",\n this.resId\n );\n }\n\n async _fetchData() {\n const record = await this.orm.call(\"documents.document\", \"join_spreadsheet_session\", [\n this.resId,\n ]);\n if (this.params.convert_from_template) {\n return {\n ...record,\n raw: await convertFromSpreadsheetTemplate(this.orm, record.raw),\n };\n }\n return record;\n }\n\n /**\n * @override\n */\n _initializeWith(record) {\n this.state.isFavorited = record.is_favorited;\n this.spreadsheetData = record.raw;\n this.stateUpdateMessages = record.revisions;\n this.snapshotRequested = record.snapshot_requested;\n this.state.spreadsheetName = record.name;\n this.isReadonly = record.isReadonly;\n }\n\n /**\n * @private\n * @param {Object}\n */\n async _onDownload({ name, files }) {\n await download({\n url: \"/spreadsheet/xlsx\",\n data: {\n zip_name: `${name}.xlsx`,\n files: JSON.stringify(files),\n },\n });\n }\n\n /**\n * @param {OdooEvent} ev\n * @returns {Promise}\n */\n async _onSpreadSheetFavoriteToggled(ev) {\n this.state.isFavorited = !this.state.isFavorited;\n return await this.orm.call(\"documents.document\", \"toggle_favorited\", [[this.resId]]);\n }\n\n /**\n * Updates the control panel with the sync status of spreadsheet\n *\n * @param {Object}\n */\n _onSpreadsheetSyncStatus({ synced, numberOfConnectedUsers }) {\n this.state.isSynced = synced;\n this.state.numberOfConnectedUsers = numberOfConnectedUsers;\n }\n\n /**\n * Reload the spreadsheet if an unexpected revision id is triggered.\n */\n _onUnexpectedRevisionId() {\n this.actionService.doAction(\"reload_context\");\n }\n\n /**\n * Create a copy of the given spreadsheet and display it\n */\n async _onMakeCopy({ data, thumbnail }) {\n const defaultValues = {\n mimetype: \"application/o-spreadsheet\",\n raw: JSON.stringify(data),\n spreadsheet_snapshot: false,\n thumbnail,\n };\n const id = await this.orm.call(\"documents.document\", \"copy\", [this.resId], {\n default: defaultValues,\n });\n this._openSpreadsheet(id);\n }\n\n /**\n * Create a new sheet and display it\n */\n async _onNewSpreadsheet() {\n const action = await this.orm.call(\"documents.document\", \"action_open_new_spreadsheet\");\n this._notifyCreation();\n this.actionService.doAction(action, { clear_breadcrumbs: true });\n }\n\n async _onSpreadsheetSaved({ thumbnail }) {\n await this.orm.write(\"documents.document\", [this.resId], { thumbnail });\n }\n\n /**\n * Saves the spreadsheet name change.\n * @param {Object} detail\n * @returns {Promise}\n */\n async _onSpreadSheetNameChanged(detail) {\n const { name } = detail;\n this.state.spreadsheetName = name;\n this.env.config.setDisplayName(this.state.spreadsheetName);\n return await this.orm.write(\"documents.document\", [this.resId], { name });\n }\n}\n\nSpreadsheetAction.template = \"documents_spreadsheet.SpreadsheetAction\";\nSpreadsheetAction.components = {\n SpreadsheetComponent,\n DocumentsSpreadsheetControlPanel,\n SpreadsheetName,\n};\n\nregistry.category(\"actions\").add(\"action_open_spreadsheet\", SpreadsheetAction, { force: true });\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"web.core\";\n\nimport SpreadsheetComponent from \"@spreadsheet_edition/bundle/actions/spreadsheet_component\";\nimport { base64ToJson, jsonToBase64 } from \"@spreadsheet_edition/bundle/helpers\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AbstractSpreadsheetAction } from \"@spreadsheet_edition/bundle/actions/abstract_spreadsheet_action\";\nimport { DocumentsSpreadsheetControlPanel } from \"@documents_spreadsheet/bundle/components/control_panel/spreadsheet_control_panel\";\n\nexport class SpreadsheetTemplateAction extends AbstractSpreadsheetAction {\n setup() {\n super.setup();\n this.notificationMessage = this.env._t(\"New spreadsheet template created\");\n this.orm = useService(\"orm\");\n }\n\n _initializeWith(record) {\n this.spreadsheetData = base64ToJson(record.data);\n this.state.spreadsheetName = record.name;\n this.isReadonly = record.isReadonly;\n }\n\n /**\n * Fetch all the necessary data to open a spreadsheet template\n * @returns {Object}\n */\n async _fetchData() {\n return this.orm.call(\"spreadsheet.template\", \"fetch_template_data\", [this.resId]);\n }\n\n /**\n * Create a new empty spreadsheet template\n * @returns {number} id of the newly created spreadsheet template\n */\n async _onNewSpreadsheet() {\n const data = {\n name: _t(\"Untitled spreadsheet template\"),\n data: btoa(\"{}\"),\n };\n const id = await this.orm.create(\"spreadsheet.template\", [data]);\n this._openSpreadsheet(id);\n return id;\n }\n\n /**\n * Save the data and thumbnail on the given template\n * @param {number} spreadsheetTemplateId\n * @param {Object} values values to save\n * @param {Object} values.data exported spreadsheet data\n * @param {string} values.thumbnail spreadsheet thumbnail\n */\n async _onSpreadsheetSaved({ data, thumbnail }) {\n await this.orm.write(\"spreadsheet.template\", [this.resId], {\n data: jsonToBase64(data),\n thumbnail,\n });\n }\n\n /**\n * Save a new name for the given template\n * @param {Object} detail\n * @param {string} detail.name\n */\n async _onSpreadSheetNameChanged(detail) {\n const { name } = detail;\n this.state.spreadsheetName = name;\n this.env.config.setDisplayName(this.state.spreadsheetName);\n await this.orm.write(\"spreadsheet.template\", [this.resId], {\n name,\n });\n }\n\n async _onMakeCopy({ data, thumbnail }) {\n const defaultValues = {\n data: jsonToBase64(data),\n thumbnail,\n };\n const id = await this.orm.call(\"spreadsheet.template\", \"copy\", [this.resId], {\n default: defaultValues,\n });\n this._openSpreadsheet(id);\n }\n}\n\nSpreadsheetTemplateAction.template = \"documents_spreadsheet.SpreadsheetTemplateAction\";\nSpreadsheetTemplateAction.components = {\n SpreadsheetComponent,\n DocumentsSpreadsheetControlPanel,\n};\n\nregistry\n .category(\"actions\")\n .add(\"action_open_template\", SpreadsheetTemplateAction, { force: true });\n", "/** @odoo-module **/\n\nimport { SpreadsheetControlPanel } from \"@spreadsheet_edition/bundle/actions/control_panel/spreadsheet_control_panel\";\n\n\nexport class DocumentsSpreadsheetControlPanel extends SpreadsheetControlPanel {}\n\nDocumentsSpreadsheetControlPanel.template =\n \"documents_spreadsheet.DocumentsSpreadsheetControlPanel\";\nDocumentsSpreadsheetControlPanel.components = {\n ...SpreadsheetControlPanel.components,\n};\nDocumentsSpreadsheetControlPanel.props = {\n ...SpreadsheetControlPanel.props,\n isFavorited: {\n type: Boolean,\n optional: true,\n },\n onFavoriteToggled: {\n type: Function,\n optional: true,\n },\n};\n", "/** @odoo-module */\n\nimport { jsonToBase64 } from \"@spreadsheet_edition/bundle/helpers\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport SpreadsheetComponent from \"@spreadsheet_edition/bundle/actions/spreadsheet_component\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nconst { Model } = spreadsheet;\n\nconst { useSubEnv } = owl;\n\npatch(SpreadsheetComponent.prototype, \"documents_spreadsheet.SpreadsheetComponent\", {\n setup() {\n this._super();\n useSubEnv({\n saveAsTemplate: this._saveAsTemplate.bind(this),\n });\n },\n\n /**\n * @private\n * @returns {Promise}\n */\n async _saveAsTemplate() {\n const model = new Model(this.model.exportData(), {\n custom: {\n env: this.env,\n dataSources: this.model.config.custom.dataSources,\n },\n });\n await model.config.custom.dataSources.waitForAllLoaded();\n const proms = [];\n for (const pivotId of model.getters.getPivotIds()) {\n proms.push(model.getters.getPivotDataSource(pivotId).prepareForTemplateGeneration());\n }\n await Promise.all(proms);\n model.dispatch(\"CONVERT_PIVOT_TO_TEMPLATE\");\n const data = model.exportData();\n const name = this.props.name;\n this.trigger(\"do-action\", {\n action: \"documents_spreadsheet.save_spreadsheet_template_action\",\n options: {\n additional_context: {\n default_template_name: sprintf(_t(\"%s - Template\"), name),\n default_data: jsonToBase64(data),\n default_thumbnail: this.getThumbnail(),\n },\n },\n });\n },\n});\n", "/** @odoo-module */\n\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { DataSources } from \"@spreadsheet/data_sources/data_sources\";\nimport { migrate } from \"@spreadsheet/o_spreadsheet/migration\";\n\nconst Model = spreadsheet.Model;\n\n/**\n * Convert PIVOT functions from relative to absolute.\n *\n * @param {object} orm\n * @param {object} data\n * @returns {Promise} spreadsheetData\n */\nexport async function convertFromSpreadsheetTemplate(orm, data) {\n const model = new Model(migrate(data), {\n custom: { dataSources: new DataSources(orm) },\n });\n await model.config.custom.dataSources.waitForAllLoaded();\n const proms = [];\n for (const pivotId of model.getters.getPivotIds()) {\n proms.push(model.getters.getPivotDataSource(pivotId).prepareForTemplateGeneration());\n }\n await Promise.all(proms);\n model.dispatch(\"CONVERT_PIVOT_FROM_TEMPLATE\");\n return model.exportData();\n}\n", "/** @odoo-module */\n\nimport PivotDataSource from \"@spreadsheet/pivot/pivot_data_source\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(PivotDataSource.prototype, \"documents_spreadsheet_templates_data_source\", {\n /**\n * @param {string} fieldName\n */\n getPossibleValuesForGroupBy(fieldName) {\n this._assertDataIsLoaded();\n return this._model.getPossibleValuesForGroupBy(fieldName);\n },\n});\n", "/** @odoo-module */\n\nimport { SpreadsheetPivotModel } from \"@spreadsheet/pivot/pivot_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { Domain } from \"@web/core/domain\";\n\npatch(SpreadsheetPivotModel.prototype, \"documents_spreadsheet_templates_pivot_model\", {\n setup() {\n this._super.apply(this, arguments);\n /**\n * Contains the possible values for each group by of the pivot. This attribute is used *only* for templates,\n * so it's computed only in prepareForTemplateGeneration\n */\n this._fieldsValue = {};\n },\n\n /**\n * Get the possible values for the given groupBy\n * @param {string} groupBy\n * @returns {any[]}\n */\n getPossibleValuesForGroupBy(groupBy) {\n return this._fieldsValue[groupBy] || [];\n },\n\n /**\n * This method is used to compute the possible values for each group bys.\n * It should be run before using templates\n */\n async prepareForTemplateGeneration() {\n const colValues = [];\n const rowValues = [];\n\n function collectValues(tree, collector) {\n const group = tree.root;\n if (!tree.directSubTrees.size) {\n //It's a leaf, we can fill the cols\n collector.push([...group.values]);\n }\n [...tree.directSubTrees.values()].forEach((subTree) => {\n collectValues(subTree, collector);\n });\n }\n\n collectValues(this.data.colGroupTree, colValues);\n collectValues(this.data.rowGroupTree, rowValues);\n\n for (let i = 0; i < this.metaData.fullRowGroupBys.length; i++) {\n let vals = [\n ...new Set(rowValues.map((array) => array[i]).filter((val) => val !== undefined)),\n ];\n if (i !== 0) {\n vals = await this._orderValues(vals, this.metaData.fullRowGroupBys[i]);\n }\n this._fieldsValue[this.metaData.fullRowGroupBys[i]] = vals;\n }\n for (let i = 0; i < this.metaData.fullColGroupBys.length; i++) {\n let vals = [];\n if (i !== 0) {\n vals = await this._orderValues(vals, this.metaData.fullColGroupBys[i]);\n } else {\n vals = colValues.map((array) => array[i]).filter((val) => val !== undefined);\n vals = [...new Set(vals)];\n }\n this._fieldsValue[this.metaData.fullColGroupBys[i]] = vals;\n }\n },\n\n /**\n * Order the given values for the given groupBy. This is done by executing a\n * search_read\n */\n async _orderValues(values, groupBy) {\n const field = this.parseGroupField(groupBy).field;\n const model = this.metaData.resModel;\n const context = this.searchParams.context;\n const baseDomain = this.searchParams.domain;\n const requestField = field.relation ? \"id\" : field.name;\n const domain = Domain.and([\n field.relation ? [] : baseDomain,\n [[requestField, \"in\", values]],\n ]).toList();\n // orderby is omitted for relational fields on purpose to have the default order of the model\n const records = await this.orm.searchRead(\n field.relation ? field.relation : model,\n domain,\n [requestField],\n {\n order: field.relation ? undefined : `${field.name} ASC`,\n context: { ...context, active_test: false },\n }\n );\n return [...new Set(records.map((record) => record[requestField].toString()))];\n },\n});\n", "odoo.define(\"documents_spreadsheet.PivotTemplatePlugin\", function (require) {\n (\"use strict\");\n\n const spreadsheet = require(\"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\")[\n Symbol.for(\"default\")\n ];\n const CommandResult = require(\"@spreadsheet/o_spreadsheet/cancelled_reason\")[\n Symbol.for(\"default\")\n ];\n const { pivotFormulaRegex } = require(\"@spreadsheet/pivot/pivot_helpers\");\n const { parse, astToFormula } = spreadsheet;\n const { featurePluginRegistry } = spreadsheet.registries;\n\n /**\n * @typedef {Object} Range\n */\n\n class PivotTemplatePlugin extends spreadsheet.UIPlugin {\n allowDispatch(cmd) {\n switch (cmd.type) {\n case \"CONVERT_PIVOT_TO_TEMPLATE\":\n case \"CONVERT_PIVOT_FROM_TEMPLATE\": {\n for (const pivotId of this.getters.getPivotIds()) {\n if (!this.getters.getPivotDataSource(pivotId).isReady()) {\n return CommandResult.PivotCacheNotLoaded;\n }\n }\n break;\n }\n }\n return CommandResult.Success;\n }\n\n /**\n * Handle a spreadsheet command\n *\n * @param {Object} cmd Command\n */\n handle(cmd) {\n switch (cmd.type) {\n case \"CONVERT_PIVOT_TO_TEMPLATE\":\n this._convertFormulas(\n this._getCells(pivotFormulaRegex),\n this._absoluteToRelative.bind(this),\n this.getters.getPivotIds().map(this.getters.getPivotDefinition)\n );\n break;\n case \"CONVERT_PIVOT_FROM_TEMPLATE\":\n this._convertFormulas(\n this._getCells(pivotFormulaRegex),\n this._relativeToAbsolute.bind(this),\n this.getters.getPivotIds().map(this.getters.getPivotDefinition)\n );\n this._removeInvalidPivotRows();\n break;\n }\n }\n\n /**\n * Applies a transformation function to all given formula cells.\n * The transformation function takes as fist parameter the cell AST and should\n * return a modified AST.\n * Any additional parameter is forwarded to the transformation function.\n *\n * @param {Array} cells\n * @param {Function} convertFunction\n * @param {...any} args\n */\n _convertFormulas(cells, convertFunction, ...args) {\n cells.forEach((cell) => {\n if (cell.isFormula) {\n const { col, row, sheetId } = this.getters.getCellPosition(cell.id);\n const ast = convertFunction(parse(cell.content), ...args);\n if (ast) {\n const content = `=${astToFormula(ast)}`;\n this.dispatch(\"UPDATE_CELL\", {\n content,\n sheetId,\n col,\n row,\n });\n } else {\n this.dispatch(\"CLEAR_CELL\", {\n sheetId,\n col,\n row,\n });\n }\n }\n });\n }\n\n /**\n * Return all formula cells matching a given regular expression.\n *\n * @param {RegExp} regex\n * @returns {Array}\n */\n _getCells(regex) {\n return this.getters\n .getSheetIds()\n .map((sheetId) =>\n Object.values(this.getters.getCells(sheetId)).filter(\n (cell) =>\n cell.isFormula &&\n regex.test(this.getters.getFormulaCellContent(sheetId, cell))\n )\n )\n .flat();\n }\n\n /**\n * return AST from an relative PIVOT ast to a absolute PIVOT ast (sheet -> template)\n * *\n * relative PIVOTS formulas use the position while Absolute PIVOT\n * formulas use hardcoded ids\n *\n * e.g.\n * The following relative formula\n * `PIVOT(\"1\",\"probability\",\"product_id\",PIVOT.POSITION(\"1\",\"product_id\",0),\"bar\",\"110\")`\n * is converted to\n * `PIVOT(\"1\",\"probability\",\"product_id\",\"37\",\"bar\",\"110\")`\n *\n * @param {Object} ast\n * @returns {Object}\n */\n _relativeToAbsolute(ast) {\n switch (ast.type) {\n case \"FUNCALL\":\n switch (ast.value) {\n case \"ODOO.PIVOT.POSITION\":\n return this._pivotPositionToAbsolute(ast);\n default:\n return Object.assign({}, ast, {\n args: ast.args.map((child) => this._relativeToAbsolute(child)),\n });\n }\n case \"UNARY_OPERATION\":\n return Object.assign({}, ast, {\n operand: this._relativeToAbsolute(ast.operand),\n });\n case \"BIN_OPERATION\":\n return Object.assign({}, ast, {\n right: this._relativeToAbsolute(ast.right),\n left: this._relativeToAbsolute(ast.left),\n });\n }\n return ast;\n }\n\n /**\n * return AST from an absolute PIVOT ast to a relative ast.\n *\n * Absolute PIVOT formulas use hardcoded ids while relative PIVOTS\n * formulas use the position\n *\n * e.g.\n * The following absolute formula\n * `PIVOT(\"1\",\"probability\",\"product_id\",\"37\",\"bar\",\"110\")`\n * is converted to\n * `PIVOT(\"1\",\"probability\",\"product_id\",PIVOT.POSITION(\"1\",\"product_id\",0),\"bar\",\"110\")`\n *\n * @param {Object} ast\n * @returns {Object}\n */\n _absoluteToRelative(ast) {\n switch (ast.type) {\n case \"FUNCALL\":\n switch (ast.value) {\n case \"ODOO.PIVOT\":\n return this._pivot_absoluteToRelative(ast);\n case \"ODOO.PIVOT.HEADER\":\n return this._pivotHeader_absoluteToRelative(ast);\n default:\n return Object.assign({}, ast, {\n args: ast.args.map((child) => this._absoluteToRelative(child)),\n });\n }\n case \"UNARY_OPERATION\":\n return Object.assign({}, ast, {\n operand: this._absoluteToRelative(ast.operand),\n });\n case \"BIN_OPERATION\":\n return Object.assign({}, ast, {\n right: this._absoluteToRelative(ast.right),\n left: this._absoluteToRelative(ast.left),\n });\n }\n return ast;\n }\n\n /**\n * Convert a PIVOT.POSITION function AST to an absolute AST\n *\n * @see _relativeToAbsolute\n * @param {Object} ast\n * @returns {Object}\n */\n _pivotPositionToAbsolute(ast) {\n const [pivotIdAst, fieldAst, positionAst] = ast.args;\n const pivotId = pivotIdAst.value;\n const fieldName = fieldAst.value;\n const position = positionAst.value;\n const values = this.getters.getPivotGroupByValues(pivotId, fieldName);\n const id = values[position - 1];\n return {\n value: id ? `${id}` : `\"#IDNOTFOUND\"`,\n type: id ? \"STRING\" : \"UNKNOWN\",\n };\n }\n /**\n * Convert an absolute PIVOT.HEADER function AST to a relative AST\n *\n * @see _absoluteToRelative\n * @param {Object} ast\n * @returns {Object}\n */\n _pivotHeader_absoluteToRelative(ast) {\n ast = Object.assign({}, ast);\n const [pivotIdAst, ...domainAsts] = ast.args;\n if (pivotIdAst.type !== \"STRING\" && pivotIdAst.type !== \"NUMBER\") {\n return ast;\n }\n ast.args = [pivotIdAst, ...this._domainToRelative(pivotIdAst, domainAsts)];\n return ast;\n }\n /**\n * Convert an absolute PIVOT function AST to a relative AST\n *\n * @see _absoluteToRelative\n * @param {Object} ast\n * @returns {Object}\n */\n _pivot_absoluteToRelative(ast) {\n ast = Object.assign({}, ast);\n const [pivotIdAst, measureAst, ...domainAsts] = ast.args;\n if (pivotIdAst.type !== \"STRING\" && pivotIdAst.type !== \"NUMBER\") {\n return ast;\n }\n ast.args = [pivotIdAst, measureAst, ...this._domainToRelative(pivotIdAst, domainAsts)];\n return ast;\n }\n\n /**\n * Convert a pivot domain with hardcoded ids to a relative\n * domain with positions instead. Each domain element is\n * represented as an AST.\n *\n * e.g. (ignoring AST representation for simplicity)\n * The following domain\n * \"product_id\", \"37\", \"stage_id\", \"4\"\n * is converted to\n * \"product_id\", PIVOT.POSITION(\"#pivotId\", \"product_id\", 15), \"stage_id\", PIVOT.POSITION(\"#pivotId\", \"stage_id\", 3)\n *\n * @param {Object} pivotIdAst\n * @param {Object} domainAsts\n * @returns {Array}\n */\n _domainToRelative(pivotIdAst, domainAsts) {\n let relativeDomain = [];\n for (let i = 0; i <= domainAsts.length - 1; i += 2) {\n const fieldAst = domainAsts[i];\n const valueAst = domainAsts[i + 1];\n const pivotId = pivotIdAst.value;\n const fieldName = fieldAst.value;\n if (\n this._isAbsolute(pivotId, fieldName) &&\n fieldAst.type === \"STRING\" &&\n [\"STRING\", \"NUMBER\"].includes(valueAst.type)\n ) {\n const id = valueAst.value;\n const values = this.getters.getPivotGroupByValues(pivotId, fieldName);\n const index = values.map((val) => val.toString()).indexOf(id.toString());\n relativeDomain = relativeDomain.concat([\n fieldAst,\n {\n type: \"FUNCALL\",\n value: \"ODOO.PIVOT.POSITION\",\n args: [pivotIdAst, fieldAst, { type: \"NUMBER\", value: index + 1 }],\n },\n ]);\n } else {\n relativeDomain = relativeDomain.concat([fieldAst, valueAst]);\n }\n }\n return relativeDomain;\n }\n\n _isAbsolute(pivotId, fieldName) {\n const field = this.getters\n .getPivotDataSource(pivotId)\n .getField(fieldName.split(\":\")[0]);\n return field && field.type === \"many2one\";\n }\n\n /**\n * Remove pivot formulas with invalid ids.\n * i.e. pivot formulas containing \"#IDNOTFOUND\".\n *\n * Rows where all pivot formulas are invalid are removed, even\n * if there are others non-empty cells.\n * Invalid formulas next to valid ones (in the same row) are simply removed.\n */\n _removeInvalidPivotRows() {\n for (const sheetId of this.getters.getSheetIds()) {\n const invalidRows = [];\n\n for (let rowIndex = 0; rowIndex < this.getters.getNumberRows(sheetId); rowIndex++) {\n const cellIds = Object.values(this.getters.getRowCells(sheetId, rowIndex));\n const [valid, invalid] = cellIds\n .map((id) => this.getters.getCellById(id))\n .filter((cell) => cell.isFormula && /^\\s*=.*PIVOT/.test(cell.content))\n .reduce(\n ([valid, invalid], cell) => {\n const isInvalid = /^\\s*=.*PIVOT(\\.HEADER)?.*#IDNOTFOUND/.test(\n cell.content\n );\n return [\n isInvalid ? valid : valid + 1,\n isInvalid ? invalid + 1 : invalid,\n ];\n },\n [0, 0]\n );\n if (invalid > 0 && valid === 0) {\n invalidRows.push(rowIndex);\n }\n }\n this.dispatch(\"REMOVE_COLUMNS_ROWS\", {\n dimension: \"ROW\",\n elements: invalidRows,\n sheetId,\n });\n }\n this._convertFormulas(this._getCells(/^\\s*=.*PIVOT.*#IDNOTFOUND/), () => null);\n }\n }\n\n PivotTemplatePlugin.getters = [];\n\n featurePluginRegistry.add(\"PivotTemplate\", PivotTemplatePlugin);\n\n return PivotTemplatePlugin;\n});\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { DashboardLoader, Status } from \"./dashboard_loader\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\nimport { useSetupAction } from \"@web/webclient/actions/action_hook\";\nimport { DashboardMobileSearchPanel } from \"./mobile_search_panel/mobile_search_panel\";\nimport { MobileFigureContainer } from \"./mobile_figure_container/mobile_figure_container\";\nimport { FilterValue } from \"@spreadsheet/global_filters/components/filter_value/filter_value\";\nimport { loadSpreadsheetDependencies } from \"@spreadsheet/helpers/helpers\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst { Spreadsheet } = spreadsheet;\nconst { Component, onWillStart, useState, useEffect } = owl;\n\nexport class SpreadsheetDashboardAction extends Component {\n setup() {\n this.Status = Status;\n this.controlPanelDisplay = {\n \"top-left\": true,\n \"top-right\": true,\n \"bottom-left\": false,\n \"bottom-right\": false,\n };\n this.orm = useService(\"orm\");\n this.router = useService(\"router\");\n // Use the non-protected orm service (`this.env.services.orm` instead of `useService(\"orm\")`)\n // because spreadsheets models are preserved across multiple components when navigating\n // with the breadcrumb\n // TODO write a test\n /** @type {DashboardLoader}*/\n this.loader = useState(\n new DashboardLoader(this.env, this.env.services.orm, this._fetchDashboardData)\n );\n onWillStart(async () => {\n await loadSpreadsheetDependencies();\n if (this.props.state && this.props.state.dashboardLoader) {\n const { groups, dashboards } = this.props.state.dashboardLoader;\n this.loader.restoreFromState(groups, dashboards);\n } else {\n await this.loader.load();\n }\n const activeDashboardId = this.getInitialActiveDashboard();\n if (activeDashboardId) {\n this.openDashboard(activeDashboardId);\n }\n });\n useEffect(\n () => this.router.pushState({ dashboard_id: this.activeDashboardId }),\n () => [this.activeDashboardId]\n );\n useEffect(\n () => {\n const dashboard = this.state.activeDashboard;\n if (dashboard && dashboard.status === Status.Loaded) {\n const render = () => this.render(true);\n dashboard.model.on(\"update\", this, render);\n return () => dashboard.model.off(\"update\", this, render);\n }\n },\n () => {\n const dashboard = this.state.activeDashboard;\n return [dashboard && dashboard.model, dashboard && dashboard.status];\n }\n );\n useSetupAction({\n getLocalState: () => {\n return {\n activeDashboardId: this.activeDashboardId,\n dashboardLoader: this.loader.getState(),\n };\n },\n });\n /** @type {{ activeDashboard: import(\"./dashboard_loader\").Dashboard}} */\n this.state = useState({ activeDashboard: undefined });\n }\n\n /**\n * @returns {number | undefined}\n */\n get activeDashboardId() {\n return this.state.activeDashboard ? this.state.activeDashboard.id : undefined;\n }\n\n /**\n * @returns {object[]}\n */\n get filters() {\n const dashboard = this.state.activeDashboard;\n if (!dashboard || dashboard.status !== Status.Loaded) {\n return [];\n }\n return dashboard.model.getters.getGlobalFilters();\n }\n\n /**\n * @private\n * @returns {number | undefined}\n */\n getInitialActiveDashboard() {\n if (this.props.state && this.props.state.activeDashboardId) {\n return this.props.state.activeDashboardId;\n }\n const params = this.props.action.params || this.props.action.context.params;\n if (params && params.dashboard_id) {\n return params.dashboard_id;\n }\n const [firstSection] = this.getDashboardGroups();\n if (firstSection && firstSection.dashboards.length) {\n return firstSection.dashboards[0].id;\n }\n }\n\n getDashboardGroups() {\n return this.loader.getDashboardGroups();\n }\n\n /**\n * @param {number} dashboardId\n */\n openDashboard(dashboardId) {\n this.state.activeDashboard = this.loader.getDashboard(dashboardId);\n }\n\n /**\n * @private\n * @param {number} dashboardId\n * @returns {Promise<{ data: string, revisions: object[] }>}\n */\n async _fetchDashboardData(dashboardId) {\n const [record] = await this.orm.read(\"spreadsheet.dashboard\", [dashboardId], [\"raw\"]);\n return { data: JSON.parse(record.raw), revisions: [] };\n }\n}\nSpreadsheetDashboardAction.template = \"spreadsheet_dashboard.DashboardAction\";\nSpreadsheetDashboardAction.components = {\n ControlPanel,\n Spreadsheet,\n FilterValue,\n DashboardMobileSearchPanel,\n MobileFigureContainer,\n};\n\nregistry\n .category(\"actions\")\n .add(\"action_spreadsheet_dashboard\", SpreadsheetDashboardAction, { force: true });\n", "/** @odoo-module */\n\nimport { DataSources } from \"@spreadsheet/data_sources/data_sources\";\nimport { migrate } from \"@spreadsheet/o_spreadsheet/migration\";\nimport spreadsheet from \"@spreadsheet/o_spreadsheet/o_spreadsheet_extended\";\n\nconst { Model } = spreadsheet;\n\n/**\n * @type {{\n * NotLoaded: \"NotLoaded\",\n * Loading: \"Loading\",\n * Loaded: \"Loaded\",\n * Error: \"Error\",\n * }}\n */\nexport const Status = {\n NotLoaded: \"NotLoaded\",\n Loading: \"Loading\",\n Loaded: \"Loaded\",\n Error: \"Error\",\n};\n\n/**\n * @typedef Dashboard\n * @property {number} id\n * @property {string} displayName\n * @property {string} status\n * @property {Model} [model]\n * @property {Error} [error]\n *\n * @typedef DashboardGroupData\n * @property {number} id\n * @property {string} name\n * @property {Array} dashboardIds\n *\n * @typedef DashboardGroup\n * @property {number} id\n * @property {string} name\n * @property {Array} dashboards\n *\n * @typedef {(dashboardId: number) => Promise<{ data: string, revisions: object[] }>} FetchDashboardData\n *\n * @typedef {import(\"@web/env\").OdooEnv} OdooEnv\n *\n * @typedef {import(\"@web/core/orm_service\").ORM} ORM\n */\n\nexport class DashboardLoader {\n /**\n * @param {OdooEnv} env\n * @param {ORM} orm\n * @param {FetchDashboardData} fetchDashboardData\n */\n constructor(env, orm, fetchDashboardData) {\n /** @private */\n this.env = env;\n /** @private */\n this.orm = orm;\n /** @private @type {Array} */\n this.groups = [];\n /** @private @type {Object