Allows commenting outside edit mode when seamless editing is disabled. (#5422)

This commit is contained in:
Tom Moor
2023-06-10 15:56:00 +03:00
committed by GitHub
parent 3f7e66980b
commit d319bb7d9a
12 changed files with 112 additions and 42 deletions

View File

@@ -111,7 +111,7 @@ function CollectionDescription({ collection }: Props) {
onBlur={handleStopEditing}
maxLength={1000}
embedsDisabled
readOnlyWriteCheckboxes
canUpdate
/>
</React.Suspense>
) : (

View File

@@ -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<SearchResult[]>;
@@ -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<HTMLDivElement | null>(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<void> => {
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);
}

View File

@@ -6,9 +6,11 @@ type Props = { active?: boolean; disabled?: boolean };
export default styled.button.attrs((props) => ({
type: props.type || "button",
}))<Props>`
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;

View File

@@ -45,8 +45,12 @@ function ToolbarMenu(props: Props) {
const isActive = item.active ? item.active(state) : false;
return (
<Tooltip tooltip={item.tooltip} key={index}>
<Tooltip
tooltip={item.label === item.tooltip ? undefined : item.tooltip}
key={index}
>
<ToolbarButton onClick={handleClick(item)} active={isActive}>
{item.label && <Label>{item.label}</Label>}
{item.icon}
</ToolbarButton>
</Tooltip>
@@ -56,4 +60,9 @@ function ToolbarMenu(props: Props) {
);
}
const Label = styled.span`
font-size: 15px;
font-weight: 500;
`;
export default ToolbarMenu;

View File

@@ -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 && (
<SelectionToolbar
rtl={isRTL}
readOnly={readOnly}
canComment={this.props.canComment}
isTemplate={this.props.template === true}
onOpen={this.handleOpenSelectionToolbar}
onClose={this.handleCloseSelectionToolbar}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onCreateLink={this.props.onCreateLink}
/>
)}
{!readOnly && this.view && (
<>
{this.marks.link && (
@@ -799,15 +821,6 @@ export class Editor extends React.PureComponent<
}
/>
)}
<SelectionToolbar
rtl={isRTL}
isTemplate={this.props.template === true}
onOpen={this.handleOpenSelectionToolbar}
onClose={this.handleCloseSelectionToolbar}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onCreateLink={this.props.onCreateLink}
/>
<BlockMenu
rtl={isRTL}
isActive={

View File

@@ -0,0 +1,26 @@
import { CommentIcon } from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
import isInCode from "@shared/editor/queries/isInCode";
import isMarkActive from "@shared/editor/queries/isMarkActive";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
export default function readOnlyMenuItems(
state: EditorState,
dictionary: Dictionary
): MenuItem[] {
const { schema } = state;
const isCode = isInCode(state);
return [
{
name: "comment",
tooltip: dictionary.comment,
label: dictionary.comment,
icon: <CommentIcon />,
active: isMarkActive(schema.marks.comment),
visible: !isCode,
},
];
}

View File

@@ -510,7 +510,8 @@ class DocumentScene extends React.Component<Props> {
onPublish={this.onPublish}
onCancel={this.goBack}
readOnly={readOnly}
readOnlyWriteCheckboxes={readOnly && abilities.update}
canUpdate={abilities.update}
canComment={abilities.comment}
>
{shareId && (
<ReferencesWrapper isOnlyTitle={document.isOnlyTitle}>

View File

@@ -578,6 +578,7 @@ h6 {
border-radius: 2px;
&:hover {
${props.readOnly ? "cursor: var(--pointer);" : ""}
background: ${transparentize(0.5, props.theme.brand.marine)};
}
}

View File

@@ -41,6 +41,10 @@ export default class Extension {
return {};
}
get allowInReadOnly(): boolean {
return false;
}
keys(_options: {
type?: NodeType | MarkType;
schema: Schema;

View File

@@ -205,7 +205,7 @@ export default class ExtensionManager {
callback: CommandFactory,
attrs: Record<string, any>
) => {
if (!view.editable) {
if (!view.editable && !extension.allowInReadOnly) {
return false;
}
view.focus();

View File

@@ -27,6 +27,10 @@ export default class Comment extends Mark {
};
}
get allowInReadOnly() {
return true;
}
keys({ type }: { type: MarkType }): Record<string, Command> {
return this.options.onCreateCommentMark
? {

View File

@@ -19,6 +19,7 @@ export type MenuItem = {
shortcut?: string;
keywords?: string;
tooltip?: string;
label?: string;
defaultHidden?: boolean;
attrs?: Record<string, any>;
visible?: boolean;