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

Commit 001011d

Browse files
committed
Initial version of rich text editor
1 parent 07cc9bf commit 001011d

File tree

3 files changed

+166
-76
lines changed

3 files changed

+166
-76
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
},
2424
"dependencies": {
2525
"classnames": "^2.1.2",
26+
"draft-js": "^0.7.0",
27+
"draft-js-export-html": "^0.2.2",
2628
"favico.js": "^0.3.10",
2729
"filesize": "^3.1.2",
2830
"flux": "^2.0.3",

src/RichText.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {Editor, ContentState, convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle} from 'draft-js';
2+
const ReactDOM = require('react-dom');
3+
4+
const styles = {
5+
BOLD: 'strong',
6+
CODE: 'code',
7+
ITALIC: 'em',
8+
STRIKETHROUGH: 's',
9+
UNDERLINE: 'u'
10+
};
11+
12+
export function contentStateToHTML(contentState:ContentState): String {
13+
const elem = contentState.getBlockMap().map((block) => {
14+
const elem = DefaultDraftBlockRenderMap.get(block.getType()).element;
15+
const content = [];
16+
block.findStyleRanges(() => true, (s, e) => {
17+
console.log(block.getInlineStyleAt(s));
18+
const tags = block.getInlineStyleAt(s).map(style => styles[style]);
19+
const open = tags.map(tag => `<${tag}>`).join('');
20+
const close = tags.map(tag => `</${tag}>`).reverse().join('');
21+
content.push(`${open}${block.getText().substring(s, e)}${close}`);
22+
});
23+
24+
return (`
25+
<${elem}>
26+
${content.join('')}
27+
</${elem}>
28+
`);
29+
}).join('');
30+
31+
32+
return elem;
33+
}
34+
35+
export function HTMLtoContentState(html:String): ContentState {
36+
return ContentState.createFromBlockArray(convertFromHTML(html));
37+
}

src/components/views/rooms/MessageComposerInput.js

Lines changed: 127 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ marked.setOptions({
2727
smartypants: false
2828
});
2929

30+
import {Editor, EditorState, RichUtils} from 'draft-js';
31+
import {stateToHTML} from 'draft-js-export-html';
32+
3033
var MatrixClientPeg = require("../../../MatrixClientPeg");
3134
var SlashCommands = require("../../../SlashCommands");
3235
var Modal = require("../../../Modal");
@@ -36,6 +39,8 @@ var sdk = require('../../../index');
3639
var dis = require("../../../dispatcher");
3740
var KeyCode = require("../../../KeyCode");
3841

42+
import {contentStateToHTML} from '../../../RichText';
43+
3944
var TYPING_USER_TIMEOUT = 10000;
4045
var TYPING_SERVER_TIMEOUT = 30000;
4146
var MARKDOWN_ENABLED = true;
@@ -56,26 +61,18 @@ function mdownToHtml(mdown) {
5661
/*
5762
* The textInput part of the MessageComposer
5863
*/
59-
module.exports = React.createClass({
60-
displayName: 'MessageComposerInput',
61-
62-
statics: {
63-
// the height we limit the composer to
64-
MAX_HEIGHT: 100,
65-
},
66-
67-
propTypes: {
68-
tabComplete: React.PropTypes.any,
69-
70-
// a callback which is called when the height of the composer is
71-
// changed due to a change in content.
72-
onResize: React.PropTypes.func,
73-
74-
// js-sdk Room object
75-
room: React.PropTypes.object.isRequired,
76-
},
64+
module.exports = class extends React.Component {
65+
constructor(props, context) {
66+
super(props, context);
67+
this.onAction = this.onAction.bind(this);
68+
this.onInputClick = this.onInputClick.bind(this);
69+
70+
this.state = {
71+
editorState: EditorState.createEmpty()
72+
};
73+
}
7774

78-
componentWillMount: function() {
75+
componentWillMount() {
7976
this.oldScrollHeight = 0;
8077
this.markdownEnabled = MARKDOWN_ENABLED;
8178
var self = this;
@@ -157,21 +154,22 @@ module.exports = React.createClass({
157154
// save the currently entered text in order to restore it later.
158155
// NB: This isn't 'originalText' because we want to restore
159156
// sent history items too!
160-
var text = this.element.value;
161-
window.sessionStorage.setItem("input_" + this.roomId, text);
157+
console.error('fixme');
158+
// window.sessionStorage.setItem("input_" + this.roomId, text);
162159
},
163160

164161
setLastTextEntry: function() {
165-
var text = window.sessionStorage.getItem("input_" + this.roomId);
166-
if (text) {
167-
this.element.value = text;
168-
self.resizeInput();
169-
}
162+
console.error('fixme');
163+
// var text = window.sessionStorage.getItem("input_" + this.roomId);
164+
// if (text) {
165+
// this.element.value = text;
166+
// self.resizeInput();
167+
// }
170168
}
171169
};
172-
},
170+
}
173171

174-
componentDidMount: function() {
172+
componentDidMount() {
175173
this.dispatcherRef = dis.register(this.onAction);
176174
this.sentHistory.init(
177175
this.refs.textarea,
@@ -181,18 +179,19 @@ module.exports = React.createClass({
181179
if (this.props.tabComplete) {
182180
this.props.tabComplete.setTextArea(this.refs.textarea);
183181
}
184-
},
182+
}
185183

186-
componentWillUnmount: function() {
184+
componentWillUnmount() {
187185
dis.unregister(this.dispatcherRef);
188186
this.sentHistory.saveLastTextEntry();
189-
},
187+
}
190188

191-
onAction: function(payload) {
192-
var textarea = this.refs.textarea;
189+
onAction(payload) {
190+
var editor = this.refs.editor;
193191
switch (payload.action) {
194192
case 'focus_composer':
195-
textarea.focus();
193+
console.error('fixme');
194+
editor.focus();
196195
break;
197196
case 'insert_displayname':
198197
if (textarea.value.length) {
@@ -214,9 +213,9 @@ module.exports = React.createClass({
214213
}
215214
break;
216215
}
217-
},
216+
}
218217

219-
onKeyDown: function (ev) {
218+
onKeyDown(ev) {
220219
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
221220
var input = this.refs.textarea.value;
222221
if (input.length === 0) {
@@ -252,33 +251,34 @@ module.exports = React.createClass({
252251
self.onFinishedTyping();
253252
}
254253
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
255-
},
254+
}
256255

257-
resizeInput: function() {
256+
resizeInput() {
257+
console.error('fixme');
258258
// scrollHeight is at least equal to clientHeight, so we have to
259259
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
260-
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
261-
var newHeight = Math.min(this.refs.textarea.scrollHeight,
262-
this.constructor.MAX_HEIGHT);
263-
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
264-
this.oldScrollHeight = this.refs.textarea.scrollHeight;
265-
266-
if (this.props.onResize) {
267-
// kick gemini-scrollbar to re-layout
268-
this.props.onResize();
269-
}
270-
},
260+
// this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
261+
// var newHeight = Math.min(this.refs.textarea.scrollHeight,
262+
// this.constructor.MAX_HEIGHT);
263+
// this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
264+
// this.oldScrollHeight = this.refs.textarea.scrollHeight;
265+
//
266+
// if (this.props.onResize) {
267+
// // kick gemini-scrollbar to re-layout
268+
// this.props.onResize();
269+
// }
270+
}
271271

272-
onKeyUp: function(ev) {
272+
onKeyUp(ev) {
273273
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
274274
ev.keyCode === KeyCode.DELETE ||
275275
ev.keyCode === KeyCode.BACKSPACE)
276276
{
277277
this.resizeInput();
278278
}
279-
},
279+
}
280280

281-
onEnter: function(ev) {
281+
onEnter(ev) {
282282
var contentText = this.refs.textarea.value;
283283

284284
// bodge for now to set markdown state on/off. We probably want a separate
@@ -365,42 +365,42 @@ module.exports = React.createClass({
365365
this.refs.textarea.value = '';
366366
this.resizeInput();
367367
ev.preventDefault();
368-
},
368+
}
369369

370-
onTypingActivity: function() {
370+
onTypingActivity() {
371371
this.isTyping = true;
372372
if (!this.userTypingTimer) {
373373
this.sendTyping(true);
374374
}
375375
this.startUserTypingTimer();
376376
this.startServerTypingTimer();
377-
},
377+
}
378378

379-
onFinishedTyping: function() {
379+
onFinishedTyping() {
380380
this.isTyping = false;
381381
this.sendTyping(false);
382382
this.stopUserTypingTimer();
383383
this.stopServerTypingTimer();
384-
},
384+
}
385385

386-
startUserTypingTimer: function() {
386+
startUserTypingTimer() {
387387
this.stopUserTypingTimer();
388388
var self = this;
389389
this.userTypingTimer = setTimeout(function() {
390390
self.isTyping = false;
391391
self.sendTyping(self.isTyping);
392392
self.userTypingTimer = null;
393393
}, TYPING_USER_TIMEOUT);
394-
},
394+
}
395395

396-
stopUserTypingTimer: function() {
396+
stopUserTypingTimer() {
397397
if (this.userTypingTimer) {
398398
clearTimeout(this.userTypingTimer);
399399
this.userTypingTimer = null;
400400
}
401-
},
401+
}
402402

403-
startServerTypingTimer: function() {
403+
startServerTypingTimer() {
404404
if (!this.serverTypingTimer) {
405405
var self = this;
406406
this.serverTypingTimer = setTimeout(function() {
@@ -410,39 +410,90 @@ module.exports = React.createClass({
410410
}
411411
}, TYPING_SERVER_TIMEOUT / 2);
412412
}
413-
},
413+
}
414414

415-
stopServerTypingTimer: function() {
415+
stopServerTypingTimer() {
416416
if (this.serverTypingTimer) {
417417
clearTimeout(this.servrTypingTimer);
418418
this.serverTypingTimer = null;
419419
}
420-
},
420+
}
421421

422-
sendTyping: function(isTyping) {
422+
sendTyping(isTyping) {
423423
MatrixClientPeg.get().sendTyping(
424424
this.props.room.roomId,
425425
this.isTyping, TYPING_SERVER_TIMEOUT
426426
).done();
427-
},
427+
}
428428

429-
refreshTyping: function() {
429+
refreshTyping() {
430430
if (this.typingTimeout) {
431431
clearTimeout(this.typingTimeout);
432432
this.typingTimeout = null;
433433
}
434-
},
434+
}
435+
436+
onInputClick(ev) {
437+
this.refs.editor.focus();
438+
}
439+
440+
onChange(editorState) {
441+
this.setState({editorState});
442+
}
443+
444+
handleKeyCommand(command) {
445+
const newState = RichUtils.handleKeyCommand(this.state.editorState, command);
446+
if (newState) {
447+
this.onChange(newState);
448+
return true;
449+
}
450+
return false;
451+
}
452+
453+
handleReturn(ev) {
454+
if(ev.shiftKey)
455+
return false;
435456

436-
onInputClick: function(ev) {
437-
this.refs.textarea.focus();
438-
},
457+
const contentState = this.state.editorState.getCurrentContent();
458+
if(!contentState.hasText())
459+
return false;
439460

440-
render: function() {
461+
const contentText = contentState.getPlainText(),
462+
contentHTML = contentStateToHTML(contentState);
463+
464+
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, contentHTML);
465+
466+
this.setState({
467+
editorState: EditorState.createEmpty()
468+
});
469+
470+
return true;
471+
}
472+
473+
render() {
441474
return (
442-
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
443-
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
475+
<div className="mx_MessageComposer_input" onClick={ this.onInputClick } style={{overflow: 'auto'}}>
476+
<Editor ref="editor"
477+
editorState={this.state.editorState}
478+
onChange={(state) => this.onChange(state)}
479+
handleKeyCommand={(command) => this.handleKeyCommand(command)}
480+
handleReturn={ev => this.handleReturn(ev)} />
444481
</div>
445482
);
446483
}
447-
});
484+
};
485+
486+
module.exports.propTypes = {
487+
tabComplete: React.PropTypes.any,
488+
489+
// a callback which is called when the height of the composer is
490+
// changed due to a change in content.
491+
onResize: React.PropTypes.func,
492+
493+
// js-sdk Room object
494+
room: React.PropTypes.object.isRequired
495+
};
496+
497+
// the height we limit the composer to
498+
module.exports.MAX_HEIGHT = 100;
448499

0 commit comments

Comments
 (0)