mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 03:29:03 +00:00
chore(ui): block editor improvement part2 (#13362)
* chore(ui): block editor improvement part2 * chore: update placeholder logic for editor
This commit is contained in:
parent
63ac994371
commit
dbe545d434
Binary file not shown.
|
After Width: | Height: | Size: 533 B |
@ -12,19 +12,14 @@
|
||||
*/
|
||||
import { Editor } from '@tiptap/core';
|
||||
import { BubbleMenu as CoreBubbleMenu } from '@tiptap/react';
|
||||
import { Button, Dropdown, Tooltip, Typography } from 'antd';
|
||||
import { TitleProps } from 'antd/lib/typography/Title';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { isString } from 'lodash';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { ReactComponent as FormatBoldIcon } from '../../../assets/svg/ic-format-bold.svg';
|
||||
import { ReactComponent as FormatBulletListIcon } from '../../../assets/svg/ic-format-bullet-list.svg';
|
||||
import { ReactComponent as FormatCodeBlockIcon } from '../../../assets/svg/ic-format-code-block.svg';
|
||||
import { ReactComponent as FormatDividerIcon } from '../../../assets/svg/ic-format-divider.svg';
|
||||
import { ReactComponent as FormatInlineCodeIcon } from '../../../assets/svg/ic-format-inline-code.svg';
|
||||
import { ReactComponent as FormatItalicIcon } from '../../../assets/svg/ic-format-italic.svg';
|
||||
import { ReactComponent as FormatLinkIcon } from '../../../assets/svg/ic-format-link.svg';
|
||||
import { ReactComponent as FormatNumberListIcon } from '../../../assets/svg/ic-format-numbered-list.svg';
|
||||
import { ReactComponent as FormatQuoteIcon } from '../../../assets/svg/ic-format-quote.svg';
|
||||
import { ReactComponent as FormatStrikeIcon } from '../../../assets/svg/ic-format-strike.svg';
|
||||
|
||||
interface BubbleMenuProps {
|
||||
@ -33,8 +28,44 @@ interface BubbleMenuProps {
|
||||
}
|
||||
|
||||
const BubbleMenu: FC<BubbleMenuProps> = ({ editor, toggleLink }) => {
|
||||
const { menuList, headings } = useMemo(() => {
|
||||
const { menuList } = useMemo(() => {
|
||||
const menuList = [
|
||||
{
|
||||
ariaLabel: 'Heading 1',
|
||||
className: 'm-x-xs',
|
||||
disabled: !editor
|
||||
.can()
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleHeading({ level: 1 })
|
||||
.run(),
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
icon: 'H1',
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Heading 2',
|
||||
className: 'm-r-xs',
|
||||
disabled: !editor
|
||||
.can()
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleHeading({ level: 2 })
|
||||
.run(),
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
icon: 'H2',
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Heading 3',
|
||||
className: '',
|
||||
disabled: !editor
|
||||
.can()
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleHeading({ level: 3 })
|
||||
.run(),
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
icon: 'H3',
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Bold',
|
||||
className: editor.isActive('bold') ? 'is-active' : '',
|
||||
@ -57,7 +88,7 @@ const BubbleMenu: FC<BubbleMenuProps> = ({ editor, toggleLink }) => {
|
||||
icon: FormatStrikeIcon,
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Code',
|
||||
ariaLabel: 'Inline code',
|
||||
className: editor.isActive('code') ? 'is-active' : '',
|
||||
disabled: !editor.can().chain().focus().toggleCode().run(),
|
||||
onClick: () => editor.chain().focus().toggleCode().run(),
|
||||
@ -70,84 +101,13 @@ const BubbleMenu: FC<BubbleMenuProps> = ({ editor, toggleLink }) => {
|
||||
onClick: () => toggleLink(),
|
||||
icon: FormatLinkIcon,
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Bullet List',
|
||||
className: editor.isActive('bulletList') ? 'is-active' : '',
|
||||
disabled: false,
|
||||
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
||||
icon: FormatBulletListIcon,
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Ordered List',
|
||||
className: editor.isActive('orderedList') ? 'is-active' : '',
|
||||
disabled: false,
|
||||
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
||||
icon: FormatNumberListIcon,
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Code block',
|
||||
className: editor.isActive('codeBlock') ? 'is-active' : '',
|
||||
disabled: false,
|
||||
onClick: () => editor.chain().focus().toggleCodeBlock().run(),
|
||||
icon: FormatCodeBlockIcon,
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Blockquote',
|
||||
className: editor.isActive('blockquote') ? 'is-active' : '',
|
||||
disabled: false,
|
||||
onClick: () => editor.chain().focus().toggleBlockquote().run(),
|
||||
icon: FormatQuoteIcon,
|
||||
},
|
||||
{
|
||||
ariaLabel: 'Horizontal Line',
|
||||
className: '',
|
||||
disabled: false,
|
||||
onClick: () => editor.chain().focus().setHorizontalRule().run(),
|
||||
icon: FormatDividerIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const headings = [
|
||||
{
|
||||
label: 'Heading 1',
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
label: 'Heading 2',
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
label: 'Heading 3',
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
level: 3,
|
||||
},
|
||||
];
|
||||
|
||||
return { menuList, headings };
|
||||
return { menuList };
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<CoreBubbleMenu className="menu-wrapper" editor={editor}>
|
||||
<Dropdown.Button
|
||||
className="headings-dropdown"
|
||||
icon={<i>H</i>}
|
||||
menu={{
|
||||
items: headings.map(({ label, level, onClick }) => ({
|
||||
key: label,
|
||||
icon: (
|
||||
<Typography.Title
|
||||
className="m-b-0"
|
||||
level={(level + 2) as TitleProps['level']}>
|
||||
{label}
|
||||
</Typography.Title>
|
||||
),
|
||||
onClick,
|
||||
})),
|
||||
}}
|
||||
type="text"
|
||||
/>
|
||||
{menuList.map(
|
||||
({ icon: Icon, ariaLabel, className, disabled, onClick }) => (
|
||||
<Tooltip key={ariaLabel} title={ariaLabel}>
|
||||
@ -157,7 +117,11 @@ const BubbleMenu: FC<BubbleMenuProps> = ({ editor, toggleLink }) => {
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
onClick={onClick}>
|
||||
<Icon className="d-flex" height={24} width={24} />
|
||||
{isString(Icon) ? (
|
||||
<Typography>{Icon}</Typography>
|
||||
) : (
|
||||
<Icon className="d-flex" height={24} width={24} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@ -130,7 +130,9 @@ export default forwardRef<
|
||||
<div className="w-5" style={{ marginTop: '6px' }}>
|
||||
{getEntityIcon(item.type)}
|
||||
</div>
|
||||
<Typography>{item.label}</Typography>
|
||||
<Typography className="truncate w-max-200">
|
||||
{item.label}
|
||||
</Typography>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
|
||||
@ -111,7 +111,7 @@ export default forwardRef<ExtensionRef, SuggestionProps<SuggestionItem>>(
|
||||
key={item.id}
|
||||
onClick={() => selectItem(index)}>
|
||||
<ProfilePicture id="" name={item.name} type="circle" width="20" />
|
||||
<Typography>{item.label}</Typography>
|
||||
<Typography className="truncate w-max-200">{item.label}</Typography>
|
||||
</Space>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
@ -11,6 +11,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Editor, Range } from '@tiptap/core';
|
||||
import HashtagImage from 'assets/img/ic-format-hashtag.png';
|
||||
import MentionImage from 'assets/svg/ic-mentions.svg';
|
||||
import BulletListImage from '../../../../assets/img/ic-slash-bullet-list.png';
|
||||
import DividerImage from '../../../../assets/img/ic-slash-divider.png';
|
||||
import H1Image from '../../../../assets/img/ic-slash-h1.png';
|
||||
@ -19,21 +21,19 @@ import H3Image from '../../../../assets/img/ic-slash-h3.png';
|
||||
import NumberedListImage from '../../../../assets/img/ic-slash-numbered-list.png';
|
||||
import QuoteImage from '../../../../assets/img/ic-slash-quote.png';
|
||||
import TextImage from '../../../../assets/img/ic-slash-text.png';
|
||||
import TaskListImage from '../../../../assets/img/ic-task-list.png';
|
||||
import CodeBlockImage from '../../../../assets/svg/ic-format-code-block.svg';
|
||||
|
||||
export enum SuggestionItemType {
|
||||
BASIC_BLOCKS = 'Basic blocks',
|
||||
BASIC_BLOCKS = 'Basic',
|
||||
ADVANCED_BLOCKS = 'Advanced',
|
||||
}
|
||||
|
||||
export interface SuggestionItem {
|
||||
title: string;
|
||||
description: string;
|
||||
type: SuggestionItemType;
|
||||
shortcut: string | null;
|
||||
imgSrc: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
command: (props: { editor: Editor; range: Range; props: any }) => void;
|
||||
command: (props: { editor: Editor; range: Range }) => void;
|
||||
}
|
||||
|
||||
export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
@ -41,8 +41,7 @@ export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
[
|
||||
{
|
||||
title: 'Text',
|
||||
description: 'Plain text',
|
||||
shortcut: null,
|
||||
description: 'Start writing with plain text',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).setNode('paragraph').run();
|
||||
@ -51,8 +50,7 @@ export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
},
|
||||
{
|
||||
title: 'H1',
|
||||
description: 'Heading 1',
|
||||
shortcut: '#',
|
||||
description: 'Big heading',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
@ -66,8 +64,7 @@ export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
},
|
||||
{
|
||||
title: 'H2',
|
||||
description: 'Heading 2',
|
||||
shortcut: '##',
|
||||
description: 'Medium heading',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
@ -81,8 +78,7 @@ export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
},
|
||||
{
|
||||
title: 'H3',
|
||||
description: 'Heading 3',
|
||||
shortcut: '###',
|
||||
description: 'Small heading',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
@ -94,20 +90,9 @@ export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
},
|
||||
imgSrc: H3Image,
|
||||
},
|
||||
{
|
||||
title: 'Code Block',
|
||||
description: 'Add a code block',
|
||||
shortcut: '```',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||
},
|
||||
imgSrc: CodeBlockImage,
|
||||
},
|
||||
{
|
||||
title: 'Bullet List',
|
||||
description: 'Create a simple bulleted list',
|
||||
shortcut: null,
|
||||
description: 'Create a simple bullet list',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
||||
@ -116,8 +101,7 @@ export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
},
|
||||
{
|
||||
title: 'Numbered List',
|
||||
description: 'Create a list with numbering',
|
||||
shortcut: null,
|
||||
description: 'Create a simple numbered list',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||
@ -125,34 +109,57 @@ export const getSuggestionItems = (props: { query: string; editor: Editor }) =>
|
||||
imgSrc: NumberedListImage,
|
||||
},
|
||||
{
|
||||
title: 'Task List',
|
||||
description: 'Create a task list',
|
||||
shortcut: null,
|
||||
title: 'Divider',
|
||||
description: 'Insert a dividing line',
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setHardBreak()
|
||||
.setHorizontalRule()
|
||||
.run();
|
||||
},
|
||||
imgSrc: TaskListImage,
|
||||
imgSrc: DividerImage,
|
||||
},
|
||||
// advanced blocks
|
||||
{
|
||||
title: 'Code Block',
|
||||
description: 'Add a block of code',
|
||||
type: SuggestionItemType.ADVANCED_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||
},
|
||||
imgSrc: CodeBlockImage,
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Quote',
|
||||
description: 'Capture a quote',
|
||||
shortcut: null,
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
description: 'Add a quote',
|
||||
type: SuggestionItemType.ADVANCED_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
||||
},
|
||||
imgSrc: QuoteImage,
|
||||
},
|
||||
{
|
||||
title: 'Divider',
|
||||
description: 'Visually divide blocks',
|
||||
shortcut: null,
|
||||
type: SuggestionItemType.BASIC_BLOCKS,
|
||||
command: ({ editor }) => {
|
||||
editor.chain().focus().setHorizontalRule().run();
|
||||
title: 'Mention',
|
||||
description: 'Add a user or team',
|
||||
type: SuggestionItemType.ADVANCED_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).insertContent('@').run();
|
||||
},
|
||||
imgSrc: DividerImage,
|
||||
imgSrc: MentionImage,
|
||||
},
|
||||
{
|
||||
title: 'Link data asset',
|
||||
description: 'Add a data asset',
|
||||
type: SuggestionItemType.ADVANCED_BLOCKS,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).insertContent('#').run();
|
||||
},
|
||||
imgSrc: HashtagImage,
|
||||
},
|
||||
] as SuggestionItem[]
|
||||
).filter((item) =>
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
|
||||
.mention-menu-wrapper {
|
||||
max-height: 300px;
|
||||
max-width: 300px;
|
||||
padding: 12px 0px;
|
||||
background-color: @white;
|
||||
border: @global-border;
|
||||
@ -222,6 +223,7 @@
|
||||
}
|
||||
.hashtag-menu-wrapper {
|
||||
max-height: 300px;
|
||||
max-width: 300px;
|
||||
padding: 12px 0px;
|
||||
background-color: @white;
|
||||
border: @global-border;
|
||||
|
||||
@ -39,33 +39,12 @@ export const EDITOR_OPTIONS: Partial<EditorOptions> = {
|
||||
showOnlyCurrent: false,
|
||||
emptyEditorClass: 'is-editor-empty',
|
||||
emptyNodeClass: 'is-node-empty',
|
||||
placeholder: ({ node, editor: coreEditor }) => {
|
||||
placeholder: ({ editor: coreEditor }) => {
|
||||
if (coreEditor.isDestroyed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const headingPlaceholders: {
|
||||
[key: number]: string;
|
||||
} = {
|
||||
1: 'Heading 1',
|
||||
2: 'Heading 2',
|
||||
3: 'Heading 3',
|
||||
};
|
||||
|
||||
if (node.type.name === 'heading') {
|
||||
const level = node.attrs.level as number;
|
||||
|
||||
return headingPlaceholders[level];
|
||||
}
|
||||
|
||||
if (
|
||||
node.type.name === 'paragraph' &&
|
||||
coreEditor.getJSON().content?.length === 1
|
||||
) {
|
||||
return 'Type / to get started';
|
||||
}
|
||||
|
||||
return 'Type / for commands';
|
||||
return 'Type "/" for commands...';
|
||||
},
|
||||
}),
|
||||
LinkExtension.configure({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user