Insert document title when pasting internal doc url (#6352)
* refactor * DRY
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Plugin, PluginKey } from "prosemirror-state";
|
import { Plugin, PluginKey } from "prosemirror-state";
|
||||||
import Extension from "../lib/Extension";
|
import Extension from "@shared/editor/lib/Extension";
|
||||||
import textBetween from "../lib/textBetween";
|
import textBetween from "@shared/editor/lib/textBetween";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A plugin that allows overriding the default behavior of the editor to allow
|
* A plugin that allows overriding the default behavior of the editor to allow
|
||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
EditorState,
|
EditorState,
|
||||||
Command,
|
Command,
|
||||||
} from "prosemirror-state";
|
} from "prosemirror-state";
|
||||||
import Extension from "../lib/Extension";
|
import Extension from "@shared/editor/lib/Extension";
|
||||||
import isInCode from "../queries/isInCode";
|
import isInCode from "@shared/editor/queries/isInCode";
|
||||||
|
|
||||||
export default class Keys extends Extension {
|
export default class Keys extends Extension {
|
||||||
get name() {
|
get name() {
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "@getoutline/y-prosemirror";
|
} from "@getoutline/y-prosemirror";
|
||||||
import { keymap } from "prosemirror-keymap";
|
import { keymap } from "prosemirror-keymap";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import Extension from "../lib/Extension";
|
import Extension from "@shared/editor/lib/Extension";
|
||||||
|
|
||||||
export default class Multiplayer extends Extension {
|
export default class Multiplayer extends Extension {
|
||||||
get name() {
|
get name() {
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { toggleMark } from "prosemirror-commands";
|
import { toggleMark } from "prosemirror-commands";
|
||||||
import { Slice } from "prosemirror-model";
|
import { Slice } from "prosemirror-model";
|
||||||
import { Plugin } from "prosemirror-state";
|
import { Plugin } from "prosemirror-state";
|
||||||
import { isUrl } from "../../utils/urls";
|
import { LANGUAGES } from "@shared/editor/extensions/Prism";
|
||||||
import Extension from "../lib/Extension";
|
import Extension from "@shared/editor/lib/Extension";
|
||||||
import isMarkdown from "../lib/isMarkdown";
|
import isMarkdown from "@shared/editor/lib/isMarkdown";
|
||||||
import normalizePastedMarkdown from "../lib/markdown/normalize";
|
import normalizePastedMarkdown from "@shared/editor/lib/markdown/normalize";
|
||||||
import isInCode from "../queries/isInCode";
|
import isInCode from "@shared/editor/queries/isInCode";
|
||||||
import isInList from "../queries/isInList";
|
import isInList from "@shared/editor/queries/isInList";
|
||||||
import { LANGUAGES } from "./Prism";
|
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
||||||
|
import { isDocumentUrl, isUrl } from "@shared/utils/urls";
|
||||||
|
import stores from "~/stores";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the HTML string is likely coming from Dropbox Paper.
|
* Checks if the HTML string is likely coming from Dropbox Paper.
|
||||||
@@ -108,10 +110,35 @@ export default class PasteHandler extends Extension {
|
|||||||
const html = event.clipboardData.getData("text/html");
|
const html = event.clipboardData.getData("text/html");
|
||||||
const vscode = event.clipboardData.getData("vscode-editor-data");
|
const vscode = event.clipboardData.getData("vscode-editor-data");
|
||||||
|
|
||||||
// first check if the clipboard contents can be parsed as a single
|
function insertLink(href: string, title?: string) {
|
||||||
// url, this is mainly for allowing pasted urls to become embeds
|
const normalized = href.replace(/^https?:\/\//, "");
|
||||||
|
// If it's not an embed and there is no text selected – just go ahead and insert the
|
||||||
|
// link directly
|
||||||
|
const transaction = view.state.tr
|
||||||
|
.insertText(
|
||||||
|
title ?? normalized,
|
||||||
|
state.selection.from,
|
||||||
|
state.selection.to
|
||||||
|
)
|
||||||
|
.addMark(
|
||||||
|
state.selection.from,
|
||||||
|
state.selection.to + (title ?? normalized).length,
|
||||||
|
state.schema.marks.link.create({ href })
|
||||||
|
);
|
||||||
|
view.dispatch(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the users selection is currently in a code block then paste
|
||||||
|
// as plain text, ignore all formatting and HTML content.
|
||||||
|
if (isInCode(state)) {
|
||||||
|
event.preventDefault();
|
||||||
|
view.dispatch(state.tr.insertText(text));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the clipboard contents can be parsed as a single url
|
||||||
if (isUrl(text)) {
|
if (isUrl(text)) {
|
||||||
// just paste the link mark directly onto the selected text
|
// If there is selected text then we want to wrap it in a link to the url
|
||||||
if (!state.selection.empty) {
|
if (!state.selection.empty) {
|
||||||
toggleMark(this.editor.schema.marks.link, { href: text })(
|
toggleMark(this.editor.schema.marks.link, { href: text })(
|
||||||
state,
|
state,
|
||||||
@@ -122,7 +149,6 @@ export default class PasteHandler extends Extension {
|
|||||||
|
|
||||||
// Is this link embeddable? Create an embed!
|
// Is this link embeddable? Create an embed!
|
||||||
const { embeds } = this.editor.props;
|
const { embeds } = this.editor.props;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
embeds &&
|
embeds &&
|
||||||
this.editor.commands.embed &&
|
this.editor.commands.embed &&
|
||||||
@@ -140,25 +166,35 @@ export default class PasteHandler extends Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// well, it's not an embed and there is no text selected – so just
|
// Is the link a link to a document? If so, we can grab the title and insert it.
|
||||||
// go ahead and insert the link directly
|
if (isDocumentUrl(text)) {
|
||||||
const transaction = view.state.tr
|
const slug = parseDocumentSlug(text);
|
||||||
.insertText(text, state.selection.from, state.selection.to)
|
|
||||||
.addMark(
|
|
||||||
state.selection.from,
|
|
||||||
state.selection.to + text.length,
|
|
||||||
state.schema.marks.link.create({ href: text })
|
|
||||||
);
|
|
||||||
view.dispatch(transaction);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the users selection is currently in a code block then paste
|
if (slug) {
|
||||||
// as plain text, ignore all formatting and HTML content.
|
void stores.documents
|
||||||
if (isInCode(state)) {
|
.fetch(slug)
|
||||||
event.preventDefault();
|
.then((document) => {
|
||||||
|
if (view.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (document) {
|
||||||
|
const title = `${
|
||||||
|
document.emoji ? document.emoji + " " : ""
|
||||||
|
}${document.titleWithDefault}`;
|
||||||
|
insertLink(document.path, title);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (view.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertLink(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
insertLink(text);
|
||||||
|
}
|
||||||
|
|
||||||
view.dispatch(state.tr.insertText(text));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Command } from "prosemirror-state";
|
import { Command } from "prosemirror-state";
|
||||||
import Extension from "../lib/Extension";
|
import Extension from "@shared/editor/lib/Extension";
|
||||||
|
|
||||||
export default class PreventTab extends Extension {
|
export default class PreventTab extends Extension {
|
||||||
get name() {
|
get name() {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ellipsis, smartQuotes, InputRule } from "prosemirror-inputrules";
|
import { ellipsis, smartQuotes, InputRule } from "prosemirror-inputrules";
|
||||||
import Extension from "../lib/Extension";
|
import Extension from "@shared/editor/lib/Extension";
|
||||||
|
|
||||||
const rightArrow = new InputRule(/->$/, "→");
|
const rightArrow = new InputRule(/->$/, "→");
|
||||||
const oneHalf = new InputRule(/1\/2$/, "½");
|
const oneHalf = new InputRule(/1\/2$/, "½");
|
||||||
@@ -2,13 +2,24 @@ import * as React from "react";
|
|||||||
import { basicExtensions, withComments } from "@shared/editor/nodes";
|
import { basicExtensions, withComments } from "@shared/editor/nodes";
|
||||||
import Editor, { Props as EditorProps } from "~/components/Editor";
|
import Editor, { Props as EditorProps } from "~/components/Editor";
|
||||||
import type { Editor as SharedEditor } from "~/editor";
|
import type { Editor as SharedEditor } from "~/editor";
|
||||||
|
import ClipboardTextSerializer from "~/editor/extensions/ClipboardTextSerializer";
|
||||||
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
|
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
|
||||||
|
import Keys from "~/editor/extensions/Keys";
|
||||||
import MentionMenuExtension from "~/editor/extensions/MentionMenu";
|
import MentionMenuExtension from "~/editor/extensions/MentionMenu";
|
||||||
|
import PasteHandler from "~/editor/extensions/PasteHandler";
|
||||||
|
import PreventTab from "~/editor/extensions/PreventTab";
|
||||||
|
import SmartText from "~/editor/extensions/SmartText";
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
...withComments(basicExtensions),
|
...withComments(basicExtensions),
|
||||||
|
SmartText,
|
||||||
|
PasteHandler,
|
||||||
|
ClipboardTextSerializer,
|
||||||
EmojiMenuExtension,
|
EmojiMenuExtension,
|
||||||
MentionMenuExtension,
|
MentionMenuExtension,
|
||||||
|
// Order these default key handlers last
|
||||||
|
PreventTab,
|
||||||
|
Keys,
|
||||||
];
|
];
|
||||||
|
|
||||||
const CommentEditor = (
|
const CommentEditor = (
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ import { useDocumentContext } from "~/components/DocumentContext";
|
|||||||
import Editor, { Props as EditorProps } from "~/components/Editor";
|
import Editor, { Props as EditorProps } from "~/components/Editor";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import BlockMenuExtension from "~/editor/extensions/BlockMenu";
|
import BlockMenuExtension from "~/editor/extensions/BlockMenu";
|
||||||
|
import ClipboardTextSerializer from "~/editor/extensions/ClipboardTextSerializer";
|
||||||
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
|
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
|
||||||
import FindAndReplaceExtension from "~/editor/extensions/FindAndReplace";
|
import FindAndReplaceExtension from "~/editor/extensions/FindAndReplace";
|
||||||
import HoverPreviewsExtension from "~/editor/extensions/HoverPreviews";
|
import HoverPreviewsExtension from "~/editor/extensions/HoverPreviews";
|
||||||
|
import Keys from "~/editor/extensions/Keys";
|
||||||
import MentionMenuExtension from "~/editor/extensions/MentionMenu";
|
import MentionMenuExtension from "~/editor/extensions/MentionMenu";
|
||||||
|
import PasteHandler from "~/editor/extensions/PasteHandler";
|
||||||
|
import PreventTab from "~/editor/extensions/PreventTab";
|
||||||
|
import SmartText from "~/editor/extensions/SmartText";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
import useFocusedComment from "~/hooks/useFocusedComment";
|
import useFocusedComment from "~/hooks/useFocusedComment";
|
||||||
@@ -32,11 +37,17 @@ import DocumentTitle from "./DocumentTitle";
|
|||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
...withComments(richExtensions),
|
...withComments(richExtensions),
|
||||||
|
SmartText,
|
||||||
|
PasteHandler,
|
||||||
|
ClipboardTextSerializer,
|
||||||
BlockMenuExtension,
|
BlockMenuExtension,
|
||||||
EmojiMenuExtension,
|
EmojiMenuExtension,
|
||||||
MentionMenuExtension,
|
MentionMenuExtension,
|
||||||
FindAndReplaceExtension,
|
FindAndReplaceExtension,
|
||||||
HoverPreviewsExtension,
|
HoverPreviewsExtension,
|
||||||
|
// Order these default key handlers last
|
||||||
|
PreventTab,
|
||||||
|
Keys,
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = Omit<EditorProps, "editorStyle"> & {
|
type Props = Omit<EditorProps, "editorStyle"> & {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { useHistory } from "react-router-dom";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { IndexeddbPersistence } from "y-indexeddb";
|
import { IndexeddbPersistence } from "y-indexeddb";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import MultiplayerExtension from "@shared/editor/extensions/Multiplayer";
|
|
||||||
import { supportsPassiveListener } from "@shared/utils/browser";
|
import { supportsPassiveListener } from "@shared/utils/browser";
|
||||||
import Editor, { Props as EditorProps } from "~/components/Editor";
|
import Editor, { Props as EditorProps } from "~/components/Editor";
|
||||||
|
import MultiplayerExtension from "~/editor/extensions/Multiplayer";
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
import useIdle from "~/hooks/useIdle";
|
import useIdle from "~/hooks/useIdle";
|
||||||
|
|||||||
@@ -180,6 +180,13 @@ export default class DocumentsStore extends Store<Document> {
|
|||||||
return naturalSort(this.inCollection(collectionId), "title");
|
return naturalSort(this.inCollection(collectionId), "title");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get(id: string): Document | undefined {
|
||||||
|
return (
|
||||||
|
this.data.get(id) ??
|
||||||
|
this.orderedData.find((doc) => id.endsWith(doc.urlId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get archived(): Document[] {
|
get archived(): Document[] {
|
||||||
return orderBy(this.orderedData, "archivedAt", "desc").filter(
|
return orderBy(this.orderedData, "archivedAt", "desc").filter(
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export default abstract class Store<T extends Model> {
|
|||||||
throw new Error(`Cannot fetch ${this.modelName}`);
|
throw new Error(`Cannot fetch ${this.modelName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = this.data.get(id);
|
const item = this.get(id);
|
||||||
if (item && !options.force) {
|
if (item && !options.force) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { InputRule } from "prosemirror-inputrules";
|
|||||||
import { NodeType, MarkType, Schema } from "prosemirror-model";
|
import { NodeType, MarkType, Schema } from "prosemirror-model";
|
||||||
import { Command, Plugin } from "prosemirror-state";
|
import { Command, Plugin } from "prosemirror-state";
|
||||||
import { Primitive } from "utility-types";
|
import { Primitive } from "utility-types";
|
||||||
import { Editor } from "../../../app/editor";
|
import type { Editor } from "../../../app/editor";
|
||||||
|
|
||||||
export type CommandFactory = (attrs?: Record<string, Primitive>) => Command;
|
export type CommandFactory = (attrs?: Record<string, Primitive>) => Command;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import ClipboardTextSerializer from "../extensions/ClipboardTextSerializer";
|
|
||||||
import DateTime from "../extensions/DateTime";
|
import DateTime from "../extensions/DateTime";
|
||||||
import History from "../extensions/History";
|
import History from "../extensions/History";
|
||||||
import Keys from "../extensions/Keys";
|
|
||||||
import MaxLength from "../extensions/MaxLength";
|
import MaxLength from "../extensions/MaxLength";
|
||||||
import PasteHandler from "../extensions/PasteHandler";
|
|
||||||
import Placeholder from "../extensions/Placeholder";
|
import Placeholder from "../extensions/Placeholder";
|
||||||
import PreventTab from "../extensions/PreventTab";
|
|
||||||
import SmartText from "../extensions/SmartText";
|
|
||||||
import TrailingNode from "../extensions/TrailingNode";
|
import TrailingNode from "../extensions/TrailingNode";
|
||||||
import Extension from "../lib/Extension";
|
import Extension from "../lib/Extension";
|
||||||
import Bold from "../marks/Bold";
|
import Bold from "../marks/Bold";
|
||||||
@@ -68,14 +63,10 @@ export const basicExtensions: Nodes = [
|
|||||||
Link,
|
Link,
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
History,
|
History,
|
||||||
SmartText,
|
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
PasteHandler,
|
|
||||||
Placeholder,
|
Placeholder,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
DateTime,
|
DateTime,
|
||||||
Keys,
|
|
||||||
ClipboardTextSerializer,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +74,7 @@ export const basicExtensions: Nodes = [
|
|||||||
* editors that need advanced formatting.
|
* editors that need advanced formatting.
|
||||||
*/
|
*/
|
||||||
export const richExtensions: Nodes = [
|
export const richExtensions: Nodes = [
|
||||||
...basicExtensions.filter((n) => n !== SimpleImage && n !== Keys),
|
...basicExtensions.filter((n) => n !== SimpleImage),
|
||||||
Image,
|
Image,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
@@ -108,8 +99,6 @@ export const richExtensions: Nodes = [
|
|||||||
TemplatePlaceholder,
|
TemplatePlaceholder,
|
||||||
Math,
|
Math,
|
||||||
MathBlock,
|
MathBlock,
|
||||||
PreventTab,
|
|
||||||
Keys,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -38,10 +38,28 @@ export function isInternalUrl(href: string) {
|
|||||||
return (
|
return (
|
||||||
outline.host === domain.host ||
|
outline.host === domain.host ||
|
||||||
(domain.host.endsWith(getBaseDomain()) &&
|
(domain.host.endsWith(getBaseDomain()) &&
|
||||||
!RESERVED_SUBDOMAINS.includes(domain.teamSubdomain))
|
!RESERVED_SUBDOMAINS.find((reserved) => domain.host.startsWith(reserved)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given string is a link to a documement.
|
||||||
|
*
|
||||||
|
* @param options Parsing options.
|
||||||
|
* @returns True if a document, false otherwise.
|
||||||
|
*/
|
||||||
|
export function isDocumentUrl(url: string) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return (
|
||||||
|
isInternalUrl(url) &&
|
||||||
|
(parsed.pathname.startsWith("/doc/") || parsed.pathname.startsWith("/d/"))
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given string is a url.
|
* Returns true if the given string is a url.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user