Editor embeds (#680)

- [x] Make deleting an embed easier
- [x] Add document level ability to disable embeds
- [x] Add team level ability to disable embeds
- [x] GitHub
- [x] Numeracy
- [x] Mode Analytics
- [x] Figma
- [x] Airtable
- [x] Vimeo
- [x] RealtimeBoard
- [x] Loom
- [x] Lucidcharts
- [x] Framer
- [x] InVision
- [x] Typeform
- [x] Marvel
- [x] Spotify
- [x] Codepen
- [x] Trello
This commit is contained in:
Tom Moor
2018-12-15 14:06:29 -08:00
committed by GitHub
parent 836f9a88a2
commit 044b4f16bc
50 changed files with 1121 additions and 18 deletions

27
app/embeds/Airtable.js Normal file
View File

@@ -0,0 +1,27 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp('https://airtable.com/(?:embed/)?(shr.*)$');
type Props = {
url: string,
matches: string[],
};
export default class Airtable extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const { matches } = this.props;
const shareId = matches[1];
return (
<Frame
src={`https://airtable.com/embed/${shareId}`}
title={`Airtable (${shareId})`}
border
/>
);
}
}

View File

@@ -0,0 +1,23 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Airtable } = embeds;
describe('Airtable', () => {
const match = Airtable.ENABLED[0];
test('to be enabled on share link', () => {
expect('https://airtable.com/shrEoQs3erLnppMie'.match(match)).toBeTruthy();
});
test('to be enabled on embed link', () => {
expect(
'https://airtable.com/embed/shrEoQs3erLnppMie'.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://airtable.com'.match(match)).toBe(null);
expect('https://airtable.com/features'.match(match)).toBe(null);
expect('https://airtable.com/pricing'.match(match)).toBe(null);
});
});

19
app/embeds/Codepen.js Normal file
View File

@@ -0,0 +1,19 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp('^https://codepen.io/(.*?)/(pen|embed)/(.*)$');
type Props = {
url: string,
};
export default class Codepen extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const normalizedUrl = this.props.url.replace(/\/pen\//, '/embed/');
return <Frame src={normalizedUrl} title="Codepen Embed" />;
}
}

View File

@@ -0,0 +1,24 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Codepen } = embeds;
describe('Codepen', () => {
const match = Codepen.ENABLED[0];
test('to be enabled on pen link', () => {
expect(
'https://codepen.io/chriscoyier/pen/gfdDu'.match(match)
).toBeTruthy();
});
test('to be enabled on embed link', () => {
expect(
'https://codepen.io/chriscoyier/embed/gfdDu'.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://codepen.io'.match(match)).toBe(null);
expect('https://codepen.io/chriscoyier'.match(match)).toBe(null);
});
});

27
app/embeds/Figma.js Normal file
View File

@@ -0,0 +1,27 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp(
'https://([w.-]+.)?figma.com/(file|proto)/([0-9a-zA-Z]{22,128})(?:/.*)?$'
);
type Props = {
url: string,
};
export default class Figma extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return (
<Frame
src={`https://www.figma.com/embed?embed_host=outline&url=${
this.props.url
}`}
title="Figma Embed"
border
/>
);
}
}

24
app/embeds/Figma.test.js Normal file
View File

@@ -0,0 +1,24 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Figma } = embeds;
describe('Figma', () => {
const match = Figma.ENABLED[0];
test('to be enabled on file link', () => {
expect(
'https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931'.match(match)
).toBeTruthy();
});
test('to be enabled on prototype link', () => {
expect(
'https://www.figma.com/proto/LKQ4FJ4bTnCSjedbRpk931'.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://www.figma.com'.match(match)).toBe(null);
expect('https://www.figma.com/features'.match(match)).toBe(null);
});
});

17
app/embeds/Framer.js Normal file
View File

@@ -0,0 +1,17 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp('^https://framer.cloud/(.*)$');
type Props = {
url: string,
};
export default class Framer extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.url} title="Framer Embed" border />;
}
}

15
app/embeds/Framer.test.js Normal file
View File

@@ -0,0 +1,15 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Framer } = embeds;
describe('Framer', () => {
const match = Framer.ENABLED[0];
test('to be enabled on share link', () => {
expect('https://framer.cloud/PVwJO'.match(match)).toBeTruthy();
});
test('to not be enabled on root', () => {
expect('https://framer.cloud'.match(match)).toBe(null);
});
});

70
app/embeds/Gist.js Normal file
View File

@@ -0,0 +1,70 @@
// @flow
import * as React from 'react';
const URL_REGEX = new RegExp(
'^https://gist.github.com/([a-zd](?:[a-zd]|-(?=[a-zd])){0,38})/(.*)$'
);
type Props = {
url: string,
};
class Gist extends React.Component<Props> {
iframeNode: ?HTMLIFrameElement;
static ENABLED = [URL_REGEX];
componentDidMount() {
this.updateIframeContent();
}
get id() {
const gistUrl = new URL(this.props.url);
return gistUrl.pathname.split('/')[2];
}
updateIframeContent() {
const id = this.id;
const iframe = this.iframeNode;
if (!iframe) return;
// $FlowFixMe
let doc = iframe.document;
if (iframe.contentDocument) doc = iframe.contentDocument;
else if (iframe.contentWindow) doc = iframe.contentWindow.document;
const gistLink = `https://gist.github.com/${id}.js`;
const gistScript = `<script type="text/javascript" src="${
gistLink
}"></script>`;
const styles =
'<style>*{ font-size:12px; } body { margin: 0; } .gist .blob-wrapper.data { max-height:150px; overflow:auto; }</style>';
const iframeHtml = `<html><head><base target="_parent">${
styles
}</head><body>${gistScript}</body></html>`;
doc.open();
doc.writeln(iframeHtml);
doc.close();
}
render() {
const id = this.id;
return (
<iframe
ref={ref => {
this.iframeNode = ref;
}}
type="text/html"
frameBorder="0"
width="100%"
height="200px"
id={`gist-${id}`}
title={`Github Gist (${id})`}
/>
);
}
}
export default Gist;

19
app/embeds/Gist.test.js Normal file
View File

@@ -0,0 +1,19 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Gist } = embeds;
describe('Gist', () => {
const match = Gist.ENABLED[0];
test('to be enabled on gist link', () => {
expect(
'https://gist.github.com/wmertens/0b4fd66ca7055fd290ecc4b9d95271a9'.match(
match
)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://gist.github.com/tommoor'.match(match)).toBe(null);
});
});

19
app/embeds/InVision.js Normal file
View File

@@ -0,0 +1,19 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp(
'^https://(invis.io/.*)|(projects.invisionapp.com/share/.*)$'
);
type Props = {
url: string,
};
export default class InVision extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.url} title="InVision Embed" />;
}
}

View File

@@ -0,0 +1,23 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { InVision } = embeds;
describe('InVision', () => {
const match = InVision.ENABLED[0];
test('to be enabled on shortlink', () => {
expect('https://invis.io/69PG07QYQTE'.match(match)).toBeTruthy();
});
test('to be enabled on share', () => {
expect(
'https://projects.invisionapp.com/share/69PG07QYQTE'.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://invis.io'.match(match)).toBe(null);
expect('https://invisionapp.com'.match(match)).toBe(null);
expect('https://projects.invisionapp.com'.match(match)).toBe(null);
});
});

26
app/embeds/Loom.js Normal file
View File

@@ -0,0 +1,26 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = /^https:\/\/(www\.)?useloom.com\/(embed|share)\/(.*)$/;
type Props = {
url: string,
};
export default class Loom extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const normalizedUrl = this.props.url.replace('share', 'embed');
return (
<Frame
width="420px"
height="235px"
src={normalizedUrl}
title="Loom Embed"
/>
);
}
}

28
app/embeds/Loom.test.js Normal file
View File

@@ -0,0 +1,28 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Loom } = embeds;
describe('Loom', () => {
const match = Loom.ENABLED[0];
test('to be enabled on share link', () => {
expect(
'https://www.useloom.com/share/55327cbb265743f39c2c442c029277e0'.match(
match
)
).toBeTruthy();
});
test('to be enabled on embed link', () => {
expect(
'https://www.useloom.com/embed/55327cbb265743f39c2c442c029277e0'.match(
match
)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://www.useloom.com'.match(match)).toBe(null);
expect('https://www.useloom.com/features'.match(match)).toBe(null);
});
});

26
app/embeds/Lucidchart.js Normal file
View File

@@ -0,0 +1,26 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = /^https:\/\/(www\.)?lucidchart.com\/documents\/(embeddedchart|view)\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?:\/.*)?$/;
type Props = {
url: string,
matches: string[],
};
export default class Lucidchart extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const { matches } = this.props;
const chartId = matches[3];
return (
<Frame
src={`http://lucidchart.com/documents/embeddedchart/${chartId}`}
title="Lucidchart Embed"
/>
);
}
}

View File

@@ -0,0 +1,30 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Lucidchart } = embeds;
describe('Lucidchart', () => {
const match = Lucidchart.ENABLED[0];
test('to be enabled on view link', () => {
expect(
'https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7'.match(
match
)
).toBeTruthy();
});
test('to be enabled on visited link', () => {
expect(
'https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7/0'.match(
match
)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://lucidchart.com'.match(match)).toBe(null);
expect('https://www.lucidchart.com'.match(match)).toBe(null);
expect('https://www.lucidchart.com/features'.match(match)).toBe(null);
expect('https://www.lucidchart.com/documents/view'.match(match)).toBe(null);
});
});

17
app/embeds/Marvel.js Normal file
View File

@@ -0,0 +1,17 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp('^https://marvelapp.com/([A-Za-z0-9-]{6})/?$');
type Props = {
url: string,
};
export default class Marvel extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.url} title="Marvel Embed" border />;
}
}

16
app/embeds/Marvel.test.js Normal file
View File

@@ -0,0 +1,16 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Marvel } = embeds;
describe('Marvel', () => {
const match = Marvel.ENABLED[0];
test('to be enabled on share link', () => {
expect('https://marvelapp.com/75hj91'.match(match)).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://marvelapp.com'.match(match)).toBe(null);
expect('https://marvelapp.com/features'.match(match)).toBe(null);
});
});

View File

@@ -0,0 +1,24 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp(
'https://([w.-]+.)?modeanalytics.com/(.*)/reports/(.*)$'
);
type Props = {
url: string,
};
export default class ModeAnalytics extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
// Allow users to paste embed or standard urls and handle them the same
const normalizedUrl = this.props.url.replace(/\/embed$/, '');
return (
<Frame src={`${normalizedUrl}/embed`} title="Mode Analytics Embed" />
);
}
}

View File

@@ -0,0 +1,19 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { ModeAnalytics } = embeds;
describe('ModeAnalytics', () => {
const match = ModeAnalytics.ENABLED[0];
test('to be enabled on report link', () => {
expect(
'https://modeanalytics.com/outline/reports/5aca06064f56'.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://modeanalytics.com'.match(match)).toBe(null);
expect('https://modeanalytics.com/outline'.match(match)).toBe(null);
expect('https://modeanalytics.com/outline/reports'.match(match)).toBe(null);
});
});

22
app/embeds/Numeracy.js Normal file
View File

@@ -0,0 +1,22 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp('https://([w.-]+.)?numeracy.co/(.*)/(.*)$');
type Props = {
url: string,
};
export default class Numeracy extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
// Allow users to paste embed or standard urls and handle them the same
const normalizedUrl = this.props.url.replace(/\.embed$/, '');
return (
<Frame src={`${normalizedUrl}.embed`} title="Numeracy Embed" border />
);
}
}

View File

@@ -0,0 +1,22 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Numeracy } = embeds;
describe('Numeracy', () => {
const match = Numeracy.ENABLED[0];
test('to be enabled on share link', () => {
expect('https://numeracy.co/outline/n8ZIVOC2OS'.match(match)).toBeTruthy();
});
test('to be enabled on embed link', () => {
expect(
'https://numeracy.co/outline/n8ZIVOC2OS.embed'.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://numeracy.co'.match(match)).toBe(null);
expect('https://numeracy.co/outline'.match(match)).toBe(null);
});
});

View File

@@ -0,0 +1,26 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = /^https:\/\/realtimeboard.com\/app\/board\/(.*)$/;
type Props = {
url: string,
matches: string[],
};
export default class RealtimeBoard extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const { matches } = this.props;
const boardId = matches[1];
return (
<Frame
src={`http://realtimeboard.com/app/embed/${boardId}`}
title={`RealtimeBoard (${boardId})`}
/>
);
}
}

View File

@@ -0,0 +1,18 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { RealtimeBoard } = embeds;
describe('RealtimeBoard', () => {
const match = RealtimeBoard.ENABLED[0];
test('to be enabled on share link', () => {
expect(
'https://realtimeboard.com/app/board/o9J_k0fwiss='.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://realtimeboard.com'.match(match)).toBe(null);
expect('https://realtimeboard.com/features'.match(match)).toBe(null);
});
});

36
app/embeds/Spotify.js Normal file
View File

@@ -0,0 +1,36 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp('https?://open.spotify.com/(.*)$');
type Props = {
url: string,
};
export default class Spotify extends React.Component<Props> {
static ENABLED = [URL_REGEX];
get pathname() {
try {
const parsed = new URL(this.props.url);
return parsed.pathname;
} catch (err) {
return '';
}
}
render() {
const normalizedPath = this.pathname.replace(/^\/embed/, '/');
return (
<Frame
width="300px"
height="380px"
src={`https://open.spotify.com/embed${normalizedPath}`}
title="Spotify Embed"
allow="encrypted-media"
/>
);
}
}

View File

@@ -0,0 +1,29 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Spotify } = embeds;
describe('Spotify', () => {
const match = Spotify.ENABLED[0];
test('to be enabled on song link', () => {
expect(
'https://open.spotify.com/track/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg'.match(
match
)
).toBeTruthy();
});
test('to be enabled on playlist link', () => {
expect(
'https://open.spotify.com/user/spotify/playlist/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg'.match(
match
)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://spotify.com'.match(match)).toBe(null);
expect('https://open.spotify.com'.match(match)).toBe(null);
expect('https://www.spotify.com'.match(match)).toBe(null);
});
});

39
app/embeds/Trello.js Normal file
View File

@@ -0,0 +1,39 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = /^https:\/\/trello.com\/(c|b)\/(.*)$/;
type Props = {
url: string,
matches: string[],
};
export default class Trello extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const { matches } = this.props;
const objectId = matches[2];
if (matches[1] === 'c') {
return (
<Frame
width="316px"
height="158px"
src={`https://trello.com/embed/card?id=${objectId}`}
title={`Trello Card (${objectId})`}
/>
);
}
return (
<Frame
width="248px"
height="185px"
src={`https://trello.com/embed/board?id=${objectId}`}
title={`Trello Board (${objectId})`}
/>
);
}
}

19
app/embeds/Typeform.js Normal file
View File

@@ -0,0 +1,19 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = new RegExp(
'^https://([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?).typeform.com/to/(.*)$'
);
type Props = {
url: string,
};
export default class Typeform extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.url} title="Typeform Embed" />;
}
}

View File

@@ -0,0 +1,19 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Typeform } = embeds;
describe('Typeform', () => {
const match = Typeform.ENABLED[0];
test('to be enabled on share link', () => {
expect(
'https://beardyman.typeform.com/to/zvlr4L'.match(match)
).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://www.typeform.com'.match(match)).toBe(null);
expect('https://typeform.com/to/zvlr4L'.match(match)).toBe(null);
expect('https://typeform.com/features'.match(match)).toBe(null);
});
});

28
app/embeds/Vimeo.js Normal file
View File

@@ -0,0 +1,28 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)/;
type Props = {
url: string,
matches: string[],
};
export default class Vimeo extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const { matches } = this.props;
const videoId = matches[4];
return (
<Frame
width="420px"
height="235px"
src={`http://player.vimeo.com/video/${videoId}?byline=0`}
title={`Vimeo Embed (${videoId})`}
/>
);
}
}

20
app/embeds/Vimeo.test.js Normal file
View File

@@ -0,0 +1,20 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { Vimeo } = embeds;
describe('Vimeo', () => {
const match = Vimeo.ENABLED[0];
test('to be enabled on video link', () => {
expect('https://vimeo.com/265045525'.match(match)).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://vimeo.com'.match(match)).toBe(null);
expect('https://www.vimeo.com'.match(match)).toBe(null);
expect('https://vimeo.com/upgrade'.match(match)).toBe(null);
expect('https://vimeo.com/features/video-marketing'.match(match)).toBe(
null
);
});
});

28
app/embeds/YouTube.js Normal file
View File

@@ -0,0 +1,28 @@
// @flow
import * as React from 'react';
import Frame from './components/Frame';
const URL_REGEX = /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})$/i;
type Props = {
url: string,
matches: string[],
};
export default class YouTube extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const { matches } = this.props;
const videoId = matches[1];
return (
<Frame
width="420px"
height="235px"
src={`https://www.youtube.com/embed/${videoId}?modestbranding=1`}
title={`YouTube (${videoId})`}
/>
);
}
}

View File

@@ -0,0 +1,33 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import embeds from '.';
const { YouTube } = embeds;
describe('YouTube', () => {
const match = YouTube.ENABLED[0];
test('to be enabled on video link', () => {
expect(
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'.match(match)
).toBeTruthy();
});
test('to be enabled on embed link', () => {
expect(
'https://www.youtube.com/embed?v=dQw4w9WgXcQ'.match(match)
).toBeTruthy();
});
test('to be enabled on shortlink', () => {
expect('https://youtu.be/dQw4w9WgXcQ'.match(match)).toBeTruthy();
});
test('to not be enabled elsewhere', () => {
expect('https://youtu.be'.match(match)).toBe(null);
expect('https://youtube.com'.match(match)).toBe(null);
expect('https://www.youtube.com'.match(match)).toBe(null);
expect('https://www.youtube.com/logout'.match(match)).toBe(null);
expect('https://www.youtube.com/feed/subscriptions'.match(match)).toBe(
null
);
});
});

View File

@@ -0,0 +1,82 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
type Props = {
src?: string,
border?: boolean,
forwardedRef: *,
width?: string,
height?: string,
};
type State = {
isLoaded: boolean,
};
class Frame extends React.Component<Props, State> {
mounted: boolean;
state = { isLoaded: false };
componentDidMount() {
this.mounted = true;
setImmediate(this.loadIframe);
}
componentWillUnmount() {
this.mounted = false;
}
loadIframe = () => {
if (!this.mounted) return;
this.setState({ isLoaded: true });
};
render() {
const {
border,
width = '100%',
height = '400',
forwardedRef,
...rest
} = this.props;
const Component = border ? Iframe : 'iframe';
return (
<Rounded width={width} height={height}>
{this.state.isLoaded && (
<Component
ref={forwardedRef}
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
width={width}
height={height}
type="text/html"
frameBorder="0"
title="embed"
allowFullScreen
{...rest}
/>
)}
</Rounded>
);
}
}
const Rounded = styled.div`
border-radius: 3px;
overflow: hidden;
width: ${props => props.width};
height: ${props => props.height};
`;
const Iframe = styled.iframe`
border: 1px solid;
border-color: #ddd #ddd #ccc;
border-radius: 3px;
`;
// $FlowIssue - https://github.com/facebook/flow/issues/6103
export default React.forwardRef((props, ref) => (
<Frame {...props} forwardedRef={ref} />
));

38
app/embeds/index.js Normal file
View File

@@ -0,0 +1,38 @@
// @flow
import Airtable from './Airtable';
import Codepen from './Codepen';
import Figma from './Figma';
import Framer from './Framer';
import Gist from './Gist';
import InVision from './InVision';
import Loom from './Loom';
import Lucidchart from './Lucidchart';
import Marvel from './Marvel';
import ModeAnalytics from './ModeAnalytics';
import Numeracy from './Numeracy';
import RealtimeBoard from './RealtimeBoard';
import Spotify from './Spotify';
import Trello from './Trello';
import Typeform from './Typeform';
import Vimeo from './Vimeo';
import YouTube from './YouTube';
export default {
Airtable,
Codepen,
Figma,
Framer,
Gist,
InVision,
Loom,
Lucidchart,
Marvel,
ModeAnalytics,
Numeracy,
RealtimeBoard,
Spotify,
Trello,
Typeform,
Vimeo,
YouTube,
};