feat: Allow Google Embeds from regular (non publish-to-web) links (#1533)
Improve styling to allow getting back to source document
This commit is contained in:
@@ -2,9 +2,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Frame from "./components/Frame";
|
import Frame from "./components/Frame";
|
||||||
|
|
||||||
const URL_REGEX = new RegExp(
|
const URL_REGEX = new RegExp("^https?://docs.google.com/document/(.*)$");
|
||||||
"^https?://docs.google.com/document/d/(.*)/pub(.*)$"
|
|
||||||
);
|
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
attrs: {|
|
attrs: {|
|
||||||
@@ -18,7 +16,20 @@ export default class GoogleDocs extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Frame src={this.props.attrs.href} title="Google Docs Embed" border />
|
<Frame
|
||||||
|
src={this.props.attrs.href.replace("/edit", "/preview")}
|
||||||
|
icon={
|
||||||
|
<img
|
||||||
|
src="/images/google-docs.png"
|
||||||
|
alt="Google Docs Icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
canonicalUrl={this.props.attrs.href}
|
||||||
|
title="Google Docs"
|
||||||
|
border
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,19 @@ describe("GoogleDocs", () => {
|
|||||||
match
|
match
|
||||||
)
|
)
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
"https://docs.google.com/document/d/1SsDfWzFFTjZM2LanvpyUzjKhqVQpwpTMeiPeYxhVqOg/edit".match(
|
||||||
|
match
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
"https://docs.google.com/document/d/1SsDfWzFFTjZM2LanvpyUzjKhqVQpwpTMeiPeYxhVqOg/preview".match(
|
||||||
|
match
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("to not be enabled elsewhere", () => {
|
test("to not be enabled elsewhere", () => {
|
||||||
expect(
|
|
||||||
"https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
|
|
||||||
match
|
|
||||||
)
|
|
||||||
).toBe(null);
|
|
||||||
expect("https://docs.google.com/document".match(match)).toBe(null);
|
expect("https://docs.google.com/document".match(match)).toBe(null);
|
||||||
expect("https://docs.google.com".match(match)).toBe(null);
|
expect("https://docs.google.com".match(match)).toBe(null);
|
||||||
expect("https://www.google.com".match(match)).toBe(null);
|
expect("https://www.google.com".match(match)).toBe(null);
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Frame from "./components/Frame";
|
import Frame from "./components/Frame";
|
||||||
|
|
||||||
const URL_REGEX = new RegExp(
|
const URL_REGEX = new RegExp("^https?://docs.google.com/spreadsheets/d/(.*)$");
|
||||||
"^https?://docs.google.com/spreadsheets/d/(.*)/pub(.*)$"
|
|
||||||
);
|
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
attrs: {|
|
attrs: {|
|
||||||
@@ -18,7 +16,20 @@ export default class GoogleSlides extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Frame src={this.props.attrs.href} title="Google Sheets Embed" border />
|
<Frame
|
||||||
|
src={this.props.attrs.href.replace("/edit", "/preview")}
|
||||||
|
icon={
|
||||||
|
<img
|
||||||
|
src="/images/google-sheets.png"
|
||||||
|
alt="Google Sheets Icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
canonicalUrl={this.props.attrs.href}
|
||||||
|
title="Google Sheets"
|
||||||
|
border
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ describe("GoogleSheets", () => {
|
|||||||
match
|
match
|
||||||
)
|
)
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
|
||||||
|
|
||||||
test("to not be enabled elsewhere", () => {
|
|
||||||
expect(
|
expect(
|
||||||
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
|
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
|
||||||
match
|
match
|
||||||
)
|
)
|
||||||
).toBe(null);
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("to not be enabled elsewhere", () => {
|
||||||
expect("https://docs.google.com/spreadsheets".match(match)).toBe(null);
|
expect("https://docs.google.com/spreadsheets".match(match)).toBe(null);
|
||||||
expect("https://docs.google.com".match(match)).toBe(null);
|
expect("https://docs.google.com".match(match)).toBe(null);
|
||||||
expect("https://www.google.com".match(match)).toBe(null);
|
expect("https://www.google.com".match(match)).toBe(null);
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Frame from "./components/Frame";
|
import Frame from "./components/Frame";
|
||||||
|
|
||||||
const URL_REGEX = new RegExp(
|
const URL_REGEX = new RegExp("^https?://docs.google.com/presentation/d/(.*)$");
|
||||||
"^https?://docs.google.com/presentation/d/(.*)/pub(.*)$"
|
|
||||||
);
|
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
attrs: {|
|
attrs: {|
|
||||||
@@ -19,8 +17,19 @@ export default class GoogleSlides extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Frame
|
<Frame
|
||||||
src={this.props.attrs.href.replace("/pub", "/embed")}
|
src={this.props.attrs.href
|
||||||
title="Google Slides Embed"
|
.replace("/edit", "/preview")
|
||||||
|
.replace("/pub", "/embed")}
|
||||||
|
icon={
|
||||||
|
<img
|
||||||
|
src="/images/google-slides.png"
|
||||||
|
alt="Google Slides Icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
canonicalUrl={this.props.attrs.href}
|
||||||
|
title="Google Slides"
|
||||||
border
|
border
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ describe("GoogleSlides", () => {
|
|||||||
match
|
match
|
||||||
)
|
)
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
|
||||||
|
|
||||||
test("to not be enabled elsewhere", () => {
|
|
||||||
expect(
|
expect(
|
||||||
"https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
|
"https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
|
||||||
match
|
match
|
||||||
)
|
)
|
||||||
).toBe(null);
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("to not be enabled elsewhere", () => {
|
||||||
expect("https://docs.google.com/presentation".match(match)).toBe(null);
|
expect("https://docs.google.com/presentation".match(match)).toBe(null);
|
||||||
expect("https://docs.google.com".match(match)).toBe(null);
|
expect("https://docs.google.com".match(match)).toBe(null);
|
||||||
expect("https://www.google.com".match(match)).toBe(null);
|
expect("https://www.google.com".match(match)).toBe(null);
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { OpenIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import Flex from "components/Flex";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
src?: string,
|
src?: string,
|
||||||
border?: boolean,
|
border?: boolean,
|
||||||
|
title?: string,
|
||||||
|
icon?: React.Node,
|
||||||
|
canonicalUrl?: string,
|
||||||
width?: string,
|
width?: string,
|
||||||
height?: string,
|
height?: string,
|
||||||
};
|
};
|
||||||
@@ -40,15 +45,20 @@ class Frame extends React.Component<PropsWithRef> {
|
|||||||
width = "100%",
|
width = "100%",
|
||||||
height = "400px",
|
height = "400px",
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
canonicalUrl,
|
||||||
...rest
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const Component = border ? StyledIframe : "iframe";
|
const Component = border ? StyledIframe : "iframe";
|
||||||
|
const withBar = !!(icon || canonicalUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Rounded width={width} height={height}>
|
<Rounded width={width} height={height} withBar={withBar}>
|
||||||
{this.isLoaded && (
|
{this.isLoaded && (
|
||||||
<Component
|
<Component
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
|
withBar={withBar}
|
||||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
@@ -60,16 +70,56 @@ class Frame extends React.Component<PropsWithRef> {
|
|||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{withBar && (
|
||||||
|
<Bar align="center">
|
||||||
|
{icon} <Title>{title}</Title>
|
||||||
|
{canonicalUrl && (
|
||||||
|
<Open
|
||||||
|
href={canonicalUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<OpenIcon color="currentColor" size={18} /> Open
|
||||||
|
</Open>
|
||||||
|
)}
|
||||||
|
</Bar>
|
||||||
|
)}
|
||||||
</Rounded>
|
</Rounded>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Rounded = styled.div`
|
const Rounded = styled.div`
|
||||||
border-radius: 3px;
|
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: ${(props) => props.width};
|
width: ${(props) => props.width};
|
||||||
height: ${(props) => props.height};
|
height: ${(props) => (props.withBar ? props.height + 28 : props.height)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Open = styled.a`
|
||||||
|
color: ${(props) => props.theme.textSecondary} !important;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled.span`
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-left: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Bar = styled(Flex)`
|
||||||
|
background: ${(props) => props.theme.secondaryBackground};
|
||||||
|
color: ${(props) => props.theme.textSecondary};
|
||||||
|
padding: 0 8px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
|
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
|
||||||
@@ -79,7 +129,8 @@ const Iframe = (props) => <iframe {...props} />;
|
|||||||
const StyledIframe = styled(Iframe)`
|
const StyledIframe = styled(Iframe)`
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-color: ${(props) => props.theme.embedBorder};
|
border-color: ${(props) => props.theme.embedBorder};
|
||||||
border-radius: 3px;
|
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
|
||||||
|
display: block;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
|
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export const light = {
|
|||||||
quote: colors.slateLight,
|
quote: colors.slateLight,
|
||||||
codeBackground: colors.smoke,
|
codeBackground: colors.smoke,
|
||||||
codeBorder: colors.smokeDark,
|
codeBorder: colors.smokeDark,
|
||||||
embedBorder: "#DDD #DDD #CCC",
|
embedBorder: colors.slateLight,
|
||||||
horizontalRule: colors.smokeDark,
|
horizontalRule: colors.smokeDark,
|
||||||
|
|
||||||
noticeInfoBackground: colors.warmGrey,
|
noticeInfoBackground: colors.warmGrey,
|
||||||
|
|||||||
Reference in New Issue
Block a user