feat: Seamless Edit (#2701)
* feat: Remove explicit edit * Restore revision remains disabled for now * Bump RME, better differentiation of focused state * fix: Star not visible in edit mode * remove stray log * fix: Occassional user context not available in collaborative persistence
This commit is contained in:
@@ -88,7 +88,10 @@ class DataLoader extends React.Component<Props> {
|
||||
}
|
||||
|
||||
get isEditing() {
|
||||
return this.props.match.path === matchDocumentEdit;
|
||||
return (
|
||||
this.props.match.path === matchDocumentEdit ||
|
||||
this.props.auth?.team?.collaborativeEditing
|
||||
);
|
||||
}
|
||||
|
||||
onSearchLink = async (term: string) => {
|
||||
@@ -244,7 +247,9 @@ class DataLoader extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<Loading location={location} />
|
||||
{this.isEditing && <HideSidebar ui={ui} />}
|
||||
{this.isEditing && !team?.collaborativeEditing && (
|
||||
<HideSidebar ui={ui} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -261,7 +266,9 @@ class DataLoader extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
{this.isEditing && <HideSidebar ui={ui} />}
|
||||
{this.isEditing && !team.collaborativeEditing && (
|
||||
<HideSidebar ui={ui} />
|
||||
)}
|
||||
{this.props.children({
|
||||
document,
|
||||
revision,
|
||||
|
||||
@@ -357,8 +357,8 @@ class DocumentScene extends React.Component<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
onChangeTitle = (event) => {
|
||||
this.title = event.target.value;
|
||||
onChangeTitle = (value) => {
|
||||
this.title = value;
|
||||
this.updateIsDirtyDebounced();
|
||||
this.autosave();
|
||||
};
|
||||
@@ -389,7 +389,8 @@ class DocumentScene extends React.Component<Props> {
|
||||
const headings = this.editor.current
|
||||
? this.editor.current.getHeadings()
|
||||
: [];
|
||||
const showContents = ui.tocVisible && readOnly;
|
||||
const showContents =
|
||||
ui.tocVisible && (readOnly || team?.collaborativeEditing);
|
||||
|
||||
const collaborativeEditing =
|
||||
team?.collaborativeEditing &&
|
||||
@@ -473,7 +474,7 @@ class DocumentScene extends React.Component<Props> {
|
||||
shareId={shareId}
|
||||
isRevision={!!revision}
|
||||
isDraft={document.isDraft}
|
||||
isEditing={!readOnly}
|
||||
isEditing={!readOnly && !team?.collaborativeEditing}
|
||||
isSaving={this.isSaving}
|
||||
isPublishing={this.isPublishing}
|
||||
publishingIsDisabled={
|
||||
|
||||
151
app/scenes/Document/components/EditableTitle.js
Normal file
151
app/scenes/Document/components/EditableTitle.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { MAX_TITLE_LENGTH } from "shared/constants";
|
||||
import { light } from "shared/theme";
|
||||
import parseTitle from "shared/utils/parseTitle";
|
||||
import Document from "models/Document";
|
||||
import ContentEditable from "components/ContentEditable";
|
||||
import Star, { AnimatedStar } from "components/Star";
|
||||
import useStores from "hooks/useStores";
|
||||
import { isModKey } from "utils/keyboard";
|
||||
|
||||
type Props = {
|
||||
value: string,
|
||||
document: Document,
|
||||
readOnly: boolean,
|
||||
onChange: (text: string) => void,
|
||||
onGoToNextInput: (insertParagraph?: boolean) => void,
|
||||
onSave: (options: { publish?: boolean, done?: boolean }) => void,
|
||||
};
|
||||
|
||||
function EditableTitle({
|
||||
value,
|
||||
document,
|
||||
readOnly,
|
||||
onChange,
|
||||
onSave,
|
||||
onGoToNextInput,
|
||||
}: Props) {
|
||||
const ref = React.useRef();
|
||||
const { policies } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const can = policies.abilities(document.id);
|
||||
const { emoji } = parseTitle(value);
|
||||
const startsWithEmojiAndSpace = !!(emoji && value.startsWith(`${emoji} `));
|
||||
const normalizedTitle =
|
||||
!value && readOnly ? document.titleWithDefault : value;
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: SyntheticKeyboardEvent<>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (isModKey(event)) {
|
||||
onSave({ done: true });
|
||||
return;
|
||||
}
|
||||
|
||||
onGoToNextInput(true);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Tab" || event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
onGoToNextInput();
|
||||
return;
|
||||
}
|
||||
if (event.key === "p" && isModKey(event) && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
onSave({ publish: true, done: true });
|
||||
return;
|
||||
}
|
||||
if (event.key === "s" && isModKey(event)) {
|
||||
event.preventDefault();
|
||||
onSave({});
|
||||
return;
|
||||
}
|
||||
},
|
||||
[onGoToNextInput, onSave]
|
||||
);
|
||||
|
||||
return (
|
||||
<Title
|
||||
ref={ref}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
document.isTemplate
|
||||
? t("Start your template…")
|
||||
: t("Start with a title…")
|
||||
}
|
||||
value={normalizedTitle}
|
||||
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
|
||||
$isStarred={document.isStarred}
|
||||
autoFocus={!value}
|
||||
maxLength={MAX_TITLE_LENGTH}
|
||||
readOnly={readOnly}
|
||||
dir="auto"
|
||||
>
|
||||
{(can.star || can.unstar) && <StarButton document={document} size={32} />}
|
||||
</Title>
|
||||
);
|
||||
}
|
||||
|
||||
const StarButton = styled(Star)`
|
||||
position: relative;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
`;
|
||||
|
||||
const Title = styled(ContentEditable)`
|
||||
line-height: 1.25;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
color: ${(props) => props.theme.text};
|
||||
-webkit-text-fill-color: ${(props) => props.theme.text};
|
||||
font-size: 2.25em;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
resize: none;
|
||||
|
||||
> span {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.placeholder};
|
||||
-webkit-text-fill-color: ${(props) => props.theme.placeholder};
|
||||
}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
margin-left: ${(props) => (props.$startsWithEmojiAndSpace ? "-1.2em" : 0)};
|
||||
`};
|
||||
|
||||
${AnimatedStar} {
|
||||
opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
${AnimatedStar} {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
color: ${(props) => light.text};
|
||||
-webkit-text-fill-color: ${(props) => light.text};
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(EditableTitle);
|
||||
@@ -2,13 +2,7 @@
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Textarea from "react-autosize-textarea";
|
||||
import { type TFunction, withTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { MAX_TITLE_LENGTH } from "shared/constants";
|
||||
import { light } from "shared/theme";
|
||||
import parseTitle from "shared/utils/parseTitle";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import Document from "models/Document";
|
||||
import ClickablePadding from "components/ClickablePadding";
|
||||
@@ -16,14 +10,13 @@ import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
||||
import Editor, { type Props as EditorProps } from "components/Editor";
|
||||
import Flex from "components/Flex";
|
||||
import HoverPreview from "components/HoverPreview";
|
||||
import Star, { AnimatedStar } from "components/Star";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import MultiplayerEditor from "./MultiplayerEditor";
|
||||
import { isModKey } from "utils/keyboard";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
...EditorProps,
|
||||
onChangeTitle: (event: SyntheticInputEvent<>) => void,
|
||||
onChangeTitle: (text: string) => void,
|
||||
title: string,
|
||||
document: Document,
|
||||
isDraft: boolean,
|
||||
@@ -61,35 +54,6 @@ class DocumentEditor extends React.Component<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
handleTitleKeyDown = (event: SyntheticKeyboardEvent<>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (isModKey(event)) {
|
||||
this.props.onSave({ done: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.insertParagraph();
|
||||
this.focusAtStart();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Tab" || event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
this.focusAtStart();
|
||||
return;
|
||||
}
|
||||
if (event.key === "p" && isModKey(event) && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.props.onSave({ publish: true, done: true });
|
||||
return;
|
||||
}
|
||||
if (event.key === "s" && isModKey(event)) {
|
||||
event.preventDefault();
|
||||
this.props.onSave({});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
handleLinkActive = (event: MouseEvent) => {
|
||||
this.activeLinkEvent = event;
|
||||
};
|
||||
@@ -98,6 +62,13 @@ class DocumentEditor extends React.Component<Props> {
|
||||
this.activeLinkEvent = null;
|
||||
};
|
||||
|
||||
handleGoToNextInput = (insertParagraph: boolean) => {
|
||||
if (insertParagraph) {
|
||||
this.insertParagraph();
|
||||
}
|
||||
this.focusAtStart();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
document,
|
||||
@@ -115,45 +86,16 @@ class DocumentEditor extends React.Component<Props> {
|
||||
} = this.props;
|
||||
|
||||
const EditorComponent = multiplayer ? MultiplayerEditor : Editor;
|
||||
const can = policies.abilities(document.id);
|
||||
const { emoji } = parseTitle(title);
|
||||
const startsWithEmojiAndSpace = !!(emoji && title.startsWith(`${emoji} `));
|
||||
const normalizedTitle =
|
||||
!title && readOnly ? document.titleWithDefault : title;
|
||||
|
||||
return (
|
||||
<Flex auto column>
|
||||
{readOnly ? (
|
||||
<Title
|
||||
as="div"
|
||||
ref={this.ref}
|
||||
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
|
||||
$isStarred={document.isStarred}
|
||||
dir="auto"
|
||||
>
|
||||
<span>{normalizedTitle}</span>{" "}
|
||||
{(can.star || can.unstar) && (
|
||||
<StarButton document={document} size={32} />
|
||||
)}
|
||||
</Title>
|
||||
) : (
|
||||
<Title
|
||||
type="text"
|
||||
ref={this.ref}
|
||||
onChange={onChangeTitle}
|
||||
onKeyDown={this.handleTitleKeyDown}
|
||||
placeholder={
|
||||
document.isTemplate
|
||||
? t("Start your template…")
|
||||
: t("Start with a title…")
|
||||
}
|
||||
value={normalizedTitle}
|
||||
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
|
||||
autoFocus={!title}
|
||||
maxLength={MAX_TITLE_LENGTH}
|
||||
dir="auto"
|
||||
/>
|
||||
)}
|
||||
<EditableTitle
|
||||
value={title}
|
||||
readOnly={readOnly}
|
||||
document={document}
|
||||
onGoToNextInput={this.handleGoToNextInput}
|
||||
onChange={onChangeTitle}
|
||||
/>
|
||||
{!shareId && (
|
||||
<DocumentMetaWithViews
|
||||
isDraft={isDraft}
|
||||
@@ -191,56 +133,6 @@ class DocumentEditor extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const StarButton = styled(Star)`
|
||||
position: relative;
|
||||
top: 4px;
|
||||
`;
|
||||
|
||||
const Title = styled(Textarea)`
|
||||
line-height: 1.25;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
color: ${(props) => props.theme.text};
|
||||
-webkit-text-fill-color: ${(props) => props.theme.text};
|
||||
font-size: 2.25em;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
resize: none;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.placeholder};
|
||||
-webkit-text-fill-color: ${(props) => props.theme.placeholder};
|
||||
}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
margin-left: ${(props) => (props.$startsWithEmojiAndSpace ? "-1.2em" : 0)};
|
||||
`};
|
||||
|
||||
${AnimatedStar} {
|
||||
opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
${AnimatedStar} {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
color: ${(props) => light.text};
|
||||
-webkit-text-fill-color: ${(props) => light.text};
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default withTranslation()<DocumentEditor>(
|
||||
inject("policies")(DocumentEditor)
|
||||
);
|
||||
|
||||
@@ -230,7 +230,7 @@ function DocumentHeader({
|
||||
</Action>
|
||||
</>
|
||||
)}
|
||||
{canEdit && editAction}
|
||||
{canEdit && !team.collaborativeEditing && editAction}
|
||||
{canEdit && can.createChildDocument && !isMobile && (
|
||||
<Action>
|
||||
<NewChildDocumentMenu
|
||||
|
||||
Reference in New Issue
Block a user