chore: remove options to add new embeds and render existing twitter embeds as links (#2917)
This commit is contained in:
@@ -12,9 +12,6 @@ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
|
|||||||
import { ListPlugin } from '@lexical/react/LexicalListPlugin'
|
import { ListPlugin } from '@lexical/react/LexicalListPlugin'
|
||||||
import { EditorState, LexicalEditor } from 'lexical'
|
import { EditorState, LexicalEditor } from 'lexical'
|
||||||
import HorizontalRulePlugin from './Plugins/HorizontalRulePlugin'
|
import HorizontalRulePlugin from './Plugins/HorizontalRulePlugin'
|
||||||
import TwitterPlugin from './Plugins/TwitterPlugin'
|
|
||||||
import YouTubePlugin from './Plugins/YouTubePlugin'
|
|
||||||
import AutoEmbedPlugin from './Plugins/AutoEmbedPlugin'
|
|
||||||
import CollapsiblePlugin from './Plugins/CollapsiblePlugin'
|
import CollapsiblePlugin from './Plugins/CollapsiblePlugin'
|
||||||
import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin'
|
import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin'
|
||||||
import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin'
|
import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin'
|
||||||
@@ -125,9 +122,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
<CodeHighlightPlugin />
|
<CodeHighlightPlugin />
|
||||||
<LinkPlugin />
|
<LinkPlugin />
|
||||||
<HashtagPlugin />
|
<HashtagPlugin />
|
||||||
<AutoEmbedPlugin />
|
|
||||||
<TwitterPlugin />
|
|
||||||
<YouTubePlugin />
|
|
||||||
<CollapsiblePlugin />
|
<CollapsiblePlugin />
|
||||||
<TabIndentationPlugin />
|
<TabIndentationPlugin />
|
||||||
<RemoveBrokenTablesPlugin />
|
<RemoveBrokenTablesPlugin />
|
||||||
|
|||||||
@@ -20,22 +20,6 @@ import type {
|
|||||||
|
|
||||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||||
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
const WIDGET_SCRIPT_URL = '/dist/twitter-widgets.js'
|
|
||||||
|
|
||||||
type TweetComponentProps = Readonly<{
|
|
||||||
className: Readonly<{
|
|
||||||
base: string
|
|
||||||
focus: string
|
|
||||||
}>
|
|
||||||
format: ElementFormatType | null
|
|
||||||
loadingComponent?: JSX.Element | string
|
|
||||||
nodeKey: NodeKey
|
|
||||||
onError?: (error: string) => void
|
|
||||||
onLoad?: () => void
|
|
||||||
tweetID: string
|
|
||||||
}>
|
|
||||||
|
|
||||||
function convertTweetElement(domNode: HTMLDivElement): DOMConversionOutput | null {
|
function convertTweetElement(domNode: HTMLDivElement): DOMConversionOutput | null {
|
||||||
const id = domNode.getAttribute('data-lexical-tweet-id')
|
const id = domNode.getAttribute('data-lexical-tweet-id')
|
||||||
@@ -46,71 +30,6 @@ function convertTweetElement(domNode: HTMLDivElement): DOMConversionOutput | nul
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let isTwitterScriptLoading = true
|
|
||||||
|
|
||||||
function TweetComponent({
|
|
||||||
className,
|
|
||||||
format,
|
|
||||||
loadingComponent,
|
|
||||||
nodeKey,
|
|
||||||
onError,
|
|
||||||
onLoad,
|
|
||||||
tweetID,
|
|
||||||
}: TweetComponentProps) {
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
const previousTweetIDRef = useRef<string>('')
|
|
||||||
const [isTweetLoading, setIsTweetLoading] = useState(false)
|
|
||||||
|
|
||||||
const createTweet = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
// @ts-expect-error Twitter is attached to the window.
|
|
||||||
await window.twttr.widgets.createTweet(tweetID, containerRef.current)
|
|
||||||
|
|
||||||
setIsTweetLoading(false)
|
|
||||||
isTwitterScriptLoading = false
|
|
||||||
|
|
||||||
if (onLoad) {
|
|
||||||
onLoad()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (onError) {
|
|
||||||
onError(String(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [onError, onLoad, tweetID])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (tweetID !== previousTweetIDRef.current) {
|
|
||||||
setIsTweetLoading(true)
|
|
||||||
|
|
||||||
if (isTwitterScriptLoading) {
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.src = WIDGET_SCRIPT_URL
|
|
||||||
script.async = true
|
|
||||||
document.body?.appendChild(script)
|
|
||||||
script.onload = createTweet
|
|
||||||
if (onError) {
|
|
||||||
script.onerror = onError as OnErrorEventHandler
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createTweet().catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousTweetIDRef) {
|
|
||||||
previousTweetIDRef.current = tweetID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [createTweet, onError, tweetID])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
|
||||||
{isTweetLoading ? loadingComponent : null}
|
|
||||||
<div style={{ display: 'inline-block', width: '550px' }} ref={containerRef} />
|
|
||||||
</BlockWithAlignableContents>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedTweetNode = Spread<
|
export type SerializedTweetNode = Spread<
|
||||||
{
|
{
|
||||||
id: string
|
id: string
|
||||||
@@ -172,7 +91,7 @@ export class TweetNode extends DecoratorBlockNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getTextContent(_includeInert?: boolean | undefined, _includeDirectionless?: false | undefined): string {
|
override getTextContent(_includeInert?: boolean | undefined, _includeDirectionless?: false | undefined): string {
|
||||||
return `https://twitter.com/i/web/status/${this.__id}`
|
return `https://x.com/i/web/status/${this.__id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
override decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
override decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||||
@@ -181,14 +100,13 @@ export class TweetNode extends DecoratorBlockNode {
|
|||||||
base: embedBlockTheme.base || '',
|
base: embedBlockTheme.base || '',
|
||||||
focus: embedBlockTheme.focus || '',
|
focus: embedBlockTheme.focus || '',
|
||||||
}
|
}
|
||||||
|
const link = this.getTextContent()
|
||||||
return (
|
return (
|
||||||
<TweetComponent
|
<BlockWithAlignableContents className={className} format={this.__format} nodeKey={this.getKey()}>
|
||||||
className={className}
|
<a href={link} target="_blank" rel="noreferrer noopener">
|
||||||
format={this.__format}
|
{link}
|
||||||
loadingComponent="Loading..."
|
</a>
|
||||||
nodeKey={this.getKey()}
|
</BlockWithAlignableContents>
|
||||||
tweetID={this.__id}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,306 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { LexicalEditor } from 'lexical'
|
|
||||||
|
|
||||||
import {
|
|
||||||
AutoEmbedOption,
|
|
||||||
EmbedConfig,
|
|
||||||
EmbedMatchResult,
|
|
||||||
LexicalAutoEmbedPlugin,
|
|
||||||
URL_MATCHER,
|
|
||||||
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
|
||||||
import * as ReactDOM from 'react-dom'
|
|
||||||
|
|
||||||
import useModal from '../../Lexical/Hooks/useModal'
|
|
||||||
import { INSERT_TWEET_COMMAND } from '../TwitterPlugin'
|
|
||||||
import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin'
|
|
||||||
import { classNames } from '@standardnotes/snjs'
|
|
||||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
|
||||||
import Button from '@/Components/Button/Button'
|
|
||||||
import { isMobileScreen } from '../../../../Utils'
|
|
||||||
|
|
||||||
interface PlaygroundEmbedConfig extends EmbedConfig {
|
|
||||||
// Human readable name of the embeded content e.g. Tweet or Google Map.
|
|
||||||
contentName: string
|
|
||||||
|
|
||||||
// Icon for display.
|
|
||||||
icon?: JSX.Element
|
|
||||||
iconName: string
|
|
||||||
|
|
||||||
// An example of a matching url https://twitter.com/jack/status/20
|
|
||||||
exampleUrl: string
|
|
||||||
|
|
||||||
// For extra searching.
|
|
||||||
keywords: Array<string>
|
|
||||||
|
|
||||||
// Embed a Figma Project.
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
|
|
||||||
contentName: 'Youtube Video',
|
|
||||||
|
|
||||||
exampleUrl: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
|
||||||
|
|
||||||
// Icon for display.
|
|
||||||
icon: <i className="icon youtube" />,
|
|
||||||
iconName: 'youtube',
|
|
||||||
|
|
||||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
|
||||||
editor.dispatchCommand(INSERT_YOUTUBE_COMMAND, result.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
keywords: ['youtube', 'video'],
|
|
||||||
|
|
||||||
// Determine if a given URL is a match and return url data.
|
|
||||||
parseUrl: (url: string) => {
|
|
||||||
const match = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/.exec(url)
|
|
||||||
|
|
||||||
const id = match ? (match?.[2].length === 11 ? match[2] : null) : null
|
|
||||||
|
|
||||||
if (id != null) {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
type: 'youtube-video',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
|
|
||||||
// e.g. Tweet or Google Map.
|
|
||||||
contentName: 'Tweet',
|
|
||||||
|
|
||||||
exampleUrl: 'https://twitter.com/jack/status/20',
|
|
||||||
|
|
||||||
// Icon for display.
|
|
||||||
icon: <i className="icon tweet" />,
|
|
||||||
iconName: 'tweet',
|
|
||||||
|
|
||||||
// Create the Lexical embed node from the url data.
|
|
||||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
|
||||||
editor.dispatchCommand(INSERT_TWEET_COMMAND, result.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
// For extra searching.
|
|
||||||
keywords: ['tweet', 'twitter'],
|
|
||||||
|
|
||||||
// Determine if a given URL is a match and return url data.
|
|
||||||
parseUrl: (text: string) => {
|
|
||||||
const match = /^https:\/\/twitter\.com\/(#!\/)?(\w+)\/status(es)*\/(\d+)$/.exec(text)
|
|
||||||
|
|
||||||
if (match != null) {
|
|
||||||
return {
|
|
||||||
id: match[4],
|
|
||||||
url: match[0],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
type: 'tweet',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmbedConfigs = [TwitterEmbedConfig, YoutubeEmbedConfig]
|
|
||||||
|
|
||||||
function AutoEmbedMenuItem({
|
|
||||||
index,
|
|
||||||
isSelected,
|
|
||||||
onClick,
|
|
||||||
onMouseEnter,
|
|
||||||
option,
|
|
||||||
}: {
|
|
||||||
index: number
|
|
||||||
isSelected: boolean
|
|
||||||
onClick: () => void
|
|
||||||
onMouseEnter: () => void
|
|
||||||
option: AutoEmbedOption
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={option.key}
|
|
||||||
tabIndex={-1}
|
|
||||||
className={classNames('cursor-pointer rounded px-2 py-1', isSelected && 'bg-info-backdrop')}
|
|
||||||
ref={option.setRefElement}
|
|
||||||
role="option"
|
|
||||||
aria-selected={isSelected}
|
|
||||||
id={'typeahead-item-' + index}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<span className="text">{option.title}</span>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AutoEmbedMenu({
|
|
||||||
options,
|
|
||||||
selectedItemIndex,
|
|
||||||
onOptionClick,
|
|
||||||
onOptionMouseEnter,
|
|
||||||
}: {
|
|
||||||
selectedItemIndex: number | null
|
|
||||||
onOptionClick: (option: AutoEmbedOption, index: number) => void
|
|
||||||
onOptionMouseEnter: (index: number) => void
|
|
||||||
options: Array<AutoEmbedOption>
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="typeahead-popover min-w-max rounded border border-border bg-default p-1">
|
|
||||||
<ul className="list-none">
|
|
||||||
{options.map((option: AutoEmbedOption, i: number) => (
|
|
||||||
<AutoEmbedMenuItem
|
|
||||||
index={i}
|
|
||||||
isSelected={selectedItemIndex === i}
|
|
||||||
onClick={() => onOptionClick(option, i)}
|
|
||||||
onMouseEnter={() => onOptionMouseEnter(i)}
|
|
||||||
key={option.key}
|
|
||||||
option={option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const debounce = (callback: (text: string) => void, delay: number) => {
|
|
||||||
let timeoutId: number
|
|
||||||
|
|
||||||
return (text: string) => {
|
|
||||||
window.clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = window.setTimeout(() => {
|
|
||||||
callback(text)
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AutoEmbedDialog({
|
|
||||||
embedConfig,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
embedConfig: PlaygroundEmbedConfig
|
|
||||||
onClose: () => void
|
|
||||||
}): JSX.Element {
|
|
||||||
const [text, setText] = useState('')
|
|
||||||
const [editor] = useLexicalComposerContext()
|
|
||||||
const [embedResult, setEmbedResult] = useState<EmbedMatchResult | null>(null)
|
|
||||||
|
|
||||||
const validateText = useMemo(
|
|
||||||
() =>
|
|
||||||
debounce((inputText: string) => {
|
|
||||||
const urlMatch = URL_MATCHER.exec(inputText)
|
|
||||||
if (embedConfig != null && inputText != null && urlMatch != null) {
|
|
||||||
void Promise.resolve(embedConfig.parseUrl(inputText)).then((parseResult) => {
|
|
||||||
setEmbedResult(parseResult)
|
|
||||||
})
|
|
||||||
} else if (embedResult != null) {
|
|
||||||
setEmbedResult(null)
|
|
||||||
}
|
|
||||||
}, 200),
|
|
||||||
[embedConfig, embedResult],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
if (embedResult != null) {
|
|
||||||
embedConfig.insertNode(editor, embedResult)
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
|
|
||||||
if (element) {
|
|
||||||
setTimeout(() => element.focus())
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="flex flex-col gap-1.5">
|
|
||||||
URL:
|
|
||||||
<DecoratedInput
|
|
||||||
value={text}
|
|
||||||
onChange={(text) => {
|
|
||||||
setText(text)
|
|
||||||
validateText(text)
|
|
||||||
}}
|
|
||||||
ref={focusOnMount}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="mt-2.5 flex justify-end">
|
|
||||||
<Button disabled={!embedResult} onClick={onClick} small={isMobileScreen()}>
|
|
||||||
Embed
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AutoEmbedPlugin(): JSX.Element {
|
|
||||||
const [modal, showModal] = useModal()
|
|
||||||
|
|
||||||
const openEmbedModal = (embedConfig: PlaygroundEmbedConfig) => {
|
|
||||||
showModal(`Embed ${embedConfig.contentName}`, (onClose) => (
|
|
||||||
<AutoEmbedDialog embedConfig={embedConfig} onClose={onClose} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMenuOptions = (activeEmbedConfig: PlaygroundEmbedConfig, embedFn: () => void, dismissFn: () => void) => {
|
|
||||||
return [
|
|
||||||
new AutoEmbedOption('Dismiss', {
|
|
||||||
onSelect: dismissFn,
|
|
||||||
}),
|
|
||||||
new AutoEmbedOption(`Embed ${activeEmbedConfig.contentName}`, {
|
|
||||||
onSelect: embedFn,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{modal}
|
|
||||||
<LexicalAutoEmbedPlugin<PlaygroundEmbedConfig>
|
|
||||||
embedConfigs={EmbedConfigs}
|
|
||||||
onOpenEmbedModalForConfig={openEmbedModal}
|
|
||||||
getMenuOptions={getMenuOptions}
|
|
||||||
menuRenderFn={(anchorElementRef, { selectedIndex, options, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
|
||||||
return anchorElementRef.current
|
|
||||||
? ReactDOM.createPortal(
|
|
||||||
<div
|
|
||||||
className="typeahead-popover auto-embed-menu"
|
|
||||||
style={{
|
|
||||||
marginLeft: anchorElementRef.current.style.width,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AutoEmbedMenu
|
|
||||||
options={options}
|
|
||||||
selectedItemIndex={selectedIndex}
|
|
||||||
onOptionClick={(option: AutoEmbedOption, index: number) => {
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
selectOptionAndCleanUp(option)
|
|
||||||
}}
|
|
||||||
onOptionMouseEnter={(index: number) => {
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>,
|
|
||||||
anchorElementRef.current,
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,6 @@ import { GetCodeBlockOption } from '../Blocks/Code'
|
|||||||
import { GetQuoteBlockOption } from '../Blocks/Quote'
|
import { GetQuoteBlockOption } from '../Blocks/Quote'
|
||||||
import { GetDividerBlockOption } from '../Blocks/Divider'
|
import { GetDividerBlockOption } from '../Blocks/Divider'
|
||||||
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
|
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
|
||||||
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
|
|
||||||
import { GetUploadFileOption } from '../Blocks/File'
|
import { GetUploadFileOption } from '../Blocks/File'
|
||||||
|
|
||||||
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
|
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
|
||||||
@@ -72,7 +71,6 @@ export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex
|
|||||||
GetJustifyAlignBlockOption(editor),
|
GetJustifyAlignBlockOption(editor),
|
||||||
GetPasswordBlockOption(editor),
|
GetPasswordBlockOption(editor),
|
||||||
GetCollapsibleBlockOption(editor),
|
GetCollapsibleBlockOption(editor),
|
||||||
...GetEmbedsBlockOptions(editor),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const dynamicOptions = [
|
const dynamicOptions = [
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { LexicalEditor } from 'lexical'
|
|
||||||
import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'
|
|
||||||
import { EmbedConfigs } from '../AutoEmbedPlugin'
|
|
||||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
|
||||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
|
||||||
|
|
||||||
export function GetEmbedsBlocks(editor: LexicalEditor) {
|
|
||||||
return EmbedConfigs.map((embedConfig) => ({
|
|
||||||
name: `Embed ${embedConfig.contentName}`,
|
|
||||||
iconName: embedConfig.iconName as LexicalIconName,
|
|
||||||
keywords: [...embedConfig.keywords, 'embed'],
|
|
||||||
onSelect: () => editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GetEmbedsBlockOptions(editor: LexicalEditor) {
|
|
||||||
return GetEmbedsBlocks(editor).map(
|
|
||||||
(block) =>
|
|
||||||
new BlockPickerOption(block.name, {
|
|
||||||
iconName: block.iconName as LexicalIconName,
|
|
||||||
keywords: block.keywords,
|
|
||||||
onSelect: block.onSelect,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { $insertNodeToNearestRoot } from '@lexical/utils'
|
|
||||||
import { COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand } from 'lexical'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import { $createTweetNode, TweetNode } from '../../Lexical/Nodes/TweetNode'
|
|
||||||
|
|
||||||
export const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand('INSERT_TWEET_COMMAND')
|
|
||||||
|
|
||||||
export default function TwitterPlugin(): JSX.Element | null {
|
|
||||||
const [editor] = useLexicalComposerContext()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor.hasNodes([TweetNode])) {
|
|
||||||
throw new Error('TwitterPlugin: TweetNode not registered on editor')
|
|
||||||
}
|
|
||||||
|
|
||||||
return editor.registerCommand<string>(
|
|
||||||
INSERT_TWEET_COMMAND,
|
|
||||||
(payload) => {
|
|
||||||
const tweetNode = $createTweetNode(payload)
|
|
||||||
$insertNodeToNearestRoot(tweetNode)
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { $insertNodeToNearestRoot } from '@lexical/utils'
|
|
||||||
import { COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand } from 'lexical'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import { $createYouTubeNode, YouTubeNode } from '../../Lexical/Nodes/YouTubeNode'
|
|
||||||
|
|
||||||
export const INSERT_YOUTUBE_COMMAND: LexicalCommand<string> = createCommand('INSERT_YOUTUBE_COMMAND')
|
|
||||||
|
|
||||||
export default function YouTubePlugin(): JSX.Element | null {
|
|
||||||
const [editor] = useLexicalComposerContext()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor.hasNodes([YouTubeNode])) {
|
|
||||||
throw new Error('YouTubePlugin: YouTubeNode not registered on editor')
|
|
||||||
}
|
|
||||||
|
|
||||||
return editor.registerCommand<string>(
|
|
||||||
INSERT_YOUTUBE_COMMAND,
|
|
||||||
(payload) => {
|
|
||||||
const youTubeNode = $createYouTubeNode(payload)
|
|
||||||
$insertNodeToNearestRoot(youTubeNode)
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
8
packages/web/src/vendor/twitter-widgets.js
vendored
8
packages/web/src/vendor/twitter-widgets.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user