Allows commenting outside edit mode when seamless editing is disabled. (#5422)
This commit is contained in:
@@ -111,7 +111,7 @@ function CollectionDescription({ collection }: Props) {
|
||||
onBlur={handleStopEditing}
|
||||
maxLength={1000}
|
||||
embedsDisabled
|
||||
readOnlyWriteCheckboxes
|
||||
canUpdate
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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={
|
||||
|
||||
26
app/editor/menus/readOnly.tsx
Normal file
26
app/editor/menus/readOnly.tsx
Normal 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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -578,6 +578,7 @@ h6 {
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
${props.readOnly ? "cursor: var(--pointer);" : ""}
|
||||
background: ${transparentize(0.5, props.theme.brand.marine)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ export default class Extension {
|
||||
return {};
|
||||
}
|
||||
|
||||
get allowInReadOnly(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
keys(_options: {
|
||||
type?: NodeType | MarkType;
|
||||
schema: Schema;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -19,6 +19,7 @@ export type MenuItem = {
|
||||
shortcut?: string;
|
||||
keywords?: string;
|
||||
tooltip?: string;
|
||||
label?: string;
|
||||
defaultHidden?: boolean;
|
||||
attrs?: Record<string, any>;
|
||||
visible?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user