diff --git a/app/components/Editor.tsx b/app/components/Editor.tsx index de61e1d8b..1616c2f9b 100644 --- a/app/components/Editor.tsx +++ b/app/components/Editor.tsx @@ -1,15 +1,23 @@ +import { formatDistanceToNow } from "date-fns"; +import { deburr, sortBy } from "lodash"; import * as React from "react"; import { Optional } from "utility-types"; import embeds from "@shared/editor/embeds"; +import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; import { isInternalUrl } from "@shared/utils/urls"; +import Document from "~/models/Document"; import ErrorBoundary from "~/components/ErrorBoundary"; +import HoverPreview from "~/components/HoverPreview"; import type { Props as EditorProps, Editor as SharedEditor } from "~/editor"; import useDictionary from "~/hooks/useDictionary"; +import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; +import { NotFoundError } from "~/utils/errors"; import { uploadFile } from "~/utils/files"; import history from "~/utils/history"; import { isModKey } from "~/utils/keyboard"; import { isHash } from "~/utils/urls"; +import DocumentBreadcrumb from "./DocumentBreadcrumb"; const LazyLoadedEditor = React.lazy( () => @@ -38,8 +46,74 @@ export type Props = Optional< function Editor(props: Props, ref: React.Ref) { const { id, shareId } = props; + const { documents } = useStores(); const { showToast } = useToasts(); const dictionary = useDictionary(); + const [ + activeLinkEvent, + setActiveLinkEvent, + ] = React.useState(null); + + const handleLinkActive = React.useCallback((event: MouseEvent) => { + setActiveLinkEvent(event); + return false; + }, []); + + const handleLinkInactive = React.useCallback(() => { + setActiveLinkEvent(null); + }, []); + + const handleSearchLink = React.useCallback( + async (term: string) => { + if (isInternalUrl(term)) { + // search for exact internal document + const slug = parseDocumentSlug(term); + if (!slug) { + return []; + } + + try { + const document = await documents.fetch(slug); + const time = formatDistanceToNow(Date.parse(document.updatedAt), { + addSuffix: true, + }); + + return [ + { + title: document.title, + subtitle: `Updated ${time}`, + url: document.url, + }, + ]; + } catch (error) { + // NotFoundError could not find document for slug + if (!(error instanceof NotFoundError)) { + throw error; + } + } + } + + // default search for anything that doesn't look like a URL + const results = await documents.searchTitles(term); + + return sortBy( + results.map((document: Document) => { + return { + title: document.title, + subtitle: , + url: document.url, + }; + }), + (document) => + deburr(document.title) + .toLowerCase() + .startsWith(deburr(term).toLowerCase()) + ? -1 + : 1 + ); + }, + [documents] + ); const onUploadFile = React.useCallback( async (file: File) => { @@ -87,17 +161,28 @@ function Editor(props: Props, ref: React.Ref) { return ( - + <> + + {activeLinkEvent && !shareId && ( + + )} + ); } diff --git a/app/components/HoverPreview.tsx b/app/components/HoverPreview.tsx index c32ff6bd4..67e12ab4a 100644 --- a/app/components/HoverPreview.tsx +++ b/app/components/HoverPreview.tsx @@ -126,7 +126,7 @@ function HoverPreviewInternal({ node, onClose }: Props) { function HoverPreview({ node, ...rest }: Props) { const isMobile = useMobile(); - if (!isMobile) { + if (isMobile) { return null; } @@ -157,7 +157,7 @@ const Margin = styled.div` const CardContent = styled.div` overflow: hidden; - max-height: 350px; + max-height: 20em; user-select: none; `; diff --git a/app/scenes/Document/components/DataLoader.tsx b/app/scenes/Document/components/DataLoader.tsx index 9f9d6c6da..452281e87 100644 --- a/app/scenes/Document/components/DataLoader.tsx +++ b/app/scenes/Document/components/DataLoader.tsx @@ -1,18 +1,13 @@ -import { formatDistanceToNow } from "date-fns"; import invariant from "invariant"; -import { deburr, sortBy } from "lodash"; import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import { RouteComponentProps, StaticContext } from "react-router"; -import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; -import { isInternalUrl } from "@shared/utils/urls"; import RootStore from "~/stores/RootStore"; import Document from "~/models/Document"; import Revision from "~/models/Revision"; import Error404 from "~/scenes/Error404"; import ErrorOffline from "~/scenes/ErrorOffline"; -import DocumentBreadcrumb from "~/components/DocumentBreadcrumb"; import withStores from "~/components/withStores"; import { NavigationNode } from "~/types"; import { NotFoundError, OfflineError } from "~/utils/errors"; @@ -100,55 +95,6 @@ class DataLoader extends React.Component { return this.isEditRoute || this.props.auth?.team?.collaborativeEditing; } - onSearchLink = async (term: string) => { - if (isInternalUrl(term)) { - // search for exact internal document - const slug = parseDocumentSlug(term); - if (!slug) { - return; - } - - try { - const document = await this.props.documents.fetch(slug); - const time = formatDistanceToNow(Date.parse(document.updatedAt), { - addSuffix: true, - }); - - return [ - { - title: document.title, - subtitle: `Updated ${time}`, - url: document.url, - }, - ]; - } catch (error) { - // NotFoundError could not find document for slug - if (!(error instanceof NotFoundError)) { - throw error; - } - } - } - - // default search for anything that doesn't look like a URL - const results = await this.props.documents.searchTitles(term); - - return sortBy( - results.map((document: Document) => { - return { - title: document.title, - subtitle: , - url: document.url, - }; - }), - (document) => - deburr(document.title) - .toLowerCase() - .startsWith(deburr(term).toLowerCase()) - ? -1 - : 1 - ); - }; - onCreateLink = async (title: string) => { const document = this.document; invariant(document, "document must be loaded to create link"); @@ -277,7 +223,6 @@ class DataLoader extends React.Component { !abilities.update || document.isArchived || !!revisionId, - onSearchLink: this.onSearchLink, onCreateLink: this.onCreateLink, sharedTree: this.sharedTree, })} diff --git a/app/scenes/Document/components/Editor.tsx b/app/scenes/Document/components/Editor.tsx index f2118727d..350e5bdc3 100644 --- a/app/scenes/Document/components/Editor.tsx +++ b/app/scenes/Document/components/Editor.tsx @@ -9,7 +9,6 @@ import { RefHandle } from "~/components/ContentEditable"; import DocumentMetaWithViews from "~/components/DocumentMetaWithViews"; import Editor, { Props as EditorProps } from "~/components/Editor"; import Flex from "~/components/Flex"; -import HoverPreview from "~/components/HoverPreview"; import { documentHistoryUrl, documentUrl, @@ -38,10 +37,6 @@ type Props = Omit & { * and support for hover previews of internal links. */ function DocumentEditor(props: Props, ref: React.RefObject) { - const [ - activeLinkEvent, - setActiveLinkEvent, - ] = React.useState(null); const titleRef = React.useRef(null); const { t } = useTranslation(); const match = useRouteMatch(); @@ -58,15 +53,6 @@ function DocumentEditor(props: Props, ref: React.RefObject) { } }, [ref]); - const handleLinkActive = React.useCallback((event: MouseEvent) => { - setActiveLinkEvent(event); - return false; - }, []); - - const handleLinkInactive = React.useCallback(() => { - setActiveLinkEvent(null); - }, []); - const handleGoToNextInput = React.useCallback( (insertParagraph: boolean) => { if (insertParagraph && ref.current) { @@ -123,7 +109,6 @@ function DocumentEditor(props: Props, ref: React.RefObject) { ref={ref} autoFocus={!!title && !props.defaultValue} placeholder={t("Type '/' to insert, or start writing…")} - onHoverLink={handleLinkActive} scrollTo={window.location.hash} readOnly={readOnly} shareId={shareId} @@ -132,13 +117,6 @@ function DocumentEditor(props: Props, ref: React.RefObject) { {...rest} /> {!readOnly && } - {activeLinkEvent && !shareId && ( - - )} {children} );