refactor: blocks plugins (#1956)
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import {FunctionComponent, useCallback, useState} from 'react';
|
||||
import {LexicalComposer} from '@lexical/react/LexicalComposer';
|
||||
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
||||
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
|
||||
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
|
||||
@@ -20,27 +19,25 @@ import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
|
||||
import {LinkPlugin} from '@lexical/react/LexicalLinkPlugin';
|
||||
import {ListPlugin} from '@lexical/react/LexicalListPlugin';
|
||||
import {EditorState, LexicalEditor} from 'lexical';
|
||||
|
||||
import ComponentPickerMenuPlugin from '../Lexical/Plugins/ComponentPickerPlugin';
|
||||
import BlocksEditorTheme from '../Lexical/Theme/Theme';
|
||||
import HorizontalRulePlugin from '../Lexical/Plugins/HorizontalRulePlugin';
|
||||
import TwitterPlugin from '../Lexical/Plugins/TwitterPlugin';
|
||||
import YouTubePlugin from '../Lexical/Plugins/YouTubePlugin';
|
||||
import AutoEmbedPlugin from '../Lexical/Plugins/AutoEmbedPlugin';
|
||||
import CollapsiblePlugin from '../Lexical/Plugins/CollapsiblePlugin';
|
||||
import {BlockEditorNodes} from '../Lexical/Nodes/AllNodes';
|
||||
// import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin';
|
||||
import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin';
|
||||
|
||||
const BlockDragEnabled = false;
|
||||
|
||||
type BlocksEditorProps = {
|
||||
initialValue: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
initialValue,
|
||||
onChange,
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
const handleChange = useCallback(
|
||||
(editorState: EditorState, _editor: LexicalEditor) => {
|
||||
@@ -60,56 +57,46 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<LexicalComposer
|
||||
initialConfig={{
|
||||
namespace: 'BlocksEditor',
|
||||
theme: BlocksEditorTheme,
|
||||
onError: (error: Error) => console.error(error),
|
||||
editorState:
|
||||
initialValue && initialValue.length > 0 ? initialValue : undefined,
|
||||
nodes: BlockEditorNodes,
|
||||
}}>
|
||||
<>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<div id="blocks-editor" className="editor-scroller">
|
||||
<div className="editor" ref={onRef}>
|
||||
<ContentEditable
|
||||
className={`ContentEditable__root ${className}`}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{children}
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<div id="blocks-editor" className="editor-scroller">
|
||||
<div className="editor" ref={onRef}>
|
||||
<ContentEditable
|
||||
className={`ContentEditable__root ${className}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
placeholder=""
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<ListPlugin />
|
||||
<MarkdownShortcutPlugin
|
||||
transformers={[
|
||||
CHECK_LIST,
|
||||
...ELEMENT_TRANSFORMERS,
|
||||
...TEXT_FORMAT_TRANSFORMERS,
|
||||
...TEXT_MATCH_TRANSFORMERS,
|
||||
]}
|
||||
/>
|
||||
<TablePlugin />
|
||||
<OnChangePlugin onChange={handleChange} />
|
||||
<HistoryPlugin />
|
||||
<HorizontalRulePlugin />
|
||||
<AutoFocusPlugin />
|
||||
<ComponentPickerMenuPlugin />
|
||||
<ClearEditorPlugin />
|
||||
<CheckListPlugin />
|
||||
<LinkPlugin />
|
||||
<HashtagPlugin />
|
||||
<AutoEmbedPlugin />
|
||||
<TwitterPlugin />
|
||||
<YouTubePlugin />
|
||||
<CollapsiblePlugin />
|
||||
{floatingAnchorElem && (
|
||||
<>{/* <DraggableBlockPlugin anchorElem={floatingAnchorElem} /> */}</>
|
||||
)}
|
||||
</>
|
||||
</LexicalComposer>
|
||||
</div>
|
||||
}
|
||||
placeholder=""
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<ListPlugin />
|
||||
<MarkdownShortcutPlugin
|
||||
transformers={[
|
||||
CHECK_LIST,
|
||||
...ELEMENT_TRANSFORMERS,
|
||||
...TEXT_FORMAT_TRANSFORMERS,
|
||||
...TEXT_MATCH_TRANSFORMERS,
|
||||
]}
|
||||
/>
|
||||
<TablePlugin />
|
||||
<OnChangePlugin onChange={handleChange} />
|
||||
<HistoryPlugin />
|
||||
<HorizontalRulePlugin />
|
||||
<AutoFocusPlugin />
|
||||
<ClearEditorPlugin />
|
||||
<CheckListPlugin />
|
||||
<LinkPlugin />
|
||||
<HashtagPlugin />
|
||||
<AutoEmbedPlugin />
|
||||
<TwitterPlugin />
|
||||
<YouTubePlugin />
|
||||
<CollapsiblePlugin />
|
||||
{floatingAnchorElem && BlockDragEnabled && (
|
||||
<>{<DraggableBlockPlugin anchorElem={floatingAnchorElem} />}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
29
packages/blocks-editor/src/Editor/BlocksEditorComposer.tsx
Normal file
29
packages/blocks-editor/src/Editor/BlocksEditorComposer.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import {FunctionComponent} from 'react';
|
||||
import {LexicalComposer} from '@lexical/react/LexicalComposer';
|
||||
import BlocksEditorTheme from '../Lexical/Theme/Theme';
|
||||
import {BlockEditorNodes} from '../Lexical/Nodes/AllNodes';
|
||||
import {Klass, LexicalNode} from 'lexical';
|
||||
|
||||
type BlocksEditorComposerProps = {
|
||||
initialValue: string;
|
||||
children: React.ReactNode;
|
||||
nodes: Array<Klass<LexicalNode>>;
|
||||
};
|
||||
|
||||
export const BlocksEditorComposer: FunctionComponent<
|
||||
BlocksEditorComposerProps
|
||||
> = ({initialValue, children, nodes}) => {
|
||||
return (
|
||||
<LexicalComposer
|
||||
initialConfig={{
|
||||
namespace: 'BlocksEditor',
|
||||
theme: BlocksEditorTheme,
|
||||
onError: (error: Error) => console.error(error),
|
||||
editorState:
|
||||
initialValue && initialValue.length > 0 ? initialValue : undefined,
|
||||
nodes: [...nodes, ...BlockEditorNodes],
|
||||
}}>
|
||||
<>{children}</>
|
||||
</LexicalComposer>
|
||||
);
|
||||
};
|
||||
19
packages/blocks-editor/src/Editor/ClassNames.ts
Normal file
19
packages/blocks-editor/src/Editor/ClassNames.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
const classNames = (...values: (string | boolean | undefined)[]): string => {
|
||||
return values
|
||||
.map((value) => (typeof value === 'string' ? value : null))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
export const PopoverClassNames = classNames(
|
||||
'typeahead-popover file-picker-menu absolute z-dropdown-menu flex w-full min-w-80',
|
||||
'cursor-auto flex-col overflow-y-auto rounded bg-default md:h-auto md:max-w-xs h-auto overflow-y-scroll',
|
||||
);
|
||||
|
||||
export const PopoverItemClassNames = classNames(
|
||||
'flex w-full items-center text-base gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground',
|
||||
'focus:bg-info-backdrop cursor-pointer m-0 focus:bg-contrast focus:text-foreground',
|
||||
);
|
||||
|
||||
export const PopoverItemSelectedClassNames = classNames(
|
||||
'bg-contrast text-foreground',
|
||||
);
|
||||
5
packages/blocks-editor/src/Editor/Commands.ts
Normal file
5
packages/blocks-editor/src/Editor/Commands.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {createCommand, LexicalCommand} from 'lexical';
|
||||
|
||||
export const INSERT_FILE_COMMAND: LexicalCommand<string> = createCommand(
|
||||
'INSERT_FILE_COMMAND',
|
||||
);
|
||||
@@ -31,6 +31,7 @@ interface PlaygroundEmbedConfig extends EmbedConfig {
|
||||
|
||||
// Icon for display.
|
||||
icon?: JSX.Element;
|
||||
iconName: string;
|
||||
|
||||
// An example of a matching url https://twitter.com/jack/status/20
|
||||
exampleUrl: string;
|
||||
@@ -49,6 +50,7 @@ export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
|
||||
|
||||
// Icon for display.
|
||||
icon: <i className="icon youtube" />,
|
||||
iconName: 'youtube',
|
||||
|
||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
||||
editor.dispatchCommand(INSERT_YOUTUBE_COMMAND, result.id);
|
||||
@@ -84,6 +86,7 @@ export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
|
||||
|
||||
// Icon for display.
|
||||
icon: <i className="icon tweet" />,
|
||||
iconName: 'tweet',
|
||||
|
||||
// Create the Lexical embed node from the url data.
|
||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
||||
|
||||
@@ -1,357 +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 {$createCodeNode} from '@lexical/code';
|
||||
import {
|
||||
INSERT_CHECK_LIST_COMMAND,
|
||||
INSERT_ORDERED_LIST_COMMAND,
|
||||
INSERT_UNORDERED_LIST_COMMAND,
|
||||
} from '@lexical/list';
|
||||
import {INSERT_EMBED_COMMAND} from '@lexical/react/LexicalAutoEmbedPlugin';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {INSERT_HORIZONTAL_RULE_COMMAND} from '@lexical/react/LexicalHorizontalRuleNode';
|
||||
import {
|
||||
LexicalTypeaheadMenuPlugin,
|
||||
TypeaheadOption,
|
||||
useBasicTypeaheadTriggerMatch,
|
||||
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
|
||||
import {$createHeadingNode, $createQuoteNode} from '@lexical/rich-text';
|
||||
import {$wrapNodes} from '@lexical/selection';
|
||||
import {INSERT_TABLE_COMMAND} from '@lexical/table';
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
FORMAT_ELEMENT_COMMAND,
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
import {useCallback, useMemo, useState} from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import useModal from '../../Hooks/useModal';
|
||||
import {EmbedConfigs} from '../AutoEmbedPlugin';
|
||||
import {INSERT_COLLAPSIBLE_COMMAND} from '../CollapsiblePlugin';
|
||||
import {InsertTableDialog} from '../TablePlugin';
|
||||
|
||||
class ComponentPickerOption extends TypeaheadOption {
|
||||
// What shows up in the editor
|
||||
title: string;
|
||||
// Icon for display
|
||||
icon?: JSX.Element;
|
||||
// For extra searching.
|
||||
keywords: Array<string>;
|
||||
// TBD
|
||||
keyboardShortcut?: string;
|
||||
// What happens when you select this option?
|
||||
onSelect: (queryString: string) => void;
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
options: {
|
||||
icon?: JSX.Element;
|
||||
keywords?: Array<string>;
|
||||
keyboardShortcut?: string;
|
||||
onSelect: (queryString: string) => void;
|
||||
},
|
||||
) {
|
||||
super(title);
|
||||
this.title = title;
|
||||
this.keywords = options.keywords || [];
|
||||
this.icon = options.icon;
|
||||
this.keyboardShortcut = options.keyboardShortcut;
|
||||
this.onSelect = options.onSelect.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
function ComponentPickerMenuItem({
|
||||
index,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
}: {
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
onMouseEnter: () => void;
|
||||
option: ComponentPickerOption;
|
||||
}) {
|
||||
let className = 'item';
|
||||
if (isSelected) {
|
||||
className += ' selected';
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={option.key}
|
||||
tabIndex={-1}
|
||||
className={className}
|
||||
ref={option.setRefElement}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={'typeahead-item-' + index}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}>
|
||||
{option.icon}
|
||||
<span className="text">{option.title}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ComponentPickerMenuPlugin(): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [modal, showModal] = useModal();
|
||||
const [queryString, setQueryString] = useState<string | null>(null);
|
||||
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||
minLength: 0,
|
||||
});
|
||||
|
||||
const getDynamicOptions = useCallback(() => {
|
||||
const options: Array<ComponentPickerOption> = [];
|
||||
|
||||
if (queryString == null) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const fullTableRegex = new RegExp(/^([1-9]|10)x([1-9]|10)$/);
|
||||
const partialTableRegex = new RegExp(/^([1-9]|10)x?$/);
|
||||
|
||||
const fullTableMatch = fullTableRegex.exec(queryString);
|
||||
const partialTableMatch = partialTableRegex.exec(queryString);
|
||||
|
||||
if (fullTableMatch) {
|
||||
const [rows, columns] = fullTableMatch[0]
|
||||
.split('x')
|
||||
.map((n: string) => parseInt(n, 10));
|
||||
|
||||
options.push(
|
||||
new ComponentPickerOption(`${rows}x${columns} Table`, {
|
||||
icon: <i className="icon table" />,
|
||||
keywords: ['table'],
|
||||
onSelect: () =>
|
||||
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
||||
editor.dispatchCommand(INSERT_TABLE_COMMAND, {columns, rows}),
|
||||
}),
|
||||
);
|
||||
} else if (partialTableMatch) {
|
||||
const rows = parseInt(partialTableMatch[0], 10);
|
||||
|
||||
options.push(
|
||||
...Array.from({length: 5}, (_, i) => i + 1).map(
|
||||
(columns) =>
|
||||
new ComponentPickerOption(`${rows}x${columns} Table`, {
|
||||
icon: <i className="icon table" />,
|
||||
keywords: ['table'],
|
||||
onSelect: () =>
|
||||
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
||||
editor.dispatchCommand(INSERT_TABLE_COMMAND, {columns, rows}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [editor, queryString]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const baseOptions = [
|
||||
new ComponentPickerOption('Paragraph', {
|
||||
icon: <i className="icon paragraph" />,
|
||||
keywords: ['normal', 'paragraph', 'p', 'text'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createParagraphNode());
|
||||
}
|
||||
}),
|
||||
}),
|
||||
...Array.from({length: 3}, (_, i) => i + 1).map(
|
||||
(n) =>
|
||||
new ComponentPickerOption(`Heading ${n}`, {
|
||||
icon: <i className={`icon h${n}`} />,
|
||||
keywords: ['heading', 'header', `h${n}`],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () =>
|
||||
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
||||
$createHeadingNode(`h${n}`),
|
||||
);
|
||||
}
|
||||
}),
|
||||
}),
|
||||
),
|
||||
new ComponentPickerOption('Table', {
|
||||
icon: <i className="icon table" />,
|
||||
keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'],
|
||||
onSelect: () =>
|
||||
showModal('Insert Table', (onClose) => (
|
||||
<InsertTableDialog activeEditor={editor} onClose={onClose} />
|
||||
)),
|
||||
}),
|
||||
new ComponentPickerOption('Numbered List', {
|
||||
icon: <i className="icon number" />,
|
||||
keywords: ['numbered list', 'ordered list', 'ol'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
|
||||
}),
|
||||
new ComponentPickerOption('Bulleted List', {
|
||||
icon: <i className="icon bullet" />,
|
||||
keywords: ['bulleted list', 'unordered list', 'ul'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
|
||||
}),
|
||||
new ComponentPickerOption('Check List', {
|
||||
icon: <i className="icon check" />,
|
||||
keywords: ['check list', 'todo list'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
|
||||
}),
|
||||
new ComponentPickerOption('Quote', {
|
||||
icon: <i className="icon quote" />,
|
||||
keywords: ['block quote'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createQuoteNode());
|
||||
}
|
||||
}),
|
||||
}),
|
||||
new ComponentPickerOption('Code', {
|
||||
icon: <i className="icon code" />,
|
||||
keywords: ['javascript', 'python', 'js', 'codeblock'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
if (selection.isCollapsed()) {
|
||||
$wrapNodes(selection, () => $createCodeNode());
|
||||
} else {
|
||||
// Will this ever happen?
|
||||
const textContent = selection.getTextContent();
|
||||
const codeNode = $createCodeNode();
|
||||
selection.insertNodes([codeNode]);
|
||||
selection.insertRawText(textContent);
|
||||
}
|
||||
}
|
||||
}),
|
||||
}),
|
||||
new ComponentPickerOption('Divider', {
|
||||
icon: <i className="icon horizontal-rule" />,
|
||||
keywords: ['horizontal rule', 'divider', 'hr'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
|
||||
}),
|
||||
...EmbedConfigs.map(
|
||||
(embedConfig) =>
|
||||
new ComponentPickerOption(`Embed ${embedConfig.contentName}`, {
|
||||
icon: embedConfig.icon,
|
||||
keywords: [...embedConfig.keywords, 'embed'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
|
||||
}),
|
||||
),
|
||||
|
||||
new ComponentPickerOption('Collapsible', {
|
||||
icon: <i className="icon caret-right" />,
|
||||
keywords: ['collapse', 'collapsible', 'toggle'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined),
|
||||
}),
|
||||
...['left', 'center', 'right', 'justify'].map(
|
||||
(alignment) =>
|
||||
new ComponentPickerOption(`Align ${alignment}`, {
|
||||
icon: <i className={`icon ${alignment}-align`} />,
|
||||
keywords: ['align', 'justify', alignment],
|
||||
onSelect: () =>
|
||||
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment),
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
const dynamicOptions = getDynamicOptions();
|
||||
|
||||
return queryString
|
||||
? [
|
||||
...dynamicOptions,
|
||||
...baseOptions.filter((option) => {
|
||||
return new RegExp(queryString, 'gi').exec(option.title) ||
|
||||
option.keywords != null
|
||||
? option.keywords.some((keyword) =>
|
||||
new RegExp(queryString, 'gi').exec(keyword),
|
||||
)
|
||||
: false;
|
||||
}),
|
||||
]
|
||||
: baseOptions;
|
||||
}, [editor, getDynamicOptions, queryString, showModal]);
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
selectedOption: ComponentPickerOption,
|
||||
nodeToRemove: TextNode | null,
|
||||
closeMenu: () => void,
|
||||
matchingString: string,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
if (nodeToRemove) {
|
||||
nodeToRemove.remove();
|
||||
}
|
||||
selectedOption.onSelect(matchingString);
|
||||
closeMenu();
|
||||
});
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
<LexicalTypeaheadMenuPlugin<ComponentPickerOption>
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
options={options}
|
||||
menuRenderFn={(
|
||||
anchorElementRef,
|
||||
{selectedIndex, selectOptionAndCleanUp, setHighlightedIndex},
|
||||
) =>
|
||||
anchorElementRef.current && options.length
|
||||
? ReactDOM.createPortal(
|
||||
<div className="typeahead-popover component-picker-menu">
|
||||
<ul>
|
||||
{options.map((option, i: number) => (
|
||||
<ComponentPickerMenuItem
|
||||
index={i}
|
||||
isSelected={selectedIndex === i}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(i);
|
||||
selectOptionAndCleanUp(option);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(i);
|
||||
}}
|
||||
key={option.key}
|
||||
option={option}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
.typeahead-popover {
|
||||
background: #fff;
|
||||
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.typeahead-popover ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
border-radius: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.typeahead-popover ul::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.typeahead-popover ul {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.typeahead-popover ul li {
|
||||
margin: 0;
|
||||
min-width: 180px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.typeahead-popover ul li.selected {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.typeahead-popover li {
|
||||
margin: 0 8px 0 8px;
|
||||
padding: 8px;
|
||||
color: #050505;
|
||||
cursor: pointer;
|
||||
line-height: 16px;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.typeahead-popover li.active {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.typeahead-popover li:first-child {
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
}
|
||||
|
||||
.typeahead-popover li:last-child {
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
}
|
||||
|
||||
.typeahead-popover li:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.typeahead-popover li .text {
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
flex-grow: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.typeahead-popover li .icon {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
user-select: none;
|
||||
margin-right: 8px;
|
||||
line-height: 16px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.component-picker-menu {
|
||||
width: 200px;
|
||||
}
|
||||
@@ -21,13 +21,13 @@
|
||||
}
|
||||
.Lexical__h1 {
|
||||
font-size: 24px;
|
||||
color: rgb(5, 5, 5);
|
||||
color: var(--sn-stylekit-editor-foreground-color);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
.Lexical__h2 {
|
||||
font-size: 15px;
|
||||
color: rgb(101, 103, 107);
|
||||
color: var(--sn-stylekit-editor-foreground-color);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import 'base';
|
||||
@import 'component_picker';
|
||||
@import 'custom';
|
||||
@import 'editor';
|
||||
@import 'icons';
|
||||
@@ -1 +1,4 @@
|
||||
export * from './Editor/BlocksEditor';
|
||||
export * from './Editor/BlocksEditorComposer';
|
||||
export * from './Editor/Commands';
|
||||
export * from './Editor/ClassNames';
|
||||
|
||||
Reference in New Issue
Block a user