Various commenting improvements (#4938)

* fix: New threads attached to previous as replies

* fix: Cannot use floating toolbar properly in comments

* perf: Avoid re-writing history on click in editor

* fix: Comment on text selection

* fix: 'Copy link' on comments uses wrong hostname

* Show comment buttons on input focus rather than non-empty input
Increase maximum sidebar size

* Allow opening comments from document menu

* fix: Clicking comment menu should not focus thread
This commit is contained in:
Tom Moor
2023-02-26 14:19:12 -05:00
committed by GitHub
parent b813f20f8f
commit 08df14618c
16 changed files with 219 additions and 141 deletions

View File

@@ -3,7 +3,6 @@ import { findDomRefAtPos, findParentNode } from "prosemirror-utils";
import { EditorView } from "prosemirror-view";
import * as React from "react";
import { Trans } from "react-i18next";
import { Portal } from "react-portal";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import insertFiles from "@shared/editor/commands/insertFiles";
@@ -14,6 +13,7 @@ import { MenuItem } from "@shared/editor/types";
import { depths } from "@shared/styles";
import { getEventFiles } from "@shared/utils/files";
import { AttachmentValidation } from "@shared/validations";
import { Portal } from "~/components/Portal";
import Scrollable from "~/components/Scrollable";
import { Dictionary } from "~/hooks/useDictionary";
import Input from "./Input";
@@ -406,7 +406,16 @@ class CommandMenu<T extends MenuItem> extends React.Component<Props<T>, State> {
const { top, bottom, right } = paragraph.node.getBoundingClientRect();
const margin = 24;
let leftPos = left + window.scrollX;
const offsetParent = ref?.offsetParent
? ref.offsetParent.getBoundingClientRect()
: ({
width: 0,
height: 0,
top: 0,
left: 0,
} as DOMRect);
let leftPos = left - offsetParent.left;
if (props.rtl && ref) {
leftPos = right - ref.scrollWidth;
}
@@ -414,14 +423,14 @@ class CommandMenu<T extends MenuItem> extends React.Component<Props<T>, State> {
if (startPos.top - offsetHeight > margin) {
return {
left: leftPos,
top: undefined,
bottom: window.innerHeight - top - window.scrollY,
top: top - offsetParent.top - offsetHeight,
bottom: undefined,
isAbove: false,
};
} else {
return {
left: leftPos,
top: bottom + window.scrollY,
top: bottom - offsetParent.top,
bottom: undefined,
isAbove: true,
};

View File

@@ -1,9 +1,9 @@
import { NodeSelection } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { depths } from "@shared/styles";
import { Portal } from "~/components/Portal";
import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener";
import useMediaQuery from "~/hooks/useMediaQuery";
@@ -80,6 +80,15 @@ function usePosition({
right: Math.max(fromPos.right, toPos.right),
};
const offsetParent = menuRef.current.offsetParent
? menuRef.current.offsetParent.getBoundingClientRect()
: ({
width: 0,
height: 0,
top: 0,
left: 0,
} as DOMRect);
// tables are an oddity, and need their own positioning logic
const isColSelection =
selection instanceof CellSelection &&
@@ -116,8 +125,8 @@ function usePosition({
const { left, top, width } = imageElement.getBoundingClientRect();
return {
left: Math.round(left + width / 2 + window.scrollX - menuWidth / 2),
top: Math.round(top + window.scrollY - menuHeight),
left: Math.round(left + width / 2 - menuWidth / 2 - offsetParent.left),
top: Math.round(top - menuHeight - offsetParent.top),
offset: 0,
visible: true,
};
@@ -145,8 +154,8 @@ function usePosition({
// of the selection still
const offset = left - (centerOfSelection - menuWidth / 2);
return {
left: Math.round(left + window.scrollX),
top: Math.round(top + window.scrollY),
left: Math.round(left - offsetParent.left),
top: Math.round(top - offsetParent.top),
offset: Math.round(offset),
visible: true,
};

View File

@@ -2,7 +2,9 @@ import styled from "styled-components";
type Props = { active?: boolean; disabled?: boolean };
export default styled.button<Props>`
export default styled.button.attrs((props) => ({
type: props.type || "button",
}))<Props>`
display: inline-block;
flex: 0;
width: 24px;

View File

@@ -32,6 +32,7 @@ import { UserPreferences } from "@shared/types";
import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper";
import EventEmitter from "@shared/utils/events";
import Flex from "~/components/Flex";
import { PortalContext } from "~/components/Portal";
import { Dictionary } from "~/hooks/useDictionary";
import Logger from "~/utils/Logger";
import BlockMenu from "./components/BlockMenu";
@@ -178,7 +179,8 @@ export class Editor extends React.PureComponent<
isBlurred: boolean;
extensions: ExtensionManager;
element = React.createRef<HTMLDivElement>();
elementRef = React.createRef<HTMLDivElement>();
wrapperRef = React.createRef<HTMLDivElement>();
view: EditorView;
schema: Schema;
serializer: MarkdownSerializer;
@@ -435,7 +437,7 @@ export class Editor extends React.PureComponent<
}
private createView() {
if (!this.element.current) {
if (!this.elementRef.current) {
throw new Error("createView called before ref available");
}
@@ -448,7 +450,7 @@ export class Editor extends React.PureComponent<
};
const self = this; // eslint-disable-line
const view = new EditorView(this.element.current, {
const view = new EditorView(this.elementRef.current, {
handleDOMEvents: {
blur: this.handleEditorBlur,
focus: this.handleEditorFocus,
@@ -521,13 +523,13 @@ export class Editor extends React.PureComponent<
};
private calculateDir = () => {
if (!this.element.current) {
if (!this.elementRef.current) {
return;
}
const isRTL =
this.props.dir === "rtl" ||
getComputedStyle(this.element.current).direction === "rtl";
getComputedStyle(this.elementRef.current).direction === "rtl";
if (this.state.isRTL !== isRTL) {
this.setState({ isRTL });
@@ -718,75 +720,78 @@ export class Editor extends React.PureComponent<
const { isRTL } = this.state;
return (
<EditorContext.Provider value={this}>
<Flex
onKeyDown={onKeyDown}
style={style}
className={className}
align="flex-start"
justify="center"
column
>
<EditorContainer
dir={dir}
rtl={isRTL}
grow={grow}
readOnly={readOnly}
readOnlyWriteCheckboxes={readOnlyWriteCheckboxes}
focusedCommentId={this.props.focusedCommentId}
ref={this.element}
/>
{!readOnly && this.view && (
<>
<SelectionToolbar
view={this.view}
dictionary={dictionary}
commands={this.commands}
rtl={isRTL}
isTemplate={this.props.template === true}
onOpen={this.handleOpenSelectionMenu}
onClose={this.handleCloseSelectionMenu}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onCreateLink={this.props.onCreateLink}
onShowToast={this.props.onShowToast}
/>
<LinkToolbar
isActive={this.state.linkMenuOpen}
onCreateLink={this.props.onCreateLink}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onClose={this.handleCloseLinkMenu}
/>
<EmojiMenu
view={this.view}
commands={this.commands}
dictionary={dictionary}
rtl={isRTL}
onShowToast={this.props.onShowToast}
isActive={this.state.emojiMenuOpen}
search={this.state.blockMenuSearch}
onClose={this.handleCloseEmojiMenu}
/>
<BlockMenu
view={this.view}
commands={this.commands}
dictionary={dictionary}
rtl={isRTL}
isActive={this.state.blockMenuOpen}
search={this.state.blockMenuSearch}
onClose={this.handleCloseBlockMenu}
uploadFile={this.props.uploadFile}
onLinkToolbarOpen={this.handleOpenLinkMenu}
onFileUploadStart={this.props.onFileUploadStart}
onFileUploadStop={this.props.onFileUploadStop}
onShowToast={this.props.onShowToast}
embeds={this.props.embeds}
/>
</>
)}
</Flex>
</EditorContext.Provider>
<PortalContext.Provider value={this.wrapperRef.current}>
<EditorContext.Provider value={this}>
<Flex
ref={this.wrapperRef}
onKeyDown={onKeyDown}
style={style}
className={className}
align="flex-start"
justify="center"
column
>
<EditorContainer
dir={dir}
rtl={isRTL}
grow={grow}
readOnly={readOnly}
readOnlyWriteCheckboxes={readOnlyWriteCheckboxes}
focusedCommentId={this.props.focusedCommentId}
ref={this.elementRef}
/>
{!readOnly && this.view && (
<>
<SelectionToolbar
view={this.view}
dictionary={dictionary}
commands={this.commands}
rtl={isRTL}
isTemplate={this.props.template === true}
onOpen={this.handleOpenSelectionMenu}
onClose={this.handleCloseSelectionMenu}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onCreateLink={this.props.onCreateLink}
onShowToast={this.props.onShowToast}
/>
<LinkToolbar
isActive={this.state.linkMenuOpen}
onCreateLink={this.props.onCreateLink}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onClose={this.handleCloseLinkMenu}
/>
<EmojiMenu
view={this.view}
commands={this.commands}
dictionary={dictionary}
rtl={isRTL}
onShowToast={this.props.onShowToast}
isActive={this.state.emojiMenuOpen}
search={this.state.blockMenuSearch}
onClose={this.handleCloseEmojiMenu}
/>
<BlockMenu
view={this.view}
commands={this.commands}
dictionary={dictionary}
rtl={isRTL}
isActive={this.state.blockMenuOpen}
search={this.state.blockMenuSearch}
onClose={this.handleCloseBlockMenu}
uploadFile={this.props.uploadFile}
onLinkToolbarOpen={this.handleOpenLinkMenu}
onFileUploadStart={this.props.onFileUploadStart}
onFileUploadStop={this.props.onFileUploadStop}
onShowToast={this.props.onShowToast}
embeds={this.props.embeds}
/>
</>
)}
</Flex>
</EditorContext.Provider>
</PortalContext.Provider>
);
}
}