initial hookup editor code with react component

pull/21833/head
Bruno Windels 2019-05-07 16:27:09 +02:00
parent 9f98a6c0e6
commit 76bb56a2bf
4 changed files with 75 additions and 18 deletions

View File

@ -17,12 +17,28 @@ limitations under the License.
.mx_MessageEditor { .mx_MessageEditor {
border-radius: 4px; border-radius: 4px;
background-color: #f3f8fd; background-color: #f3f8fd;
padding: 10px; padding: 11px 13px 7px 56px;
.editor { .editor {
border-radius: 4px; border-radius: 4px;
border: solid 1px #e9edf1; border: solid 1px #e9edf1;
background-color: #ffffff; background-color: #ffffff;
padding: 10px;
span {
display: inline-block;
padding: 0 5px;
border-radius: 4px;
color: white;
}
span.user-pill {
background: red;
}
span.room-pill {
background: green;
}
} }
.buttons { .buttons {
@ -39,4 +55,12 @@ limitations under the License.
font-weight: 600; font-weight: 600;
} }
} }
.model {
background: lightgrey;
padding: 5px;
display: block;
white-space: pre;
font-size: 12px;
}
} }

View File

@ -18,6 +18,9 @@ import sdk from '../../../index';
import {_t} from '../../../languageHandler'; import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import {PlainPart} from '../../../editor/parts';
import {getCaretOffset, setCaretPosition} from '../../../editor/caret';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
export default class MessageEditor extends React.Component { export default class MessageEditor extends React.Component {
@ -34,8 +37,24 @@ export default class MessageEditor extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = {}; const body = this.props.event.getContent().body;
this.model = new EditorModel();
this.model.update(body, undefined, {offset: body.length});
this.state = {
parts: this.model.serializeParts(),
};
this._onCancelClicked = this._onCancelClicked.bind(this); this._onCancelClicked = this._onCancelClicked.bind(this);
this._onInput = this._onInput.bind(this);
}
_onInput(event) {
const editor = event.target;
const caretOffset = getCaretOffset(editor);
const caret = this.model.update(editor.textContent, event.inputType, caretOffset);
const parts = this.model.serializeParts();
this.setState({parts}, () => {
setCaretPosition(editor, caret);
});
} }
_onCancelClicked() { _onCancelClicked() {
@ -43,14 +62,24 @@ export default class MessageEditor extends React.Component {
} }
render() { render() {
const parts = this.state.parts.map((p, i) => {
const key = `${i}-${p.type}`;
switch (p.type) {
case "plain": return p.text;
case "room-pill": return (<span key={key} className="room-pill">{p.text}</span>);
case "user-pill": return (<span key={key} className="user-pill">{p.text}</span>);
}
});
const modelOutput = JSON.stringify(this.state.parts, undefined, 2);
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div className="mx_MessageEditor"> return <div className="mx_MessageEditor">
<div className="editor" contentEditable="true"> <div className="editor" contentEditable="true" tabIndex="1" suppressContentEditableWarning={true} onInput={this._onInput}>
{this.props.event.getContent().body} {parts}
</div> </div>
<div className="buttons"> <div className="buttons">
<AccessibleButton onClick={this._onCancelClicked}>{_t("Cancel")}</AccessibleButton> <AccessibleButton onClick={this._onCancelClicked}>{_t("Cancel")}</AccessibleButton>
</div> </div>
<code className="model">{modelOutput}</code>
</div>; </div>;
} }
} }

View File

@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export function getCaretPosition(editor) { export function getCaretOffset(editor) {
const sel = document.getSelection(); const sel = document.getSelection();
const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length;
let position = sel.focusOffset; let offset = sel.focusOffset;
let node = sel.focusNode; let node = sel.focusNode;
// when deleting the last character of a node, // when deleting the last character of a node,
// the caret gets reported as being after the focusOffset-th node, // the caret gets reported as being after the focusOffset-th node,
// with the focusNode being the editor // with the focusNode being the editor
if (node === editor) { if (node === editor) {
let position = 0; let offset = 0;
for (let i = 0; i < sel.focusOffset; ++i) { for (let i = 0; i < sel.focusOffset; ++i) {
position += editor.childNodes[i].textContent.length; offset += editor.childNodes[i].textContent.length;
} }
return {position, atNodeEnd: false}; return {offset, atNodeEnd: false};
} }
// first make sure we're at the level of a direct child of editor // first make sure we're at the level of a direct child of editor
@ -36,7 +36,7 @@ export function getCaretPosition(editor) {
// include all preceding siblings of the non-direct editor children // include all preceding siblings of the non-direct editor children
while (node.previousSibling) { while (node.previousSibling) {
node = node.previousSibling; node = node.previousSibling;
position += node.textContent.length; offset += node.textContent.length;
} }
// then move up // then move up
// I guess technically there could be preceding text nodes in the parents here as well, // I guess technically there could be preceding text nodes in the parents here as well,
@ -48,13 +48,13 @@ export function getCaretPosition(editor) {
// now include the text length of all preceding direct editor children // now include the text length of all preceding direct editor children
while (node.previousSibling) { while (node.previousSibling) {
node = node.previousSibling; node = node.previousSibling;
position += node.textContent.length; offset += node.textContent.length;
} }
{ // {
const {focusOffset, focusNode} = sel; // const {focusOffset, focusNode} = sel;
console.log("selection", {focusOffset, focusNode, position, atNodeEnd}); // console.log("selection", {focusOffset, focusNode, position, atNodeEnd});
} // }
return {position, atNodeEnd}; return {offset, atNodeEnd};
} }
export function setCaretPosition(editor, caretPosition) { export function setCaretPosition(editor, caretPosition) {

View File

@ -40,18 +40,22 @@ export default class EditorModel {
return this._parts; return this._parts;
} }
serializeParts() {
return this._parts.map(({type, text}) => {return {type, text};});
}
_diff(newValue, inputType, caret) { _diff(newValue, inputType, caret) {
if (inputType === "deleteByDrag") { if (inputType === "deleteByDrag") {
return diffDeletion(this._previousValue, newValue); return diffDeletion(this._previousValue, newValue);
} else { } else {
return diffAtCaret(this._previousValue, newValue, caret.position); return diffAtCaret(this._previousValue, newValue, caret.offset);
} }
} }
update(newValue, inputType, caret) { update(newValue, inputType, caret) {
const diff = this._diff(newValue, inputType, caret); const diff = this._diff(newValue, inputType, caret);
const position = this._positionForOffset(diff.at, caret.atNodeEnd); const position = this._positionForOffset(diff.at, caret.atNodeEnd);
console.log("update at", {position, diff}); console.log("update at", {position, diff, newValue, prevValue: this._previousValue});
if (diff.removed) { if (diff.removed) {
this._removeText(position, diff.removed.length); this._removeText(position, diff.removed.length);
} }