mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
move format bar to own component
This commit is contained in:
parent
02681d50b9
commit
da29057fd8
5 changed files with 186 additions and 123 deletions
|
@ -146,6 +146,7 @@
|
||||||
@import "./views/rooms/_MemberInfo.scss";
|
@import "./views/rooms/_MemberInfo.scss";
|
||||||
@import "./views/rooms/_MemberList.scss";
|
@import "./views/rooms/_MemberList.scss";
|
||||||
@import "./views/rooms/_MessageComposer.scss";
|
@import "./views/rooms/_MessageComposer.scss";
|
||||||
|
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
|
|
|
@ -73,69 +73,4 @@ limitations under the License.
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 0;
|
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
81
res/css/views/rooms/_MessageComposerFormatBar.scss
Normal file
81
res/css/views/rooms/_MessageComposerFormatBar.scss
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ import TypingStore from "../../../stores/TypingStore";
|
||||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
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$');
|
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) {
|
if (this._hasTextSelected && selection.isCollapsed) {
|
||||||
this._hasTextSelected = false;
|
this._hasTextSelected = false;
|
||||||
if (this._formatBarRef) {
|
if (this._formatBarRef) {
|
||||||
this._formatBarRef.classList.remove("mx_BasicMessageComposer_formatBar_shown");
|
this._formatBarRef.hide();
|
||||||
}
|
}
|
||||||
} else if (!selection.isCollapsed) {
|
} else if (!selection.isCollapsed) {
|
||||||
this._hasTextSelected = true;
|
this._hasTextSelected = true;
|
||||||
if (this._formatBarRef) {
|
if (this._formatBarRef) {
|
||||||
this._formatBarRef.classList.add("mx_BasicMessageComposer_formatBar_shown");
|
|
||||||
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
|
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
|
||||||
|
this._formatBarRef.showAt(selectionRect);
|
||||||
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`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,40 +413,28 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
return caretPosition;
|
return caretPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
_wrapSelectionAsInline(prefix, suffix = prefix) {
|
_onFormatAction = (action) => {
|
||||||
const range = getRangeForSelection(
|
const range = getRangeForSelection(
|
||||||
this._editorRef,
|
this._editorRef,
|
||||||
this.props.model,
|
this.props.model,
|
||||||
document.getSelection());
|
document.getSelection());
|
||||||
formatInline(range, prefix, suffix);
|
switch (action) {
|
||||||
}
|
case "bold":
|
||||||
|
formatInline(range, "**");
|
||||||
_formatBold = () => {
|
break;
|
||||||
this._wrapSelectionAsInline("**");
|
case "italics":
|
||||||
}
|
formatInline(range, "*");
|
||||||
|
break;
|
||||||
_formatItalic = () => {
|
case "strikethrough":
|
||||||
this._wrapSelectionAsInline("*");
|
formatInline(range, "<del>", "</del>");
|
||||||
}
|
break;
|
||||||
|
case "code":
|
||||||
_formatStrikethrough = () => {
|
formatRangeAsCode(range);
|
||||||
this._wrapSelectionAsInline("<del>", "</del>");
|
break;
|
||||||
}
|
case "quote":
|
||||||
|
formatRangeAsQuote(range);
|
||||||
_formatQuote = () => {
|
break;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -486,15 +456,12 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
const classes = classNames("mx_BasicMessageComposer", {
|
const classes = classNames("mx_BasicMessageComposer", {
|
||||||
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
|
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar');
|
||||||
|
|
||||||
return (<div className={classes}>
|
return (<div className={classes}>
|
||||||
{ autoComplete }
|
{ autoComplete }
|
||||||
<div className="mx_BasicMessageComposer_formatBar" ref={ref => this._formatBarRef = ref}>
|
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} />
|
||||||
<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>
|
|
||||||
<div
|
<div
|
||||||
className="mx_BasicMessageComposer_input"
|
className="mx_BasicMessageComposer_input"
|
||||||
contentEditable="true"
|
contentEditable="true"
|
||||||
|
|
79
src/components/views/rooms/MessageComposerFormatBar.js
Normal file
79
src/components/views/rooms/MessageComposerFormatBar.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
|
||||||
|
return (
|
||||||
|
<span aria-label={this.props.label}
|
||||||
|
role="button"
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
className={className}>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue