Files
ThreadNet-Web/src/components/views/elements/EditableText.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

226 lines
6.8 KiB
TypeScript
Raw Normal View History

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
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;
initialValue: string;
label: string;
placeholder: string;
className: string;
2021-09-16 19:33:17 +02:00
labelClassName?: string;
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
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
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
}
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 {
this.value = this.props.initialValue;
2021-09-16 19:33:17 +02:00
if (this.editableDiv.current) {
this.showPlaceholder(!this.value);
}
2020-08-29 12:14:16 +01:00
}
2021-09-16 19:33:17 +02:00
private showPlaceholder = (show: boolean): void => {
if (!this.editableDiv.current) return;
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,
);
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);
this.placeholder = false;
}
2020-08-29 12:14:16 +01:00
};
2021-09-16 19:33:17 +02:00
private cancelEdit = (): void => {
this.setState({
2021-09-16 19:33:17 +02:00
phase: Phases.Display,
2015-07-15 13:52:27 +01:00
});
this.value = this.props.initialValue;
this.showPlaceholder(!this.value);
this.onValueChanged(false);
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 => {
this.props.onValueChanged?.(this.value, shouldSubmit);
2020-08-29 12:14:16 +01:00
};
2021-09-16 19:33:17 +02:00
private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
if (this.placeholder) {
this.showPlaceholder(false);
}
const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Enter:
ev.stopPropagation();
ev.preventDefault();
break;
}
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) {
this.showPlaceholder(true);
2017-10-11 17:56:17 +01:00
} else if (!this.placeholder) {
this.value = (ev.target as HTMLDivElement).textContent ?? "";
}
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);
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>,
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;
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
const submit = action === KeyBindingAction.Enter || shouldSubmit;
this.setState(
{
2021-09-16 19:33:17 +02:00
phase: Phases.Display,
2018-06-18 18:21:16 +01:00
},
() => {
if (this.value !== this.props.initialValue) {
self.onValueChanged(submit);
}
},
);
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 => {
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);
}
this.showPlaceholder(!this.value);
2020-08-29 12:14:16 +01:00
};
2015-12-23 16:02:18 +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)) {
// show the label
2018-06-18 18:21:16 +01:00
editableEl = (
<div className={className + " " + labelClassName} onClick={this.onClickDiv}>
{label || initialValue}
</div>
);
} 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
}
}