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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user