diff --git a/app/components/CollectionDescription.tsx b/app/components/CollectionDescription.tsx index 31132bc8b..e26baa637 100644 --- a/app/components/CollectionDescription.tsx +++ b/app/components/CollectionDescription.tsx @@ -111,7 +111,7 @@ function CollectionDescription({ collection }: Props) { onBlur={handleStopEditing} maxLength={1000} embedsDisabled - readOnlyWriteCheckboxes + canUpdate /> ) : ( diff --git a/app/editor/components/SelectionToolbar.tsx b/app/editor/components/SelectionToolbar.tsx index 6f03396f1..143efbafb 100644 --- a/app/editor/components/SelectionToolbar.tsx +++ b/app/editor/components/SelectionToolbar.tsx @@ -18,6 +18,7 @@ import useToasts from "~/hooks/useToasts"; import getDividerMenuItems from "../menus/divider"; import getFormattingMenuItems from "../menus/formatting"; import getImageMenuItems from "../menus/image"; +import getReadOnlyMenuItems from "../menus/readOnly"; import getTableMenuItems from "../menus/table"; import getTableColMenuItems from "../menus/tableCol"; import getTableRowMenuItems from "../menus/tableRow"; @@ -29,6 +30,8 @@ import ToolbarMenu from "./ToolbarMenu"; type Props = { rtl: boolean; isTemplate: boolean; + readOnly?: boolean; + canComment?: boolean; onOpen: () => void; onClose: () => void; onSearchLink?: (term: string) => Promise; @@ -82,25 +85,25 @@ function useIsDragging() { } export default function SelectionToolbar(props: Props) { - const { onClose, onOpen } = props; + const { onClose, readOnly, onOpen } = props; const { view, commands } = useEditor(); const { showToast: onShowToast } = useToasts(); const dictionary = useDictionary(); const menuRef = React.useRef(null); const isActive = useIsActive(view.state); const isDragging = useIsDragging(); - const previousIsActuve = usePrevious(isActive); + const previousIsActive = usePrevious(isActive); const isMobile = useMobile(); React.useEffect(() => { // Trigger callbacks when the toolbar is opened or closed - if (previousIsActuve && !isActive) { + if (previousIsActive && !isActive) { onClose(); } - if (!previousIsActuve && isActive) { + if (!previousIsActive && isActive) { onOpen(); } - }, [isActive, onClose, onOpen, previousIsActuve]); + }, [isActive, onClose, onOpen, previousIsActive]); React.useEffect(() => { const handleClickOutside = (ev: MouseEvent): void => { @@ -111,12 +114,11 @@ export default function SelectionToolbar(props: Props) { ) { return; } - - if (!isActive || document.activeElement?.tagName === "INPUT") { + if (view.dom.contains(ev.target as HTMLElement)) { return; } - if (view.hasFocus()) { + if (!isActive || document.activeElement?.tagName === "INPUT") { return; } @@ -131,7 +133,7 @@ export default function SelectionToolbar(props: Props) { return () => { window.removeEventListener("mouseup", handleClickOutside); }; - }, [isActive, view]); + }, [isActive, previousIsActive, readOnly, view]); const handleOnCreateLink = async (title: string): Promise => { const { onCreateLink } = props; @@ -143,7 +145,7 @@ export default function SelectionToolbar(props: Props) { const { dispatch, state } = view; const { from, to } = state.selection; if (from === to) { - // selection cannot be collapsed + // Do not display a selection toolbar for collapsed selections return; } @@ -184,7 +186,7 @@ export default function SelectionToolbar(props: Props) { ); }; - const { onCreateLink, isTemplate, rtl, ...rest } = props; + const { onCreateLink, isTemplate, rtl, canComment, ...rest } = props; const { state } = view; const { selection }: { selection: any } = state; const isCodeSelection = isNodeActive(state.schema.nodes.code_block)(state); @@ -195,6 +197,11 @@ export default function SelectionToolbar(props: Props) { return null; } + // no toolbar in this circumstance + if (readOnly && !canComment) { + return null; + } + const colIndex = getColumnIndex(state); const rowIndex = getRowIndex(state); const isTableSelection = colIndex !== undefined && rowIndex !== undefined; @@ -213,6 +220,8 @@ export default function SelectionToolbar(props: Props) { items = getImageMenuItems(state, dictionary); } else if (isDividerSelection) { items = getDividerMenuItems(state, dictionary); + } else if (readOnly) { + items = getReadOnlyMenuItems(state, dictionary); } else { items = getFormattingMenuItems(state, isTemplate, isMobile, dictionary); } diff --git a/app/editor/components/ToolbarButton.tsx b/app/editor/components/ToolbarButton.tsx index 4888375d7..288470a6f 100644 --- a/app/editor/components/ToolbarButton.tsx +++ b/app/editor/components/ToolbarButton.tsx @@ -6,9 +6,11 @@ type Props = { active?: boolean; disabled?: boolean }; export default styled.button.attrs((props) => ({ type: props.type || "button", }))` - display: inline-block; + display: inline-flex; + align-items: center; + gap: 4px; flex: 0; - width: 24px; + min-width: 24px; height: 24px; cursor: var(--pointer); border: none; diff --git a/app/editor/components/ToolbarMenu.tsx b/app/editor/components/ToolbarMenu.tsx index 1f69b91d6..bb10a77fa 100644 --- a/app/editor/components/ToolbarMenu.tsx +++ b/app/editor/components/ToolbarMenu.tsx @@ -45,8 +45,12 @@ function ToolbarMenu(props: Props) { const isActive = item.active ? item.active(state) : false; return ( - + + {item.label && } {item.icon} @@ -56,4 +60,9 @@ function ToolbarMenu(props: Props) { ); } +const Label = styled.span` + font-size: 15px; + font-weight: 500; +`; + export default ToolbarMenu; diff --git a/app/editor/index.tsx b/app/editor/index.tsx index 9ed4d4869..4a0ee6efa 100644 --- a/app/editor/index.tsx +++ b/app/editor/index.tsx @@ -14,6 +14,12 @@ import { Node as ProsemirrorNode, } from "prosemirror-model"; import { EditorState, Selection, Plugin, Transaction } from "prosemirror-state"; +import { + AddMarkStep, + RemoveMarkStep, + ReplaceAroundStep, + ReplaceStep, +} from "prosemirror-transform"; import { Decoration, EditorView, NodeViewConstructor } from "prosemirror-view"; import * as React from "react"; import styled, { css, DefaultTheme, ThemeProps } from "styled-components"; @@ -70,7 +76,9 @@ export type Props = { /** If the editor should not allow editing */ readOnly?: boolean; /** If the editor should still allow editing checkboxes when it is readOnly */ - readOnlyWriteCheckboxes?: boolean; + canUpdate?: boolean; + /** If the editor should still allow commenting when it is readOnly */ + canComment?: boolean; /** A dictionary of translated strings used in the editor */ dictionary: Dictionary; /** The reading direction of the text content, if known */ @@ -441,9 +449,17 @@ export class Editor extends React.PureComponent< const isEditingCheckbox = (tr: Transaction) => tr.steps.some( - (step: any) => - step.slice?.content?.firstChild?.type.name === - this.schema.nodes.checkbox_item.name + (step) => + (step instanceof ReplaceAroundStep || step instanceof ReplaceStep) && + step.slice.content?.firstChild?.type.name === + this.schema.nodes.checkbox_item.name + ); + + const isEditingComment = (tr: Transaction) => + tr.steps.some( + (step) => + (step instanceof AddMarkStep || step instanceof RemoveMarkStep) && + step.mark.type.name === this.schema.marks.comment.name ); const self = this; // eslint-disable-line @@ -469,8 +485,8 @@ export class Editor extends React.PureComponent< if ( transactions.some((tr) => tr.docChanged) && (!self.props.readOnly || - (self.props.readOnlyWriteCheckboxes && - transactions.some(isEditingCheckbox))) + (self.props.canUpdate && transactions.some(isEditingCheckbox)) || + (self.props.canComment && transactions.some(isEditingComment))) ) { self.handleChange(); } @@ -723,15 +739,8 @@ export class Editor extends React.PureComponent< }; public render() { - const { - dir, - readOnly, - readOnlyWriteCheckboxes, - grow, - style, - className, - onKeyDown, - } = this.props; + const { dir, readOnly, canUpdate, grow, style, className, onKeyDown } = + this.props; const { isRTL } = this.state; return ( @@ -751,11 +760,24 @@ export class Editor extends React.PureComponent< rtl={isRTL} grow={grow} readOnly={readOnly} - readOnlyWriteCheckboxes={readOnlyWriteCheckboxes} + readOnlyWriteCheckboxes={canUpdate} focusedCommentId={this.props.focusedCommentId} editorStyle={this.props.editorStyle} ref={this.elementRef} /> + {this.view && ( + + )} {!readOnly && this.view && ( <> {this.marks.link && ( @@ -799,15 +821,6 @@ export class Editor extends React.PureComponent< } /> )} - , + active: isMarkActive(schema.marks.comment), + visible: !isCode, + }, + ]; +} diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 90f14d632..fac830c75 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -510,7 +510,8 @@ class DocumentScene extends React.Component { onPublish={this.onPublish} onCancel={this.goBack} readOnly={readOnly} - readOnlyWriteCheckboxes={readOnly && abilities.update} + canUpdate={abilities.update} + canComment={abilities.comment} > {shareId && ( diff --git a/shared/editor/components/Styles.ts b/shared/editor/components/Styles.ts index f0f9deaa0..70de99c7e 100644 --- a/shared/editor/components/Styles.ts +++ b/shared/editor/components/Styles.ts @@ -578,6 +578,7 @@ h6 { border-radius: 2px; &:hover { + ${props.readOnly ? "cursor: var(--pointer);" : ""} background: ${transparentize(0.5, props.theme.brand.marine)}; } } diff --git a/shared/editor/lib/Extension.ts b/shared/editor/lib/Extension.ts index c20f3161a..3194a00c3 100644 --- a/shared/editor/lib/Extension.ts +++ b/shared/editor/lib/Extension.ts @@ -41,6 +41,10 @@ export default class Extension { return {}; } + get allowInReadOnly(): boolean { + return false; + } + keys(_options: { type?: NodeType | MarkType; schema: Schema; diff --git a/shared/editor/lib/ExtensionManager.ts b/shared/editor/lib/ExtensionManager.ts index ab7ce298e..b0b71d8e8 100644 --- a/shared/editor/lib/ExtensionManager.ts +++ b/shared/editor/lib/ExtensionManager.ts @@ -205,7 +205,7 @@ export default class ExtensionManager { callback: CommandFactory, attrs: Record ) => { - if (!view.editable) { + if (!view.editable && !extension.allowInReadOnly) { return false; } view.focus(); diff --git a/shared/editor/marks/Comment.ts b/shared/editor/marks/Comment.ts index d967a9513..290739667 100644 --- a/shared/editor/marks/Comment.ts +++ b/shared/editor/marks/Comment.ts @@ -27,6 +27,10 @@ export default class Comment extends Mark { }; } + get allowInReadOnly() { + return true; + } + keys({ type }: { type: MarkType }): Record { return this.options.onCreateCommentMark ? { diff --git a/shared/editor/types/index.ts b/shared/editor/types/index.ts index 7d542aee6..8dfaaa004 100644 --- a/shared/editor/types/index.ts +++ b/shared/editor/types/index.ts @@ -19,6 +19,7 @@ export type MenuItem = { shortcut?: string; keywords?: string; tooltip?: string; + label?: string; defaultHidden?: boolean; attrs?: Record; visible?: boolean;