add visual bell when no replacements are available

also add try/catch in _tabCompleteName so errors don't get swallowed
This commit is contained in:
Bruno Windels 2019-08-28 15:53:16 +02:00
parent c44fbb73d0
commit 85efb71a23
3 changed files with 53 additions and 18 deletions

View file

@ -27,6 +27,15 @@ limitations under the License.
white-space: nowrap; white-space: nowrap;
} }
@keyframes visualbell {
from { background-color: #faa; }
to { background-color: $primary-bg-color; }
}
&.mx_BasicMessageComposer_input_error {
animation: 0.2s visualbell;
}
.mx_BasicMessageComposer_input { .mx_BasicMessageComposer_input {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;

View file

@ -14,6 +14,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import classNames from 'classnames';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import EditorModel from '../../../editor/model'; import EditorModel from '../../../editor/model';
@ -271,7 +273,7 @@ export default class BasicMessageEditor extends React.Component {
return; // don't preventDefault on anything else return; // don't preventDefault on anything else
} }
} else if (event.key === "Tab") { } else if (event.key === "Tab") {
this._tabCompleteName(event); this._tabCompleteName();
handled = true; handled = true;
} }
} }
@ -281,7 +283,9 @@ export default class BasicMessageEditor extends React.Component {
} }
} }
async _tabCompleteName(event) { async _tabCompleteName() {
try {
await new Promise(resolve => this.setState({showVisualBell: false}, resolve));
const {model} = this.props; const {model} = this.props;
const caret = this.getCaret(); const caret = this.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd); const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
@ -290,11 +294,19 @@ export default class BasicMessageEditor extends React.Component {
return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate"); return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate");
}); });
const {partCreator} = model; const {partCreator} = model;
// await for auto-complete to be open
await model.transform(() => { await model.transform(() => {
const addedLen = range.replace([partCreator.pillCandidate(range.text)]); const addedLen = range.replace([partCreator.pillCandidate(range.text)]);
return model.positionForOffset(caret.offset + addedLen, true); return model.positionForOffset(caret.offset + addedLen, true);
}); });
await model.autoComplete.onTab(); await model.autoComplete.onTab();
if (!model.autoComplete.hasSelection()) {
this.setState({showVisualBell: true});
model.autoComplete.close();
}
} catch (err) {
console.error(err);
}
} }
isModified() { isModified() {
@ -324,7 +336,14 @@ export default class BasicMessageEditor extends React.Component {
// not really, but we could not serialize the parts, and just change the autoCompleter // not really, but we could not serialize the parts, and just change the autoCompleter
partCreator.setAutoCompleteCreator(autoCompleteCreator( partCreator.setAutoCompleteCreator(autoCompleteCreator(
() => this._autocompleteRef, () => this._autocompleteRef,
query => new Promise(resolve => this.setState({query}, resolve)), query => {
return new Promise(resolve => this.setState({query}, resolve));
// if setState
// if (this.state.query === query) {
// return Promise.resolve();
// } else {
// }
},
)); ));
this.historyManager = new HistoryManager(partCreator); this.historyManager = new HistoryManager(partCreator);
// initial render of model // initial render of model
@ -365,7 +384,10 @@ export default class BasicMessageEditor extends React.Component {
/> />
</div>); </div>);
} }
return (<div className="mx_BasicMessageComposer"> const classes = classNames("mx_BasicMessageComposer", {
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
});
return (<div className={classes}>
{ autoComplete } { autoComplete }
<div <div
className="mx_BasicMessageComposer_input" className="mx_BasicMessageComposer_input"

View file

@ -33,6 +33,10 @@ export default class AutocompleteWrapperModel {
}); });
} }
close() {
this._updateCallback({close: true});
}
hasSelection() { hasSelection() {
return this._getAutocompleterComponent().hasSelection(); return this._getAutocompleterComponent().hasSelection();
} }
@ -67,7 +71,7 @@ export default class AutocompleteWrapperModel {
// so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text)
this._queryPart = part; this._queryPart = part;
this._queryOffset = offset; this._queryOffset = offset;
this._updateQuery(part.text); return this._updateQuery(part.text);
} }
onComponentSelectionChange(completion) { onComponentSelectionChange(completion) {