From 684255044aff2bcc2ca1e2b2ab2cf9105b0ee7c2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 10 Jan 2016 12:56:45 +0000 Subject: [PATCH] switch EditableText to be built on contentEditable rather than switching divs and inputs, so that it can be used for managing multiline content like topics and room names, and use it in RoomHeader/RoomSettings --- src/components/structures/RoomView.js | 4 +- src/components/views/elements/EditableText.js | 133 +++++++++++------- src/components/views/rooms/RoomHeader.js | 95 +++++++++---- src/components/views/rooms/RoomSettings.js | 6 +- 4 files changed, 158 insertions(+), 80 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 08dfe75584..bc1758c26f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -767,7 +767,7 @@ module.exports = React.createClass({ var deferreds = []; - if (old_name != new_name && new_name != undefined && new_name) { + if (old_name != new_name && new_name != undefined) { deferreds.push( MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name) ); @@ -900,7 +900,7 @@ module.exports = React.createClass({ }); var new_name = this.refs.header.getRoomName(); - var new_topic = this.refs.room_settings.getTopic(); + var new_topic = this.refs.header.getTopic(); var new_join_rule = this.refs.room_settings.getJoinRules(); var new_history_visibility = this.refs.room_settings.getHistoryVisibility(); var new_power_levels = this.refs.room_settings.getPowerLevels(); diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index beedfc35c8..5d509a7831 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -18,13 +18,21 @@ limitations under the License. var React = require('react'); +const KEY_TAB = 9; +const KEY_SHIFT = 16; +const KEY_WINDOWS = 91; + module.exports = React.createClass({ displayName: 'EditableText', propTypes: { onValueChanged: React.PropTypes.func, initialValue: React.PropTypes.string, label: React.PropTypes.string, - placeHolder: React.PropTypes.string, + placeholder: React.PropTypes.string, + className: React.PropTypes.string, + labelClassName: React.PropTypes.string, + placeholderClassName: React.PropTypes.string, + blurToCancel: React.PropTypes.bool, }, Phases: { @@ -32,42 +40,51 @@ module.exports = React.createClass({ Edit: "edit", }, + value: '', + placeholder: false, + getDefaultProps: function() { return { onValueChanged: function() {}, initialValue: '', - label: 'Click to set', + label: '', placeholder: '', }; }, getInitialState: function() { return { - value: this.props.initialValue, phase: this.Phases.Display, } }, componentWillReceiveProps: function(nextProps) { - this.setState({ - value: nextProps.initialValue - }); + this.value = nextProps.initialValue; + }, + + componentDidMount: function() { + this.value = this.props.initialValue; + if (this.refs.editable_div) { + this.showPlaceholder(!this.value); + } + }, + + showPlaceholder: function(show) { + if (show) { + this.refs.editable_div.textContent = this.props.placeholder; + this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName); + this.placeholder = true; + this.value = ''; + } + else { + this.refs.editable_div.textContent = this.value; + this.refs.editable_div.setAttribute("class", this.props.className); + this.placeholder = false; + } }, getValue: function() { - return this.state.value; - }, - - setValue: function(val, shouldSubmit, suppressListener) { - var self = this; - this.setState({ - value: val, - phase: this.Phases.Display, - }, function() { - if (!suppressListener) { - self.onValueChanged(shouldSubmit); - } - }); + return this.value; }, edit: function() { @@ -84,61 +101,83 @@ module.exports = React.createClass({ }, onValueChanged: function(shouldSubmit) { - this.props.onValueChanged(this.state.value, shouldSubmit); + this.props.onValueChanged(this.value, shouldSubmit); + }, + + onKeyDown: function(ev) { + // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); + + if (this.placeholder) { + if (ev.keyCode !== KEY_SHIFT && !ev.metaKey && !ev.ctrlKey && !ev.altKey && ev.keyCode !== KEY_WINDOWS && ev.keyCode !== KEY_TAB) { + this.showPlaceholder(false); + } + } + + if (ev.key == "Enter") { + ev.stopPropagation(); + ev.preventDefault(); + } + + // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); }, onKeyUp: function(ev) { + // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); + + if (!ev.target.textContent) { + this.showPlaceholder(true); + } + else if (!this.placeholder) { + this.value = ev.target.textContent; + } + if (ev.key == "Enter") { this.onFinish(ev); } else if (ev.key == "Escape") { this.cancelEdit(); } + + // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); }, - onClickDiv: function() { + onClickDiv: function(ev) { this.setState({ phase: this.Phases.Edit, }) }, onFocus: function(ev) { - ev.target.setSelectionRange(0, ev.target.value.length); + //ev.target.setSelectionRange(0, ev.target.textContent.length); }, onFinish: function(ev) { - if (ev.target.value) { - this.setValue(ev.target.value, ev.key === "Enter"); - } else { - this.cancelEdit(); - } + var self = this; + this.setState({ + phase: this.Phases.Display, + }, function() { + self.onValueChanged(ev.key === "Enter"); + }); }, - onBlur: function() { - this.cancelEdit(); + onBlur: function(ev) { + if (this.props.blurToCancel) + this.cancelEdit(); + else + this.onFinish(ev) }, render: function() { var editable_el; - if (this.state.phase == this.Phases.Display) { - if (this.state.value) { - editable_el =
{this.state.value}
; - } else { - editable_el =
{this.props.label}
; - } - } else if (this.state.phase == this.Phases.Edit) { - editable_el = ( -
- -
- ); + if (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value) { + // show the label + editable_el =
{this.props.label}
; + } else { + // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together + editable_el =
; } - return ( -
- {editable_el} -
- ); + return editable_el; } }); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index cdfbc0bfc8..072397526b 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -41,6 +41,17 @@ module.exports = React.createClass({ }; }, + componentWillReceiveProps: function(newProps) { + if (newProps.editing) { + var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); + + this.setState({ + name: this.props.room.name, + topic: topic ? topic.getContent().topic : '', + }); + } + }, + onVideoClick: function(e) { dis.dispatch({ action: 'place_call', @@ -57,14 +68,20 @@ module.exports = React.createClass({ }); }, - onNameChange: function(new_name) { - if (this.props.room.name != new_name && new_name) { - MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name); - } + onNameChanged: function(value) { + this.setState({ name : value }); + }, + + onTopicChanged: function(value) { + this.setState({ topic : value }); }, getRoomName: function() { - return this.refs.name_edit.value; + return this.state.name; + }, + + getTopic: function() { + return this.state.topic; }, render: function() { @@ -76,7 +93,7 @@ module.exports = React.createClass({ if (this.props.simpleHeader) { var cancel; if (this.props.onCancelClick) { - cancel = Close + cancel = Close } header =
@@ -87,27 +104,45 @@ module.exports = React.createClass({
} else { - var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); - var name = null; var searchStatus = null; var topic_el = null; var cancel_button = null; var save_button = null; var settings_button = null; - var actual_name = this.props.room.currentState.getStateEvents('m.room.name', ''); - if (actual_name) actual_name = actual_name.getContent().name; + // var actual_name = this.props.room.currentState.getStateEvents('m.room.name', ''); + // if (actual_name) actual_name = actual_name.getContent().name; if (this.props.editing) { - name = -
- -
+ // name = + //
+ // + //
// if (topic) topic_el =
- cancel_button =
Cancel
- save_button =
Save Changes
+ + name = +
+ +
+ + topic_el = + + + save_button =
Save
+ cancel_button =
Cancel
} else { // - var searchStatus; // don't display the search count until the search completes and // gives us a valid (possibly zero) searchCount. @@ -123,7 +158,9 @@ module.exports = React.createClass({ - if (topic) topic_el =
{ topic.getContent().topic }
; + + var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); + if (topic) topic_el =
{ topic.getContent().topic }
; } var roomAvatar = null; @@ -149,6 +186,18 @@ module.exports = React.createClass({ ; } + var right_row; + if (!this.props.editing) { + right_row = +
+ { forget_button } + { leave_button } +
+ +
+
; + } + header =
@@ -160,15 +209,9 @@ module.exports = React.createClass({ { topic_el }
- {cancel_button} {save_button} -
- { forget_button } - { leave_button } -
- -
-
+ {cancel_button} + {right_row} } diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 0864dc15c7..c601858757 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -136,9 +136,6 @@ module.exports = React.createClass({ render: function() { var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); - var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); - if (topic) topic = topic.getContent().topic; - var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', ''); if (join_rule) join_rule = join_rule.getContent().join_rule; @@ -216,7 +213,7 @@ module.exports = React.createClass({ ./ } - var boundClick = self.onColorSchemeChanged.bind(this, i) + var boundClick = self.onColorSchemeChanged.bind(self, i) return (
-