mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 03:36:07 +03:00
Initial version of rich text editor
This commit is contained in:
parent
07cc9bf77d
commit
001011df27
3 changed files with 165 additions and 75 deletions
|
@ -23,6 +23,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
|
"draft-js": "^0.7.0",
|
||||||
|
"draft-js-export-html": "^0.2.2",
|
||||||
"favico.js": "^0.3.10",
|
"favico.js": "^0.3.10",
|
||||||
"filesize": "^3.1.2",
|
"filesize": "^3.1.2",
|
||||||
"flux": "^2.0.3",
|
"flux": "^2.0.3",
|
||||||
|
|
37
src/RichText.js
Normal file
37
src/RichText.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import {Editor, ContentState, convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle} from 'draft-js';
|
||||||
|
const ReactDOM = require('react-dom');
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
BOLD: 'strong',
|
||||||
|
CODE: 'code',
|
||||||
|
ITALIC: 'em',
|
||||||
|
STRIKETHROUGH: 's',
|
||||||
|
UNDERLINE: 'u'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function contentStateToHTML(contentState:ContentState): String {
|
||||||
|
const elem = contentState.getBlockMap().map((block) => {
|
||||||
|
const elem = DefaultDraftBlockRenderMap.get(block.getType()).element;
|
||||||
|
const content = [];
|
||||||
|
block.findStyleRanges(() => true, (s, e) => {
|
||||||
|
console.log(block.getInlineStyleAt(s));
|
||||||
|
const tags = block.getInlineStyleAt(s).map(style => styles[style]);
|
||||||
|
const open = tags.map(tag => `<${tag}>`).join('');
|
||||||
|
const close = tags.map(tag => `</${tag}>`).reverse().join('');
|
||||||
|
content.push(`${open}${block.getText().substring(s, e)}${close}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (`
|
||||||
|
<${elem}>
|
||||||
|
${content.join('')}
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HTMLtoContentState(html:String): ContentState {
|
||||||
|
return ContentState.createFromBlockArray(convertFromHTML(html));
|
||||||
|
}
|
|
@ -27,6 +27,9 @@ marked.setOptions({
|
||||||
smartypants: false
|
smartypants: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import {Editor, EditorState, RichUtils} from 'draft-js';
|
||||||
|
import {stateToHTML} from 'draft-js-export-html';
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var SlashCommands = require("../../../SlashCommands");
|
var SlashCommands = require("../../../SlashCommands");
|
||||||
var Modal = require("../../../Modal");
|
var Modal = require("../../../Modal");
|
||||||
|
@ -36,6 +39,8 @@ var sdk = require('../../../index');
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var KeyCode = require("../../../KeyCode");
|
var KeyCode = require("../../../KeyCode");
|
||||||
|
|
||||||
|
import {contentStateToHTML} from '../../../RichText';
|
||||||
|
|
||||||
var TYPING_USER_TIMEOUT = 10000;
|
var TYPING_USER_TIMEOUT = 10000;
|
||||||
var TYPING_SERVER_TIMEOUT = 30000;
|
var TYPING_SERVER_TIMEOUT = 30000;
|
||||||
var MARKDOWN_ENABLED = true;
|
var MARKDOWN_ENABLED = true;
|
||||||
|
@ -56,26 +61,18 @@ function mdownToHtml(mdown) {
|
||||||
/*
|
/*
|
||||||
* The textInput part of the MessageComposer
|
* The textInput part of the MessageComposer
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({
|
module.exports = class extends React.Component {
|
||||||
displayName: 'MessageComposerInput',
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.onAction = this.onAction.bind(this);
|
||||||
|
this.onInputClick = this.onInputClick.bind(this);
|
||||||
|
|
||||||
statics: {
|
this.state = {
|
||||||
// the height we limit the composer to
|
editorState: EditorState.createEmpty()
|
||||||
MAX_HEIGHT: 100,
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
propTypes: {
|
componentWillMount() {
|
||||||
tabComplete: React.PropTypes.any,
|
|
||||||
|
|
||||||
// a callback which is called when the height of the composer is
|
|
||||||
// changed due to a change in content.
|
|
||||||
onResize: React.PropTypes.func,
|
|
||||||
|
|
||||||
// js-sdk Room object
|
|
||||||
room: React.PropTypes.object.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
this.oldScrollHeight = 0;
|
this.oldScrollHeight = 0;
|
||||||
this.markdownEnabled = MARKDOWN_ENABLED;
|
this.markdownEnabled = MARKDOWN_ENABLED;
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -157,21 +154,22 @@ module.exports = React.createClass({
|
||||||
// save the currently entered text in order to restore it later.
|
// save the currently entered text in order to restore it later.
|
||||||
// NB: This isn't 'originalText' because we want to restore
|
// NB: This isn't 'originalText' because we want to restore
|
||||||
// sent history items too!
|
// sent history items too!
|
||||||
var text = this.element.value;
|
console.error('fixme');
|
||||||
window.sessionStorage.setItem("input_" + this.roomId, text);
|
// window.sessionStorage.setItem("input_" + this.roomId, text);
|
||||||
},
|
},
|
||||||
|
|
||||||
setLastTextEntry: function() {
|
setLastTextEntry: function() {
|
||||||
var text = window.sessionStorage.getItem("input_" + this.roomId);
|
console.error('fixme');
|
||||||
if (text) {
|
// var text = window.sessionStorage.getItem("input_" + this.roomId);
|
||||||
this.element.value = text;
|
// if (text) {
|
||||||
self.resizeInput();
|
// this.element.value = text;
|
||||||
}
|
// self.resizeInput();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.sentHistory.init(
|
this.sentHistory.init(
|
||||||
this.refs.textarea,
|
this.refs.textarea,
|
||||||
|
@ -181,18 +179,19 @@ module.exports = React.createClass({
|
||||||
if (this.props.tabComplete) {
|
if (this.props.tabComplete) {
|
||||||
this.props.tabComplete.setTextArea(this.refs.textarea);
|
this.props.tabComplete.setTextArea(this.refs.textarea);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this.sentHistory.saveLastTextEntry();
|
this.sentHistory.saveLastTextEntry();
|
||||||
},
|
}
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction(payload) {
|
||||||
var textarea = this.refs.textarea;
|
var editor = this.refs.editor;
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
textarea.focus();
|
console.error('fixme');
|
||||||
|
editor.focus();
|
||||||
break;
|
break;
|
||||||
case 'insert_displayname':
|
case 'insert_displayname':
|
||||||
if (textarea.value.length) {
|
if (textarea.value.length) {
|
||||||
|
@ -214,9 +213,9 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onKeyDown: function (ev) {
|
onKeyDown(ev) {
|
||||||
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
|
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
|
||||||
var input = this.refs.textarea.value;
|
var input = this.refs.textarea.value;
|
||||||
if (input.length === 0) {
|
if (input.length === 0) {
|
||||||
|
@ -252,33 +251,34 @@ module.exports = React.createClass({
|
||||||
self.onFinishedTyping();
|
self.onFinishedTyping();
|
||||||
}
|
}
|
||||||
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
|
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
|
||||||
},
|
}
|
||||||
|
|
||||||
resizeInput: function() {
|
resizeInput() {
|
||||||
|
console.error('fixme');
|
||||||
// scrollHeight is at least equal to clientHeight, so we have to
|
// scrollHeight is at least equal to clientHeight, so we have to
|
||||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||||
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
|
// this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
|
||||||
var newHeight = Math.min(this.refs.textarea.scrollHeight,
|
// var newHeight = Math.min(this.refs.textarea.scrollHeight,
|
||||||
this.constructor.MAX_HEIGHT);
|
// this.constructor.MAX_HEIGHT);
|
||||||
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
// this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
||||||
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
// this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
||||||
|
//
|
||||||
|
// if (this.props.onResize) {
|
||||||
|
// // kick gemini-scrollbar to re-layout
|
||||||
|
// this.props.onResize();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.onResize) {
|
onKeyUp(ev) {
|
||||||
// kick gemini-scrollbar to re-layout
|
|
||||||
this.props.onResize();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onKeyUp: function(ev) {
|
|
||||||
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
|
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
|
||||||
ev.keyCode === KeyCode.DELETE ||
|
ev.keyCode === KeyCode.DELETE ||
|
||||||
ev.keyCode === KeyCode.BACKSPACE)
|
ev.keyCode === KeyCode.BACKSPACE)
|
||||||
{
|
{
|
||||||
this.resizeInput();
|
this.resizeInput();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onEnter: function(ev) {
|
onEnter(ev) {
|
||||||
var contentText = this.refs.textarea.value;
|
var contentText = this.refs.textarea.value;
|
||||||
|
|
||||||
// bodge for now to set markdown state on/off. We probably want a separate
|
// bodge for now to set markdown state on/off. We probably want a separate
|
||||||
|
@ -365,25 +365,25 @@ module.exports = React.createClass({
|
||||||
this.refs.textarea.value = '';
|
this.refs.textarea.value = '';
|
||||||
this.resizeInput();
|
this.resizeInput();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
},
|
}
|
||||||
|
|
||||||
onTypingActivity: function() {
|
onTypingActivity() {
|
||||||
this.isTyping = true;
|
this.isTyping = true;
|
||||||
if (!this.userTypingTimer) {
|
if (!this.userTypingTimer) {
|
||||||
this.sendTyping(true);
|
this.sendTyping(true);
|
||||||
}
|
}
|
||||||
this.startUserTypingTimer();
|
this.startUserTypingTimer();
|
||||||
this.startServerTypingTimer();
|
this.startServerTypingTimer();
|
||||||
},
|
}
|
||||||
|
|
||||||
onFinishedTyping: function() {
|
onFinishedTyping() {
|
||||||
this.isTyping = false;
|
this.isTyping = false;
|
||||||
this.sendTyping(false);
|
this.sendTyping(false);
|
||||||
this.stopUserTypingTimer();
|
this.stopUserTypingTimer();
|
||||||
this.stopServerTypingTimer();
|
this.stopServerTypingTimer();
|
||||||
},
|
}
|
||||||
|
|
||||||
startUserTypingTimer: function() {
|
startUserTypingTimer() {
|
||||||
this.stopUserTypingTimer();
|
this.stopUserTypingTimer();
|
||||||
var self = this;
|
var self = this;
|
||||||
this.userTypingTimer = setTimeout(function() {
|
this.userTypingTimer = setTimeout(function() {
|
||||||
|
@ -391,16 +391,16 @@ module.exports = React.createClass({
|
||||||
self.sendTyping(self.isTyping);
|
self.sendTyping(self.isTyping);
|
||||||
self.userTypingTimer = null;
|
self.userTypingTimer = null;
|
||||||
}, TYPING_USER_TIMEOUT);
|
}, TYPING_USER_TIMEOUT);
|
||||||
},
|
}
|
||||||
|
|
||||||
stopUserTypingTimer: function() {
|
stopUserTypingTimer() {
|
||||||
if (this.userTypingTimer) {
|
if (this.userTypingTimer) {
|
||||||
clearTimeout(this.userTypingTimer);
|
clearTimeout(this.userTypingTimer);
|
||||||
this.userTypingTimer = null;
|
this.userTypingTimer = null;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
startServerTypingTimer: function() {
|
startServerTypingTimer() {
|
||||||
if (!this.serverTypingTimer) {
|
if (!this.serverTypingTimer) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.serverTypingTimer = setTimeout(function() {
|
this.serverTypingTimer = setTimeout(function() {
|
||||||
|
@ -410,39 +410,90 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}, TYPING_SERVER_TIMEOUT / 2);
|
}, TYPING_SERVER_TIMEOUT / 2);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
stopServerTypingTimer: function() {
|
stopServerTypingTimer() {
|
||||||
if (this.serverTypingTimer) {
|
if (this.serverTypingTimer) {
|
||||||
clearTimeout(this.servrTypingTimer);
|
clearTimeout(this.servrTypingTimer);
|
||||||
this.serverTypingTimer = null;
|
this.serverTypingTimer = null;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
sendTyping: function(isTyping) {
|
sendTyping(isTyping) {
|
||||||
MatrixClientPeg.get().sendTyping(
|
MatrixClientPeg.get().sendTyping(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||||
).done();
|
).done();
|
||||||
},
|
}
|
||||||
|
|
||||||
refreshTyping: function() {
|
refreshTyping() {
|
||||||
if (this.typingTimeout) {
|
if (this.typingTimeout) {
|
||||||
clearTimeout(this.typingTimeout);
|
clearTimeout(this.typingTimeout);
|
||||||
this.typingTimeout = null;
|
this.typingTimeout = null;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onInputClick: function(ev) {
|
onInputClick(ev) {
|
||||||
this.refs.textarea.focus();
|
this.refs.editor.focus();
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
onChange(editorState) {
|
||||||
|
this.setState({editorState});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyCommand(command) {
|
||||||
|
const newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
||||||
|
if (newState) {
|
||||||
|
this.onChange(newState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReturn(ev) {
|
||||||
|
if(ev.shiftKey)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const contentState = this.state.editorState.getCurrentContent();
|
||||||
|
if(!contentState.hasText())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const contentText = contentState.getPlainText(),
|
||||||
|
contentHTML = contentStateToHTML(contentState);
|
||||||
|
|
||||||
|
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, contentHTML);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
editorState: EditorState.createEmpty()
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
<div className="mx_MessageComposer_input" onClick={ this.onInputClick } style={{overflow: 'auto'}}>
|
||||||
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
<Editor ref="editor"
|
||||||
|
editorState={this.state.editorState}
|
||||||
|
onChange={(state) => this.onChange(state)}
|
||||||
|
handleKeyCommand={(command) => this.handleKeyCommand(command)}
|
||||||
|
handleReturn={ev => this.handleReturn(ev)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
module.exports.propTypes = {
|
||||||
|
tabComplete: React.PropTypes.any,
|
||||||
|
|
||||||
|
// a callback which is called when the height of the composer is
|
||||||
|
// changed due to a change in content.
|
||||||
|
onResize: React.PropTypes.func,
|
||||||
|
|
||||||
|
// js-sdk Room object
|
||||||
|
room: React.PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
// the height we limit the composer to
|
||||||
|
module.exports.MAX_HEIGHT = 100;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue