Skip to content

Commit

Permalink
Add Figma Embed to Playground (#2705)
Browse files Browse the repository at this point in the history
  • Loading branch information
tylerjbainbridge authored and thegreatercurve committed Nov 25, 2022
1 parent 3a8a531 commit 64d7bb2
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/lexical-playground/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import ComponentPickerPlugin from './plugins/ComponentPickerPlugin';
import EmojisPlugin from './plugins/EmojisPlugin';
import EquationsPlugin from './plugins/EquationsPlugin';
import ExcalidrawPlugin from './plugins/ExcalidrawPlugin';
import FigmaPlugin from './plugins/FigmaPlugin';
import HorizontalRulePlugin from './plugins/HorizontalRulePlugin';
import ImagesPlugin from './plugins/ImagesPlugin';
import KeywordsPlugin from './plugins/KeywordsPlugin';
Expand Down Expand Up @@ -138,6 +139,7 @@ export default function Editor(): JSX.Element {
<PollPlugin />
<TwitterPlugin />
<YouTubePlugin />
<FigmaPlugin />
<ClickableLinkPlugin />
<HorizontalRulePlugin />
<TextFormatFloatingToolbarPlugin />
Expand Down
1 change: 1 addition & 0 deletions packages/lexical-playground/src/images/icons/figma.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/lexical-playground/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ i.close {
background-image: url(images/icons/close.svg);
}

i.figma {
background-image: url(images/icons/figma.svg);
}

i.poll {
background-image: url(images/icons/card-checklist.svg);
}
Expand Down
130 changes: 130 additions & 0 deletions packages/lexical-playground/src/nodes/FigmaNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type {
EditorConfig,
ElementFormatType,
LexicalEditor,
LexicalNode,
NodeKey,
Spread,
} from 'lexical';

import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
import {
DecoratorBlockNode,
SerializedDecoratorBlockNode,
} from '@lexical/react/LexicalDecoratorBlockNode';
import * as React from 'react';

type FigmaComponentProps = Readonly<{
className: Readonly<{
base: string;
focus: string;
}>;
format: ElementFormatType | null;
nodeKey: NodeKey;
documentID: string;
}>;

function FigmaComponent({
className,
format,
nodeKey,
documentID,
}: FigmaComponentProps) {
return (
<BlockWithAlignableContents
className={className}
format={format}
nodeKey={nodeKey}>
<iframe
width="560"
height="315"
src={`https://www.figma.com/embed?embed_host=lexical&url=\
https://www.figma.com/file/${documentID}`}
allowFullScreen={true}
/>
</BlockWithAlignableContents>
);
}

export type SerializedFigmaNode = Spread<
{
documentID: string;
type: 'figma';
version: 1;
},
SerializedDecoratorBlockNode
>;

export class FigmaNode extends DecoratorBlockNode {
__id: string;

static getType(): string {
return 'figma';
}

static clone(node: FigmaNode): FigmaNode {
return new FigmaNode(node.__id, node.__format, node.__key);
}

static importJSON(serializedNode: SerializedFigmaNode): FigmaNode {
const node = $createFigmaNode(serializedNode.documentID);
node.setFormat(serializedNode.format);
return node;
}

exportJSON(): SerializedFigmaNode {
return {
...super.exportJSON(),
documentID: this.__id,
type: 'figma',
version: 1,
};
}

constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
super(format, key);
this.__id = id;
}

updateDOM(): false {
return false;
}

decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
const embedBlockTheme = config.theme.embedBlock || {};
const className = {
base: embedBlockTheme.base || '',
focus: embedBlockTheme.focus || '',
};
return (
<FigmaComponent
className={className}
format={this.__format}
nodeKey={this.getKey()}
documentID={this.__id}
/>
);
}

isTopLevel(): true {
return true;
}
}

export function $createFigmaNode(documentID: string): FigmaNode {
return new FigmaNode(documentID);
}

export function $isFigmaNode(
node: FigmaNode | LexicalNode | null | undefined,
): node is FigmaNode {
return node instanceof FigmaNode;
}
2 changes: 2 additions & 0 deletions packages/lexical-playground/src/nodes/PlaygroundNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {AutocompleteNode} from './AutocompleteNode';
import {EmojiNode} from './EmojiNode';
import {EquationNode} from './EquationNode';
import {ExcalidrawNode} from './ExcalidrawNode';
import {FigmaNode} from './FigmaNode';
import {ImageNode} from './ImageNode';
import {KeywordNode} from './KeywordNode';
import {MentionNode} from './MentionNode';
Expand Down Expand Up @@ -58,6 +59,7 @@ const PlaygroundNodes: Array<Klass<LexicalNode>> = [
HorizontalRuleNode,
TweetNode,
YouTubeNode,
FigmaNode,
MarkNode,
];

Expand Down
42 changes: 40 additions & 2 deletions packages/lexical-playground/src/plugins/AutoEmbedPlugin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as React from 'react';

import useModal from '../../hooks/useModal';
import Button from '../../ui/Button';
import {INSERT_FIGMA_COMMAND} from '../FigmaPlugin';
import {INSERT_TWEET_COMMAND} from '../TwitterPlugin';
import {INSERT_YOUTUBE_COMMAND} from '../YouTubePlugin';

Expand Down Expand Up @@ -111,6 +112,45 @@ export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
type: 'tweet',
};

export const FigmaEmbedConfig: PlaygroundEmbedConfig = {
contentName: 'Figma Document',

exampleUrl: 'https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931/Sample-File',

icon: <i className="icon figma" />,

insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
editor.dispatchCommand(INSERT_FIGMA_COMMAND, result.id);
},

keywords: ['figma', 'figma.com', 'mock-up'],

// Determine if a given URL is a match and return url data.
parseUrl: (text: string) => {
const match =
/https:\/\/([\w.-]+\.)?figma.com\/(file|proto)\/([0-9a-zA-Z]{22,128})(?:\/.*)?$/.exec(
text,
);

if (match != null) {
return {
id: match[3],
url: match[0],
};
}

return null;
},

type: 'figma',
};

export const EmbedConfigs = [
TwitterEmbedConfig,
YoutubeEmbedConfig,
FigmaEmbedConfig,
];

function AutoEmbedMenuItem({
index,
isSelected,
Expand Down Expand Up @@ -214,8 +254,6 @@ export function AutoEmbedDialog({
);
}

export const EmbedConfigs = [TwitterEmbedConfig, YoutubeEmbedConfig];

export default function AutoEmbedPlugin(): JSX.Element {
const [modal, showModal] = useModal();

Expand Down
38 changes: 38 additions & 0 deletions packages/lexical-playground/src/plugins/FigmaPlugin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$insertBlockNode} from '@lexical/utils';
import {COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand} from 'lexical';
import {useEffect} from 'react';

import {$createFigmaNode, FigmaNode} from '../../nodes/FigmaNode';

export const INSERT_FIGMA_COMMAND: LexicalCommand<string> = createCommand();

export default function FigmaPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();

useEffect(() => {
if (!editor.hasNodes([FigmaNode])) {
throw new Error('FigmaPlugin: FigmaNode not registered on editor');
}

return editor.registerCommand<string>(
INSERT_FIGMA_COMMAND,
(payload) => {
const figmaNode = $createFigmaNode(payload);
$insertBlockNode(figmaNode);
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor]);

return null;
}

0 comments on commit 64d7bb2

Please sign in to comment.