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 = [];
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();

View File

@ -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 = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>;
} else {
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>;
}
} else if (this.state.phase == this.Phases.Edit) {
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>
);
if (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value) {
// show the label
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{this.props.label}</div>;
} else {
// 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}
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
}
return (
<div className="mx_EditableText">
{editable_el}
</div>
);
return editable_el;
}
});

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) {
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 = <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 =
<div className="mx_RoomHeader_wrapper">
@ -87,27 +104,45 @@ module.exports = React.createClass({
</div>
}
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 =
<div className="mx_RoomHeader_nameEditing">
<input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>
</div>
// name =
// <div className="mx_RoomHeader_nameEditing">
// <input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>
// </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 {
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
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({
<TintableSvg src="img/settings.svg" width="12" height="12"/>
</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;
@ -149,6 +186,18 @@ module.exports = React.createClass({
</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 =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow" onClick={this.props.onSettingsClick}>
@ -160,15 +209,9 @@ module.exports = React.createClass({
{ topic_el }
</div>
</div>
{cancel_button}
{save_button}
<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>
{cancel_button}
{right_row}
</div>
}

View File

@ -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({
<img src="img/tick.svg" width="17" height="14" alt="./"/>
</div>
}
var boundClick = self.onColorSchemeChanged.bind(this, i)
var boundClick = self.onColorSchemeChanged.bind(self, i)
return (
<div className="mx_RoomSettings_roomColor"
key={ "room_color_" + i }
@ -278,7 +275,6 @@ module.exports = React.createClass({
return (
<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="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>