Skip to content

Commit

Permalink
[FIX] Composer: Persistent composition when starting the edition
Browse files Browse the repository at this point in the history
When inputing a character in the grid, we capture the character and use it
to start the edition, which opens a new composer and focus it. This
change of focus closes the Input Method Editor if present. This
typically affect people from Asia writing in their native language
(chinese, japanese, etc)

This revision addresses the issue by replacing the hidden input in the
grid with the `GridComposer`. The later will therefore always be in the
DOM, capture the characters with the IME and then change its position
based on the edition mode.

closes #3456

Task: 3685891
Signed-off-by: Lucas Lefèvre (lul) <[email protected]>
  • Loading branch information
rrahir committed Feb 6, 2024
1 parent b6c608e commit d0a8860
Show file tree
Hide file tree
Showing 20 changed files with 278 additions and 200 deletions.
93 changes: 63 additions & 30 deletions src/components/composer/composer/composer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, onMounted, onPatched, onWillUnmount, useRef, useState } from "@odoo/owl";
import { Component, onMounted, onPatched, useRef, useState } from "@odoo/owl";
import { ComponentsImportance, DEFAULT_FONT, SELECTION_BORDER_COLOR } from "../../../constants";
import { EnrichedToken } from "../../../formulas/index";
import { functionRegistry } from "../../../functions/index";
import { isEqual, rangeReference, splitReference, zoneToDimension } from "../../../helpers/index";
import { ComposerSelection, SelectionIndicator } from "../../../plugins/ui/edition";
import { SelectionIndicator } from "../../../plugins/ui/edition";
import {
Color,
DOMDimension,
Expand Down Expand Up @@ -93,13 +93,14 @@ css/* scss */ `
}
`;

interface Props {
export interface ComposerProps {
inputStyle: string;
rect?: Rect;
delimitation?: DOMDimension;
focus: "inactive" | "cellFocus" | "contentFocus";
onComposerUnmounted?: () => void;
onComposerContentFocused: (selection: ComposerSelection) => void;
onComposerContentFocused: () => void;
onComposerCellFocused?: (content: String) => void;
isDefaultFocus?: boolean;
}

interface ComposerState {
Expand All @@ -120,12 +121,13 @@ interface FunctionDescriptionState {
argToFocus: number;
}

export class Composer extends Component<Props, SpreadsheetChildEnv> {
export class Composer extends Component<ComposerProps, SpreadsheetChildEnv> {
static template = "o-spreadsheet-Composer";
static components = { TextValueProvider, FunctionDescriptionProvider };
static defaultProps = {
inputStyle: "",
focus: "inactive",
isDefaultFocus: false,
};

composerRef = useRef("o_composer");
Expand Down Expand Up @@ -195,15 +197,13 @@ export class Composer extends Component<Props, SpreadsheetChildEnv> {
setup() {
onMounted(() => {
const el = this.composerRef.el!;

if (this.props.isDefaultFocus) {
this.env.focusableElement.setFocusableElement(el);
}
this.contentHelper.updateEl(el);
this.processContent();
});

onWillUnmount(() => {
this.props.onComposerUnmounted?.();
});

onPatched(() => {
if (!this.isKeyStillDown) {
this.processContent();
Expand All @@ -216,7 +216,10 @@ export class Composer extends Component<Props, SpreadsheetChildEnv> {
// ---------------------------------------------------------------------------

private processArrowKeys(ev: KeyboardEvent) {
if (this.env.model.getters.isSelectingForComposer()) {
if (
this.env.model.getters.isSelectingForComposer() ||
this.env.model.getters.getEditionMode() === "inactive"
) {
this.functionDescriptionState.showDescription = false;
// Prevent the default content editable behavior which moves the cursor
// but don't stop the event and let it bubble to the grid which will
Expand Down Expand Up @@ -253,21 +256,22 @@ export class Composer extends Component<Props, SpreadsheetChildEnv> {
private processTabKey(ev: KeyboardEvent) {
ev.preventDefault();
ev.stopPropagation();
if (this.autoCompleteState.showProvider && this.autocompleteAPI) {
const autoCompleteValue = this.autocompleteAPI.getValueToFill();
if (autoCompleteValue) {
this.autoComplete(autoCompleteValue);
return;
if (this.env.model.getters.getEditionMode() !== "inactive") {
if (this.autoCompleteState.showProvider && this.autocompleteAPI) {
const autoCompleteValue = this.autocompleteAPI.getValueToFill();
if (autoCompleteValue) {
this.autoComplete(autoCompleteValue);
return;
}
} else {
// when completing with tab, if there is no value to complete, the active cell will be moved to the right.
// we can't let the model think that it is for a ref selection.
// todo: check if this can be removed someday
this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
}
} else {
// when completing with tab, if there is no value to complete, the active cell will be moved to the right.
// we can't let the model think that it is for a ref selection.
// todo: check if this can be removed someday
this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
this.env.model.dispatch("STOP_EDITION");
}

const direction = ev.shiftKey ? "left" : "right";
this.env.model.dispatch("STOP_EDITION");
this.env.model.selection.moveAnchorCell(direction, 1);
}

Expand Down Expand Up @@ -304,6 +308,9 @@ export class Composer extends Component<Props, SpreadsheetChildEnv> {
}

onKeydown(ev: KeyboardEvent) {
if (this.env.model.getters.getEditionMode() === "inactive") {
return;
}
let handler = this.keyMapping[ev.key];
if (handler) {
handler.call(this, ev);
Expand All @@ -314,24 +321,50 @@ export class Composer extends Component<Props, SpreadsheetChildEnv> {
}

private updateCursorIfNeeded() {
if (!this.env.model.getters.isSelectingForComposer()) {
const moveCursor =
!this.env.model.getters.isSelectingForComposer() &&
!(this.env.model.getters.getEditionMode() === "inactive");
if (moveCursor) {
const { start, end } = this.contentHelper.getCurrentSelection();
this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", { start, end });
this.isKeyStillDown = true;
}
}

onPaste(ev: ClipboardEvent) {
if (this.env.model.getters.getEditionMode() !== "inactive") {
ev.stopPropagation();
}
}

/*
* Triggered automatically by the content-editable between the keydown and key up
* */
onInput() {
if (this.props.focus === "inactive" || !this.shouldProcessInputEvents) {
onInput(ev: InputEvent) {
if (!this.shouldProcessInputEvents) {
return;
}
if (
ev.inputType === "insertFromPaste" &&
this.env.model.getters.getEditionMode() === "inactive"
) {
return;
}
ev.stopPropagation();
let content: string;

if (this.env.model.getters.getEditionMode() === "inactive") {
content = ev.data || "";
} else {
const el = this.composerRef.el! as HTMLInputElement;
content = el.childNodes.length ? el.textContent! : "";
}
if (this.props.focus === "inactive") {
return this.props.onComposerCellFocused?.(content);
}
this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
const el = this.composerRef.el! as HTMLInputElement;
this.env.model.dispatch("SET_CURRENT_CONTENT", {
content: el.childNodes.length ? el.textContent! : "",
content,
selection: this.contentHelper.getCurrentSelection(),
});
}
Expand Down Expand Up @@ -395,8 +428,8 @@ export class Composer extends Component<Props, SpreadsheetChildEnv> {
const newSelection = this.contentHelper.getCurrentSelection();

this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
this.props.onComposerContentFocused();
if (this.props.focus === "inactive") {
this.props.onComposerContentFocused(newSelection);
}
this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", newSelection);
this.processTokenAtCursor();
Expand Down
2 changes: 1 addition & 1 deletion src/components/composer/composer/composer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
t-on-keyup="onKeyup"
t-on-click="onClick"
t-on-blur="onBlur"
t-on-paste.stop=""
t-on-paste="onPaste"
t-on-compositionstart="onCompositionStart"
t-on-compositionend="onCompositionEnd"
/>
Expand Down
107 changes: 57 additions & 50 deletions src/components/composer/grid_composer/grid_composer.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { Component, onMounted, useRef, useState } from "@odoo/owl";
import { Component, onWillUpdateProps } from "@odoo/owl";
import {
ComponentsImportance,
DEFAULT_CELL_HEIGHT,
SELECTION_BORDER_COLOR,
} from "../../../constants";
import { fontSizeMap } from "../../../fonts";
import { DOMDimension, Rect, Ref, SpreadsheetChildEnv, Zone } from "../../../types/index";
import { positionToZone } from "../../../helpers";
import { Rect, SpreadsheetChildEnv } from "../../../types/index";
import { getTextDecoration } from "../../helpers";
import { css } from "../../helpers/css";
import { Composer } from "../composer/composer";
import { Composer, ComposerProps } from "../composer/composer";
import { Style } from "./../../../types/misc";

const SCROLLBAR_WIDTH = 14;
const SCROLLBAR_HIGHT = 15;

const COMPOSER_BORDER_WIDTH = 3 * 0.4 * window.devicePixelRatio || 1;
css/* scss */ `
div.o-grid-composer {
Expand All @@ -24,15 +22,11 @@ css/* scss */ `
}
`;

interface ComposerState {
rect: Rect | null;
delimitation: DOMDimension | null;
}

interface Props {
focus: "inactive" | "cellFocus" | "contentFocus";
content: string;
onComposerUnmounted: () => void;
onComposerContentFocused: () => void;
onComposerCellFocused: () => void;
}

/**
Expand All @@ -43,53 +37,59 @@ export class GridComposer extends Component<Props, SpreadsheetChildEnv> {
static template = "o-spreadsheet-GridComposer";
static components = { Composer };

private gridComposerRef!: Ref<HTMLElement>;

private zone!: Zone;
private rect!: Rect;
private rect: Rect | undefined = undefined;
private isEditing: boolean = false;

private composerState!: ComposerState;
get defaultRect() {
return { x: 0, y: 0, width: 0, height: 0 };
}

setup() {
this.gridComposerRef = useRef("gridComposer");
this.composerState = useState({
rect: null,
delimitation: null,
});
const { col, row } = this.env.model.getters.getPosition();
this.zone = this.env.model.getters.expandZone(this.env.model.getters.getActiveSheetId(), {
left: col,
right: col,
top: row,
bottom: row,
});
this.rect = this.env.model.getters.getVisibleRect(this.zone);
onMounted(() => {
const el = this.gridComposerRef.el!;

//TODO Should be more correct to have a props that give the parent's clientHeight and clientWidth
const maxHeight = el.parentElement!.clientHeight - this.rect.y - SCROLLBAR_HIGHT;
el.style.maxHeight = (maxHeight + "px") as string;

const maxWidth = el.parentElement!.clientWidth - this.rect.x - SCROLLBAR_WIDTH;
el.style.maxWidth = (maxWidth + "px") as string;

this.composerState.rect = {
x: this.rect.x,
y: this.rect.y,
width: el!.clientWidth,
height: el!.clientHeight,
};
this.composerState.delimitation = {
width: el!.parentElement!.clientWidth,
height: el!.parentElement!.clientHeight,
};
onWillUpdateProps(() => {
const isEditing = this.env.model.getters.getEditionMode() !== "inactive";
if (this.isEditing !== isEditing) {
this.isEditing = isEditing;
if (!isEditing) {
this.rect = undefined;
this.env.focusableElement.focus();
return;
}
const position = this.env.model.getters.getPosition();
const zone = this.env.model.getters.expandZone(
this.env.model.getters.getActiveSheetId(),
positionToZone(position)
);
this.rect = this.env.model.getters.getVisibleRect(zone);
}
});
}

get composerProps(): ComposerProps {
const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
return {
rect: this.rect && { ...this.rect },
delimitation: {
width,
height,
},
inputStyle: this.composerStyle,
focus: this.props.focus,
isDefaultFocus: true,
onComposerContentFocused: this.props.onComposerContentFocused,
onComposerCellFocused: this.props.onComposerCellFocused,
};
}

get containerStyle(): string {
if (this.env.model.getters.getEditionMode() === "inactive" || !this.rect) {
return `
position: absolute;
z-index: -1000;
`;
}
const isFormula = this.env.model.getters.getCurrentContent().startsWith("=");
const cell = this.env.model.getters.getActiveCell();

let style: Style = {};
if (cell) {
const cellPosition = this.env.model.getters.getCellPosition(cell.id);
Expand Down Expand Up @@ -119,13 +119,20 @@ export class GridComposer extends Component<Props, SpreadsheetChildEnv> {
if (!isFormula) {
textAlign = style.align || cell?.defaultAlign || "left";
}
const sheetDimensions = this.env.model.getters.getSheetViewDimensionWithHeaders();

const maxWidth = sheetDimensions.width - this.rect.x;
const maxHeight = sheetDimensions.height - this.rect.y;

return `
left: ${left - 1}px;
top: ${top}px;
min-width: ${width + 2}px;
min-height: ${height + 1}px;
max-width: ${maxWidth}px;
max-height: ${maxHeight}px;
background: ${background};
color: ${color};
Expand Down
9 changes: 1 addition & 8 deletions src/components/composer/grid_composer/grid_composer.xml
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
<templates>
<t t-name="o-spreadsheet-GridComposer" owl="1">
<div class="o-grid-composer" t-att-style="containerStyle" t-ref="gridComposer">
<Composer
focus="props.focus"
inputStyle="composerStyle"
rect="composerState.rect"
delimitation="composerState.delimitation"
onComposerUnmounted="props.onComposerUnmounted"
onComposerContentFocused="props.onComposerContentFocused"
/>
<Composer t-props="composerProps"/>
</div>
</t>
</templates>
Loading

0 comments on commit d0a8860

Please sign in to comment.