diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx
index 78dd82bbf..7cfe17ce8 100644
--- a/app/actions/definitions/documents.tsx
+++ b/app/actions/definitions/documents.tsx
@@ -22,9 +22,10 @@ import {
LightBulbIcon,
UnpublishIcon,
PublishIcon,
+ CommentIcon,
} from "outline-icons";
import * as React from "react";
-import { ExportContentType } from "@shared/types";
+import { ExportContentType, TeamPreference } from "@shared/types";
import { getEventFiles } from "@shared/utils/files";
import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentMove from "~/scenes/DocumentMove";
@@ -466,7 +467,7 @@ export const printDocument = createAction({
icon: ,
visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print),
perform: async () => {
- window.print();
+ queueMicrotask(window.print);
},
});
@@ -708,6 +709,29 @@ export const permanentlyDeleteDocument = createAction({
},
});
+export const openDocumentComments = createAction({
+ name: ({ t }) => t("Comments"),
+ analyticsName: "Open comments",
+ section: DocumentSection,
+ icon: ,
+ visible: ({ activeDocumentId, stores }) => {
+ const can = stores.policies.abilities(activeDocumentId ?? "");
+ return (
+ !!activeDocumentId &&
+ can.read &&
+ !can.restore &&
+ !!stores.auth.team?.getPreference(TeamPreference.Commenting)
+ );
+ },
+ perform: ({ activeDocumentId, stores }) => {
+ if (!activeDocumentId) {
+ return;
+ }
+
+ stores.ui.toggleComments();
+ },
+});
+
export const openDocumentHistory = createAction({
name: ({ t }) => t("History"),
analyticsName: "Open document history",
@@ -771,6 +795,7 @@ export const rootDocumentActions = [
printDocument,
pinDocumentToCollection,
pinDocumentToHome,
+ openDocumentComments,
openDocumentHistory,
openDocumentInsights,
];
diff --git a/app/components/Editor.tsx b/app/components/Editor.tsx
index 0459e7133..94ba811d4 100644
--- a/app/components/Editor.tsx
+++ b/app/components/Editor.tsx
@@ -21,7 +21,6 @@ import HoverPreview from "~/components/HoverPreview";
import type { Props as EditorProps, Editor as SharedEditor } from "~/editor";
import useDictionary from "~/hooks/useDictionary";
import useEmbeds from "~/hooks/useEmbeds";
-import useFocusedComment from "~/hooks/useFocusedComment";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { NotFoundError } from "~/utils/errors";
@@ -61,7 +60,6 @@ function Editor(props: Props, ref: React.RefObject | null) {
onDeleteCommentMark,
} = props;
const { auth, comments, documents } = useStores();
- const focusedComment = useFocusedComment();
const { showToast } = useToasts();
const dictionary = useDictionary();
const embeds = useEmbeds(!shareId);
@@ -343,7 +341,6 @@ function Editor(props: Props, ref: React.RefObject | null) {
onChange={handleChange}
placeholder={props.placeholder || ""}
defaultValue={props.defaultValue || ""}
- focusedCommentId={focusedComment?.id}
/>
{props.bottomPadding && !props.readOnly && (
(undefined);
+
+/**
+ * A portal component that uses context to render into a different dom node
+ * or the root of body if no context is available.
+ */
+export function Portal(props: { children: React.ReactNode }) {
+ const node = React.useContext(PortalContext);
+ return {props.children};
+}
diff --git a/app/editor/components/CommandMenu.tsx b/app/editor/components/CommandMenu.tsx
index 5abe98851..57fed095f 100644
--- a/app/editor/components/CommandMenu.tsx
+++ b/app/editor/components/CommandMenu.tsx
@@ -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 extends React.Component, 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 extends React.Component, 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,
};
diff --git a/app/editor/components/FloatingToolbar.tsx b/app/editor/components/FloatingToolbar.tsx
index c288d5b9a..b903f3f96 100644
--- a/app/editor/components/FloatingToolbar.tsx
+++ b/app/editor/components/FloatingToolbar.tsx
@@ -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,
};
diff --git a/app/editor/components/ToolbarButton.tsx b/app/editor/components/ToolbarButton.tsx
index 7c059bbe8..049bbeabc 100644
--- a/app/editor/components/ToolbarButton.tsx
+++ b/app/editor/components/ToolbarButton.tsx
@@ -2,7 +2,9 @@ import styled from "styled-components";
type Props = { active?: boolean; disabled?: boolean };
-export default styled.button`
+export default styled.button.attrs((props) => ({
+ type: props.type || "button",
+}))`
display: inline-block;
flex: 0;
width: 24px;
diff --git a/app/editor/index.tsx b/app/editor/index.tsx
index be796cc48..e17e34ee6 100644
--- a/app/editor/index.tsx
+++ b/app/editor/index.tsx
@@ -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();
+ elementRef = React.createRef();
+ wrapperRef = React.createRef();
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 (
-
-
-
- {!readOnly && this.view && (
- <>
-
-
-
-
- >
- )}
-
-
+
+
+
+
+ {!readOnly && this.view && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
);
}
}
diff --git a/app/menus/CommentMenu.tsx b/app/menus/CommentMenu.tsx
index c3a18a841..a561a80dc 100644
--- a/app/menus/CommentMenu.tsx
+++ b/app/menus/CommentMenu.tsx
@@ -9,6 +9,7 @@ import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Separator from "~/components/ContextMenu/Separator";
+import EventBoundary from "~/components/EventBoundary";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -52,11 +53,14 @@ function CommentMenu({ comment, onEdit, onDelete, className }: Props) {
return (
<>
-
+
+
+
+
{can.update && (