From 6daf0c35591f0bd29c73bf4eb679f2b9ab289db5 Mon Sep 17 00:00:00 2001 From: Florian Damhaut Date: Wed, 11 Dec 2024 16:43:18 +0100 Subject: [PATCH] [IMP] figures: add anchor to figures --- demo/data.js | 45 ++++--- src/actions/menu_items_actions.ts | 19 ++- src/clipboard_handlers/chart_clipboard.ts | 15 +-- src/clipboard_handlers/image_clipboard.ts | 18 +-- .../figures/chart/chartJs/chartjs.ts | 8 +- .../chart/gauge/gauge_chart_component.ts | 10 +- .../chart/scorecard/chart_scorecard.ts | 13 +- src/components/figures/figure/figure.ts | 32 +++-- src/components/figures/figure/figure.xml | 8 +- .../figures/figure_chart/figure_chart.ts | 10 +- .../figures/figure_chart/figure_chart.xml | 4 +- .../figure_container/figure_container.ts | 124 ++++++++++-------- .../figure_container/figure_container.xml | 10 +- .../figures/figure_image/figure_image.ts | 8 +- .../helpers/figure_container_helper.ts | 40 ------ src/components/helpers/figure_drag_helper.ts | 45 +++---- src/components/helpers/figure_snap_helper.ts | 68 +++++----- src/helpers/figures/figure/figure.ts | 25 +++- .../repeat_commands_specific.ts | 2 +- src/plugins/core/chart.ts | 25 +++- src/plugins/core/figures.ts | 107 ++++++++++----- src/plugins/core/image.ts | 39 ++++-- src/plugins/ui_stateful/sheetview.ts | 67 +++++++--- src/types/commands.ts | 20 +-- src/types/figure.ts | 16 ++- src/types/workbook_data.ts | 18 ++- src/xlsx/conversion/figure_conversion.ts | 30 ++++- src/xlsx/functions/drawings.ts | 75 ++++++----- 28 files changed, 524 insertions(+), 377 deletions(-) delete mode 100644 src/components/helpers/figure_container_helper.ts diff --git a/demo/data.js b/demo/data.js index 86dcb94c4d..e64f1d2755 100644 --- a/demo/data.js +++ b/demo/data.js @@ -187,8 +187,9 @@ export const demoData = { tag: "chart", width: 400, height: 300, - x: 100, - y: 100, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, data: { type: "line", dataSetsHaveTitle: true, @@ -205,8 +206,9 @@ export const demoData = { tag: "chart", width: 400, height: 300, - x: 600, - y: 100, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, data: { type: "bar", dataSetsHaveTitle: false, @@ -223,8 +225,9 @@ export const demoData = { tag: "chart", width: 900, height: 400, - x: 100, - y: 420, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, data: { type: "pie", dataSetsHaveTitle: true, @@ -237,8 +240,9 @@ export const demoData = { }, { id: "4", - x: 1015, - y: 102, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, height: 296, width: 465, tag: "chart", @@ -255,8 +259,9 @@ export const demoData = { }, { id: "5", - x: 1015, - y: 420, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, height: 400, width: 465, tag: "chart", @@ -279,8 +284,9 @@ export const demoData = { tag: "chart", width: 500, height: 300, - x: 100, - y: 850, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, data: { type: "scatter", dataSetsHaveTitle: true, @@ -293,8 +299,9 @@ export const demoData = { }, { id: "7", - x: 619, - y: 850, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, width: 500, height: 300, tag: "chart", @@ -311,8 +318,9 @@ export const demoData = { }, { id: "8", - x: 1138, - y: 850, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, width: 500, height: 300, tag: "chart", @@ -332,8 +340,9 @@ export const demoData = { }, { id: "9", - x: 100, - y: 1175, + anchor: { col: 0, row: 0 }, + offset: { x: 0, y: 0 }, + fixed_position: false, height: 300, width: 500, tag: "chart", diff --git a/src/actions/menu_items_actions.ts b/src/actions/menu_items_actions.ts index b7a9755601..1b8b107df8 100644 --- a/src/actions/menu_items_actions.ts +++ b/src/actions/menu_items_actions.ts @@ -1,10 +1,7 @@ import { CellPopoverStore } from "../components/popover"; import { DEFAULT_FIGURE_HEIGHT, DEFAULT_FIGURE_WIDTH } from "../constants"; import { parseOSClipboardContent } from "../helpers/clipboard/clipboard_helpers"; -import { - getChartPositionAtCenterOfViewport, - getSmartChartDefinition, -} from "../helpers/figures/charts"; +import { getSmartChartDefinition } from "../helpers/figures/charts"; import { centerFigurePosition, getMaxFigureSize } from "../helpers/figures/figure/figure"; import { areZonesContinuous, @@ -389,12 +386,13 @@ export const CREATE_CHART = (env: SpreadsheetChildEnv) => { } const size = { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT }; - const position = getChartPositionAtCenterOfViewport(getters, size); + const { anchor, offset } = centerFigurePosition(getters, size); const result = env.model.dispatch("CREATE_CHART", { sheetId, id, - position, + anchor, + offset, size, definition: getSmartChartDefinition(env.model.getters.getSelectedZone(), env.model.getters), }); @@ -474,17 +472,18 @@ async function requestImage(env: SpreadsheetChildEnv): Promise { if (env.imageProvider) { const sheetId = env.model.getters.getActiveSheetId(); - const figureId = env.model.uuidGenerator.uuidv4(); + const id = env.model.uuidGenerator.uuidv4(); const image = await requestImage(env); if (!image) { throw new Error("No image provider was given to the environment"); } const size = getMaxFigureSize(env.model.getters, image.size); - const position = centerFigurePosition(env.model.getters, size); + const { anchor, offset } = centerFigurePosition(env.model.getters, size); env.model.dispatch("CREATE_IMAGE", { sheetId, - figureId, - position, + id, + anchor, + offset, size, definition: image, }); diff --git a/src/clipboard_handlers/chart_clipboard.ts b/src/clipboard_handlers/chart_clipboard.ts index 888ec16af2..5232df24c1 100644 --- a/src/clipboard_handlers/chart_clipboard.ts +++ b/src/clipboard_handlers/chart_clipboard.ts @@ -56,22 +56,15 @@ export class ChartClipboardHandler extends AbstractFigureClipboardHandler { static template = "o-spreadsheet-ChartJsComponent"; static props = { - figure: Object, + figureUI: Object, }; private canvas = useRef("graphContainer"); @@ -32,7 +32,7 @@ export class ChartJsComponent extends Component { } get chartRuntime(): ChartJSRuntime { - const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id); + const runtime = this.env.model.getters.getChartRuntime(this.props.figureUI.figure.id); if (!("chartJsConfig" in runtime)) { throw new Error("Unsupported chart runtime"); } diff --git a/src/components/figures/chart/gauge/gauge_chart_component.ts b/src/components/figures/chart/gauge/gauge_chart_component.ts index 6a3b434594..071aaa1c47 100644 --- a/src/components/figures/chart/gauge/gauge_chart_component.ts +++ b/src/components/figures/chart/gauge/gauge_chart_component.ts @@ -1,10 +1,10 @@ import { Component, useEffect, useRef } from "@odoo/owl"; import { drawGaugeChart } from "../../../../helpers/figures/charts/gauge_chart_rendering"; -import { Figure, SpreadsheetChildEnv } from "../../../../types"; +import { FigureUI, SpreadsheetChildEnv } from "../../../../types"; import { GaugeChartRuntime } from "../../../../types/chart"; interface Props { - figure: Figure; + figureUI: FigureUI; } export class GaugeChartComponent extends Component { @@ -12,7 +12,9 @@ export class GaugeChartComponent extends Component { private canvas = useRef("chartContainer"); get runtime(): GaugeChartRuntime { - return this.env.model.getters.getChartRuntime(this.props.figure.id) as GaugeChartRuntime; + return this.env.model.getters.getChartRuntime( + this.props.figureUI.figure.id + ) as GaugeChartRuntime; } setup() { @@ -28,5 +30,5 @@ export class GaugeChartComponent extends Component { } GaugeChartComponent.props = { - figure: Object, + figureUI: Object, }; diff --git a/src/components/figures/chart/scorecard/chart_scorecard.ts b/src/components/figures/chart/scorecard/chart_scorecard.ts index dfcf1de74f..afd811f779 100644 --- a/src/components/figures/chart/scorecard/chart_scorecard.ts +++ b/src/components/figures/chart/scorecard/chart_scorecard.ts @@ -2,26 +2,29 @@ import { Component, useEffect, useRef } from "@odoo/owl"; import { drawScoreChart } from "../../../../helpers/figures/charts/scorecard_chart"; import { getScorecardConfiguration } from "../../../../helpers/figures/charts/scorecard_chart_config_builder"; import { _t } from "../../../../translation"; -import { Figure, SpreadsheetChildEnv } from "../../../../types"; +import { FigureUI, SpreadsheetChildEnv } from "../../../../types"; import { ScorecardChartRuntime } from "../../../../types/chart/scorecard_chart"; interface Props { - figure: Figure; + figureUI: FigureUI; } export class ScorecardChart extends Component { static template = "o-spreadsheet-ScorecardChart"; static props = { - figure: Object, + figureUI: Object, }; private canvas = useRef("chartContainer"); get runtime(): ScorecardChartRuntime { - return this.env.model.getters.getChartRuntime(this.props.figure.id) as ScorecardChartRuntime; + return this.env.model.getters.getChartRuntime( + this.props.figureUI.figure.id + ) as ScorecardChartRuntime; } get title(): string { - const title = this.env.model.getters.getChartDefinition(this.props.figure.id).title.text ?? ""; + const title = + this.env.model.getters.getChartDefinition(this.props.figureUI.figure.id).title.text ?? ""; // chart titles are extracted from .json files and they are translated at runtime here return _t(title); } diff --git a/src/components/figures/figure/figure.ts b/src/components/figures/figure/figure.ts index 997f46e8ef..7bf4ff171c 100644 --- a/src/components/figures/figure/figure.ts +++ b/src/components/figures/figure/figure.ts @@ -9,7 +9,7 @@ import { figureRegistry } from "../../../registries/index"; import { CSSProperties, DOMCoordinates, - Figure, + FigureUI, Pixel, ResizeDirection, SpreadsheetChildEnv, @@ -113,7 +113,7 @@ css/*SCSS*/ ` `; interface Props { - figure: Figure; + figureUI: FigureUI; style: string; onFigureDeleted: () => void; onMouseDown: (ev: MouseEvent) => void; @@ -123,7 +123,7 @@ interface Props { export class FigureComponent extends Component { static template = "o-spreadsheet-FigureComponent"; static props = { - figure: Object, + figureUI: Object, style: { type: String, optional: true }, onFigureDeleted: { type: Function, optional: true }, onMouseDown: { type: Function, optional: true }, @@ -145,7 +145,7 @@ export class FigureComponent extends Component { private borderWidth!: number; get isSelected(): boolean { - return this.env.model.getters.getSelectedFigureId() === this.props.figure.id; + return this.env.model.getters.getSelectedFigureId() === this.props.figureUI.figure.id; } get figureRegistry() { @@ -164,7 +164,8 @@ export class FigureComponent extends Component { } get wrapperStyle() { - const { x, y, width, height } = this.props.figure; + const { x, y, figure } = this.props.figureUI; + const { width, height } = figure; return cssPropertiesToCss({ left: `${x}px`, top: `${y}px`, @@ -196,7 +197,7 @@ export class FigureComponent extends Component { } setup() { - const borderWidth = figureRegistry.get(this.props.figure.tag).borderWidth; + const borderWidth = figureRegistry.get(this.props.figureUI.figure.tag).borderWidth; this.borderWidth = borderWidth !== undefined ? borderWidth : BORDER_WIDTH; useEffect( (selectedFigureId: UID | null, thisFigureId: UID, el: HTMLElement | null) => { @@ -212,7 +213,11 @@ export class FigureComponent extends Component { el?.focus({ preventScroll: true }); } }, - () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el] + () => [ + this.env.model.getters.getSelectedFigureId(), + this.props.figureUI.figure.id, + this.figureRef.el, + ] ); onWillUnmount(() => { @@ -229,7 +234,7 @@ export class FigureComponent extends Component { } onKeyDown(ev: KeyboardEvent) { - const figure = this.props.figure; + const figure = this.props.figureUI.figure; const keyDownShortcut = keyboardEventToShortcutString(ev); switch (keyDownShortcut) { @@ -253,12 +258,15 @@ export class FigureComponent extends Component { ArrowRight: [1, 0], ArrowUp: [0, -1], }; + // TODO check cell change const delta = deltaMap[ev.key]; this.env.model.dispatch("UPDATE_FIGURE", { sheetId: this.env.model.getters.getActiveSheetId(), id: figure.id, - x: figure.x + delta[0], - y: figure.y + delta[1], + offset: { + x: figure.offset.x + delta[0], + y: figure.offset.y + delta[1], + }, }); ev.preventDefault(); ev.stopPropagation(); @@ -303,7 +311,7 @@ export class FigureComponent extends Component { this.menuState.isOpen = true; this.menuState.position = position; this.menuState.menuItems = figureRegistry - .get(this.props.figure.tag) - .menuBuilder(this.props.figure.id, this.props.onFigureDeleted, this.env); + .get(this.props.figureUI.figure.tag) + .menuBuilder(this.props.figureUI.figure.id, this.props.onFigureDeleted, this.env); } } diff --git a/src/components/figures/figure/figure.xml b/src/components/figures/figure/figure.xml index 48bb4e081d..168884c657 100644 --- a/src/components/figures/figure/figure.xml +++ b/src/components/figures/figure/figure.xml @@ -7,15 +7,15 @@ t-on-contextmenu.prevent.stop="(ev) => !env.model.getters.isReadonly() and this.onContextMenu(ev)" t-ref="figure" t-att-style="props.style" - t-att-data-id="props.figure.id" + t-att-data-id="props.figureUI.figure.id" tabindex="0" t-on-keydown="(ev) => this.onKeyDown(ev)" t-on-keyup.stop="">
void; } export class ChartFigure extends Component { static template = "o-spreadsheet-ChartFigure"; static props = { - figure: Object, + figureUI: Object, onFigureDeleted: Function, }; static components = {}; onDoubleClick() { - this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id }); + this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figureUI.figure.id }); this.env.openSidePanel("ChartPanel"); } get chartType(): ChartType { - return this.env.model.getters.getChartType(this.props.figure.id); + return this.env.model.getters.getChartType(this.props.figureUI.figure.id); } get chartComponent(): new (...args: any) => Component { diff --git a/src/components/figures/figure_chart/figure_chart.xml b/src/components/figures/figure_chart/figure_chart.xml index cedf0efd48..fb90a1f959 100644 --- a/src/components/figures/figure_chart/figure_chart.xml +++ b/src/components/figures/figure_chart/figure_chart.xml @@ -3,8 +3,8 @@
diff --git a/src/components/figures/figure_container/figure_container.ts b/src/components/figures/figure_container/figure_container.ts index 630b9c0b64..eb30bde8fd 100644 --- a/src/components/figures/figure_container/figure_container.ts +++ b/src/components/figures/figure_container/figure_container.ts @@ -3,13 +3,16 @@ import { ComponentsImportance, MIN_FIG_SIZE } from "../../../constants"; import { isDefined } from "../../../helpers"; import { rectIntersection, rectUnion } from "../../../helpers/rectangle"; import { figureRegistry } from "../../../registries"; -import { Figure, Rect, ResizeDirection, SpreadsheetChildEnv, UID } from "../../../types/index"; +import { + Figure, + FigureUI, + Rect, + ResizeDirection, + SpreadsheetChildEnv, + UID, +} from "../../../types/index"; import { css, cssPropertiesToCss } from "../../helpers"; import { startDnd } from "../../helpers/drag_and_drop"; -import { - internalFigureToScreen, - screenFigureToInternal, -} from "../../helpers/figure_container_helper"; import { dragFigureForMove, dragFigureForResize } from "../../helpers/figure_drag_helper"; import { HFigureAxisType, @@ -28,7 +31,7 @@ interface Props { interface Container { type: ContainerType; - figures: Figure[]; + figures: FigureUI[]; style: string; inverseViewportStyle: string; } @@ -40,7 +43,7 @@ interface Snap { } interface DndState { - draggedFigure?: Figure; + draggedFigure?: FigureUI; horizontalSnap?: Snap; verticalSnap?: Snap; } @@ -150,17 +153,18 @@ export class FiguresContainer extends Component { }); } - private getVisibleFigures(): Figure[] { + private getVisibleFigures(): FigureUI[] { const visibleFigures = this.env.model.getters.getVisibleFigures(); if ( this.dnd.draggedFigure && - !visibleFigures.some((figure) => figure.id === this.dnd.draggedFigure?.id) + !visibleFigures.some((figureUI) => figureUI.figure.id === this.dnd.draggedFigure?.figure.id) ) { + const sheetId = this.env.model.getters.getActiveSheetId(); visibleFigures.push( - this.env.model.getters.getFigure( - this.env.model.getters.getActiveSheetId(), - this.dnd.draggedFigure?.id - )! + this.env.model.getters.getFigureUI( + sheetId, + this.env.model.getters.getFigure(sheetId, this.dnd.draggedFigure?.figure.id)! + ) ); } return visibleFigures; @@ -240,27 +244,27 @@ export class FiguresContainer extends Component { }); } - private getFigureContainer(figure: Figure): ContainerType { + private getFigureContainer(figureUI: FigureUI): ContainerType { const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates(); - if (figure.id === this.dnd.draggedFigure?.id) { + if (figureUI.figure.id === this.dnd.draggedFigure?.figure.id) { return "dnd"; - } else if (figure.x < viewportX && figure.y < viewportY) { + } else if (figureUI.x < viewportX && figureUI.y < viewportY) { return "topLeft"; - } else if (figure.x < viewportX) { + } else if (figureUI.x < viewportX) { return "bottomLeft"; - } else if (figure.y < viewportY) { + } else if (figureUI.y < viewportY) { return "topRight"; } else { return "bottomRight"; } } - startDraggingFigure(figure: Figure, ev: MouseEvent) { + startDraggingFigure(figureUI: FigureUI, ev: MouseEvent) { if (ev.button > 0 || this.env.model.getters.isReadonly()) { // not main button, probably a context menu and no d&d in readonly mode return; } - const selectResult = this.env.model.dispatch("SELECT_FIGURE", { id: figure.id }); + const selectResult = this.env.model.dispatch("SELECT_FIGURE", { id: figureUI.figure.id }); if (!selectResult.isSuccessful) { return; } @@ -280,27 +284,22 @@ export class FiguresContainer extends Component { ).end, }; - const { x, y } = internalFigureToScreen(this.env.model.getters, figure); - - const initialFig = { ...figure, x, y }; - const onMouseMove = (ev: MouseEvent) => { const getters = this.env.model.getters; const currentMousePosition = { x: ev.clientX, y: ev.clientY }; const draggedFigure = dragFigureForMove( currentMousePosition, initialMousePosition, - initialFig, + figureUI, this.env.model.getters.getMainViewportCoordinates(), maxDimensions, getters.getActiveSheetScrollInfo() ); - const otherFigures = this.getOtherFigures(figure.id); - const internalDragged = screenFigureToInternal(getters, draggedFigure); - const snapResult = snapForMove(getters, internalDragged, otherFigures); + const otherFigures = this.getOtherFigures(figureUI.figure.id); + const snapResult = snapForMove(getters, draggedFigure, otherFigures); - this.dnd.draggedFigure = internalFigureToScreen(getters, snapResult.snappedFigure); + this.dnd.draggedFigure = snapResult.snappedFigure; this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine); this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine); }; @@ -308,11 +307,14 @@ export class FiguresContainer extends Component { if (!this.dnd.draggedFigure) { return; } - let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure); + const { anchor, offset } = this.env.model.getters.getAnchorOffset( + sheetId, + this.dnd.draggedFigure + ); this.dnd.draggedFigure = undefined; this.dnd.horizontalSnap = undefined; this.dnd.verticalSnap = undefined; - this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y }); + this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figureUI.figure.id, offset, anchor }); }; startDnd(onMouseMove, onMouseUp); } @@ -326,20 +328,17 @@ export class FiguresContainer extends Component { * resize from the bottom border of the figure * @param ev Mouse Event */ - startResize(figure: Figure, dirX: ResizeDirection, dirY: ResizeDirection, ev: MouseEvent) { + startResize(figureUI: FigureUI, dirX: ResizeDirection, dirY: ResizeDirection, ev: MouseEvent) { ev.stopPropagation(); const initialMousePosition = { x: ev.clientX, y: ev.clientY }; - const { x, y } = internalFigureToScreen(this.env.model.getters, figure); - - const initialFig = { ...figure, x, y }; - const keepRatio = figureRegistry.get(figure.tag).keepRatio || false; - const minFigSize = figureRegistry.get(figure.tag).minFigSize || MIN_FIG_SIZE; + const keepRatio = figureRegistry.get(figureUI.figure.tag).keepRatio || false; + const minFigSize = figureRegistry.get(figureUI.figure.tag).minFigSize || MIN_FIG_SIZE; const onMouseMove = (ev: MouseEvent) => { const currentMousePosition = { x: ev.clientX, y: ev.clientY }; const draggedFigure = dragFigureForResize( - initialFig, + figureUI, dirX, dirY, currentMousePosition, @@ -349,7 +348,7 @@ export class FiguresContainer extends Component { this.env.model.getters.getActiveSheetScrollInfo() ); - const otherFigures = this.getOtherFigures(figure.id); + const otherFigures = this.getOtherFigures(figureUI.figure.id); const snapResult = snapForResize( this.env.model.getters, dirX, @@ -365,17 +364,21 @@ export class FiguresContainer extends Component { if (!this.dnd.draggedFigure) { return; } - let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure); - const update: Partial
= { x, y }; + const sheetId = this.env.model.getters.getActiveSheetId(); + const { anchor, offset } = this.env.model.getters.getAnchorOffset( + sheetId, + this.dnd.draggedFigure + ); + const update: Partial
= { anchor, offset }; if (dirX) { - update.width = this.dnd.draggedFigure.width; + update.width = this.dnd.draggedFigure.figure.width; } if (dirY) { - update.height = this.dnd.draggedFigure.height; + update.height = this.dnd.draggedFigure.figure.height; } this.env.model.dispatch("UPDATE_FIGURE", { sheetId: this.env.model.getters.getActiveSheetId(), - id: figure.id, + id: figureUI.figure.id, ...update, }); this.dnd.draggedFigure = undefined; @@ -385,12 +388,14 @@ export class FiguresContainer extends Component { startDnd(onMouseMove, onMouseUp); } - private getOtherFigures(figId: UID): Figure[] { - return this.getVisibleFigures().filter((f) => f.id !== figId); + private getOtherFigures(figId: UID): FigureUI[] { + return this.getVisibleFigures().filter((f) => f.figure.id !== figId); } - private getDndFigure(): Figure { - const figure = this.getVisibleFigures().find((fig) => fig.id === this.dnd.draggedFigure?.id); + private getDndFigure(): FigureUI { + const figure = this.getVisibleFigures().find( + (fig) => fig.figure.id === this.dnd.draggedFigure?.figure.id + ); if (!figure) throw new Error("Dnd figure not found"); return { ...figure, @@ -398,8 +403,8 @@ export class FiguresContainer extends Component { }; } - getFigureStyle(figure: Figure): string { - if (figure.id !== this.dnd.draggedFigure?.id) return ""; + getFigureStyle(figureUI: FigureUI): string { + if (figureUI.figure.id !== this.dnd.draggedFigure?.figure.id) return ""; return cssPropertiesToCss({ opacity: "0.9", cursor: "grabbing", @@ -412,16 +417,21 @@ export class FiguresContainer extends Component { if (!snapLine || !this.dnd.draggedFigure) return undefined; const figureVisibleRects = snapLine.matchedFigIds - .map((id) => this.getVisibleFigures().find((fig) => fig.id === id)) + .map((id) => this.getVisibleFigures().find((figureUI) => figureUI.figure.id === id)) .filter(isDefined) - .map((fig) => { - const figOnSCreen = internalFigureToScreen(this.env.model.getters, fig); - const container = this.getFigureContainer(fig); - return rectIntersection(figOnSCreen, this.getContainerRect(container)); + .map((figureUI) => { + const container = this.getFigureContainer(figureUI); + return rectIntersection( + { ...figureUI, ...figureUI.figure }, + this.getContainerRect(container) + ); }) .filter(isDefined); - - const containerRect = rectUnion(this.dnd.draggedFigure, ...figureVisibleRects); + // TODO maybe cleaner ? + const containerRect = rectUnion( + { ...this.dnd.draggedFigure, ...this.dnd.draggedFigure.figure }, + ...figureVisibleRects + ); return { line: snapLine, diff --git a/src/components/figures/figure_container/figure_container.xml b/src/components/figures/figure_container/figure_container.xml index 2cff25b0c1..4c6a4bccc3 100644 --- a/src/components/figures/figure_container/figure_container.xml +++ b/src/components/figures/figure_container/figure_container.xml @@ -9,13 +9,13 @@ diff --git a/src/components/figures/figure_image/figure_image.ts b/src/components/figures/figure_image/figure_image.ts index 1e8ef7fecd..0877cf45e4 100644 --- a/src/components/figures/figure_image/figure_image.ts +++ b/src/components/figures/figure_image/figure_image.ts @@ -1,15 +1,15 @@ import { Component } from "@odoo/owl"; -import { Figure, SpreadsheetChildEnv, UID } from "../../../types"; +import { FigureUI, SpreadsheetChildEnv, UID } from "../../../types"; interface Props { - figure: Figure; + figureUI: FigureUI; onFigureDeleted: () => void; } export class ImageFigure extends Component { static template = "o-spreadsheet-ImageFigure"; static props = { - figure: Object, + figureUI: Object, onFigureDeleted: Function, }; static components = {}; @@ -19,7 +19,7 @@ export class ImageFigure extends Component { // --------------------------------------------------------------------------- get figureId(): UID { - return this.props.figure.id; + return this.props.figureUI.figure.id; } get getImagePath(): string { diff --git a/src/components/helpers/figure_container_helper.ts b/src/components/helpers/figure_container_helper.ts deleted file mode 100644 index 113d620b72..0000000000 --- a/src/components/helpers/figure_container_helper.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DOMCoordinates, Figure, Getters } from "../../types"; - -/** - * Transform a figure with coordinates from the model, to coordinates as they are shown on the screen, - * taking into account the scroll position of the active sheet and the frozen panes. - */ -export function internalFigureToScreen(getters: Getters, fig: Figure): Figure { - return { ...fig, ...internalToScreenCoordinates(getters, { x: fig.x, y: fig.y }) }; -} - -/** - * Transform a figure with coordinates as they are shown on the screen, to coordinates as they are in the model, - * taking into account the scroll position of the active sheet and the frozen panes. - * - * Note that this isn't exactly the reverse operation as internalFigureToScreen, because the figure will always be on top - * of the frozen panes. - */ -export function screenFigureToInternal(getters: Getters, fig: Figure): Figure { - return { ...fig, ...screenCoordinatesToInternal(getters, { x: fig.x, y: fig.y }) }; -} - -function internalToScreenCoordinates(getters: Getters, { x, y }: DOMCoordinates): DOMCoordinates { - const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates(); - const { scrollX, scrollY } = getters.getActiveSheetScrollInfo(); - - x = x < viewportX ? x : x - scrollX; - y = y < viewportY ? y : y - scrollY; - - return { x, y }; -} - -function screenCoordinatesToInternal(getters: Getters, { x, y }: DOMCoordinates): DOMCoordinates { - const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates(); - const { scrollX, scrollY } = getters.getActiveSheetScrollInfo(); - - x = viewportX && x < viewportX ? x : x + scrollX; - y = viewportY && y < viewportY ? y : y + scrollY; - - return { x, y }; -} diff --git a/src/components/helpers/figure_drag_helper.ts b/src/components/helpers/figure_drag_helper.ts index 654bb303b8..c71b68e0fd 100644 --- a/src/components/helpers/figure_drag_helper.ts +++ b/src/components/helpers/figure_drag_helper.ts @@ -1,25 +1,25 @@ import { clip } from "../../helpers"; -import { Figure, PixelPosition, SheetScrollInfo } from "../../types"; +import { FigureUI, PixelPosition, SheetScrollInfo } from "../../types"; export function dragFigureForMove( { x: mouseX, y: mouseY }: PixelPosition, { x: mouseInitialX, y: mouseInitialY }: PixelPosition, - initialFigure: Figure, + initialFigure: FigureUI, { x: viewportX, y: viewportY }: PixelPosition, { maxX, maxY }: { maxX: number; maxY: number }, { scrollX, scrollY }: SheetScrollInfo -): Figure { +): FigureUI { const minX = viewportX ? 0 : -scrollX; const minY = viewportY ? 0 : -scrollY; const deltaX = mouseX - mouseInitialX; - const newX = clip(initialFigure.x + deltaX, minX, maxX - initialFigure.width - scrollX); + const newX = clip(initialFigure.x + deltaX, minX, maxX - initialFigure.figure.width - scrollX); const deltaY = mouseY - mouseInitialY; - const newY = clip(initialFigure.y + deltaY, minY, maxY - initialFigure.height - scrollY); + const newY = clip(initialFigure.y + deltaY, minY, maxY - initialFigure.figure.height - scrollY); return { ...initialFigure, x: newX, y: newY }; } export function dragFigureForResize( - initialFigure: Figure, + initialFigure: FigureUI, dirX: -1 | 0 | 1, dirY: -1 | 0 | 1, { x: mouseX, y: mouseY }: PixelPosition, @@ -27,32 +27,33 @@ export function dragFigureForResize( keepRatio: boolean, minFigSize: number, { scrollX, scrollY }: SheetScrollInfo -): Figure { - let { x, y, width, height } = initialFigure; +): FigureUI { + let { x, y, figure } = initialFigure; + let { width, height } = figure; if (keepRatio && dirX != 0 && dirY != 0) { - const deltaX = Math.min(dirX * (mouseInitialX - mouseX), initialFigure.width - minFigSize); - const deltaY = Math.min(dirY * (mouseInitialY - mouseY), initialFigure.height - minFigSize); - const fraction = Math.min(deltaX / initialFigure.width, deltaY / initialFigure.height); - width = initialFigure.width * (1 - fraction); - height = initialFigure.height * (1 - fraction); + const deltaX = Math.min(dirX * (mouseInitialX - mouseX), width - minFigSize); + const deltaY = Math.min(dirY * (mouseInitialY - mouseY), height - minFigSize); + const fraction = Math.min(deltaX / width, deltaY / height); + width = width * (1 - fraction); + height = height * (1 - fraction); if (dirX < 0) { - x = initialFigure.x + initialFigure.width * fraction; + x = x + width * fraction; } if (dirY < 0) { - y = initialFigure.y + initialFigure.height * fraction; + y = y + height * fraction; } } else { - const deltaX = Math.max(dirX * (mouseX - mouseInitialX), minFigSize - initialFigure.width); - const deltaY = Math.max(dirY * (mouseY - mouseInitialY), minFigSize - initialFigure.height); - width = initialFigure.width + deltaX; - height = initialFigure.height + deltaY; + const deltaX = Math.max(dirX * (mouseX - mouseInitialX), minFigSize - width); + const deltaY = Math.max(dirY * (mouseY - mouseInitialY), minFigSize - height); + width = width + deltaX; + height = height + deltaY; if (dirX < 0) { - x = initialFigure.x - deltaX; + x = x - deltaX; } if (dirY < 0) { - y = initialFigure.y - deltaY; + y = y - deltaY; } } @@ -66,5 +67,5 @@ export function dragFigureForResize( y = -scrollY; } - return { ...initialFigure, x, y, width, height }; + return { x, y, figure: { ...figure, width, height } }; } diff --git a/src/components/helpers/figure_snap_helper.ts b/src/components/helpers/figure_snap_helper.ts index 7d62c52d53..737a05789f 100644 --- a/src/components/helpers/figure_snap_helper.ts +++ b/src/components/helpers/figure_snap_helper.ts @@ -1,6 +1,5 @@ -import { Figure, Getters, Pixel, PixelPosition, UID } from "../../types"; +import { FigureUI, Getters, Pixel, PixelPosition, UID } from "../../types"; import { FIGURE_BORDER_WIDTH } from "./../../constants"; -import { internalFigureToScreen } from "./figure_container_helper"; const SNAP_MARGIN: Pixel = 5; @@ -20,7 +19,7 @@ export interface SnapLine { } interface SnapReturn { - snappedFigure: Figure; + snappedFigure: FigureUI; verticalSnapLine?: SnapLine; horizontalSnapLine?: SnapLine; } @@ -31,8 +30,8 @@ interface SnapReturn { */ export function snapForMove( getters: Getters, - figureToSnap: Figure, - otherFigures: Figure[] + figureToSnap: FigureUI, + otherFigures: FigureUI[] ): SnapReturn { const snappedFigure = { ...figureToSnap }; @@ -87,8 +86,8 @@ export function snapForResize( getters: Getters, resizeDirX: -1 | 0 | 1, resizeDirY: -1 | 0 | 1, - figureToSnap: Figure, - otherFigures: Figure[] + figureToSnap: FigureUI, + otherFigures: FigureUI[] ): SnapReturn { const snappedFigure = { ...figureToSnap }; @@ -102,10 +101,10 @@ export function snapForResize( ); if (verticalSnapLine) { if (resizeDirX === 1) { - snappedFigure.width -= verticalSnapLine.snapOffset; + snappedFigure.figure.width -= verticalSnapLine.snapOffset; } else if (resizeDirX === -1) { snappedFigure.x -= verticalSnapLine.snapOffset; - snappedFigure.width += verticalSnapLine.snapOffset; + snappedFigure.figure.width += verticalSnapLine.snapOffset; } } @@ -119,17 +118,17 @@ export function snapForResize( ); if (horizontalSnapLine) { if (resizeDirY === 1) { - snappedFigure.height -= horizontalSnapLine.snapOffset; + snappedFigure.figure.height -= horizontalSnapLine.snapOffset; } else if (resizeDirY === -1) { snappedFigure.y -= horizontalSnapLine.snapOffset; - snappedFigure.height += horizontalSnapLine.snapOffset; + snappedFigure.figure.height += horizontalSnapLine.snapOffset; } } snappedFigure.x = Math.round(snappedFigure.x); snappedFigure.y = Math.round(snappedFigure.y); - snappedFigure.height = Math.round(snappedFigure.height); - snappedFigure.width = Math.round(snappedFigure.width); + snappedFigure.figure.height = Math.round(snappedFigure.figure.height); + snappedFigure.figure.width = Math.round(snappedFigure.figure.width); return { snappedFigure, verticalSnapLine, horizontalSnapLine }; } @@ -142,7 +141,7 @@ export function snapForResize( */ function getVisibleAxes( getters: Getters, - figure: Figure, + figure: FigureUI, axesTypes: T[] ): FigureAxis[] { const axes = axesTypes.map((axisType) => getAxis(figure, axisType)); @@ -160,16 +159,15 @@ function getVisibleAxes( */ function getAxisScreenPosition( getters: Getters, - figure: Figure, + figure: FigureUI, figureAxis: FigureAxis ): FigureAxis { - const screenFigure = internalFigureToScreen(getters, figure); - return getAxis(screenFigure, figureAxis.axisType); + return getAxis(figure, figureAxis.axisType); } function isAxisVisible( getters: Getters, - figure: Figure, + figureUI: FigureUI, axis: FigureAxis ): boolean { const { x: mainViewportX, y: mainViewportY } = getters.getMainViewportCoordinates(); @@ -179,16 +177,16 @@ function isAxisVisible( case "top": case "bottom": case "vCenter": - if (figure.y < mainViewportY) return true; - axisStartEndPositions.push({ x: figure.x, y: axis.position }); - axisStartEndPositions.push({ x: figure.x + figure.width, y: axis.position }); + if (figureUI.y < mainViewportY) return true; + axisStartEndPositions.push({ x: figureUI.x, y: axis.position }); + axisStartEndPositions.push({ x: figureUI.x + figureUI.figure.width, y: axis.position }); break; case "left": case "right": case "hCenter": - if (figure.x < mainViewportX) return true; - axisStartEndPositions.push({ x: axis.position, y: figure.y }); - axisStartEndPositions.push({ x: axis.position, y: figure.y + figure.height }); + if (figureUI.x < mainViewportX) return true; + axisStartEndPositions.push({ x: axis.position, y: figureUI.y }); + axisStartEndPositions.push({ x: axis.position, y: figureUI.y + figureUI.figure.height }); break; } @@ -206,9 +204,9 @@ function isAxisVisible( function getSnapLine( getters: Getters, - figureToSnap: Figure, + figureToSnap: FigureUI, figAxesTypes: T, - otherFigures: Figure[], + otherFigures: FigureUI[], otherAxesTypes: T ): SnapLine | undefined { const axesOfFigure = getVisibleAxes(getters, figureToSnap, figAxesTypes); @@ -225,10 +223,10 @@ function getSnapLine( const snapOffset = axisOfFigure.position - axisOfOtherFig.position; if (closestMatch && snapOffset === closestMatch.snapOffset) { - closestMatch.matchedFigIds.push(otherFigure.id); + closestMatch.matchedFigIds.push(otherFigure.figure.id); } else if (!closestMatch || Math.abs(snapOffset) <= Math.abs(closestMatch.snapOffset)) { closestMatch = { - matchedFigIds: [otherFigure.id], + matchedFigIds: [otherFigure.figure.id], snapOffset, snappedAxisType: axisOfFigure.axisType, position: axisOfOtherFig.position, @@ -246,28 +244,28 @@ function canSnap(axisPosition1: Pixel, axisPosition2: Pixel) { } function getAxis( - fig: Figure, + figureUI: FigureUI, axisType: T ): FigureAxis { let position = 0; switch (axisType) { case "top": - position = fig.y; + position = figureUI.y; break; case "bottom": - position = fig.y + fig.height - FIGURE_BORDER_WIDTH; + position = figureUI.y + figureUI.figure.height - FIGURE_BORDER_WIDTH; break; case "vCenter": - position = fig.y + Math.floor(fig.height / 2) - FIGURE_BORDER_WIDTH; + position = figureUI.y + Math.floor(figureUI.figure.height / 2) - FIGURE_BORDER_WIDTH; break; case "left": - position = fig.x; + position = figureUI.x; break; case "right": - position = fig.x + fig.width - FIGURE_BORDER_WIDTH; + position = figureUI.x + figureUI.figure.width - FIGURE_BORDER_WIDTH; break; case "hCenter": - position = fig.x + Math.floor(fig.width / 2) - FIGURE_BORDER_WIDTH; + position = figureUI.x + Math.floor(figureUI.figure.width / 2) - FIGURE_BORDER_WIDTH; break; } diff --git a/src/helpers/figures/figure/figure.ts b/src/helpers/figures/figure/figure.ts index 6f612cdea9..8c5472b322 100644 --- a/src/helpers/figures/figure/figure.ts +++ b/src/helpers/figures/figure/figure.ts @@ -1,19 +1,32 @@ -import { FigureSize, Getters } from "../../../types"; +import { AnchorOffset, FigureSize, Getters } from "../../../types"; import { deepCopy } from "../../misc"; -export function centerFigurePosition(getters: Getters, size: FigureSize) { +export function centerFigurePosition(getters: Getters, size: FigureSize): AnchorOffset { const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates(); const { scrollX, scrollY } = getters.getActiveSheetScrollInfo(); + const sheetId = getters.getActiveSheetId(); const dim = getters.getSheetViewDimension(); const rect = getters.getVisibleRect(getters.getActiveMainViewport()); const scrollableViewportWidth = Math.min(rect.width, dim.width - offsetCorrectionX); const scrollableViewportHeight = Math.min(rect.height, dim.height - offsetCorrectionY); - return { - x: offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2), - y: offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2), - }; // Position at the center of the scrollable viewport + const posX = + offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2); + const posY = + offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2); + + const anchor = { + col: getters.getColIndex(Math.max(0, (scrollableViewportWidth - size.width) / 2)), + row: getters.getRowIndex(Math.max(0, (scrollableViewportHeight - size.height) / 2)), + }; + + const offset = { + x: posX - getters.getColDimensions(sheetId, anchor.col)["start"], + y: posY - getters.getRowDimensions(sheetId, anchor.row)["start"], + }; + + return { anchor, offset }; } export function getMaxFigureSize(getters: Getters, figureSize: FigureSize): FigureSize { diff --git a/src/history/repeat_commands/repeat_commands_specific.ts b/src/history/repeat_commands/repeat_commands_specific.ts index 7dc57f6799..4aa9ad35ac 100644 --- a/src/history/repeat_commands/repeat_commands_specific.ts +++ b/src/history/repeat_commands/repeat_commands_specific.ts @@ -39,7 +39,7 @@ export function repeatCreateImageCommand( ): CreateImageOverCommand { return { ...repeatSheetDependantCommand(getters, cmd), - figureId: uuidGenerator.uuidv4(), + id: uuidGenerator.uuidv4(), }; } diff --git a/src/plugins/core/chart.ts b/src/plugins/core/chart.ts index 7e1346f850..887bf1722f 100644 --- a/src/plugins/core/chart.ts +++ b/src/plugins/core/chart.ts @@ -8,10 +8,11 @@ import { CommandResult, CoreCommand, CreateChartCommand, - DOMCoordinates, DOMDimension, Figure, FigureData, + PixelPosition, + Position, UID, UpdateChartCommand, WorkbookData, @@ -74,7 +75,7 @@ export class ChartPlugin extends CorePlugin implements ChartState { handle(cmd: CoreCommand) { switch (cmd.type) { case "CREATE_CHART": - this.addFigure(cmd.id, cmd.sheetId, cmd.position, cmd.size); + this.addFigure(cmd.id, cmd.sheetId, cmd.anchor, cmd.offset, cmd.fixed_position, cmd.size); this.addChart(cmd.id, cmd.definition); break; case "UPDATE_CHART": { @@ -91,7 +92,9 @@ export class ChartPlugin extends CorePlugin implements ChartState { if (chart) { this.dispatch("CREATE_CHART", { id: duplicatedFigureId, - position: { x: fig.x, y: fig.y }, + anchor: fig.anchor, + offset: fig.offset, + fixed_position: fig.fixed_position, size: { width: fig.width, height: fig.height }, definition: chart.getDefinition(), sheetId: cmd.sheetIdTo, @@ -202,7 +205,12 @@ export class ChartPlugin extends CorePlugin implements ChartState { private addFigure( id: UID, sheetId: UID, - position: DOMCoordinates = { x: 0, y: 0 }, + anchor: Position | undefined, + offset: PixelPosition = { + x: 0, + y: 0, + }, + fixed_position: boolean = true, size: DOMDimension = { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT, @@ -211,10 +219,15 @@ export class ChartPlugin extends CorePlugin implements ChartState { if (this.getters.getFigure(sheetId, id)) { return; } + if (!anchor) { + anchor = { col: 0, row: 0 }; + fixed_position = true; + } const figure: Figure = { id, - x: position.x, - y: position.y, + anchor, + offset, + fixed_position, width: size.width, height: size.height, tag: "chart", diff --git a/src/plugins/core/figures.ts b/src/plugins/core/figures.ts index 4ea878ab4b..95ebd61553 100644 --- a/src/plugins/core/figures.ts +++ b/src/plugins/core/figures.ts @@ -4,11 +4,13 @@ import { CoreCommand, ExcelWorkbookData, Figure, + HeaderIndex, + PixelPosition, + Position, UID, WorkbookData, } from "../../types/index"; import { CorePlugin } from "../core_plugin"; -import { DEFAULT_CELL_HEIGHT } from "./../../constants"; interface FigureState { readonly figures: { [sheet: string]: Record | undefined }; @@ -64,43 +66,83 @@ export class FigurePlugin extends CorePlugin implements FigureState case "DELETE_FIGURE": this.removeFigure(cmd.id, cmd.sheetId); break; + case "ADD_COLUMNS_ROWS": + let baseIdx = cmd.base; + if (cmd.position === "before") { + baseIdx--; + } + if (cmd.dimension === "COL") { + this.onColAdd(cmd.sheetId, baseIdx, cmd.quantity); + } else { + this.onRowAdd(cmd.sheetId, baseIdx, cmd.quantity); + } + break; case "REMOVE_COLUMNS_ROWS": - this.onRowColDelete(cmd.sheetId, cmd.dimension); + if (cmd.dimension === "COL") { + this.onColRemove(cmd.sheetId, cmd.elements); + } else { + this.onRowRemove(cmd.sheetId, cmd.elements); + } + break; } } - private onRowColDelete(sheetId: string, dimension: string) { - dimension === "ROW" ? this.onRowDeletion(sheetId) : this.onColDeletion(sheetId); + private onColAdd(sheetId: string, index: HeaderIndex, quantity: number) { + for (const figure of this.getFigures(sheetId)) { + if (figure.anchor.col > index) { + this.history.update("figures", sheetId, figure.id!, "anchor", { + row: figure.anchor.row, + col: figure.anchor.col + quantity, + } as Position); + } + } } - private onRowDeletion(sheetId: string) { - const numHeader = this.getters.getNumberRows(sheetId); - let gridHeight = 0; - for (let i = 0; i < numHeader; i++) { - // TODO : since the row size is an UI value now, this doesn't work anymore. Using the default cell height is - // a temporary solution at best, but is broken. - gridHeight += this.getters.getUserRowSize(sheetId, i) || DEFAULT_CELL_HEIGHT; - } - const figures = this.getters.getFigures(sheetId); - for (const figure of figures) { - const newY = Math.min(figure.y, gridHeight - figure.height); - if (newY !== figure.y) { - this.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, y: newY }); + private onRowAdd(sheetId: string, index: HeaderIndex, quantity: number) { + for (const figure of this.getFigures(sheetId)) { + if (figure.anchor.row > index) { + this.history.update("figures", sheetId, figure.id!, "anchor", { + row: figure.anchor.row + quantity, + col: figure.anchor.col, + } as Position); } } } - private onColDeletion(sheetId: string) { - const numHeader = this.getters.getNumberCols(sheetId); - let gridWidth = 0; - for (let i = 0; i < numHeader; i++) { - gridWidth += this.getters.getColSize(sheetId, i); + private onColRemove(sheetId: string, elements: number[]) { + const figures = this.getFigures(sheetId).sort((a, b) => a.anchor.col - b.anchor.col); + elements.sort((a, b) => a - b); + + let elements_index = 0; + for (const fig in figures) { + const figure = figures[fig]; + while (elements_index < elements.length && elements[elements_index] <= figure.anchor.col) { + elements_index++; + } + if (elements_index) { + this.history.update("figures", sheetId, figure.id!, "anchor", { + row: figure.anchor.row, + col: figure.anchor.col - elements_index, + } as Position); + } } - const figures = this.getters.getFigures(sheetId); - for (const figure of figures) { - const newX = Math.min(figure.x, gridWidth - figure.width); - if (newX !== figure.x) { - this.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x: newX }); + } + + private onRowRemove(sheetId: string, elements: number[]) { + const figures = this.getFigures(sheetId).sort((a, b) => a.anchor.row - b.anchor.row); + elements.sort((a, b) => a - b); + + let elements_index = 0; + for (const fig in figures) { + const figure = figures[fig]; + while (elements_index < elements.length && elements[elements_index] <= figure.anchor.row) { + elements_index++; + } + if (elements_index) { + this.history.update("figures", sheetId, figure.id!, "anchor", { + row: figure.anchor.row - elements_index, + col: figure.anchor.col, + } as Position); } } } @@ -111,11 +153,12 @@ export class FigurePlugin extends CorePlugin implements FigureState } for (const [key, value] of Object.entries(figure)) { switch (key) { - case "x": - case "y": - if (value !== undefined) { - this.history.update("figures", sheetId, figure.id!, key, Math.max(value as number, 0)); - } + case "offset": + // Todo ensure that final position > 0 for x/y + this.history.update("figures", sheetId, figure.id!, key, value as PixelPosition); + break; + case "anchor": + this.history.update("figures", sheetId, figure.id!, key, value as Position); break; case "width": case "height": diff --git a/src/plugins/core/image.ts b/src/plugins/core/image.ts index f1ed070d2a..64da009410 100644 --- a/src/plugins/core/image.ts +++ b/src/plugins/core/image.ts @@ -5,11 +5,12 @@ import { Image } from "../../types/image"; import { CommandResult, CoreCommand, - DOMCoordinates, ExcelWorkbookData, Figure, FigureData, FigureSize, + PixelPosition, + Position, UID, WorkbookData, } from "../../types/index"; @@ -40,7 +41,7 @@ export class ImagePlugin extends CorePlugin implements ImageState { allowDispatch(cmd: CoreCommand) { switch (cmd.type) { case "CREATE_IMAGE": - if (this.getters.getFigure(cmd.sheetId, cmd.figureId)) { + if (this.getters.getFigure(cmd.sheetId, cmd.id)) { return CommandResult.InvalidFigureId; } return CommandResult.Success; @@ -52,8 +53,8 @@ export class ImagePlugin extends CorePlugin implements ImageState { handle(cmd: CoreCommand) { switch (cmd.type) { case "CREATE_IMAGE": - this.addImage(cmd.figureId, cmd.sheetId, cmd.position, cmd.size); - this.history.update("images", cmd.sheetId, cmd.figureId, cmd.definition); + this.addFigure(cmd.id, cmd.sheetId, cmd.anchor, cmd.offset, cmd.fixed_position, cmd.size); + this.history.update("images", cmd.sheetId, cmd.id, cmd.definition); this.syncedImages.add(cmd.definition.path); break; case "DUPLICATE_SHEET": { @@ -67,8 +68,10 @@ export class ImagePlugin extends CorePlugin implements ImageState { const size = { width: fig.width, height: fig.height }; this.dispatch("CREATE_IMAGE", { sheetId: cmd.sheetIdTo, - figureId: duplicatedFigureId, - position: { x: fig.x, y: fig.y }, + id: duplicatedFigureId, + offset: fig.offset, + anchor: fig.anchor, + fixed_position: fig.fixed_position, size, definition: deepCopy(image), }); @@ -123,11 +126,29 @@ export class ImagePlugin extends CorePlugin implements ImageState { // Private // --------------------------------------------------------------------------- - private addImage(id: UID, sheetId: UID, position: DOMCoordinates, size: FigureSize) { + private addFigure( + id: UID, + sheetId: UID, + anchor: Position | undefined, + offset: PixelPosition = { + x: 0, + y: 0, + }, + fixed_position: boolean = true, + size: FigureSize + ) { + if (this.getters.getFigure(sheetId, id)) { + return; + } + if (!anchor) { + anchor = { col: 0, row: 0 }; + fixed_position = true; + } const figure: Figure = { id, - x: position.x, - y: position.y, + anchor, + offset, + fixed_position, width: size.width, height: size.height, tag: "image", diff --git a/src/plugins/ui_stateful/sheetview.ts b/src/plugins/ui_stateful/sheetview.ts index 84ba5a01af..6778da1d21 100644 --- a/src/plugins/ui_stateful/sheetview.ts +++ b/src/plugins/ui_stateful/sheetview.ts @@ -4,6 +4,7 @@ import { scrollDelay } from "../../helpers/index"; import { InternalViewport } from "../../helpers/internal_viewport"; import { SelectionEvent } from "../../types/event_stream"; import { + AnchorOffset, CellPosition, Command, CommandResult, @@ -12,6 +13,7 @@ import { Dimension, EdgeScrollInfo, Figure, + FigureUI, HeaderIndex, LocalCommand, Pixel, @@ -104,6 +106,8 @@ export class SheetViewPlugin extends UIPlugin { "isPositionVisible", "getColDimensionsInViewport", "getRowDimensionsInViewport", + "getFigureUI", + "getAnchorOffset", ] as const; readonly viewports: Record = {}; @@ -804,35 +808,66 @@ export class SheetViewPlugin extends UIPlugin { } } - getVisibleFigures(): Figure[] { + getVisibleFigures(): FigureUI[] { const sheetId = this.getters.getActiveSheetId(); - const result: Figure[] = []; + const result: FigureUI[] = []; const figures = this.getters.getFigures(sheetId); - const { scrollX, scrollY } = this.getActiveSheetScrollInfo(); - const { x: offsetCorrectionX, y: offsetCorrectionY } = - this.getters.getMainViewportCoordinates(); const { width, height } = this.getters.getSheetViewDimensionWithHeaders(); + const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getMainViewportCoordinates(); for (const figure of figures) { - if ( - figure.x >= offsetCorrectionX && - (figure.x + figure.width <= offsetCorrectionX + scrollX || - figure.x >= width + scrollX + offsetCorrectionX) - ) { + const figureUI = this.getFigureUI(sheetId, figure); + const { x, y } = figureUI; + if (x + figure.width < offsetCorrectionX || x > width) { continue; } - if ( - figure.y >= offsetCorrectionY && - (figure.y + figure.height <= offsetCorrectionY + scrollY || - figure.y >= height + scrollY + offsetCorrectionY) - ) { + if (y + figure.height < offsetCorrectionY || y > height) { continue; } - result.push(figure); + result.push(figureUI); } return result; } + private absoluteToViewport(position: PixelPosition): DOMCoordinates { + const { scrollX, scrollY } = this.getActiveSheetScrollInfo(); + const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getMainViewportCoordinates(); + return { + x: position.x < offsetCorrectionX ? position.x : position.x - scrollX, + y: position.y < offsetCorrectionY ? position.y : position.y - scrollY, + }; + } + + private viewportToAbsolute(coord: DOMCoordinates): PixelPosition { + const { scrollX, scrollY } = this.getActiveSheetScrollInfo(); + const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getMainViewportCoordinates(); + return { + x: coord.x < offsetCorrectionX ? coord.x : coord.x + scrollX, + y: coord.y < offsetCorrectionY ? coord.y : coord.y + scrollY, + }; + } + + getFigureUI(sheetId: string, figure: Figure): FigureUI { + const absolute_x = + figure.offset.x + this.getters.getColDimensions(sheetId, figure.anchor.col)["start"]; + const absolute_y = + figure.offset.y + this.getters.getRowDimensions(sheetId, figure.anchor.row)["start"]; + const { x, y } = this.absoluteToViewport({ x: absolute_x, y: absolute_y }); + return { x, y, figure }; + } + + getAnchorOffset(sheetId: string, figureUI: FigureUI): AnchorOffset { + const position = this.viewportToAbsolute(figureUI); + if (figureUI.figure.fixed_position) { + return { anchor: { col: 0, row: 0 }, offset: { x: position.x, y: position.y } }; + } + const anchor_col = this.getColIndex(figureUI.x); + const offset_x = position.x - this.getters.getColDimensions(sheetId, anchor_col)["start"]; + const anchor_row = this.getRowIndex(figureUI.y); + const offset_y = position.y - this.getters.getRowDimensions(sheetId, anchor_row)["start"]; + return { anchor: { col: anchor_col, row: anchor_row }, offset: { x: offset_x, y: offset_y } }; + } + isPositionVisible(position: PixelPosition): boolean { const { scrollX, scrollY } = this.getters.getActiveSheetScrollInfo(); const { x: mainViewportX, y: mainViewportY } = this.getters.getMainViewportCoordinates(); diff --git a/src/types/commands.ts b/src/types/commands.ts index 050d5ebd88..c51d0a7155 100644 --- a/src/types/commands.ts +++ b/src/types/commands.ts @@ -1,6 +1,5 @@ import { ConditionalFormat, - DOMCoordinates, DataValidationRule, Figure, Format, @@ -16,6 +15,8 @@ import { Dimension, HeaderIndex, Pixel, + PixelPosition, + Position, SetDecimalStep, SortDirection, SortOptions, @@ -494,15 +495,20 @@ export interface DeleteFigureCommand extends SheetDependentCommand { id: UID; } +interface SubFigureCommand extends SheetDependentCommand { + id: UID; + anchor?: Position; + offset?: PixelPosition; + fixed_position?: boolean; + size?: FigureSize; +} + //------------------------------------------------------------------------------ // Chart //------------------------------------------------------------------------------ -export interface CreateChartCommand extends SheetDependentCommand { +export interface CreateChartCommand extends SheetDependentCommand, SubFigureCommand { type: "CREATE_CHART"; - id: UID; - position?: DOMCoordinates; - size?: FigureSize; definition: ChartDefinition; } @@ -516,10 +522,8 @@ export interface UpdateChartCommand extends SheetDependentCommand { // Image //------------------------------------------------------------------------------ -export interface CreateImageOverCommand extends SheetDependentCommand { +export interface CreateImageOverCommand extends SheetDependentCommand, SubFigureCommand { type: "CREATE_IMAGE"; - figureId: UID; - position: DOMCoordinates; size: FigureSize; definition: Image; } diff --git a/src/types/figure.ts b/src/types/figure.ts index 52b04da130..fefaa67fe5 100644 --- a/src/types/figure.ts +++ b/src/types/figure.ts @@ -1,14 +1,24 @@ -import { Pixel, UID } from "."; +import { DOMCoordinates, Pixel, PixelPosition, Position, UID } from "."; export interface Figure { id: UID; - x: Pixel; - y: Pixel; + anchor: Position; + offset: PixelPosition; + fixed_position: boolean; width: Pixel; height: Pixel; tag: string; } +export interface FigureUI extends DOMCoordinates { + figure: Figure; +} + +export interface AnchorOffset { + anchor: Position; + offset: PixelPosition; +} + export interface FigureSize { width: Pixel; height: Pixel; diff --git a/src/types/workbook_data.ts b/src/types/workbook_data.ts index ce9037b8e3..00e7446808 100644 --- a/src/types/workbook_data.ts +++ b/src/types/workbook_data.ts @@ -2,7 +2,18 @@ import { CellValue, DataValidationRule, Format, Locale } from "."; import { ExcelChartDefinition } from "./chart/chart"; import { ConditionalFormat } from "./conditional_formatting"; import { Image } from "./image"; -import { Border, Color, Dimension, HeaderGroup, PaneDivision, Pixel, Style, UID } from "./misc"; +import { + Border, + Color, + Dimension, + HeaderGroup, + PaneDivision, + Pixel, + PixelPosition, + Position, + Style, + UID, +} from "./misc"; import { PivotCoreDefinition } from "./pivot"; import { CoreTableType, TableConfig, TableStyleTemplateName } from "./table"; @@ -19,8 +30,9 @@ export interface HeaderData { export interface FigureData { id: UID; - x: Pixel; - y: Pixel; + anchor: Position; + offset: PixelPosition; + fixed_position: boolean; width: Pixel; height: Pixel; tag: string; diff --git a/src/xlsx/conversion/figure_conversion.ts b/src/xlsx/conversion/figure_conversion.ts index 2256ce2f63..82d88fe93d 100644 --- a/src/xlsx/conversion/figure_conversion.ts +++ b/src/xlsx/conversion/figure_conversion.ts @@ -5,7 +5,14 @@ import { toUnboundedZone, zoneToXc, } from "../../helpers"; -import { ChartDefinition, ExcelChartDefinition, FigureData } from "../../types"; +import { + ChartDefinition, + ExcelChartDefinition, + FigureData, + PixelPosition, + Position, +} from "../../types"; +import { AnchorOffset } from "../../types/figure"; import { ExcelImage } from "../../types/image"; import { XLSXFigure, XLSXWorksheet } from "../../types/xlsx"; import { convertEMUToDotValue, getColPosition, getRowPosition } from "../helpers/content_helpers"; @@ -24,32 +31,36 @@ function convertFigure( id: string, sheetData: XLSXWorksheet ): FigureData | undefined { - let x1: number, y1: number; + let anchor: Position; + let offset: PixelPosition; let height: number, width: number; if (figure.anchors.length === 1) { // one cell anchor - ({ x: x1, y: y1 } = getPositionFromAnchor(figure.anchors[0], sheetData)); + ({ anchor, offset } = convertAnchor(figure.anchors[0])); width = convertEMUToDotValue(figure.figureSize!.cx); height = convertEMUToDotValue(figure.figureSize!.cy); } else { - ({ x: x1, y: y1 } = getPositionFromAnchor(figure.anchors[0], sheetData)); + ({ anchor, offset } = convertAnchor(figure.anchors[0])); + const { x: x1, y: y1 } = getPositionFromAnchor(figure.anchors[1], sheetData); const { x: x2, y: y2 } = getPositionFromAnchor(figure.anchors[1], sheetData); width = x2 - x1; height = y2 - y1; } - const figureData = { id, x: x1, y: y1 }; + const figureData = { id, anchor, offset }; if (isChartData(figure.data)) { return { ...figureData, width, height, + fixed_position: false, tag: "chart", data: convertChartData(figure.data), }; } else if (isImageData(figure.data)) { return { ...figureData, + fixed_position: false, width: convertEMUToDotValue(figure.data.size.cx), height: convertEMUToDotValue(figure.data.size.cy), tag: "image", @@ -121,6 +132,15 @@ function convertExcelRangeToSheetXC(range: string, dataSetsHaveTitle: boolean): return getFullReference(sheetName, dataXC); } +function convertAnchor(XLSXanchor: XLSXFigureAnchor): AnchorOffset { + const anchor = { col: XLSXanchor.col, row: XLSXanchor.row }; + const offset = { + x: convertEMUToDotValue(XLSXanchor.colOffset), + y: convertEMUToDotValue(XLSXanchor.rowOffset), + }; + return { anchor, offset }; +} + function getPositionFromAnchor( anchor: XLSXFigureAnchor, sheetData: XLSXWorksheet diff --git a/src/xlsx/functions/drawings.ts b/src/xlsx/functions/drawings.ts index 922a2ce13a..6ff0bbf995 100644 --- a/src/xlsx/functions/drawings.ts +++ b/src/xlsx/functions/drawings.ts @@ -1,5 +1,4 @@ -import { FIGURE_BORDER_WIDTH } from "../../constants"; -import { HeaderData, SheetData } from "../../types"; +import { SheetData } from "../../types"; import { ExcelChartDefinition } from "../../types/chart/chart"; import { XMLAttributes, XMLString } from "../../types/xlsx"; import { DRAWING_NS_A, DRAWING_NS_C, NAMESPACE, RELATIONSHIP_NSR } from "../constants"; @@ -69,27 +68,27 @@ function convertFigureData( figure: FigureData, sheet: SheetData ): FigurePosition { - const { x, y, height, width } = figure; - - const cols = Object.values(sheet.cols); - const rows = Object.values(sheet.rows); - const { index: colFrom, offset: offsetColFrom } = figureCoordinates(cols, x); - const { index: colTo, offset: offsetColTo } = figureCoordinates(cols, x + width); - const { index: rowFrom, offset: offsetRowFrom } = figureCoordinates(rows, y); - const { index: rowTo, offset: offsetRowTo } = figureCoordinates(rows, y + height); + const { anchor, offset } = figure; + // const cols = Object.values(sheet.cols); + // const rows = Object.values(sheet.rows); + // const { index: colFrom, offset: offsetColFrom } = figureCoordinates(cols, x); + // const { index: colTo, offset: offsetColTo } = figureCoordinates(cols, x + width); + // const { index: rowFrom, offset: offsetRowFrom } = figureCoordinates(rows, y); + // const { index: rowTo, offset: offsetRowTo } = figureCoordinates(rows, y + height); + // TODO return { from: { - col: colFrom, - colOff: offsetColFrom, - row: rowFrom, - rowOff: offsetRowFrom, + col: anchor.col, + colOff: offset.x, + row: anchor.row, + rowOff: offset.y, }, to: { - col: colTo, - colOff: offsetColTo, - row: rowTo, - rowOff: offsetRowTo, + col: anchor.col + 1, + colOff: offset.x, + row: anchor.row + 1, + rowOff: offset.y, }, }; } @@ -97,26 +96,26 @@ function convertFigureData( /** Returns figure coordinates in EMU for a specific header dimension * See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement */ -function figureCoordinates( - headers: HeaderData[], - position: number -): { index: number; offset: number } { - let currentPosition = 0; - for (const [headerIndex, header] of headers.entries()) { - if (currentPosition <= position && position < currentPosition + header.size!) { - return { - index: headerIndex, - offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH), - }; - } else if (headerIndex < headers.length - 1) { - currentPosition += header.size!; - } - } - return { - index: headers.length - 1, - offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH), - }; -} +// function figureCoordinates( +// headers: HeaderData[], +// position: number +// ): { index: number; offset: number } { +// let currentPosition = 0; +// for (const [headerIndex, header] of headers.entries()) { +// if (currentPosition <= position && position < currentPosition + header.size!) { +// return { +// index: headerIndex, +// offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH), +// }; +// } else if (headerIndex < headers.length - 1) { +// currentPosition += header.size!; +// } +// } +// return { +// index: headers.length - 1, +// offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH), +// }; +// } function createChartDrawing( figure: FigureData,