mirror of
https://github.com/element-hq/element-web
synced 2024-11-24 10:15:43 +03:00
Merge pull request #3387 from matrix-org/bwindels/cider-formatbar-tooltips
New composer: format bar tooltips
This commit is contained in:
commit
de787c798c
6 changed files with 205 additions and 128 deletions
|
@ -146,6 +146,7 @@
|
|||
@import "./views/rooms/_MemberInfo.scss";
|
||||
@import "./views/rooms/_MemberList.scss";
|
||||
@import "./views/rooms/_MessageComposer.scss";
|
||||
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
||||
@import "./views/rooms/_PinnedEventTile.scss";
|
||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||
@import "./views/rooms/_PresenceLabel.scss";
|
||||
|
|
|
@ -73,69 +73,4 @@ limitations under the License.
|
|||
position: relative;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatBar {
|
||||
display: none;
|
||||
width: calc(26px * 5);
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
background-color: $message-action-bar-bg-color;
|
||||
user-select: none;
|
||||
|
||||
&.mx_BasicMessageComposer_formatBar_shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> * {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 1px solid $message-action-bar-border-color;
|
||||
margin-left: -1px;
|
||||
|
||||
&:hover {
|
||||
border-color: $message-action-bar-hover-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatButton {
|
||||
width: 27px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatButton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: $message-action-bar-fg-color;
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatBold::after {
|
||||
mask-image: url('$(res)/img/format/bold.svg');
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatItalic::after {
|
||||
mask-image: url('$(res)/img/format/italics.svg');
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatStrikethrough::after {
|
||||
mask-image: url('$(res)/img/format/strikethrough.svg');
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatQuote::after {
|
||||
mask-image: url('$(res)/img/format/quote.svg');
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer_formatCode::after {
|
||||
mask-image: url('$(res)/img/format/code.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
86
res/css/views/rooms/_MessageComposerFormatBar.scss
Normal file
86
res/css/views/rooms/_MessageComposerFormatBar.scss
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
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_MessageComposerFormatBar {
|
||||
display: none;
|
||||
width: calc(26px * 5);
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
background-color: $message-action-bar-bg-color;
|
||||
user-select: none;
|
||||
|
||||
&.mx_MessageComposerFormatBar_shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> * {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 1px solid $message-action-bar-border-color;
|
||||
margin-left: -1px;
|
||||
|
||||
&:hover {
|
||||
border-color: $message-action-bar-hover-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_button {
|
||||
width: 27px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: $message-action-bar-fg-color;
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonIconBold::after {
|
||||
mask-image: url('$(res)/img/format/bold.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonIconItalic::after {
|
||||
mask-image: url('$(res)/img/format/italics.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonIconStrikethrough::after {
|
||||
mask-image: url('$(res)/img/format/strikethrough.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonIconQuote::after {
|
||||
mask-image: url('$(res)/img/format/quote.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonIconCode::after {
|
||||
mask-image: url('$(res)/img/format/code.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonTooltip {
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
|
@ -35,7 +35,7 @@ import TypingStore from "../../../stores/TypingStore";
|
|||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
|
@ -251,31 +251,13 @@ export default class BasicMessageEditor extends React.Component {
|
|||
if (this._hasTextSelected && selection.isCollapsed) {
|
||||
this._hasTextSelected = false;
|
||||
if (this._formatBarRef) {
|
||||
this._formatBarRef.classList.remove("mx_BasicMessageComposer_formatBar_shown");
|
||||
this._formatBarRef.hide();
|
||||
}
|
||||
} else if (!selection.isCollapsed) {
|
||||
this._hasTextSelected = true;
|
||||
if (this._formatBarRef) {
|
||||
this._formatBarRef.classList.add("mx_BasicMessageComposer_formatBar_shown");
|
||||
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
|
||||
|
||||
let leftOffset = 0;
|
||||
let node = this._formatBarRef;
|
||||
while (node.offsetParent) {
|
||||
node = node.offsetParent;
|
||||
leftOffset += node.offsetLeft;
|
||||
}
|
||||
|
||||
let topOffset = 0;
|
||||
node = this._formatBarRef;
|
||||
while (node.offsetParent) {
|
||||
node = node.offsetParent;
|
||||
topOffset += node.offsetTop;
|
||||
}
|
||||
|
||||
this._formatBarRef.style.left = `${selectionRect.left - leftOffset}px`;
|
||||
// 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok.
|
||||
this._formatBarRef.style.top = `${selectionRect.top - topOffset - 16 - 12}px`;
|
||||
this._formatBarRef.showAt(selectionRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -431,40 +413,28 @@ export default class BasicMessageEditor extends React.Component {
|
|||
return caretPosition;
|
||||
}
|
||||
|
||||
_wrapSelectionAsInline(prefix, suffix = prefix) {
|
||||
_onFormatAction = (action) => {
|
||||
const range = getRangeForSelection(
|
||||
this._editorRef,
|
||||
this.props.model,
|
||||
document.getSelection());
|
||||
formatInline(range, prefix, suffix);
|
||||
}
|
||||
|
||||
_formatBold = () => {
|
||||
this._wrapSelectionAsInline("**");
|
||||
}
|
||||
|
||||
_formatItalic = () => {
|
||||
this._wrapSelectionAsInline("*");
|
||||
}
|
||||
|
||||
_formatStrikethrough = () => {
|
||||
this._wrapSelectionAsInline("<del>", "</del>");
|
||||
}
|
||||
|
||||
_formatQuote = () => {
|
||||
const range = getRangeForSelection(
|
||||
this._editorRef,
|
||||
this.props.model,
|
||||
document.getSelection());
|
||||
formatRangeAsQuote(range);
|
||||
}
|
||||
|
||||
_formatCode = () => {
|
||||
const range = getRangeForSelection(
|
||||
this._editorRef,
|
||||
this.props.model,
|
||||
document.getSelection());
|
||||
formatRangeAsCode(range);
|
||||
switch (action) {
|
||||
case "bold":
|
||||
formatInline(range, "**");
|
||||
break;
|
||||
case "italics":
|
||||
formatInline(range, "*");
|
||||
break;
|
||||
case "strikethrough":
|
||||
formatInline(range, "<del>", "</del>");
|
||||
break;
|
||||
case "code":
|
||||
formatRangeAsCode(range);
|
||||
break;
|
||||
case "quote":
|
||||
formatRangeAsQuote(range);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -486,15 +456,12 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const classes = classNames("mx_BasicMessageComposer", {
|
||||
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
|
||||
});
|
||||
|
||||
const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar');
|
||||
|
||||
return (<div className={classes}>
|
||||
{ autoComplete }
|
||||
<div className="mx_BasicMessageComposer_formatBar" ref={ref => this._formatBarRef = ref}>
|
||||
<span aria-label={_t("Bold")} role="button" onClick={this._formatBold} className="mx_BasicMessageComposer_formatButton mx_BasicMessageComposer_formatBold"></span>
|
||||
<span aria-label={_t("Italics")} role="button" onClick={this._formatItalic} className="mx_BasicMessageComposer_formatButton mx_BasicMessageComposer_formatItalic"></span>
|
||||
<span aria-label={_t("Strikethrough")} role="button" onClick={this._formatStrikethrough} className="mx_BasicMessageComposer_formatButton mx_BasicMessageComposer_formatStrikethrough"></span>
|
||||
<span aria-label={_t("Code block")} role="button" onClick={this._formatCode} className="mx_BasicMessageComposer_formatButton mx_BasicMessageComposer_formatCode"></span>
|
||||
<span aria-label={_t("Quote")} role="button" onClick={this._formatQuote} className="mx_BasicMessageComposer_formatButton mx_BasicMessageComposer_formatQuote"></span>
|
||||
</div>
|
||||
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} />
|
||||
<div
|
||||
className="mx_BasicMessageComposer_input"
|
||||
contentEditable="true"
|
||||
|
|
88
src/components/views/rooms/MessageComposerFormatBar.js
Normal file
88
src/components/views/rooms/MessageComposerFormatBar.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
||||
export default class MessageComposerFormatBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onAction: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="mx_MessageComposerFormatBar" ref={ref => this._formatBarRef = ref}>
|
||||
<FormatButton label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" />
|
||||
<FormatButton label={_t("Italics")} onClick={() => this.props.onAction("italics")} icon="Italic" />
|
||||
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction("strikethrough")} icon="Strikethrough" />
|
||||
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" />
|
||||
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" />
|
||||
</div>);
|
||||
}
|
||||
|
||||
showAt(selectionRect) {
|
||||
this._formatBarRef.classList.add("mx_MessageComposerFormatBar_shown");
|
||||
let leftOffset = 0;
|
||||
let node = this._formatBarRef;
|
||||
while (node.offsetParent) {
|
||||
node = node.offsetParent;
|
||||
leftOffset += node.offsetLeft;
|
||||
}
|
||||
|
||||
let topOffset = 0;
|
||||
node = this._formatBarRef;
|
||||
while (node.offsetParent) {
|
||||
node = node.offsetParent;
|
||||
topOffset += node.offsetTop;
|
||||
}
|
||||
|
||||
this._formatBarRef.style.left = `${selectionRect.left - leftOffset}px`;
|
||||
// 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok.
|
||||
this._formatBarRef.style.top = `${selectionRect.top - topOffset - 16 - 12}px`;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this._formatBarRef.classList.remove("mx_MessageComposerFormatBar_shown");
|
||||
}
|
||||
}
|
||||
|
||||
class FormatButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
|
||||
const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
|
||||
const tooltipContent = (
|
||||
<div className="mx_MessageComposerFormatBar_buttonTooltip">{this.props.label}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<InteractiveTooltip content={tooltipContent}>
|
||||
<span aria-label={this.props.label}
|
||||
role="button"
|
||||
onClick={this.props.onClick}
|
||||
className={className}>
|
||||
</span>
|
||||
</InteractiveTooltip>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -760,11 +760,6 @@
|
|||
" (unsupported)": " (unsupported)",
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
"Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.",
|
||||
"Bold": "Bold",
|
||||
"Italics": "Italics",
|
||||
"Strikethrough": "Strikethrough",
|
||||
"Code block": "Code block",
|
||||
"Quote": "Quote",
|
||||
"Some devices for this user are not trusted": "Some devices for this user are not trusted",
|
||||
"Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted",
|
||||
"All devices for this user are trusted": "All devices for this user are trusted",
|
||||
|
@ -836,6 +831,11 @@
|
|||
"The conversation continues here.": "The conversation continues here.",
|
||||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||
"Bold": "Bold",
|
||||
"Italics": "Italics",
|
||||
"Strikethrough": "Strikethrough",
|
||||
"Code block": "Code block",
|
||||
"Quote": "Quote",
|
||||
"Server error": "Server error",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||
"Command error": "Command error",
|
||||
|
|
Loading…
Reference in a new issue