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

This commit is contained in:
Matthew Hodgson 2016-01-10 12:56:45 +00:00
parent 27d72fb1dc
commit 684255044a
4 changed files with 158 additions and 80 deletions

View file

@ -767,7 +767,7 @@ module.exports = React.createClass({
var deferreds = []; var deferreds = [];
if (old_name != new_name && new_name != undefined && new_name) { if (old_name != new_name && new_name != undefined) {
deferreds.push( deferreds.push(
MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name) 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_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_join_rule = this.refs.room_settings.getJoinRules();
var new_history_visibility = this.refs.room_settings.getHistoryVisibility(); var new_history_visibility = this.refs.room_settings.getHistoryVisibility();
var new_power_levels = this.refs.room_settings.getPowerLevels(); var new_power_levels = this.refs.room_settings.getPowerLevels();

View file

@ -18,13 +18,21 @@ limitations under the License.
var React = require('react'); var React = require('react');
const KEY_TAB = 9;
const KEY_SHIFT = 16;
const KEY_WINDOWS = 91;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'EditableText', displayName: 'EditableText',
propTypes: { propTypes: {
onValueChanged: React.PropTypes.func, onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string, initialValue: React.PropTypes.string,
label: 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: { Phases: {
@ -32,42 +40,51 @@ module.exports = React.createClass({
Edit: "edit", Edit: "edit",
}, },
value: '',
placeholder: false,
getDefaultProps: function() { getDefaultProps: function() {
return { return {
onValueChanged: function() {}, onValueChanged: function() {},
initialValue: '', initialValue: '',
label: 'Click to set', label: '',
placeholder: '', placeholder: '',
}; };
}, },
getInitialState: function() { getInitialState: function() {
return { return {
value: this.props.initialValue,
phase: this.Phases.Display, phase: this.Phases.Display,
} }
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
this.setState({ this.value = nextProps.initialValue;
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() { getValue: function() {
return this.state.value; return this.value;
},
setValue: function(val, shouldSubmit, suppressListener) {
var self = this;
this.setState({
value: val,
phase: this.Phases.Display,
}, function() {
if (!suppressListener) {
self.onValueChanged(shouldSubmit);
}
});
}, },
edit: function() { edit: function() {
@ -84,61 +101,83 @@ module.exports = React.createClass({
}, },
onValueChanged: function(shouldSubmit) { 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) { 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") { if (ev.key == "Enter") {
this.onFinish(ev); this.onFinish(ev);
} else if (ev.key == "Escape") { } else if (ev.key == "Escape") {
this.cancelEdit(); this.cancelEdit();
} }
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
}, },
onClickDiv: function() { onClickDiv: function(ev) {
this.setState({ this.setState({
phase: this.Phases.Edit, phase: this.Phases.Edit,
}) })
}, },
onFocus: function(ev) { onFocus: function(ev) {
ev.target.setSelectionRange(0, ev.target.value.length); //ev.target.setSelectionRange(0, ev.target.textContent.length);
}, },
onFinish: function(ev) { onFinish: function(ev) {
if (ev.target.value) { var self = this;
this.setValue(ev.target.value, ev.key === "Enter"); this.setState({
} else { phase: this.Phases.Display,
this.cancelEdit(); }, function() {
} self.onValueChanged(ev.key === "Enter");
});
}, },
onBlur: function() { onBlur: function(ev) {
this.cancelEdit(); if (this.props.blurToCancel)
this.cancelEdit();
else
this.onFinish(ev)
}, },
render: function() { render: function() {
var editable_el; var editable_el;
if (this.state.phase == this.Phases.Display) { if (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value) {
if (this.state.value) { // show the label
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>; editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{this.props.label}</div>;
} else { } else {
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>; // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
} editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
} else if (this.state.phase == this.Phases.Edit) { onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
editable_el = (
<div>
<input type="text" defaultValue={this.state.value}
onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur} placeholder={this.props.placeHolder} autoFocus/>
</div>
);
} }
return ( return editable_el;
<div className="mx_EditableText">
{editable_el}
</div>
);
} }
}); });

View file

@ -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) { onVideoClick: function(e) {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -57,14 +68,20 @@ module.exports = React.createClass({
}); });
}, },
onNameChange: function(new_name) { onNameChanged: function(value) {
if (this.props.room.name != new_name && new_name) { this.setState({ name : value });
MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name); },
}
onTopicChanged: function(value) {
this.setState({ topic : value });
}, },
getRoomName: function() { getRoomName: function() {
return this.refs.name_edit.value; return this.state.name;
},
getTopic: function() {
return this.state.topic;
}, },
render: function() { render: function() {
@ -76,7 +93,7 @@ module.exports = React.createClass({
if (this.props.simpleHeader) { if (this.props.simpleHeader) {
var cancel; var cancel;
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel-black.png" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/> cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel.svg" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/>
} }
header = header =
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
@ -87,27 +104,45 @@ module.exports = React.createClass({
</div> </div>
} }
else { else {
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
var name = null; var name = null;
var searchStatus = null; var searchStatus = null;
var topic_el = null; var topic_el = null;
var cancel_button = null; var cancel_button = null;
var save_button = null; var save_button = null;
var settings_button = null; var settings_button = null;
var actual_name = this.props.room.currentState.getStateEvents('m.room.name', ''); // var actual_name = this.props.room.currentState.getStateEvents('m.room.name', '');
if (actual_name) actual_name = actual_name.getContent().name; // if (actual_name) actual_name = actual_name.getContent().name;
if (this.props.editing) { if (this.props.editing) {
name = // name =
<div className="mx_RoomHeader_nameEditing"> // <div className="mx_RoomHeader_nameEditing">
<input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/> // <input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>
</div> // </div>
// if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div> // if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div>
cancel_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onCancelClick}>Cancel</div>
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save Changes</div> name =
<div className="mx_RoomHeader_name">
<EditableText
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder="Unnamed Room"
blurToCancel={ false }
onValueChanged={ this.onNameChanged }
initialValue={ this.state.name }/>
</div>
topic_el =
<EditableText
className="mx_RoomHeader_topic mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder="Add a topic"
blurToCancel={ false }
onValueChanged={ this.onTopicChanged }
initialValue={ this.state.topic }/>
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div>
cancel_button = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>
} else { } else {
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} /> // <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
var searchStatus; var searchStatus;
// don't display the search count until the search completes and // don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount. // gives us a valid (possibly zero) searchCount.
@ -123,7 +158,9 @@ module.exports = React.createClass({
<TintableSvg src="img/settings.svg" width="12" height="12"/> <TintableSvg src="img/settings.svg" width="12" height="12"/>
</div> </div>
</div> </div>
if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>;
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
if (topic) topic_el = <div className="mx_RoomHeader_topic" title={ topic.getContent().topic }>{ topic.getContent().topic }</div>;
} }
var roomAvatar = null; var roomAvatar = null;
@ -149,6 +186,18 @@ module.exports = React.createClass({
</div>; </div>;
} }
var right_row;
if (!this.props.editing) {
right_row =
<div className="mx_RoomHeader_rightRow">
{ forget_button }
{ leave_button }
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/search.svg" width="21" height="19"/>
</div>
</div>;
}
header = header =
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow" onClick={this.props.onSettingsClick}> <div className="mx_RoomHeader_leftRow" onClick={this.props.onSettingsClick}>
@ -160,15 +209,9 @@ module.exports = React.createClass({
{ topic_el } { topic_el }
</div> </div>
</div> </div>
{cancel_button}
{save_button} {save_button}
<div className="mx_RoomHeader_rightRow"> {cancel_button}
{ forget_button } {right_row}
{ leave_button }
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/search.svg" width="21" height="19"/>
</div>
</div>
</div> </div>
} }

View file

@ -136,9 +136,6 @@ module.exports = React.createClass({
render: function() { render: function() {
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); 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', ''); var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
if (join_rule) join_rule = join_rule.getContent().join_rule; if (join_rule) join_rule = join_rule.getContent().join_rule;
@ -216,7 +213,7 @@ module.exports = React.createClass({
<img src="img/tick.svg" width="17" height="14" alt="./"/> <img src="img/tick.svg" width="17" height="14" alt="./"/>
</div> </div>
} }
var boundClick = self.onColorSchemeChanged.bind(this, i) var boundClick = self.onColorSchemeChanged.bind(self, i)
return ( return (
<div className="mx_RoomSettings_roomColor" <div className="mx_RoomSettings_roomColor"
key={ "room_color_" + i } key={ "room_color_" + i }
@ -278,7 +275,6 @@ module.exports = React.createClass({
return ( return (
<div className="mx_RoomSettings"> <div className="mx_RoomSettings">
<textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/> <label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/>
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/> <label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>