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

Commit b960d22

Browse files
committed
cleanup, better comments, markdown hotkeys
1 parent e4217c3 commit b960d22

2 files changed

Lines changed: 109 additions & 49 deletions

File tree

src/RichText.js

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import {Editor, ContentState, convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle, CompositeDecorator} from 'draft-js';
1+
import {
2+
Editor,
3+
Modifier,
4+
ContentState,
5+
convertFromHTML,
6+
DefaultDraftBlockRenderMap,
7+
DefaultDraftInlineStyle,
8+
CompositeDecorator
9+
} from 'draft-js';
210
import * as sdk from './index';
311

412
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
513
element: 'p' // draft uses <div> by default which we don't really like, so we're using <p>
614
});
715

8-
const styles = {
16+
const STYLES = {
917
BOLD: 'strong',
1018
CODE: 'code',
1119
ITALIC: 'em',
@@ -17,18 +25,24 @@ export function contentStateToHTML(contentState: ContentState): string {
1725
return contentState.getBlockMap().map((block) => {
1826
let elem = BLOCK_RENDER_MAP.get(block.getType()).element;
1927
let content = [];
20-
block.findStyleRanges(() => true, (start, end) => {
21-
const tags = block.getInlineStyleAt(start).map(style => styles[style]);
22-
const open = tags.map(tag => `<${tag}>`).join('');
23-
const close = tags.map(tag => `</${tag}>`).reverse().join('');
24-
content.push(`${open}${block.getText().substring(start, end)}${close}`);
25-
});
28+
block.findStyleRanges(
29+
() => true, // always return true => don't filter any ranges out
30+
(start, end) => {
31+
// map style names to elements
32+
let tags = block.getInlineStyleAt(start).map(style => STYLES[style]);
33+
// combine them to get well-nested HTML
34+
let open = tags.map(tag => `<${tag}>`).join('');
35+
let close = tags.map(tag => `</${tag}>`).reverse().join('');
36+
// and get the HTML representation of this styled range (this .substring() should never fail)
37+
content.push(`${open}${block.getText().substring(start, end)}${close}`);
38+
}
39+
);
2640

2741
return (`<${elem}>${content.join('')}</${elem}>`);
2842
}).join('');
2943
}
3044

31-
export function HTMLtoContentState(html:String): ContentState {
45+
export function HTMLtoContentState(html: string): ContentState {
3246
return ContentState.createFromBlockArray(convertFromHTML(html));
3347
}
3448

@@ -37,29 +51,25 @@ const ROOM_REGEX = /#\S+:\S+/g;
3751

3852
/**
3953
* Returns a composite decorator which has access to provided scope.
40-
*
41-
* @param scope
42-
* @returns {*}
4354
*/
44-
export function getScopedDecorator(scope) {
45-
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
55+
export function getScopedDecorator(scope: any): CompositeDecorator {
56+
let MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
4657

47-
const usernameDecorator = {
58+
let usernameDecorator = {
4859
strategy: (contentBlock, callback) => {
4960
findWithRegex(USERNAME_REGEX, contentBlock, callback);
5061
},
5162
component: (props) => {
5263
let member = scope.room.getMember(props.children[0].props.text);
5364
let name = null;
54-
if(!!member) {
55-
name = member.name;
65+
if (!!member) {
66+
name = member.name; // unused until we make these decorators immutable (autocomplete needed)
5667
}
57-
console.log(member);
58-
let avatar = member ? <MemberAvatar member={member} width={16} height={16} /> : null;
68+
let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null;
5969
return <span className="mx_UserPill">{avatar} {props.children}</span>;
6070
}
6171
};
62-
const roomDecorator = {
72+
let roomDecorator = {
6373
strategy: (contentBlock, callback) => {
6474
findWithRegex(ROOM_REGEX, contentBlock, callback);
6575
},
@@ -71,11 +81,31 @@ export function getScopedDecorator(scope) {
7181
return new CompositeDecorator([usernameDecorator, roomDecorator]);
7282
}
7383

74-
function findWithRegex(regex, contentBlock, callback) {
84+
/**
85+
* Utility function that looks for regex matches within a ContentBlock and invokes {callback} with (start, end)
86+
* From https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html
87+
*/
88+
function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: number, end: number) => any) {
7589
const text = contentBlock.getText();
7690
let matchArr, start;
7791
while ((matchArr = regex.exec(text)) !== null) {
7892
start = matchArr.index;
7993
callback(start, start + matchArr[0].length);
8094
}
8195
}
96+
97+
/**
98+
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result.
99+
*/
100+
export function modifyText(contentState: ContentState, rangeToReplace: SelectionState, modifyFn: (text: string) => string, ...rest): ContentState {
101+
let startKey = rangeToReplace.getStartKey(),
102+
endKey = contentState.getKeyAfter(rangeToReplace.getEndKey()),
103+
text = "";
104+
105+
for(let currentKey = startKey; currentKey && currentKey !== endKey; currentKey = contentState.getKeyAfter(currentKey)) {
106+
let currentBlock = contentState.getBlockForKey(currentKey);
107+
text += currentBlock.getText();
108+
}
109+
110+
return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), ...rest);
111+
}

src/components/views/rooms/MessageComposerInput.js

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ var sdk = require('../../../index');
4242
var dis = require("../../../dispatcher");
4343
var KeyCode = require("../../../KeyCode");
4444

45-
import {contentStateToHTML, HTMLtoContentState, getScopedDecorator} from '../../../RichText';
45+
import * as RichText from '../../../RichText';
4646

4747
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
4848

@@ -69,7 +69,7 @@ export default class MessageComposerInput extends React.Component {
6969
this.onInputClick = this.onInputClick.bind(this);
7070

7171
this.state = {
72-
isRichtextEnabled: true,
72+
isRichtextEnabled: false,
7373
editorState: null
7474
};
7575

@@ -95,7 +95,7 @@ export default class MessageComposerInput extends React.Component {
9595
let func = contentState ? EditorState.createWithContent : EditorState.createEmpty;
9696
let args = contentState ? [contentState] : [];
9797
if(this.state.isRichtextEnabled) {
98-
args.push(getScopedDecorator(this.props));
98+
args.push(RichText.getScopedDecorator(this.props));
9999
}
100100
return func.apply(null, args);
101101
}
@@ -114,7 +114,7 @@ export default class MessageComposerInput extends React.Component {
114114
// The textarea element to set text to.
115115
element: null,
116116

117-
init: function (element, roomId) {
117+
init: function(element, roomId) {
118118
this.roomId = roomId;
119119
this.element = element;
120120
this.position = -1;
@@ -129,7 +129,7 @@ export default class MessageComposerInput extends React.Component {
129129
}
130130
},
131131

132-
push: function (text) {
132+
push: function(text) {
133133
// store a message in the sent history
134134
this.data.unshift(text);
135135
window.sessionStorage.setItem(
@@ -142,7 +142,7 @@ export default class MessageComposerInput extends React.Component {
142142
},
143143

144144
// move in the history. Returns true if we managed to move.
145-
next: function (offset) {
145+
next: function(offset) {
146146
if (this.position === -1) {
147147
// user is going into the history, save the current line.
148148
this.originalText = this.element.value;
@@ -175,15 +175,15 @@ export default class MessageComposerInput extends React.Component {
175175
return true;
176176
},
177177

178-
saveLastTextEntry: function () {
178+
saveLastTextEntry: function() {
179179
// save the currently entered text in order to restore it later.
180180
// NB: This isn't 'originalText' because we want to restore
181181
// sent history items too!
182182
let contentJSON = JSON.stringify(convertToRaw(component.state.editorState.getCurrentContent()));
183183
window.sessionStorage.setItem("input_" + this.roomId, contentJSON);
184184
},
185185

186-
setLastTextEntry: function () {
186+
setLastTextEntry: function() {
187187
let contentJSON = window.sessionStorage.getItem("input_" + this.roomId);
188188
if (contentJSON) {
189189
let content = convertFromRaw(JSON.parse(contentJSON));
@@ -404,7 +404,7 @@ export default class MessageComposerInput extends React.Component {
404404
this.refs.editor.focus();
405405
}
406406

407-
onChange(editorState) {
407+
onChange(editorState: EditorState) {
408408
this.setState({editorState});
409409

410410
if(editorState.getCurrentContent().hasText()) {
@@ -414,30 +414,60 @@ export default class MessageComposerInput extends React.Component {
414414
}
415415
}
416416

417-
handleKeyCommand(command) {
418-
if(command === 'toggle-mode') {
417+
enableRichtext(enabled: boolean) {
418+
this.setState({
419+
isRichtextEnabled: enabled
420+
});
421+
422+
if(!this.state.isRichtextEnabled) {
423+
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
419424
this.setState({
420-
isRichtextEnabled: !this.state.isRichtextEnabled
425+
editorState: this.createEditorState(RichText.HTMLtoContentState(html))
421426
});
427+
} else {
428+
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent());
429+
let contentState = ContentState.createFromText(markdown);
430+
this.setState({
431+
editorState: this.createEditorState(contentState)
432+
});
433+
}
434+
}
422435

423-
if(!this.state.isRichtextEnabled) {
424-
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
425-
this.setState({
426-
editorState: this.createEditorState(HTMLtoContentState(html))
427-
});
428-
} else {
429-
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent());
430-
let contentState = ContentState.createFromText(markdown);
431-
this.setState({
432-
editorState: this.createEditorState(contentState)
433-
});
434-
}
435-
436+
handleKeyCommand(command: string): boolean {
437+
if(command === 'toggle-mode') {
438+
this.enableRichtext(!this.state.isRichtextEnabled);
436439
return true;
437440
}
438441

439-
let newState = RichUtils.handleKeyCommand(this.state.editorState, command);
440-
if (newState) {
442+
let newState: ?EditorState = null;
443+
444+
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
445+
if(!this.state.isRichtextEnabled) {
446+
let contentState = this.state.editorState.getCurrentContent(),
447+
selection = this.state.editorState.getSelection();
448+
449+
let modifyFn = {
450+
bold: text => `**${text}**`,
451+
italic: text => `*${text}*`,
452+
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
453+
code: text => `\`${text}\``
454+
}[command];
455+
456+
if(modifyFn) {
457+
newState = EditorState.push(
458+
this.state.editorState,
459+
RichText.modifyText(contentState, selection, modifyFn),
460+
'insert-characters'
461+
);
462+
}
463+
console.log(modifyFn);
464+
console.log(newState);
465+
}
466+
467+
if(newState == null)
468+
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
469+
470+
if (newState != null) {
441471
this.onChange(newState);
442472
return true;
443473
}
@@ -455,7 +485,7 @@ export default class MessageComposerInput extends React.Component {
455485
let contentText = contentState.getPlainText(), contentHTML;
456486

457487
if(this.state.isRichtextEnabled) {
458-
contentHTML = contentStateToHTML(contentState);
488+
contentHTML = RichText.contentStateToHTML(contentState);
459489
} else {
460490
contentHTML = mdownToHtml(contentText);
461491
}

0 commit comments

Comments
 (0)