Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] highlight: add drag & drop to array formula highlight #4083

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/composer/composer/composer_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@ export class ComposerStore extends SpreadsheetStore {
zone,
color: rangeColor(rangeString),
sheetId: range.sheetId,
interactive: true,
movable: true,
resizable: true,
};
});
}
Expand Down
12 changes: 8 additions & 4 deletions src/components/grid/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import { useGridDrawing } from "../helpers/draw_grid_hook";
import { useAbsoluteBoundingRect } from "../helpers/position_hook";
import { updateSelectionWithArrowKeys } from "../helpers/selection_helpers";
import { useWheelHandler } from "../helpers/wheel_hook";
import { Highlight } from "../highlight/highlight/highlight";
import { HighlightOverlay } from "../highlight/highlight_overlay/highlight_overlay";
import { Menu, MenuState } from "../menu/menu";
import { CellPopoverStore } from "../popover";
import { Popover } from "../popover/popover";
Expand Down Expand Up @@ -126,7 +126,7 @@ export class Grid extends Component<Props, SpreadsheetChildEnv> {
Menu,
Autofill,
ClientTag,
Highlight,
HighlightOverlay,
Popover,
VerticalScrollBar,
HorizontalScrollBar,
Expand Down Expand Up @@ -192,8 +192,12 @@ export class Grid extends Component<Props, SpreadsheetChildEnv> {
this.hoveredCell.hover({ col, row });
}

get highlights() {
return this.highlightStore.highlights;
get interactiveHighlight() {
const activeSheetId = this.env.model.getters.getActiveSheetId();
return this.highlightStore.highlights.filter(
(highlight) =>
highlight.sheetId === activeSheetId && (highlight.resizable || highlight.movable)
);
}

get gridOverlayDimensions() {
Expand Down
7 changes: 2 additions & 5 deletions src/components/grid/grid.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,8 @@
<t t-if="env.model.getters.isGridSelectionActive()">
<Autofill position="getAutofillPosition()" isVisible="isAutofillVisible"/>
</t>
<t t-foreach="highlights" t-as="highlight" t-key="highlight_index">
<t
t-if="highlight.interactive and highlight.sheetId === env.model.getters.getActiveSheetId()">
<Highlight zone="highlight.zone" color="highlight.color"/>
</t>
<t t-foreach="interactiveHighlight" t-as="highlight" t-key="highlight_index">
<HighlightOverlay highlight="highlight"/>
</t>
<Menu
t-if="menuState.isOpen"
Expand Down
12 changes: 6 additions & 6 deletions src/components/highlight/border/border.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,18 @@ export class Border extends Component<Props, SpreadsheetChildEnv> {
const isVertical = ["w", "e"].includes(this.props.orientation);

const z = this.props.zone;
const margin = 2;

const rect = this.env.model.getters.getVisibleRect(z);

const left = rect.x;
const right = rect.x + rect.width - 2 * margin;
const right = rect.x + rect.width;
const top = rect.y;
const bottom = rect.y + rect.height - 2 * margin;
const bottom = rect.y + rect.height;

const lineWidth = 4;
const leftValue = isLeft ? left : right;
const topValue = isTop ? top : bottom;
const lineWidth = 8;
const centeringOffset = lineWidth / 2;
const leftValue = (isLeft ? left : right) - (isVertical ? centeringOffset : 0);
const topValue = (isTop ? top : bottom) - (isHorizontal ? centeringOffset : 0);
const widthValue = isHorizontal ? right - left : lineWidth;
const heightValue = isVertical ? bottom - top : lineWidth;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, useState } from "@odoo/owl";
import { ComponentsImportance } from "../../../constants";
import { clip, isEqual } from "../../../helpers";
import { Color, HeaderIndex, Pixel, SpreadsheetChildEnv, Zone } from "../../../types";
import { HeaderIndex, Highlight, Pixel, SpreadsheetChildEnv, Zone } from "../../../types";
import { css } from "../../helpers/css";
import { gridOverlayPosition } from "../../helpers/dom_helpers";
import { dragAndDropBeyondTheViewport } from "../../helpers/drag_and_drop";
Expand All @@ -15,19 +15,15 @@ css/*SCSS*/ `
`;

interface Props {
zone: Zone;
color: Color;
highlight: Highlight;
}

interface HighlightState {
shiftingMode: "isMoving" | "isResizing" | "none";
}
export class Highlight extends Component<Props, SpreadsheetChildEnv> {
static template = "o-spreadsheet-Highlight";
static props = {
zone: Object,
color: String,
};
export class HighlightOverlay extends Component<Props, SpreadsheetChildEnv> {
static template = "o-spreadsheet-HighlightOverlay";
static props = { highlight: Object };
static components = {
Corner,
Border,
Expand All @@ -38,9 +34,12 @@ export class Highlight extends Component<Props, SpreadsheetChildEnv> {
});

onResizeHighlight(isLeft: boolean, isTop: boolean) {
if (!this.isResizable) {
return;
}
const activeSheetId = this.env.model.getters.getActiveSheetId();
this.highlightState.shiftingMode = "isResizing";
const z = this.props.zone;
const z = this.props.highlight.zone;

const pivotCol = isLeft ? z.right : z.left;
const pivotRow = isTop ? z.bottom : z.top;
Expand Down Expand Up @@ -85,14 +84,18 @@ export class Highlight extends Component<Props, SpreadsheetChildEnv> {

const mouseUp = () => {
this.highlightState.shiftingMode = "none";
this.env.model.dispatch("STOP_CHANGE_HIGHLIGHT");
};

dragAndDropBeyondTheViewport(this.env, mouseMove, mouseUp);
}

onMoveHighlight(clientX: Pixel, clientY: Pixel) {
if (!this.isMovable) {
return;
}
this.highlightState.shiftingMode = "isMoving";
const z = this.props.zone;
const z = this.props.highlight.zone;

const position = gridOverlayPosition();
const activeSheetId = this.env.model.getters.getActiveSheetId();
Expand Down Expand Up @@ -141,8 +144,17 @@ export class Highlight extends Component<Props, SpreadsheetChildEnv> {

const mouseUp = () => {
this.highlightState.shiftingMode = "none";
this.env.model.dispatch("STOP_CHANGE_HIGHLIGHT");
};

dragAndDropBeyondTheViewport(this.env, mouseMove, mouseUp);
}

get isResizable() {
return this.props.highlight.resizable;
}

get isMovable() {
return this.props.highlight.movable;
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
<templates>
<t t-name="o-spreadsheet-Highlight">
<t t-name="o-spreadsheet-HighlightOverlay">
<div class="o-highlight" t-ref="highlight">
<t t-foreach="['n', 's', 'w', 'e']" t-as="orientation" t-key="orientation">
<t t-if="isMovable" t-foreach="['n', 's', 'w', 'e']" t-as="orientation" t-key="orientation">
<Border
onMoveHighlight="(x, y) => this.onMoveHighlight(x,y)"
isMoving='highlightState.shiftingMode === "isMoving"'
orientation="orientation"
zone="props.zone"
zone="props.highlight.zone"
/>
</t>
<t t-foreach="['nw', 'ne', 'sw', 'se']" t-as="orientation" t-key="orientation">
<t
t-if="isResizable"
t-foreach="['nw', 'ne', 'sw', 'se']"
t-as="orientation"
t-key="orientation">
<Corner
onResizeHighlight="(isLeft, isTop) => this.onResizeHighlight(isLeft, isTop)"
isResizing='highlightState.shiftingMode === "isResizing"'
orientation="orientation"
zone="props.zone"
color="props.color"
zone="props.highlight.zone"
color="props.highlight.color"
/>
</t>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/selection_input/selection_input_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,8 @@ export class SelectionInputStore extends SpreadsheetStore {
zone: this.getters.getRangeFromSheetXC(this.inputSheetId, xc).zone,
sheetId: (sheetName && this.getters.getSheetIdByName(sheetName)) || this.inputSheetId,
color,
interactive: true,
movable: true,
resizable: true,
};
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { debounce, getSearchRegex, isInside, positionToZone } from "../../../helpers";
import {
debounce,
getSearchRegex,
isInside,
positionToZone,
withOneHistoryStep,
} from "../../../helpers";
import { HighlightProvider, HighlightStore } from "../../../stores/highlight_store";
import { CellPosition, Color, Command, Highlight } from "../../../types";

import { toRaw } from "@odoo/owl";
import { canonicalizeNumberContent } from "../../../helpers/locale";
import { Get } from "../../../store_engine";
import { SpreadsheetStore } from "../../../stores";
import { SearchOptions } from "../../../types/find_and_replace";
Expand Down Expand Up @@ -280,26 +287,51 @@ export class FindAndReplaceStore extends SpreadsheetStore implements HighlightPr
return;
}

this.model.dispatch("REPLACE_SEARCH", {
searchString: this.toSearch,
replaceWith: this.toReplace,
matches: [this.searchMatches[this.selectedMatchIndex]],
searchOptions: this.searchOptions,
});
this.replaceMatch(
this.searchMatches[this.selectedMatchIndex],
this.toSearch,
this.toReplace,
this.searchOptions
);
this.selectNextCell(Direction.next);
}
/**
* Apply the replace function to all the matches one time.
*/
replaceAll() {
this.model.dispatch("REPLACE_SEARCH", {
searchString: this.toSearch,
replaceWith: this.toReplace,
matches: this.searchMatches,
searchOptions: this.searchOptions,
withOneHistoryStep(this.model, () => {
for (const match of this.searchMatches) {
this.replaceMatch(match, this.toSearch, this.toReplace, this.searchOptions);
}
});
}

private replaceMatch(
selectedMatch: CellPosition,
searchString: string,
replaceWith: string,
searchOptions: SearchOptions
) {
const cell = this.getters.getCell(selectedMatch);
if (!cell?.content) {
return;
}

if (cell?.isFormula && !searchOptions.searchFormulas) {
return;
}

const searchRegex = getSearchRegex(searchString, searchOptions);
const replaceRegex = new RegExp(searchRegex.source, searchRegex.flags + "g");
const toReplace: string | null = this.getters.getCellText(
selectedMatch,
searchOptions.searchFormulas
);
const content = toReplace.replace(replaceRegex, replaceWith);
const canonicalContent = canonicalizeNumberContent(content, this.getters.getLocale());
this.model.dispatch("UPDATE_CELL", { ...selectedMatch, content: canonicalContent });
}

private getSearchableString(position: CellPosition): string {
return this.getters.getCellText(position, this.searchOptions.searchFormulas);
}
Expand Down
5 changes: 5 additions & 0 deletions src/helpers/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Miscellaneous
//------------------------------------------------------------------------------
import { NEWLINE } from "../constants";
import { Model } from "../model";
import { ConsecutiveIndexes, Lazy, UID } from "../types";
import { SearchOptions } from "../types/find_and_replace";
import { Cloneable, DebouncedFunction } from "./../types/misc";
Expand Down Expand Up @@ -551,3 +552,7 @@ export function largeMin(array: number[]) {
}
return min;
}

export function withOneHistoryStep(model: Model, callback: () => void) {
model.dispatch("BATCH_COMMANDS", { callback });
}
20 changes: 10 additions & 10 deletions src/helpers/recompute_zones.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { deepEqualsArray } from "../helpers/misc";
import { UnboundedZone } from "../types";
import { UnboundedZone, Zone } from "../types";

/**
* ####################################################
Expand Down Expand Up @@ -128,10 +128,10 @@ import { UnboundedZone } from "../types";
* - you will find coordinate of a cell only once among all the zones
* - the number of zones will be reduced to the minimum
*/
export function recomputeZones(
zones: UnboundedZone[],
zonesToRemove: UnboundedZone[]
): UnboundedZone[] {
export function recomputeZones<T extends Zone | UnboundedZone>(
zones: T[],
zonesToRemove: T[]
): T[] {
const profilesStartingPosition: number[] = [0];
const profiles = new Map<number, number[]>([[0, []]]);

Expand All @@ -140,10 +140,10 @@ export function recomputeZones(
return constructZonesFromProfiles(profilesStartingPosition, profiles);
}

export function modifyProfiles( // export for testing only
export function modifyProfiles<T extends Zone | UnboundedZone>( // export for testing only
profilesStartingPosition: number[],
profiles: Map<number, number[]>,
zones: UnboundedZone[],
zones: T[],
toRemove: boolean = false
) {
for (const zone of zones) {
Expand Down Expand Up @@ -298,10 +298,10 @@ function removeContiguousProfiles(
}
}

function constructZonesFromProfiles(
function constructZonesFromProfiles<T extends Zone | UnboundedZone>(
profilesStartingPosition: number[],
profiles: Map<number, number[]>
): UnboundedZone[] {
): T[] {
const mergedZone: UnboundedZone[] = [];
let pendingZones: UnboundedZone[] = [];
for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
Expand Down Expand Up @@ -353,7 +353,7 @@ function constructZonesFromProfiles(
pendingZones = nextPendingZones;
}
mergedZone.push(...pendingZones);
return mergedZone;
return mergedZone as T[];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really happy about that but it's mighty annoying to type...

The rationale is that, as far as I know, calling recomputeZones on only Zones (and no unbounded zones) will always return Zones. So it'd be nice if it was typed as such.

The fact that it's typed as returning UnboundedZones make it very annoying to use for simple cases where we only handle zones. Do you have any good ideas @laa-odoo ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this is never supposed to happen: recompute non-UnboundedZones will always give non-UnBoundedZones
But it will also be complicated to type the mechanics of recomputeZone.... The best would be to force the typing within recomputeZone itself so that it doesn't bother anyone. (
imo I am for)

}

function binaryPredecessorSearch(arr: number[], val: number, start = 0, matchEqual = true) {
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ import {
AutomaticSumPlugin,
CollaborativePlugin,
DataCleanupPlugin,
FindAndReplacePlugin,
FormatPlugin,
HeaderVisibilityUIPlugin,
SheetUIPlugin,
SortPlugin,
UIOptionsPlugin,
} from "./ui_feature";
import { BatchCommandsPlugin } from "./ui_feature/batch_commands";
import { CellComputedStylePlugin } from "./ui_feature/cell_computed_style";
import { HistoryPlugin } from "./ui_feature/local_history";
import { SplitToColumnsPlugin } from "./ui_feature/split_to_columns";
Expand Down Expand Up @@ -70,10 +70,10 @@ export const corePluginRegistry = new Registry<CorePluginConstructor>()

// Plugins which handle a specific feature, without handling any core commands
export const featurePluginRegistry = new Registry<UIPluginConstructor>()
.add("batch_commands", BatchCommandsPlugin)
.add("ui_sheet", SheetUIPlugin)
.add("ui_options", UIOptionsPlugin)
.add("autofill", AutofillPlugin)
.add("find_and_replace", FindAndReplacePlugin)
.add("sort", SortPlugin)
.add("automatic_sum", AutomaticSumPlugin)
.add("format", FormatPlugin)
Expand Down