diff --git a/app/components/CollectionDescription.tsx b/app/components/CollectionDescription.tsx index 575ffabbc..750b4cd0c 100644 --- a/app/components/CollectionDescription.tsx +++ b/app/components/CollectionDescription.tsx @@ -104,7 +104,7 @@ function CollectionDescription({ collection }: Props) { autoFocus={isEditing} onBlur={handleStopEditing} maxLength={1000} - disableEmbeds + embedsDisabled readOnlyWriteCheckboxes grow /> diff --git a/app/components/Editor.tsx b/app/components/Editor.tsx index e40b5a203..a4d6e0f0d 100644 --- a/app/components/Editor.tsx +++ b/app/components/Editor.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import { Optional } from "utility-types"; import embeds from "@shared/editor/embeds"; -import { EmbedDescriptor } from "@shared/editor/types"; import ErrorBoundary from "~/components/ErrorBoundary"; import { Props as EditorProps } from "~/editor"; import useDictionary from "~/hooks/useDictionary"; @@ -19,14 +18,12 @@ const SharedEditor = React.lazy( ) ); -const EMPTY_ARRAY: EmbedDescriptor[] = []; - export type Props = Optional< EditorProps, "placeholder" | "defaultValue" | "onClickLink" | "embeds" | "dictionary" > & { shareId?: string | undefined; - disableEmbeds?: boolean; + embedsDisabled?: boolean; grow?: boolean; onSynced?: () => Promise; onPublish?: (event: React.MouseEvent) => any; @@ -94,7 +91,7 @@ function Editor(props: Props, ref: React.Ref) { ref={ref} uploadImage={onUploadImage} onShowToast={onShowToast} - embeds={props.disableEmbeds ? EMPTY_ARRAY : embeds} + embeds={embeds} dictionary={dictionary} {...props} onClickLink={onClickLink} diff --git a/app/components/HoverPreviewDocument.tsx b/app/components/HoverPreviewDocument.tsx index 0edaa96dc..e87fc7e2e 100644 --- a/app/components/HoverPreviewDocument.tsx +++ b/app/components/HoverPreviewDocument.tsx @@ -37,7 +37,7 @@ function HoverPreviewDocument({ url, children }: Props) { diff --git a/app/editor/components/ComponentView.tsx b/app/editor/components/ComponentView.tsx index 052e51cb3..5103fb0f1 100644 --- a/app/editor/components/ComponentView.tsx +++ b/app/editor/components/ComponentView.tsx @@ -99,8 +99,8 @@ export default class ComponentView { } } - stopEvent() { - return true; + stopEvent(event: Event) { + return event.type !== "mousedown" && !event.type.startsWith("drag"); } destroy() { diff --git a/app/editor/index.tsx b/app/editor/index.tsx index 7e0954b58..a098d237c 100644 --- a/app/editor/index.tsx +++ b/app/editor/index.tsx @@ -139,6 +139,8 @@ export type Props = { onKeyDown?: (event: React.KeyboardEvent) => void; /** Collection of embed types to render in the document */ embeds: EmbedDescriptor[]; + /** Whether embeds should be rendered without an iframe */ + embedsDisabled?: boolean; /** Callback when a toast message is triggered (eg "link copied") */ onShowToast?: (message: string, code: ToastType) => void; className?: string; diff --git a/app/menus/DocumentMenu.tsx b/app/menus/DocumentMenu.tsx index 158aff8dc..2f94a9838 100644 --- a/app/menus/DocumentMenu.tsx +++ b/app/menus/DocumentMenu.tsx @@ -410,20 +410,14 @@ function DocumentMenu({ type: "button", title: t("Enable embeds"), onClick: document.enableEmbeds, - visible: - !!showToggleEmbeds && - document.embedsDisabled && - !team.collaborativeEditing, + visible: !!showToggleEmbeds && document.embedsDisabled, icon: , }, { type: "button", title: t("Disable embeds"), onClick: document.disableEmbeds, - visible: - !!showToggleEmbeds && - !document.embedsDisabled && - !team.collaborativeEditing, + visible: !!showToggleEmbeds && !document.embedsDisabled, icon: , }, { diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 935056f24..364477eb2 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -392,7 +392,7 @@ class DocumentScene extends React.Component { const team = auth.team; const isShare = !!shareId; const value = revision ? revision.text : document.text; - const disableEmbeds = + const embedsDisabled = (team && team.documentEmbeds === false) || document.embedsDisabled; const headings = this.editor.current ? // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'. @@ -570,7 +570,7 @@ class DocumentScene extends React.Component { )} { document={document} value={readOnly ? value : undefined} defaultValue={value} - disableEmbeds={disableEmbeds} + embedsDisabled={embedsDisabled} onSynced={this.onSynced} onImageUploadStart={this.onImageUploadStart} onImageUploadStop={this.onImageUploadStop} diff --git a/shared/editor/embeds/components/Simple.tsx b/shared/editor/embeds/components/Simple.tsx new file mode 100644 index 000000000..c760894c8 --- /dev/null +++ b/shared/editor/embeds/components/Simple.tsx @@ -0,0 +1,70 @@ +import { OpenIcon } from "outline-icons"; +import * as React from "react"; +import styled, { DefaultTheme, ThemeProps } from "styled-components"; +import { EmbedProps as Props } from "../"; + +export default function Simple(props: Props & ThemeProps) { + return ( + + {props.embed.icon(undefined)} + + {props.embed.title} + {props.attrs.href.replace(/^https?:\/\//, "")} + + + + ); +} + +const StyledOpenIcon = styled(OpenIcon)` + margin-left: auto; +`; + +const Title = styled.strong` + font-weight: 500; + font-size: 14px; + color: ${(props) => props.theme.text}; +`; + +const Preview = styled.div` + gap: 8px; + display: flex; + flex-direction: row; + flex-grow: 1; + align-items: center; + color: ${(props) => props.theme.textTertiary}; +`; + +const Subtitle = styled.span` + font-size: 13px; + color: ${(props) => props.theme.textTertiary} !important; +`; + +const Wrapper = styled.a` + display: inline-flex; + align-items: flex-start; + gap: 4px; + color: ${(props) => props.theme.text} !important; + background: ${(props) => props.theme.secondaryBackground}; + white-space: nowrap; + border-radius: 8px; + padding: 6px 8px; + max-width: 840px; + width: 100%; + + text-overflow: ellipsis; + overflow: hidden; + + &:hover { + outline: 2px solid ${(props) => props.theme.divider}; + } +`; diff --git a/shared/editor/embeds/index.tsx b/shared/editor/embeds/index.tsx index 6701c5f10..38045dccb 100644 --- a/shared/editor/embeds/index.tsx +++ b/shared/editor/embeds/index.tsx @@ -38,6 +38,8 @@ import Image from "./components/Image"; export type EmbedProps = { isSelected: boolean; + isEditable: boolean; + embed: EmbedDescriptor; attrs: { href: string; matches: RegExpMatchArray; diff --git a/shared/editor/nodes/Embed.tsx b/shared/editor/nodes/Embed.tsx index ca98c7ac9..d50de1109 100644 --- a/shared/editor/nodes/Embed.tsx +++ b/shared/editor/nodes/Embed.tsx @@ -2,6 +2,7 @@ import Token from "markdown-it/lib/token"; import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model"; import { EditorState, Transaction } from "prosemirror-state"; import * as React from "react"; +import Simple from "../embeds/components/Simple"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import embedsRule from "../rules/embeds"; import { ComponentProps } from "../types"; @@ -24,7 +25,7 @@ export default class Embed extends Node { }, parseDOM: [ { - tag: "iframe[class=embed]", + tag: "iframe.embed", getAttrs: (dom: HTMLIFrameElement) => { const { embeds } = this.editor.props; const href = dom.getAttribute("src") || ""; @@ -43,6 +44,14 @@ export default class Embed extends Node { return {}; }, }, + { + tag: "a.disabled-embed", + getAttrs: (dom: HTMLAnchorElement) => { + return { + href: dom.getAttribute("href") || "", + }; + }, + }, ], toDOM: (node) => [ "iframe", @@ -57,22 +66,24 @@ export default class Embed extends Node { } component({ isEditable, isSelected, theme, node }: ComponentProps) { - const { embeds } = this.editor.props; + const { embeds, embedsDisabled } = this.editor.props; // matches are cached in module state to avoid re running loops and regex - // here. Unfortuantely this function is not compatible with React.memo or + // here. Unfortunately this function is not compatible with React.memo or // we would use that instead. const hit = cache[node.attrs.href]; let Component = hit ? hit.Component : undefined; let matches = hit ? hit.matches : undefined; + let embed = hit ? hit.embed : undefined; if (!Component) { - for (const embed of embeds) { - const m = embed.matcher(node.attrs.href); + for (const e of embeds) { + const m = e.matcher(node.attrs.href); if (m) { - Component = embed.component; + Component = e.component; matches = m; - cache[node.attrs.href] = { Component, matches }; + embed = e; + cache[node.attrs.href] = { Component, embed, matches }; } } } @@ -81,6 +92,18 @@ export default class Embed extends Node { return null; } + if (embedsDisabled) { + return ( + + ); + } + return ( ; matcher: (url: string) => boolean | [] | RegExpMatchArray; component: typeof React.Component | React.FC; };