Update to new version of slate

Lots of fixes here as a lot of the API has changed (eg. anchorKey /
offsetKey are now anchor.key and offset.key, and collapseFocusToThing
is moveFocusToThing).

Also changes the ref to a function (sorry for lumping this into the
same PR).

Hopefully will fix https://github.com/vector-im/riot-web/issues/7105
This commit is contained in:
David Baker 2018-10-09 10:55:55 +01:00
parent 88f969acdf
commit c1f51a76dd
2 changed files with 99 additions and 69 deletions

View file

@ -88,10 +88,10 @@
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"resize-observer-polyfill": "^1.5.0", "resize-observer-polyfill": "^1.5.0",
"sanitize-html": "^1.18.4", "sanitize-html": "^1.18.4",
"slate": "0.34.7", "slate": "^0.41.2",
"slate-html-serializer": "^0.6.1", "slate-html-serializer": "^0.6.1",
"slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3", "slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3",
"slate-react": "^0.12.4", "slate-react": "^0.18.10",
"text-encoding-utf-8": "^1.0.1", "text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0", "url": "^0.11.0",
"velocity-vector": "vector-im/velocity#059e3b2", "velocity-vector": "vector-im/velocity#059e3b2",

View file

@ -106,6 +106,17 @@ const MARK_TAGS = {
s: 'deleted', // deprecated s: 'deleted', // deprecated
}; };
const SLATE_SCHEMA = {
inlines: {
pill: {
isVoid: true,
},
emoji: {
isVoid: true,
},
},
};
function onSendMessageFailed(err, room) { function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
@ -116,10 +127,10 @@ function onSendMessageFailed(err, room) {
} }
function rangeEquals(a: Range, b: Range): boolean { function rangeEquals(a: Range, b: Range): boolean {
return (a.anchorKey === b.anchorKey return (a.anchor.key === b.anchor.key
&& a.anchorOffset === b.anchorOffset && a.anchor.offset === b.anchorOffset
&& a.focusKey === b.focusKey && a.focus.key === b.focusKey
&& a.focusOffset === b.focusOffset && a.focus.offset === b.focusOffset
&& a.isFocused === b.isFocused && a.isFocused === b.isFocused
&& a.isBackward === b.isBackward); && a.isBackward === b.isBackward);
} }
@ -239,7 +250,6 @@ export default class MessageComposerInput extends React.Component {
completion: el.innerText, completion: el.innerText,
completionId: m[1], completionId: m[1],
}, },
isVoid: true,
} }
} }
else { else {
@ -345,8 +355,12 @@ export default class MessageComposerInput extends React.Component {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_collectEditor = (e) => {
this._editor = e;
}
onAction = (payload) => { onAction = (payload) => {
const editor = this.refs.editor; const editor = this._editor;
let editorState = this.state.editorState; let editorState = this.state.editorState;
switch (payload.action) { switch (payload.action) {
@ -402,10 +416,14 @@ export default class MessageComposerInput extends React.Component {
} }
// XXX: this is to bring back the focus in a sane place and add a paragraph after it // XXX: this is to bring back the focus in a sane place and add a paragraph after it
change = change.select({ change = change.select(Range.create({
anchorKey: quote.key, anchor: {
focusKey: quote.key, key: quote.key,
}).collapseToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus(); },
focus: {
key: quote.key,
},
})).collapseToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus();
this.onChange(change); this.onChange(change);
} else { } else {
@ -497,15 +515,15 @@ export default class MessageComposerInput extends React.Component {
if (this.direction !== '') { if (this.direction !== '') {
const focusedNode = editorState.focusInline || editorState.focusText; const focusedNode = editorState.focusInline || editorState.focusText;
if (focusedNode.isVoid) { if (editorState.schema.isVoid(focusedNode)) {
// XXX: does this work in RTL? // XXX: does this work in RTL?
const edge = this.direction === 'Previous' ? 'End' : 'Start'; const edge = this.direction === 'Previous' ? 'End' : 'Start';
if (editorState.isCollapsed) { if (editorState.selection.isCollapsed) {
change = change[`collapseTo${ edge }Of${ this.direction }Text`](); change = change[`moveTo${ edge }Of${ this.direction }Text`]();
} else { } else {
const block = this.direction === 'Previous' ? editorState.previousText : editorState.nextText; const block = this.direction === 'Previous' ? editorState.previousText : editorState.nextText;
if (block) { if (block) {
change = change[`moveFocusTo${ edge }Of`](block); change = change[`moveFocusTo${ edge }OfNode`](block);
} }
} }
editorState = change.value; editorState = change.value;
@ -522,7 +540,7 @@ export default class MessageComposerInput extends React.Component {
this.autocomplete.hide(); this.autocomplete.hide();
} }
if (!editorState.document.isEmpty) { if (Plain.serialize(editorState) !== '') {
this.onTypingActivity(); this.onTypingActivity();
} else { } else {
this.onFinishedTyping(); this.onFinishedTyping();
@ -543,10 +561,14 @@ export default class MessageComposerInput extends React.Component {
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]); const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
const range = Range.create({ const range = Range.create({
anchorKey: editorState.selection.startKey, anchor: {
anchorOffset: currentStartOffset - emojiMatch[1].length - 1, key: editorState.selection.startKey,
focusKey: editorState.selection.startKey, offset: currentStartOffset - emojiMatch[1].length - 1,
focusOffset: currentStartOffset - 1, },
focus: {
key: editorState.selection.startKey,
offset: currentStartOffset - 1,
},
}); });
change = change.insertTextAtRange(range, unicodeEmoji); change = change.insertTextAtRange(range, unicodeEmoji);
editorState = change.value; editorState = change.value;
@ -560,15 +582,18 @@ export default class MessageComposerInput extends React.Component {
let match; let match;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) { while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({ const range = Range.create({
anchorKey: node.key, anchor: {
anchorOffset: match.index, key: node.key,
focusKey: node.key, offset: match.index,
focusOffset: match.index + match[0].length, },
focus: {
key: node.key,
offset: match.index + match[0].length,
},
}); });
const inline = Inline.create({ const inline = Inline.create({
type: 'emoji', type: 'emoji',
data: { emojiUnicode: match[0] }, data: { emojiUnicode: match[0] },
isVoid: true,
}); });
change = change.insertInlineAtRange(range, inline); change = change.insertInlineAtRange(range, inline);
editorState = change.value; editorState = change.value;
@ -580,10 +605,10 @@ export default class MessageComposerInput extends React.Component {
// emoji picker can leave the selection stuck in the emoji's // emoji picker can leave the selection stuck in the emoji's
// child text. This seems to happen due to selection getting // child text. This seems to happen due to selection getting
// moved in the normalisation phase after calculating these changes // moved in the normalisation phase after calculating these changes
if (editorState.anchorKey && if (editorState.selection.anchor.key &&
editorState.document.getParent(editorState.anchorKey).type === 'emoji') editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji')
{ {
change = change.collapseToStartOfNextText(); change = change.moveToStartOfNextText();
editorState = change.value; editorState = change.value;
} }
@ -673,7 +698,7 @@ export default class MessageComposerInput extends React.Component {
editorState: this.createEditorState(enabled, editorState), editorState: this.createEditorState(enabled, editorState),
isRichTextEnabled: enabled, isRichTextEnabled: enabled,
}, ()=>{ }, ()=>{
this.refs.editor.focus(); this._editor.focus();
}); });
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled); SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
@ -760,7 +785,9 @@ export default class MessageComposerInput extends React.Component {
// drop a point in history so the user can undo a word // drop a point in history so the user can undo a word
// XXX: this seems nasty but adding to history manually seems a no-go // XXX: this seems nasty but adding to history manually seems a no-go
ev.preventDefault(); ev.preventDefault();
return change.setOperationFlag("skip", false).setOperationFlag("merge", false).insertText(ev.key); return change.withoutMerging(() => {
change.insertText(ev.key);
});
}; };
onBackspace = (ev: KeyboardEvent, change: Change): Change => { onBackspace = (ev: KeyboardEvent, change: Change): Change => {
@ -771,23 +798,25 @@ export default class MessageComposerInput extends React.Component {
const { editorState } = this.state; const { editorState } = this.state;
// Allow Ctrl/Cmd-Backspace when focus starts at the start of the composer (e.g select-all) // Allow Ctrl/Cmd-Backspace when focus starts at the start of the composer (e.g select-all)
// for some reason if slate sees you Ctrl-backspace and your anchorOffset=0 it just resets your focus // for some reason if slate sees you Ctrl-backspace and your anchor.offset=0 it just resets your focus
if (!editorState.isCollapsed && editorState.anchorOffset === 0) { // XXX: Doing this now seems to put slate into a broken state, and it didn't appear to be doing
// what it claims to do on the old version of slate anyway...
/*if (!editorState.isCollapsed && editorState.selection.anchor.offset === 0) {
return change.delete(); return change.delete();
} }*/
if (this.state.isRichTextEnabled) { if (this.state.isRichTextEnabled) {
// let backspace exit lists // let backspace exit lists
const isList = this.hasBlock('list-item'); const isList = this.hasBlock('list-item');
if (isList && editorState.anchorOffset == 0) { if (isList && editorState.selection.anchor.offset == 0) {
change change
.setBlocks(DEFAULT_NODE) .setBlocks(DEFAULT_NODE)
.unwrapBlock('bulleted-list') .unwrapBlock('bulleted-list')
.unwrapBlock('numbered-list'); .unwrapBlock('numbered-list');
return change; return change;
} }
else if (editorState.anchorOffset == 0 && editorState.isCollapsed) { else if (editorState.selection.anchor.offset == 0 && editorState.isCollapsed) {
// turn blocks back into paragraphs // turn blocks back into paragraphs
if ((this.hasBlock('block-quote') || if ((this.hasBlock('block-quote') ||
this.hasBlock('heading1') || this.hasBlock('heading1') ||
@ -803,7 +832,7 @@ export default class MessageComposerInput extends React.Component {
// remove paragraphs entirely if they're nested // remove paragraphs entirely if they're nested
const parent = editorState.document.getParent(editorState.anchorBlock.key); const parent = editorState.document.getParent(editorState.anchorBlock.key);
if (editorState.anchorOffset == 0 && if (editorState.selection.anchor.offset == 0 &&
this.hasBlock('paragraph') && this.hasBlock('paragraph') &&
parent.nodes.size == 1 && parent.nodes.size == 1 &&
parent.object !== 'document') parent.object !== 'document')
@ -942,8 +971,8 @@ export default class MessageComposerInput extends React.Component {
const collapseAndOffsetSelection = (selection, offset) => { const collapseAndOffsetSelection = (selection, offset) => {
const key = selection.endKey(); const key = selection.endKey();
return new Range({ return new Range({
anchorKey: key, anchorOffset: offset, anchorKey: key, anchor.offset: offset,
focusKey: key, focusOffset: offset, focus.key: key, focus.offset: offset,
}); });
}; };
@ -1000,18 +1029,16 @@ export default class MessageComposerInput extends React.Component {
.insertFragment(fragment.document); .insertFragment(fragment.document);
} else { } else {
// in MD mode we don't want the rich content pasted as the magic was annoying people so paste plain // in MD mode we don't want the rich content pasted as the magic was annoying people so paste plain
return change return change.withoutMerging(() => {
.setOperationFlag("skip", false) change.insertText(transfer.text);
.setOperationFlag("merge", false) });
.insertText(transfer.text);
} }
} }
case 'text': case 'text':
// don't skip/merge so that multiple consecutive pastes can be undone individually // don't skip/merge so that multiple consecutive pastes can be undone individually
return change return change.withoutMerging(() => {
.setOperationFlag("skip", false) change.insertText(transfer.text);
.setOperationFlag("merge", false) });
.insertText(transfer.text);
} }
}; };
@ -1066,7 +1093,7 @@ export default class MessageComposerInput extends React.Component {
this.setState({ this.setState({
editorState: this.createEditorState(), editorState: this.createEditorState(),
}, ()=>{ }, ()=>{
this.refs.editor.focus(); this._editor.focus();
}); });
} }
if (cmd.promise) { if (cmd.promise) {
@ -1196,7 +1223,7 @@ export default class MessageComposerInput extends React.Component {
this.setState({ this.setState({
editorState: this.createEditorState(), editorState: this.createEditorState(),
}, ()=>{ this.refs.editor.focus() }); }, ()=>{ this._editor.focus() });
return true; return true;
}; };
@ -1216,9 +1243,9 @@ export default class MessageComposerInput extends React.Component {
// and we must be at the edge of the document (up=start, down=end) // and we must be at the edge of the document (up=start, down=end)
if (up) { if (up) {
if (!selection.isAtStartOf(document)) return; if (!selection.anchor.isAtStartOfNode(document)) return;
} else { } else {
if (!selection.isAtEndOf(document)) return; if (!selection.anchor.isAtEndOfNode(document)) return;
} }
const selected = this.selectHistory(up); const selected = this.selectHistory(up);
@ -1275,7 +1302,7 @@ export default class MessageComposerInput extends React.Component {
this.suppressAutoComplete = true; this.suppressAutoComplete = true;
this.setState({ editorState }, ()=>{ this.setState({ editorState }, ()=>{
this.refs.editor.focus(); this._editor.focus();
}); });
return true; return true;
}; };
@ -1345,15 +1372,11 @@ export default class MessageComposerInput extends React.Component {
inline = Inline.create({ inline = Inline.create({
type: 'pill', type: 'pill',
data: { completion, completionId, href }, data: { completion, completionId, href },
// we can't put text in here otherwise the editor tries to select it
isVoid: true,
}); });
} else if (completion === '@room') { } else if (completion === '@room') {
inline = Inline.create({ inline = Inline.create({
type: 'pill', type: 'pill',
data: { completion, completionId }, data: { completion, completionId },
// we can't put text in here otherwise the editor tries to select it
isVoid: true,
}); });
} }
@ -1361,8 +1384,9 @@ export default class MessageComposerInput extends React.Component {
if (range) { if (range) {
const change = editorState.change() const change = editorState.change()
.collapseToAnchor() .moveToAnchor()
.moveOffsetsTo(range.start, range.end) .moveAnchorTo(range.start)
.moveFocusTo(range.end)
.focus(); .focus();
editorState = change.value; editorState = change.value;
} }
@ -1433,6 +1457,7 @@ export default class MessageComposerInput extends React.Component {
room={this.props.room} room={this.props.room}
shouldShowPillAvatar={shouldShowPillAvatar} shouldShowPillAvatar={shouldShowPillAvatar}
isSelected={isSelected} isSelected={isSelected}
{...attributes}
/>; />;
} }
else if (Pill.isPillUrl(url)) { else if (Pill.isPillUrl(url)) {
@ -1441,12 +1466,14 @@ export default class MessageComposerInput extends React.Component {
room={this.props.room} room={this.props.room}
shouldShowPillAvatar={shouldShowPillAvatar} shouldShowPillAvatar={shouldShowPillAvatar}
isSelected={isSelected} isSelected={isSelected}
{...attributes}
/>; />;
} }
else { else {
const { text } = node; const { text } = node;
return <a href={url} {...props.attributes}> return <a href={url} {...props.attributes}>
{ text } { text }
{...attributes}
</a>; </a>;
} }
} }
@ -1458,7 +1485,9 @@ export default class MessageComposerInput extends React.Component {
const className = classNames('mx_emojione', { const className = classNames('mx_emojione', {
mx_emojione_selected: isSelected mx_emojione_selected: isSelected
}); });
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode }/>; const style = {};
if (props.selected) style.border = '1px solid blue';
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode } style={style}/>;
} }
} }
}; };
@ -1486,7 +1515,7 @@ export default class MessageComposerInput extends React.Component {
// of focusing it doesn't then cancel the format button being pressed // of focusing it doesn't then cancel the format button being pressed
// FIXME: can we just tell handleKeyCommand's change to invoke .focus()? // FIXME: can we just tell handleKeyCommand's change to invoke .focus()?
if (document.activeElement && document.activeElement.className !== 'mx_MessageComposer_editor') { if (document.activeElement && document.activeElement.className !== 'mx_MessageComposer_editor') {
this.refs.editor.focus(); this._editor.focus();
setTimeout(()=>{ setTimeout(()=>{
this.handleKeyCommand(name); this.handleKeyCommand(name);
}, 500); // can't find any callback to hook this to. onFocus and onChange and willComponentUpdate fire too early. }, 500); // can't find any callback to hook this to. onFocus and onChange and willComponentUpdate fire too early.
@ -1503,8 +1532,8 @@ export default class MessageComposerInput extends React.Component {
// This avoids us having to serialize the whole thing to plaintext and convert // This avoids us having to serialize the whole thing to plaintext and convert
// selection offsets in & out of the plaintext domain. // selection offsets in & out of the plaintext domain.
if (editorState.selection.anchorKey) { if (editorState.selection.anchor.key) {
return editorState.document.getDescendant(editorState.selection.anchorKey).text; return editorState.document.getDescendant(editorState.selection.anchor.key).text;
} }
else { else {
return ''; return '';
@ -1518,16 +1547,16 @@ export default class MessageComposerInput extends React.Component {
const firstGrandChild = firstChild && firstChild.nodes.get(0); const firstGrandChild = firstChild && firstChild.nodes.get(0);
beginning = (firstChild && firstGrandChild && beginning = (firstChild && firstGrandChild &&
firstChild.object === 'block' && firstGrandChild.object === 'text' && firstChild.object === 'block' && firstGrandChild.object === 'text' &&
editorState.selection.anchorKey === firstGrandChild.key); editorState.selection.anchor.key === firstGrandChild.key);
// return a character range suitable for handing to an autocomplete provider. // return a character range suitable for handing to an autocomplete provider.
// the range is relative to the anchor of the current editor selection. // the range is relative to the anchor of the current editor selection.
// if the selection spans multiple blocks, then we collapse it for the calculation. // if the selection spans multiple blocks, then we collapse it for the calculation.
const range = { const range = {
beginning, // whether the selection is in the first block of the editor or not beginning, // whether the selection is in the first block of the editor or not
start: editorState.selection.anchorOffset, start: editorState.selection.anchor.offset,
end: (editorState.selection.anchorKey == editorState.selection.focusKey) ? end: (editorState.selection.anchor.key == editorState.selection.focus.key) ?
editorState.selection.focusOffset : editorState.selection.anchorOffset, editorState.selection.focus.offset : editorState.selection.anchor.offset,
} }
if (range.start > range.end) { if (range.start > range.end) {
const tmp = range.start; const tmp = range.start;
@ -1543,7 +1572,7 @@ export default class MessageComposerInput extends React.Component {
}; };
focusComposer = () => { focusComposer = () => {
this.refs.editor.focus(); this._editor.focus();
}; };
render() { render() {
@ -1553,7 +1582,7 @@ export default class MessageComposerInput extends React.Component {
mx_MessageComposer_input_error: this.state.someCompletions === false, mx_MessageComposer_input_error: this.state.someCompletions === false,
}); });
const isEmpty = this.state.editorState.document.isEmpty; const isEmpty = Plain.serialize(this.state.editorState) === '';
let {placeholder} = this.props; let {placeholder} = this.props;
// XXX: workaround for placeholder being shown when there is a formatting block e.g blockquote but no text // XXX: workaround for placeholder being shown when there is a formatting block e.g blockquote but no text
@ -1579,7 +1608,7 @@ export default class MessageComposerInput extends React.Component {
onMouseDown={this.onMarkdownToggleClicked} onMouseDown={this.onMarkdownToggleClicked}
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")} title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
src={`img/button-md-${!this.state.isRichTextEnabled}.png`} /> src={`img/button-md-${!this.state.isRichTextEnabled}.png`} />
<Editor ref="editor" <Editor ref={this._collectEditor}
dir="auto" dir="auto"
className="mx_MessageComposer_editor" className="mx_MessageComposer_editor"
placeholder={placeholder} placeholder={placeholder}
@ -1591,6 +1620,7 @@ export default class MessageComposerInput extends React.Component {
renderMark={this.renderMark} renderMark={this.renderMark}
// disable spell check for the placeholder because browsers don't like "unencrypted" // disable spell check for the placeholder because browsers don't like "unencrypted"
spellCheck={!isEmpty} spellCheck={!isEmpty}
schema={SLATE_SCHEMA}
/> />
</div> </div>
</div> </div>