diff --git a/res/css/_components.scss b/res/css/_components.scss index fb6058df000..213d0d714c9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e897352d7e4..b32a44219a1 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.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'); - } - } } diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss new file mode 100644 index 00000000000..f56214224db --- /dev/null +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -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; +} diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index e0468e9969a..b37552da2a5 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -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) { - const range = getRangeForSelection( - this._editorRef, - this.props.model, - document.getSelection()); - formatInline(range, prefix, suffix); - } - - _formatBold = () => { - this._wrapSelectionAsInline("**"); - } - - _formatItalic = () => { - this._wrapSelectionAsInline("*"); - } - - _formatStrikethrough = () => { - this._wrapSelectionAsInline("", ""); - } - - _formatQuote = () => { + _onFormatAction = (action) => { 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, "", ""); + 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 (
{ autoComplete } -
this._formatBarRef = ref}> - - - - - -
+ this._formatBarRef = ref} onAction={this._onFormatAction} />
this._formatBarRef = ref}> + this.props.onAction("bold")} icon="Bold" /> + this.props.onAction("italics")} icon="Italic" /> + this.props.onAction("strikethrough")} icon="Strikethrough" /> + this.props.onAction("code")} icon="Code" /> + this.props.onAction("quote")} icon="Quote" /> +
); + } + + 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 = ( +
{this.props.label}
+ ); + + return ( + + + + + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d42734d5a44..6529e7322c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -760,11 +760,6 @@ " (unsupported)": " (unsupported)", "Join as voice or video.": "Join as voice or video.", "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",