-
Notifications
You must be signed in to change notification settings - Fork 532
/
CollaborativeInput.tsx
154 lines (136 loc) · 4.6 KB
/
CollaborativeInput.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { SequenceDeltaEvent, SharedString } from "@fluidframework/sequence/internal";
import React from "react";
/**
* {@link CollaborativeInput} input props.
* @internal
*/
export interface ICollaborativeInputProps {
/**
* The SharedString that will store the input value.
*/
sharedString: SharedString;
/**
* Whether or not the control should be {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#readonly | read-only}.
* @defaultValue `false`
*/
readOnly?: boolean;
/**
* Whether spellCheck should be enabled.
* @defaultValue `true`
*/
spellCheck?: boolean;
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
onInput?: (sharedString: SharedString) => void;
}
/**
* {@link CollaborativeInput} component state.
* @internal
*/
export interface ICollaborativeInputState {
selectionEnd: number;
selectionStart: number;
}
/**
* Given a {@link @fluidframework/sequence#SharedString}, will produce a collaborative input element.
* @internal
*/
export class CollaborativeInput extends React.Component<
ICollaborativeInputProps,
ICollaborativeInputState
> {
private readonly inputElementRef: React.RefObject<HTMLInputElement>;
constructor(props: ICollaborativeInputProps) {
super(props);
this.inputElementRef = React.createRef<HTMLInputElement>();
this.state = {
selectionEnd: 0,
selectionStart: 0,
};
this.handleInput = this.handleInput.bind(this);
this.updateSelection = this.updateSelection.bind(this);
}
public componentDidMount() {
// Sets an event listener so we can update our state as the value changes
this.props.sharedString.on("sequenceDelta", (ev: SequenceDeltaEvent) => {
if (!ev.isLocal) {
this.updateInputFromSharedString();
}
});
this.updateInputFromSharedString();
}
public componentDidUpdate(prevProps: ICollaborativeInputProps) {
// If the component gets a new sharedString props it needs to re-fetch the sharedString text
if (prevProps.sharedString !== this.props.sharedString) {
this.updateInputFromSharedString();
}
}
public render() {
return (
// There are a lot of different ways content can be inserted into a input box
// and not all of them trigger a onBeforeInput event. To ensure we are grabbing
// the correct selection before we modify the shared string we need to make sure
// this.updateSelection is being called for multiple cases.
<input
className={this.props.className}
style={this.props.style}
readOnly={this.props.readOnly ?? false}
spellCheck={this.props.spellCheck ?? true}
ref={this.inputElementRef}
disabled={this.props.disabled}
onBeforeInput={this.updateSelection}
onKeyDown={this.updateSelection}
onClick={this.updateSelection}
onContextMenu={this.updateSelection}
onInput={this.handleInput}
/>
);
}
private updateInputFromSharedString() {
const text = this.props.sharedString.getText();
if (this.inputElementRef.current && this.inputElementRef.current.value !== text) {
this.inputElementRef.current.value = text;
}
}
private readonly handleInput = (ev: React.FormEvent<HTMLInputElement>) => {
// We need to set the value here to keep the input responsive to the user
const newText = ev.currentTarget.value;
// Get the new caret position and use that to get the text that was inserted
const newPosition = ev.currentTarget.selectionStart ?? 0;
const isTextInserted = newPosition - this.state.selectionStart > 0;
if (isTextInserted) {
const insertedText = newText.substring(this.state.selectionStart, newPosition);
const changeRangeLength = this.state.selectionEnd - this.state.selectionStart;
if (changeRangeLength === 0) {
this.props.sharedString.insertText(this.state.selectionStart, insertedText);
} else {
this.props.sharedString.replaceText(
this.state.selectionStart,
this.state.selectionEnd,
insertedText,
);
}
} else {
this.props.sharedString.removeText(newPosition, this.state.selectionEnd);
}
this.props.onInput?.(this.props.sharedString);
};
/**
* Update the current caret selection.
* We need to do this before we do any handleInput action or we will have lost our
* cursor position and not be able to accurately update the shared string.
*/
private readonly updateSelection = () => {
if (!this.inputElementRef.current) {
return;
}
const selectionEnd = this.inputElementRef.current.selectionEnd ?? 0;
const selectionStart = this.inputElementRef.current.selectionStart ?? 0;
this.setState({ selectionEnd, selectionStart });
};
}