implement RoomAliasField component

adding a postfix to Field to show the domain name
pull/21833/head
Bruno Windels 2019-09-20 17:45:14 +02:00
parent c5f9ef87ba
commit 8a1c1bbec4
5 changed files with 198 additions and 3 deletions

View File

@ -99,6 +99,7 @@
@import "./views/elements/_ResizeHandle.scss";
@import "./views/elements/_RichText.scss";
@import "./views/elements/_RoleButton.scss";
@import "./views/elements/_RoomAliasField.scss";
@import "./views/elements/_Spinner.scss";
@import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_TextWithTooltip.scss";

View File

@ -31,6 +31,10 @@ limitations under the License.
border-right: 1px solid $input-border-color;
}
.mx_Field_postfix {
border-left: 1px solid $input-border-color;
}
.mx_Field input,
.mx_Field select,
.mx_Field textarea {

View File

@ -0,0 +1,56 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomAliasField {
// if parent is a flex container, this allows the
// width to be as wide as needed, and not 100%
flex: 0 1 auto;
display: flex;
align-items: stretch;
min-width: 0;
max-width: 100%;
input {
width: 150px;
padding-left: 0;
padding-right: 0;
}
input::placeholder {
color: $greyed-fg-color;
font-weight: normal;
}
.mx_Field_prefix, .mx_Field_postfix {
color: $greyed-fg-color;
border-left: none;
border-right: none;
font-weight: 600;
padding: 9px 10px;
flex: 0 0 auto;
}
.mx_Field_postfix {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
// this allows the domain name to show
// as long as it doesn't make the input shrink
// if it's too big, it shows an ellipsis
// 180: 28 for prefix, 152 for input
max-width: calc(100% - 180px);
}
}

View File

@ -41,6 +41,8 @@ export default class Field extends React.PureComponent {
value: PropTypes.string.isRequired,
// Optional component to include inside the field before the input.
prefix: PropTypes.node,
// Optional component to include inside the field after the input.
postfix: PropTypes.node,
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
@ -54,6 +56,8 @@ export default class Field extends React.PureComponent {
// If specified alongside tooltipContent, the class name to apply to the
// tooltip itself.
tooltipClassName: PropTypes.string,
// If specified, an additional class name to apply to the field container
className: PropTypes.string,
// All other props pass through to the <input>.
};
@ -143,8 +147,8 @@ export default class Field extends React.PureComponent {
render() {
const {
element, prefix, onValidate, children, tooltipContent, flagInvalid,
tooltipClassName, ...inputProps} = this.props;
element, prefix, postfix, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
const inputElement = element || "input";
@ -163,9 +167,13 @@ export default class Field extends React.PureComponent {
if (prefix) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
}
let postfixContainer = null;
if (postfix) {
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
}
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
@ -192,6 +200,7 @@ export default class Field extends React.PureComponent {
{prefixContainer}
{fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label>
{postfixContainer}
{fieldTooltip}
</div>;
}

View File

@ -0,0 +1,125 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { _t } from '../../../languageHandler';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import withValidation from './Validation';
import MatrixClientPeg from '../../../MatrixClientPeg';
export default class RoomAliasField extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
domain: PropTypes.string.isRequired,
onChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {isValid: true};
}
_asFullAlias(localpart) {
return `#${localpart}:${this.props.domain}`;
}
render() {
const Field = sdk.getComponent('views.elements.Field');
const poundSign = (<span>#</span>);
const aliasPostfix = ":" + this.props.domain;
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
<Field
label={_t("Room alias")}
className="mx_RoomAliasField"
prefix={poundSign}
postfix={domain}
id={this.props.id}
ref={ref => this._fieldRef = ref}
onValidate={this._onValidate}
placeholder={_t("e.g. my-room")}
onChange={this._onChange}
maxLength={maxlength} />
);
}
_onChange = (ev) => {
if (this.props.onChange) {
this.props.onChange(this._asFullAlias(ev.target.value));
}
}
_onValidate = async (fieldState) => {
const result = await this._validationRules(fieldState);
this.setState({isValid: result.valid});
return result;
};
_validationRules = withValidation({
rules: [
{
key: "safeLocalpart",
test: async ({ value }) => {
if (!value) {
return true;
}
const fullAlias = this._asFullAlias(value);
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
encodeURI(fullAlias) === fullAlias;
},
invalid: () => _t("Some characters not allowed"),
}, {
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Please provide a room alias"),
}, {
key: "taken",
test: async ({value}) => {
if (!value) {
return true;
}
const client = MatrixClientPeg.get();
try {
await client.getRoomIdForAlias(this._asFullAlias(value));
// we got a room id, so the alias is taken
return false;
} catch (err) {
// any server error code will do,
// either it M_NOT_FOUND or the alias is invalid somehow,
// in which case we don't want to show the invalid message
return !!err.errcode;
}
},
valid: () => _t("This alias is available to use"),
invalid: () => _t("This alias is already in use"),
},
],
});
get isValid() {
return this.state.isValid;
}
validate(options) {
return this._fieldRef.validate(options);
}
focus() {
this._fieldRef.focus();
}
}