mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38fa3ed903 | |||
| c269d9f1a3 |
@@ -23,6 +23,7 @@ import ExtensionManager from "@shared/editor/lib/ExtensionManager";
|
||||
import getHeadings from "@shared/editor/lib/getHeadings";
|
||||
import getTasks from "@shared/editor/lib/getTasks";
|
||||
import { MarkdownSerializer } from "@shared/editor/lib/markdown/serializer";
|
||||
import textBetween from "@shared/editor/lib/textBetween";
|
||||
import Mark from "@shared/editor/marks/Mark";
|
||||
import Node from "@shared/editor/nodes/Node";
|
||||
import ReactNode from "@shared/editor/nodes/ReactNode";
|
||||
@@ -571,6 +572,9 @@ export class Editor extends React.PureComponent<
|
||||
this.setState({ blockMenuOpen: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the editor at the start of the content.
|
||||
*/
|
||||
public focusAtStart = () => {
|
||||
const selection = Selection.atStart(this.view.state.doc);
|
||||
const transaction = this.view.state.tr.setSelection(selection);
|
||||
@@ -578,6 +582,9 @@ export class Editor extends React.PureComponent<
|
||||
this.view.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the editor at the end of the content.
|
||||
*/
|
||||
public focusAtEnd = () => {
|
||||
const selection = Selection.atEnd(this.view.state.doc);
|
||||
const transaction = this.view.state.tr.setSelection(selection);
|
||||
@@ -585,14 +592,40 @@ export class Editor extends React.PureComponent<
|
||||
this.view.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the headings in the current editor.
|
||||
*
|
||||
* @returns A list of headings in the document
|
||||
*/
|
||||
public getHeadings = () => {
|
||||
return getHeadings(this.view.state.doc);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the tasks/checkmarks in the current editor.
|
||||
*
|
||||
* @returns A list of tasks in the document
|
||||
*/
|
||||
public getTasks = () => {
|
||||
return getTasks(this.view.state.doc);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the plain text content of the current editor.
|
||||
*
|
||||
* @returns A string of text
|
||||
*/
|
||||
public getPlainText = () => {
|
||||
const { doc } = this.view.state;
|
||||
const textSerializers = Object.fromEntries(
|
||||
Object.entries(this.schema.nodes)
|
||||
.filter(([, node]) => node.spec.toPlainText)
|
||||
.map(([name, node]) => [name, node.spec.toPlainText])
|
||||
);
|
||||
|
||||
return textBetween(doc, 0, doc.content.size, textSerializers);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
dir,
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
} from "~/utils/routeHelpers";
|
||||
import Container from "./Container";
|
||||
import Contents from "./Contents";
|
||||
import DocumentContext from "./DocumentContext";
|
||||
import type { DocumentContextValue } from "./DocumentContext";
|
||||
import Editor from "./Editor";
|
||||
import Header from "./Header";
|
||||
import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
|
||||
@@ -110,6 +112,14 @@ class DocumentScene extends React.Component<Props> {
|
||||
@observable
|
||||
headings: Heading[] = [];
|
||||
|
||||
@observable
|
||||
documentContext: DocumentContextValue = {
|
||||
editor: null,
|
||||
setEditor: action((editor: TEditor) => {
|
||||
this.documentContext.editor = editor;
|
||||
}),
|
||||
};
|
||||
|
||||
getEditorText: () => string = () => this.props.document.text;
|
||||
|
||||
componentDidMount() {
|
||||
@@ -452,196 +462,203 @@ class DocumentScene extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{this.props.location.pathname !== canonicalUrl && (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: canonicalUrl,
|
||||
state: this.props.location.state,
|
||||
hash: this.props.location.hash,
|
||||
<DocumentContext.Provider value={this.documentContext}>
|
||||
{this.props.location.pathname !== canonicalUrl && (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: canonicalUrl,
|
||||
state: this.props.location.state,
|
||||
hash: this.props.location.hash,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<RegisterKeyDown trigger="m" handler={this.goToMove} />
|
||||
<RegisterKeyDown trigger="e" handler={this.goToEdit} />
|
||||
<RegisterKeyDown trigger="Escape" handler={this.goBack} />
|
||||
<RegisterKeyDown trigger="h" handler={this.goToHistory} />
|
||||
<RegisterKeyDown
|
||||
trigger="p"
|
||||
handler={(event) => {
|
||||
if (isModKey(event) && event.shiftKey) {
|
||||
this.onPublish(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<RegisterKeyDown trigger="m" handler={this.goToMove} />
|
||||
<RegisterKeyDown trigger="e" handler={this.goToEdit} />
|
||||
<RegisterKeyDown trigger="Escape" handler={this.goBack} />
|
||||
<RegisterKeyDown trigger="h" handler={this.goToHistory} />
|
||||
<RegisterKeyDown
|
||||
trigger="p"
|
||||
handler={(event) => {
|
||||
if (isModKey(event) && event.shiftKey) {
|
||||
this.onPublish(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<RegisterKeyDown
|
||||
trigger="h"
|
||||
handler={(event) => {
|
||||
if (event.ctrlKey && event.altKey) {
|
||||
this.onToggleTableOfContents(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Background key={revision ? revision.id : document.id} column auto>
|
||||
<Route
|
||||
path={`${document.url}/move`}
|
||||
component={() => (
|
||||
<Modal
|
||||
title={`Move ${document.noun}`}
|
||||
onRequestClose={this.goBack}
|
||||
isOpen
|
||||
>
|
||||
<DocumentMove
|
||||
document={document}
|
||||
onRequestClose={this.goBack}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
/>
|
||||
<PageTitle
|
||||
title={document.titleWithDefault.replace(document.emoji || "", "")}
|
||||
favicon={document.emoji ? emojiToUrl(document.emoji) : undefined}
|
||||
/>
|
||||
{(this.isUploading || this.isSaving) && <LoadingIndicator />}
|
||||
<Container justify="center" column auto>
|
||||
{!readOnly && (
|
||||
<>
|
||||
<Prompt
|
||||
when={
|
||||
this.isEditorDirty &&
|
||||
!this.isUploading &&
|
||||
!team?.collaborativeEditing
|
||||
}
|
||||
message={(location, action) => {
|
||||
if (
|
||||
// a URL replace matching the current document indicates a title change
|
||||
// no guard is needed for this transition
|
||||
action === "REPLACE" &&
|
||||
location.pathname === editDocumentUrl(document)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return t(
|
||||
`You have unsaved changes.\nAre you sure you want to discard them?`
|
||||
) as string;
|
||||
}}
|
||||
/>
|
||||
<Prompt
|
||||
when={this.isUploading && !this.isEditorDirty}
|
||||
message={t(
|
||||
`Images are still uploading.\nAre you sure you want to discard them?`
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Header
|
||||
document={document}
|
||||
documentHasHeadings={hasHeadings}
|
||||
shareId={shareId}
|
||||
isRevision={!!revision}
|
||||
isDraft={document.isDraft}
|
||||
isEditing={!readOnly && !team?.seamlessEditing}
|
||||
isSaving={this.isSaving}
|
||||
isPublishing={this.isPublishing}
|
||||
publishingIsDisabled={
|
||||
document.isSaving || this.isPublishing || this.isEmpty
|
||||
<RegisterKeyDown
|
||||
trigger="h"
|
||||
handler={(event) => {
|
||||
if (event.ctrlKey && event.altKey) {
|
||||
this.onToggleTableOfContents(event);
|
||||
}
|
||||
savingIsDisabled={document.isSaving || this.isEmpty}
|
||||
sharedTree={this.props.sharedTree}
|
||||
onSelectTemplate={this.replaceDocument}
|
||||
onSave={this.onSave}
|
||||
headings={this.headings}
|
||||
}}
|
||||
/>
|
||||
<Background key={revision ? revision.id : document.id} column auto>
|
||||
<Route
|
||||
path={`${document.url}/move`}
|
||||
component={() => (
|
||||
<Modal
|
||||
title={`Move ${document.noun}`}
|
||||
onRequestClose={this.goBack}
|
||||
isOpen
|
||||
>
|
||||
<DocumentMove
|
||||
document={document}
|
||||
onRequestClose={this.goBack}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
/>
|
||||
<MaxWidth
|
||||
archived={document.isArchived}
|
||||
showContents={showContents}
|
||||
isEditing={!readOnly}
|
||||
isFullWidth={document.fullWidth}
|
||||
column
|
||||
auto
|
||||
>
|
||||
<Notices document={document} readOnly={readOnly} />
|
||||
<React.Suspense fallback={<PlaceholderDocument />}>
|
||||
<Flex auto={!readOnly}>
|
||||
{revision ? (
|
||||
<RevisionViewer
|
||||
isDraft={document.isDraft}
|
||||
document={document}
|
||||
revision={revision}
|
||||
id={revision.id}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{showContents && (
|
||||
<Contents
|
||||
headings={this.headings}
|
||||
isFullWidth={document.fullWidth}
|
||||
/>
|
||||
)}
|
||||
<Editor
|
||||
id={document.id}
|
||||
key={embedsDisabled ? "disabled" : "enabled"}
|
||||
ref={this.editor}
|
||||
multiplayer={collaborativeEditing}
|
||||
shareId={shareId}
|
||||
<PageTitle
|
||||
title={document.titleWithDefault.replace(
|
||||
document.emoji || "",
|
||||
""
|
||||
)}
|
||||
favicon={document.emoji ? emojiToUrl(document.emoji) : undefined}
|
||||
/>
|
||||
{(this.isUploading || this.isSaving) && <LoadingIndicator />}
|
||||
<Container justify="center" column auto>
|
||||
{!readOnly && (
|
||||
<>
|
||||
<Prompt
|
||||
when={
|
||||
this.isEditorDirty &&
|
||||
!this.isUploading &&
|
||||
!team?.collaborativeEditing
|
||||
}
|
||||
message={(location, action) => {
|
||||
if (
|
||||
// a URL replace matching the current document indicates a title change
|
||||
// no guard is needed for this transition
|
||||
action === "REPLACE" &&
|
||||
location.pathname === editDocumentUrl(document)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return t(
|
||||
`You have unsaved changes.\nAre you sure you want to discard them?`
|
||||
) as string;
|
||||
}}
|
||||
/>
|
||||
<Prompt
|
||||
when={this.isUploading && !this.isEditorDirty}
|
||||
message={t(
|
||||
`Images are still uploading.\nAre you sure you want to discard them?`
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Header
|
||||
document={document}
|
||||
documentHasHeadings={hasHeadings}
|
||||
shareId={shareId}
|
||||
isRevision={!!revision}
|
||||
isDraft={document.isDraft}
|
||||
isEditing={!readOnly && !team?.seamlessEditing}
|
||||
isSaving={this.isSaving}
|
||||
isPublishing={this.isPublishing}
|
||||
publishingIsDisabled={
|
||||
document.isSaving || this.isPublishing || this.isEmpty
|
||||
}
|
||||
savingIsDisabled={document.isSaving || this.isEmpty}
|
||||
sharedTree={this.props.sharedTree}
|
||||
onSelectTemplate={this.replaceDocument}
|
||||
onSave={this.onSave}
|
||||
headings={this.headings}
|
||||
/>
|
||||
<MaxWidth
|
||||
archived={document.isArchived}
|
||||
showContents={showContents}
|
||||
isEditing={!readOnly}
|
||||
isFullWidth={document.fullWidth}
|
||||
column
|
||||
auto
|
||||
>
|
||||
<Notices document={document} readOnly={readOnly} />
|
||||
<React.Suspense fallback={<PlaceholderDocument />}>
|
||||
<Flex auto={!readOnly}>
|
||||
{revision ? (
|
||||
<RevisionViewer
|
||||
isDraft={document.isDraft}
|
||||
template={document.isTemplate}
|
||||
document={document}
|
||||
value={readOnly ? document.text : undefined}
|
||||
defaultValue={document.text}
|
||||
embedsDisabled={embedsDisabled}
|
||||
onSynced={this.onSynced}
|
||||
onFileUploadStart={this.onFileUploadStart}
|
||||
onFileUploadStop={this.onFileUploadStop}
|
||||
onSearchLink={this.props.onSearchLink}
|
||||
onCreateLink={this.props.onCreateLink}
|
||||
onChangeTitle={this.onChangeTitle}
|
||||
onChange={this.onChange}
|
||||
onHeadingsChange={this.onHeadingsChange}
|
||||
onSave={this.onSave}
|
||||
onPublish={this.onPublish}
|
||||
onCancel={this.goBack}
|
||||
readOnly={readOnly}
|
||||
readOnlyWriteCheckboxes={readOnly && abilities.update}
|
||||
>
|
||||
{shareId && (
|
||||
<ReferencesWrapper isOnlyTitle={document.isOnlyTitle}>
|
||||
<PublicReferences
|
||||
shareId={shareId}
|
||||
documentId={document.id}
|
||||
sharedTree={this.props.sharedTree}
|
||||
/>
|
||||
</ReferencesWrapper>
|
||||
revision={revision}
|
||||
id={revision.id}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{showContents && (
|
||||
<Contents
|
||||
headings={this.headings}
|
||||
isFullWidth={document.fullWidth}
|
||||
/>
|
||||
)}
|
||||
{!isShare && !revision && (
|
||||
<>
|
||||
<MarkAsViewed document={document} />
|
||||
<Editor
|
||||
id={document.id}
|
||||
key={embedsDisabled ? "disabled" : "enabled"}
|
||||
ref={this.editor}
|
||||
multiplayer={collaborativeEditing}
|
||||
shareId={shareId}
|
||||
isDraft={document.isDraft}
|
||||
template={document.isTemplate}
|
||||
document={document}
|
||||
value={readOnly ? document.text : undefined}
|
||||
defaultValue={document.text}
|
||||
embedsDisabled={embedsDisabled}
|
||||
onSynced={this.onSynced}
|
||||
onFileUploadStart={this.onFileUploadStart}
|
||||
onFileUploadStop={this.onFileUploadStop}
|
||||
onSearchLink={this.props.onSearchLink}
|
||||
onCreateLink={this.props.onCreateLink}
|
||||
onChangeTitle={this.onChangeTitle}
|
||||
onChange={this.onChange}
|
||||
onHeadingsChange={this.onHeadingsChange}
|
||||
onSave={this.onSave}
|
||||
onPublish={this.onPublish}
|
||||
onCancel={this.goBack}
|
||||
readOnly={readOnly}
|
||||
readOnlyWriteCheckboxes={readOnly && abilities.update}
|
||||
>
|
||||
{shareId && (
|
||||
<ReferencesWrapper
|
||||
isOnlyTitle={document.isOnlyTitle}
|
||||
>
|
||||
<References document={document} />
|
||||
<PublicReferences
|
||||
shareId={shareId}
|
||||
documentId={document.id}
|
||||
sharedTree={this.props.sharedTree}
|
||||
/>
|
||||
</ReferencesWrapper>
|
||||
</>
|
||||
)}
|
||||
</Editor>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</React.Suspense>
|
||||
</MaxWidth>
|
||||
{isShare &&
|
||||
!parseDomain(window.location.origin).custom &&
|
||||
!auth.user && (
|
||||
<Branding href="//www.getoutline.com?ref=sharelink" />
|
||||
)}
|
||||
</Container>
|
||||
</Background>
|
||||
{!isShare && (
|
||||
<>
|
||||
<KeyboardShortcutsButton />
|
||||
<ConnectionStatus />
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
{!isShare && !revision && (
|
||||
<>
|
||||
<MarkAsViewed document={document} />
|
||||
<ReferencesWrapper
|
||||
isOnlyTitle={document.isOnlyTitle}
|
||||
>
|
||||
<References document={document} />
|
||||
</ReferencesWrapper>
|
||||
</>
|
||||
)}
|
||||
</Editor>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</React.Suspense>
|
||||
</MaxWidth>
|
||||
{isShare &&
|
||||
!parseDomain(window.location.origin).custom &&
|
||||
!auth.user && (
|
||||
<Branding href="//www.getoutline.com?ref=sharelink" />
|
||||
)}
|
||||
</Container>
|
||||
</Background>
|
||||
{!isShare && (
|
||||
<>
|
||||
<KeyboardShortcutsButton />
|
||||
<ConnectionStatus />
|
||||
</>
|
||||
)}
|
||||
</DocumentContext.Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { Editor } from "~/editor";
|
||||
|
||||
export type DocumentContextValue = {
|
||||
/** The current editor instance for this document. */
|
||||
editor: Editor | null;
|
||||
/** Set the current editor instance for this document. */
|
||||
setEditor: (editor: Editor) => void;
|
||||
};
|
||||
|
||||
const DocumentContext = React.createContext<DocumentContextValue>({
|
||||
editor: null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setEditor() {},
|
||||
});
|
||||
|
||||
export const useDocumentContext = () => React.useContext(DocumentContext);
|
||||
|
||||
export default DocumentContext;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { mergeRefs } from "react-merge-refs";
|
||||
import { useRouteMatch } from "react-router-dom";
|
||||
import fullPackage from "@shared/editor/packages/full";
|
||||
import Document from "~/models/Document";
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
matchDocumentHistory,
|
||||
} from "~/utils/routeHelpers";
|
||||
import MultiplayerEditor from "./AsyncMultiplayerEditor";
|
||||
import { useDocumentContext } from "./DocumentContext";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
|
||||
type Props = Omit<EditorProps, "extensions"> & {
|
||||
@@ -74,6 +76,9 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
|
||||
[focusAtStart, ref]
|
||||
);
|
||||
|
||||
const { setEditor } = useDocumentContext();
|
||||
const handleRefChanged = React.useCallback(setEditor, [setEditor]);
|
||||
|
||||
const EditorComponent = multiplayer ? MultiplayerEditor : Editor;
|
||||
|
||||
return (
|
||||
@@ -103,7 +108,7 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
|
||||
/>
|
||||
)}
|
||||
<EditorComponent
|
||||
ref={ref}
|
||||
ref={mergeRefs([ref, handleRefChanged])}
|
||||
autoFocus={!!document.title && !props.defaultValue}
|
||||
placeholder={t("Type '/' to insert, or start writing…")}
|
||||
scrollTo={decodeURIComponent(window.location.hash)}
|
||||
|
||||
Reference in New Issue
Block a user