2015-07-15 13:52:27 +01:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2018-2024 New Vector Ltd.
|
2016-01-07 04:06:39 +00:00
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2015-07-15 13:52:27 +01:00
|
|
|
|
2024-09-09 14:57:16 +01:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2015-07-15 13:52:27 +01:00
|
|
|
*/
|
|
|
|
|
|
2021-06-29 13:11:58 +01:00
|
|
|
import React, { createRef } from "react";
|
2021-10-22 17:23:32 -05:00
|
|
|
|
2022-02-28 17:05:52 +01:00
|
|
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|
|
|
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
2015-07-15 13:52:27 +01:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
enum Phases {
|
|
|
|
|
Display = "display",
|
|
|
|
|
Edit = "edit",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IProps {
|
|
|
|
|
onValueChanged?: (value: string, shouldSubmit: boolean) => void;
|
2023-03-27 08:01:09 +01:00
|
|
|
initialValue: string;
|
|
|
|
|
label: string;
|
|
|
|
|
placeholder: string;
|
|
|
|
|
className: string;
|
2021-09-16 19:33:17 +02:00
|
|
|
labelClassName?: string;
|
2023-03-27 08:01:09 +01:00
|
|
|
placeholderClassName: string;
|
2021-09-16 19:33:17 +02:00
|
|
|
// Overrides blurToSubmit if true
|
|
|
|
|
blurToCancel?: boolean;
|
|
|
|
|
// Will cause onValueChanged(value, true) to fire on blur
|
2023-03-27 08:01:09 +01:00
|
|
|
blurToSubmit: boolean;
|
|
|
|
|
editable: boolean;
|
2021-09-16 19:33:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IState {
|
|
|
|
|
phase: Phases;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default class EditableText extends React.Component<IProps, IState> {
|
|
|
|
|
// we track value as an JS object field rather than in React state
|
|
|
|
|
// as React doesn't play nice with contentEditable.
|
|
|
|
|
public value = "";
|
|
|
|
|
private placeholder = false;
|
|
|
|
|
private editableDiv = createRef<HTMLDivElement>();
|
2015-07-15 13:52:27 +01:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
public static defaultProps: Partial<IProps> = {
|
2020-08-29 12:14:16 +01:00
|
|
|
onValueChanged() {},
|
|
|
|
|
initialValue: "",
|
|
|
|
|
label: "",
|
|
|
|
|
placeholder: "",
|
|
|
|
|
editable: true,
|
|
|
|
|
className: "mx_EditableText",
|
|
|
|
|
placeholderClassName: "mx_EditableText_placeholder",
|
|
|
|
|
blurToSubmit: false,
|
|
|
|
|
};
|
2015-07-15 13:52:27 +01:00
|
|
|
|
2022-12-16 12:29:59 +00:00
|
|
|
public constructor(props: IProps) {
|
2020-08-29 12:14:16 +01:00
|
|
|
super(props);
|
2015-07-15 13:52:27 +01:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
this.state = {
|
|
|
|
|
phase: Phases.Display,
|
|
|
|
|
};
|
2020-08-29 12:14:16 +01:00
|
|
|
}
|
|
|
|
|
|
2022-11-18 09:22:43 +00:00
|
|
|
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
|
|
|
|
if (prevProps.initialValue !== this.props.initialValue) {
|
|
|
|
|
this.value = this.props.initialValue;
|
2021-09-16 19:33:17 +02:00
|
|
|
if (this.editableDiv.current) {
|
2020-08-29 12:14:16 +01:00
|
|
|
this.showPlaceholder(!this.value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
public componentDidMount(): void {
|
2016-01-10 12:56:45 +00:00
|
|
|
this.value = this.props.initialValue;
|
2021-09-16 19:33:17 +02:00
|
|
|
if (this.editableDiv.current) {
|
2016-01-10 12:56:45 +00:00
|
|
|
this.showPlaceholder(!this.value);
|
|
|
|
|
}
|
2020-08-29 12:14:16 +01:00
|
|
|
}
|
2016-01-10 12:56:45 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private showPlaceholder = (show: boolean): void => {
|
2023-03-29 08:23:54 +01:00
|
|
|
if (!this.editableDiv.current) return;
|
2016-01-10 12:56:45 +00:00
|
|
|
if (show) {
|
2021-09-16 19:33:17 +02:00
|
|
|
this.editableDiv.current.textContent = this.props.placeholder;
|
|
|
|
|
this.editableDiv.current.setAttribute(
|
|
|
|
|
"class",
|
2019-12-08 12:16:17 +00:00
|
|
|
this.props.className + " " + this.props.placeholderClassName,
|
|
|
|
|
);
|
2016-01-10 12:56:45 +00:00
|
|
|
this.placeholder = true;
|
|
|
|
|
this.value = "";
|
2017-10-11 17:56:17 +01:00
|
|
|
} else {
|
2021-09-16 19:33:17 +02:00
|
|
|
this.editableDiv.current.textContent = this.value;
|
|
|
|
|
this.editableDiv.current.setAttribute("class", this.props.className);
|
2016-01-10 12:56:45 +00:00
|
|
|
this.placeholder = false;
|
2016-07-29 17:35:48 +01:00
|
|
|
}
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-09-16 14:18:25 +01:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private cancelEdit = (): void => {
|
2015-09-16 14:18:25 +01:00
|
|
|
this.setState({
|
2021-09-16 19:33:17 +02:00
|
|
|
phase: Phases.Display,
|
2015-07-15 13:52:27 +01:00
|
|
|
});
|
2016-01-17 02:48:55 +00:00
|
|
|
this.value = this.props.initialValue;
|
|
|
|
|
this.showPlaceholder(!this.value);
|
2015-09-16 14:18:25 +01:00
|
|
|
this.onValueChanged(false);
|
2023-03-27 08:01:09 +01:00
|
|
|
this.editableDiv.current?.blur();
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-07-15 13:52:27 +01:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private onValueChanged = (shouldSubmit: boolean): void => {
|
2023-03-27 08:01:09 +01:00
|
|
|
this.props.onValueChanged?.(this.value, shouldSubmit);
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2016-01-10 12:56:45 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
2016-01-10 12:56:45 +00:00
|
|
|
if (this.placeholder) {
|
2016-01-17 02:48:55 +00:00
|
|
|
this.showPlaceholder(false);
|
2016-01-10 12:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-28 17:05:52 +01:00
|
|
|
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
|
|
|
|
switch (action) {
|
|
|
|
|
case KeyBindingAction.Enter:
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
break;
|
2016-01-10 12:56:45 +00:00
|
|
|
}
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-11-26 15:16:50 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
|
|
|
if (!(ev.target as HTMLDivElement).textContent) {
|
2016-01-10 12:56:45 +00:00
|
|
|
this.showPlaceholder(true);
|
2017-10-11 17:56:17 +01:00
|
|
|
} else if (!this.placeholder) {
|
2023-03-29 08:23:54 +01:00
|
|
|
this.value = (ev.target as HTMLDivElement).textContent ?? "";
|
2016-01-10 12:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-28 17:05:52 +01:00
|
|
|
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
|
|
|
|
switch (action) {
|
|
|
|
|
case KeyBindingAction.Escape:
|
|
|
|
|
this.cancelEdit();
|
|
|
|
|
break;
|
|
|
|
|
case KeyBindingAction.Enter:
|
|
|
|
|
this.onFinish(ev);
|
|
|
|
|
break;
|
2015-11-26 15:16:50 +00:00
|
|
|
}
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-11-26 15:16:50 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private onClickDiv = (): void => {
|
2016-01-13 13:15:13 +00:00
|
|
|
if (!this.props.editable) return;
|
|
|
|
|
|
2015-11-26 15:16:50 +00:00
|
|
|
this.setState({
|
2021-09-16 19:33:17 +02:00
|
|
|
phase: Phases.Edit,
|
2017-01-20 14:22:27 +00:00
|
|
|
});
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-11-26 15:16:50 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
2017-10-11 17:56:17 +01:00
|
|
|
const node = ev.target.childNodes[0];
|
2016-01-15 12:34:53 +00:00
|
|
|
if (node) {
|
2017-10-11 17:56:17 +01:00
|
|
|
const range = document.createRange();
|
2016-01-15 12:34:53 +00:00
|
|
|
range.setStart(node, 0);
|
2021-09-16 19:33:17 +02:00
|
|
|
range.setEnd(node, ev.target.childNodes.length);
|
2016-07-29 17:35:48 +01:00
|
|
|
|
2023-03-29 08:23:54 +01:00
|
|
|
const sel = window.getSelection()!;
|
2016-01-15 12:34:53 +00:00
|
|
|
sel.removeAllRanges();
|
|
|
|
|
sel.addRange(range);
|
|
|
|
|
}
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-11-26 15:16:50 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private onFinish = (
|
|
|
|
|
ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
|
2023-03-27 08:01:09 +01:00
|
|
|
shouldSubmit = false,
|
2021-09-16 19:33:17 +02:00
|
|
|
): void => {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
2017-10-11 17:56:17 +01:00
|
|
|
const self = this;
|
2022-02-28 17:05:52 +01:00
|
|
|
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
|
|
|
|
const submit = action === KeyBindingAction.Enter || shouldSubmit;
|
2016-01-10 12:56:45 +00:00
|
|
|
this.setState(
|
|
|
|
|
{
|
2021-09-16 19:33:17 +02:00
|
|
|
phase: Phases.Display,
|
2018-06-18 18:21:16 +01:00
|
|
|
},
|
|
|
|
|
() => {
|
2017-03-02 18:07:24 +00:00
|
|
|
if (this.value !== this.props.initialValue) {
|
|
|
|
|
self.onValueChanged(submit);
|
|
|
|
|
}
|
2016-01-10 12:56:45 +00:00
|
|
|
},
|
|
|
|
|
);
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-11-26 15:16:50 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
2023-03-29 08:23:54 +01:00
|
|
|
const sel = window.getSelection()!;
|
2016-01-14 17:24:52 +00:00
|
|
|
sel.removeAllRanges();
|
|
|
|
|
|
2018-06-18 18:21:16 +01:00
|
|
|
if (this.props.blurToCancel) {
|
|
|
|
|
this.cancelEdit();
|
|
|
|
|
} else {
|
|
|
|
|
this.onFinish(ev, this.props.blurToSubmit);
|
|
|
|
|
}
|
2016-01-17 02:48:55 +00:00
|
|
|
|
|
|
|
|
this.showPlaceholder(!this.value);
|
2020-08-29 12:14:16 +01:00
|
|
|
};
|
2015-12-23 16:02:18 +00:00
|
|
|
|
2023-02-13 17:01:43 +00:00
|
|
|
public render(): React.ReactNode {
|
2021-06-29 13:11:58 +01:00
|
|
|
const { className, editable, initialValue, label, labelClassName } = this.props;
|
2018-06-18 18:21:16 +01:00
|
|
|
let editableEl;
|
2015-11-26 15:16:50 +00:00
|
|
|
|
2021-09-16 19:33:17 +02:00
|
|
|
if (!editable || (this.state.phase === Phases.Display && (label || labelClassName) && !this.value)) {
|
2016-01-10 12:56:45 +00:00
|
|
|
// show the label
|
2018-06-18 18:21:16 +01:00
|
|
|
editableEl = (
|
|
|
|
|
<div className={className + " " + labelClassName} onClick={this.onClickDiv}>
|
|
|
|
|
{label || initialValue}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2016-01-10 12:56:45 +00:00
|
|
|
} else {
|
|
|
|
|
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
2021-04-27 17:23:27 +02:00
|
|
|
editableEl = (
|
|
|
|
|
<div
|
2021-09-16 19:33:17 +02:00
|
|
|
ref={this.editableDiv}
|
2021-04-27 17:23:27 +02:00
|
|
|
contentEditable={true}
|
|
|
|
|
className={className}
|
|
|
|
|
onKeyDown={this.onKeyDown}
|
|
|
|
|
onKeyUp={this.onKeyUp}
|
|
|
|
|
onFocus={this.onFocus}
|
|
|
|
|
onBlur={this.onBlur}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2015-11-26 15:16:50 +00:00
|
|
|
}
|
|
|
|
|
|
2018-06-18 18:21:16 +01:00
|
|
|
return editableEl;
|
2020-08-29 12:14:16 +01:00
|
|
|
}
|
|
|
|
|
}
|