Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

T3chguy/slate cont2 #2049

Merged
merged 24 commits into from
Jul 16, 2018
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c6837af
import-type Change from slate
t3chguy Jul 3, 2018
372fa29
take edge into consideration when moving focus region on arrow keys
t3chguy Jul 3, 2018
483116f
add rule to slate-md-serializer: make underlined and removed work for CM
t3chguy Jul 3, 2018
faf17f0
remove debugger statement
t3chguy Jul 3, 2018
43204ea
fix Control-Backspace after select-all
t3chguy Jul 4, 2018
5b74c61
add missing import
t3chguy Jul 4, 2018
5bd4104
modify ComposerHistoryManager
t3chguy Jul 4, 2018
8665f10
pin slate to 0.33.4 to avoid https://github.com/ianstormtaylor/slate/…
ara4n Jul 8, 2018
83f2614
add guide to slate's data formats and how we convert
ara4n Jul 8, 2018
021409a
apply review feedback from @lukebarnard1
ara4n Jul 8, 2018
0d0934a
unbreak modifier+space (e.g. emoji insert on macOS)
ara4n Jul 8, 2018
8bcb987
delint
t3chguy Jul 9, 2018
51591a4
fix lint
ara4n Jul 9, 2018
58301e5
navigateHistory only when at edges of document, to prevent Firefox bug
t3chguy Jul 10, 2018
100ecfe
remove trailing spaces to make linter happy (no-trailing-spaces)
t3chguy Jul 10, 2018
abbb69d
fix fn call, fixes usage of SlashCommands
t3chguy Jul 10, 2018
fd4f967
convert md<->rt if the stored editorState was in a different state
t3chguy Jul 11, 2018
c3aef6e
workaround for tommoor/slate-md-serializer#14
t3chguy Jul 11, 2018
95909de
fix MessageComposer not marking translatable strings. run gen-i18n
t3chguy Jul 11, 2018
3e05bf1
hide autocomplete when moving caret to match existing behaviour
t3chguy Jul 11, 2018
b4bc09c
null-guard savedState since now we're accessing its props
t3chguy Jul 11, 2018
7405c5e
specify alternate history storage key to prevent conflicts with draft
t3chguy Jul 12, 2018
59a14f2
re-hydrate Values which have been serialized into LocalStorage
t3chguy Jul 15, 2018
d7ff7cd
stupid thinkotypo
ara4n Jul 16, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions docs/slate-formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
Guide to data types used by the Slate-based Rich Text Editor
------------------------------------------------------------

We always store the Slate editor state in its Value form.

The schema for the Value is the same whether the editor is in MD or rich text mode, and is currently (rather arbitrarily)
dictated by the schema expected by slate-md-serializer, simply because it was the only bit of the pipeline which
has opinions on the schema. (slate-html-serializer lets you define how to serialize whatever schema you like).

The BLOCK_TAGS and MARK_TAGS give the mapping from HTML tags to the schema's node types (for blocks, which describe
block content like divs, and marks, which describe inline formatted sections like spans).

We use <p/> as the parent tag for the message (XXX: although some tags are technically not allowed to be nested within p's)

Various conversions are performed as content is moved between HTML, MD, and plaintext representations of HTML and MD.

The primitives used are:

* Markdown.js - models commonmark-formatted MD strings (as entered by the composer in MD mode)
* toHtml() - renders them to HTML suitable for sending on the wire
* isPlainText() - checks whether the parsed MD contains anything other than simple text.
* toPlainText() - renders MD to plain text in order to remove backslashes. Only works if the MD is already plaintext (otherwise it just emits HTML)

* slate-html-serializer
* converts Values to HTML (serialising) using our schema rules
* converts HTML to Values (deserialising) using our schema rules

* slate-md-serializer
* converts rich Values to MD strings (serialising) but using a non-commonmark generic MD dialect.
* This should use commonmark, but we use the serializer here for expedience rather than writing a commonmark one.

* slate-plain-serializer
* converts Values to plain text strings (serialising them) by concatenating the strings together
* converts Values from plain text strings (deserialiasing them).
* Used to initialise the editor by deserializing "" into a Value. Apparently this is the idiomatic way to initialise a blank editor.
* Used (as a bodge) to turn a rich text editor into a MD editor, when deserialising the converted MD string of the editor into a value

* PlainWithPillsSerializer
* A fork of slate-plain-serializer which is aware of Pills (hence the name) and Emoji.
* It can be configured to output Pills as:
* "plain": Pills are rendered via their 'completion' text - e.g. 'Matthew'; used for sending messages)
* "md": Pills are rendered as MD, e.g. [Matthew](https://matrix.to/#/@matthew:matrix.org) )
* "id": Pills are rendered as IDs, e.g. '@matthew:matrix.org' (used for authoring / commands)
* Emoji nodes are converted to inline utf8 emoji.

The actual conversion transitions are:

* Quoting:
* The message being quoted is taken as HTML
* ...and deserialised into a Value
* ...and then serialised into MD via slate-md-serializer if the editor is in MD mode

* Roundtripping between MD and rich text editor mode
* From MD to richtext (mdToRichEditorState):
* Serialise the MD-format Value to a MD string (converting pills to MD) with PlainWithPillsSerializer in 'md' mode
* Convert that MD string to HTML via Markdown.js
* Deserialise that Value to HTML via slate-html-serializer
* From richtext to MD (richToMdEditorState):
* Serialise the richtext-format Value to a MD string with slate-md-serializer (XXX: this should use commonmark)
* Deserialise that to a plain text value via slate-plain-serializer

* Loading history in one format into an editor which is in the other format
* Uses the same functions as for roundtripping

* Scanning the editor for a slash command
* If the editor is a single line node starting with /, then serialize it to a string with PlainWithPillsSerializer in 'id' mode
So that pills get converted to IDs suitable for commands being passed around

* Sending messages
* In RT mode:
* If there is rich content, serialize the RT-format Value to HTML body via slate-html-serializer
* Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode
* In MD mode:
* Serialize the MD-format Value into an MD string with PlainWithPillsSerializer in 'md' mode
* Parse the string with Markdown.js
* If it contains no formatting:
* Send as plaintext (as taken from Markdown.toPlainText())
* Otherwise
* Send as HTML (as taken from Markdown.toHtml())
* Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode

* Pasting HTML
* Deserialize HTML to a RT Value via slate-html-serializer
* In RT mode, insert it straight into the editor as a fragment
* In MD mode, serialise it to an MD string via slate-md-serializer and then insert the string into the editor as a fragment.

The various scenarios and transitions could be drawn into a pretty diagram if one felt the urge, but hopefully the enough
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the above?

gives sufficient detail on how it's all meant to work.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^15.6.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"slate": "^0.33.4",
"slate": "0.33.4",
"slate-react": "^0.12.4",
"slate-html-serializer": "^0.6.1",
"slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3",
Expand Down
13 changes: 7 additions & 6 deletions src/ComposerHistoryManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ class HistoryItem {
export default class ComposerHistoryManager {
history: Array<HistoryItem> = [];
prefix: string;
lastIndex: number = 0;
currentIndex: number = 0;
lastIndex: number = 0; // used for indexing the storage
currentIndex: number = 0; // used for indexing the loaded validated history Array

constructor(roomId: string, prefix: string = 'mx_composer_history_') {
this.prefix = prefix + roomId;
Expand All @@ -69,18 +69,19 @@ export default class ComposerHistoryManager {
}
}
this.lastIndex = this.currentIndex;
// reset currentIndex to account for any unserialisable history
this.currentIndex = this.history.length;
}

save(value: Value, format: MessageFormat) {
const item = new HistoryItem(value, format);
this.history.push(item);
this.currentIndex = this.lastIndex + 1;
this.currentIndex = this.history.length;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
}

getItem(offset: number): ?HistoryItem {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
const item = this.history[this.currentIndex];
return item;
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex];
}
}
36 changes: 0 additions & 36 deletions src/HtmlUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,42 +112,6 @@ export function charactersToImageNode(alt, useSvg, ...unicode) {
/>;
}

/*
export function processHtmlForSending(html: string): string {
const contentDiv = document.createElement('div');
contentDiv.innerHTML = html;

if (contentDiv.children.length === 0) {
return contentDiv.innerHTML;
}

let contentHTML = "";
for (let i=0; i < contentDiv.children.length; i++) {
const element = contentDiv.children[i];
if (element.tagName.toLowerCase() === 'p') {
contentHTML += element.innerHTML;
// Don't add a <br /> for the last <p>
if (i !== contentDiv.children.length - 1) {
contentHTML += '<br />';
}
} else if (element.tagName.toLowerCase() === 'pre') {
// Replace "<br>\n" with "\n" within `<pre>` tags because the <br> is
// redundant. This is a workaround for a bug in draft-js-export-html:
// https://github.com/sstur/draft-js-export-html/issues/62
contentHTML += '<pre>' +
element.innerHTML.replace(/<br>\n/g, '\n').trim() +
'</pre>';
} else {
const temp = document.createElement('div');
temp.appendChild(element.cloneNode(true));
contentHTML += temp.innerHTML;
}
}

return contentHTML;
}
*/

/*
* Given an untrusted HTML string, return a React node with an sanitized version
* of that HTML.
Expand Down
8 changes: 0 additions & 8 deletions src/Markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,6 @@ export default class Markdown {
if (is_multi_line(node) && node.next) this.lit('\n\n');
};

// convert MD links into console-friendly ' < http://foo >' style links
// ...except given this function never gets called with links, it's useless.
// renderer.link = function(node, entering) {
// if (!entering) {
// this.lit(` < ${node.destination} >`);
// }
// };

return renderer.render(this.parsed);
}
}
6 changes: 3 additions & 3 deletions src/autocomplete/Autocompleter.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import NotifProvider from './NotifProvider';
import Promise from 'bluebird';

export type SelectionRange = {
beginning: boolean,
start: number,
end: number
beginning: boolean, // whether the selection is in the first block of the editor or not
start: number, // byte offset relative to the start anchor of the current editor selection.
end: number, // byte offset relative to the end anchor of the current editor selection.
};

export type Completion = {
Expand Down
2 changes: 1 addition & 1 deletion src/autocomplete/CommandProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class CommandProvider extends AutocompleteProvider {

let matches = [];
// check if the full match differs from the first word (i.e. returns false if the command has args)
if (command[0] !== command[1]) {
if (command[0] !== command[1]) {
// The input looks like a command with arguments, perform exact match
const name = command[1].substr(1); // strip leading `/`
if (CommandMap[name]) {
Expand Down
2 changes: 1 addition & 1 deletion src/autocomplete/UserProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export default class UserProvider extends AutocompleteProvider {
// relies on the length of the entity === length of the text in the decoration.
completion: user.rawDisplayName.replace(' (IRC)', ''),
completionId: user.userId,
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
suffix: (selection.beginning && selection.start === 0) ? ': ' : ' ',
href: makeUserPermalink(user.userId),
component: (
<PillCompletion
Expand Down
3 changes: 2 additions & 1 deletion src/components/structures/ContextualMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ export default class ContextualMenu extends React.Component {
{ chevron }
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
</div>
{ props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
{ props.hasBackground && <div className="mx_ContextualMenu_background"
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
<style>{ chevronCSS }</style>
</div>;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/EventTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const stateEventTileTypes = {
'm.room.topic': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
'm.room.server_acl' : 'messages.TextualEvent',
'm.room.server_acl': 'messages.TextualEvent',

'im.vector.modular.widgets': 'messages.TextualEvent',
};
Expand Down
36 changes: 23 additions & 13 deletions src/components/views/rooms/MessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
Expand All @@ -26,6 +26,17 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';

const formatButtonList = [
_td("bold"),
_td("italic"),
_td("deleted"),
_td("underlined"),
_td("inline-code"),
_td("block-quote"),
_td("bulleted-list"),
_td("numbered-list"),
];

export default class MessageComposer extends React.Component {
constructor(props, context) {
super(props, context);
Expand Down Expand Up @@ -322,18 +333,17 @@ export default class MessageComposer extends React.Component {
let formatBar;
if (this.state.showFormatting && this.state.inputState.isRichTextEnabled) {
const {marks, blockType} = this.state.inputState;
const formatButtons = ["bold", "italic", "deleted", "underlined", "inline-code", "block-quote", "bulleted-list", "numbered-list"].map(
(name) => {
const active = marks.some(mark => mark.type === name) || blockType === name;
const suffix = active ? '-on' : '';
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
return <img className={className}
title={_t(name)}
onMouseDown={onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
height="17" />;
const formatButtons = formatButtonList.map((name) => {
const active = marks.some(mark => mark.type === name) || blockType === name;
const suffix = active ? '-on' : '';
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
return <img className={className}
title={_t(name)}
onMouseDown={onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
height="17" />;
},
);

Expand Down
Loading