mirror of https://github.com/fantasticit/think.git
tiptap: improve banner
parent
367800c24f
commit
4147f3821a
|
@ -0,0 +1,276 @@
|
|||
export const EXPRESSIONES = [
|
||||
'😀',
|
||||
'😃',
|
||||
'😄',
|
||||
'😁',
|
||||
'😆',
|
||||
'😅',
|
||||
'😂',
|
||||
'🤣',
|
||||
'🥲',
|
||||
'😊',
|
||||
'😇',
|
||||
'🙂',
|
||||
'🙃',
|
||||
'😉',
|
||||
'😌',
|
||||
'😍',
|
||||
'🥰',
|
||||
'😘',
|
||||
'😗',
|
||||
'😙',
|
||||
'😚',
|
||||
'😋',
|
||||
'😛',
|
||||
'😝',
|
||||
'😜',
|
||||
'🤪',
|
||||
'🤨',
|
||||
'🧐',
|
||||
'🤓',
|
||||
'😎',
|
||||
'🥸',
|
||||
'🤩',
|
||||
'🥳',
|
||||
'😏',
|
||||
'😒',
|
||||
'😞',
|
||||
'😔',
|
||||
'😟',
|
||||
'😕',
|
||||
'🙁',
|
||||
'😣',
|
||||
'😖',
|
||||
'😫',
|
||||
'😩',
|
||||
'🥺',
|
||||
'😢',
|
||||
'😭',
|
||||
'😤',
|
||||
'😠',
|
||||
'😡',
|
||||
'🤬',
|
||||
'🤯',
|
||||
'😳',
|
||||
'🥵',
|
||||
'🥶',
|
||||
'😱',
|
||||
'😨',
|
||||
'😰',
|
||||
'😥',
|
||||
'😓',
|
||||
'🤗',
|
||||
'🤔',
|
||||
'🤭',
|
||||
'🤫',
|
||||
'🤥',
|
||||
'😶',
|
||||
'😐',
|
||||
'😑',
|
||||
'😬',
|
||||
'🙄',
|
||||
'😯',
|
||||
'😦',
|
||||
'😧',
|
||||
'😮',
|
||||
'😲',
|
||||
'🥱',
|
||||
'😴',
|
||||
'🤤',
|
||||
'😪',
|
||||
'😵',
|
||||
'🤐',
|
||||
'🥴',
|
||||
'🤢',
|
||||
'🤮',
|
||||
'🤧',
|
||||
'😷',
|
||||
'🤒',
|
||||
'🤕',
|
||||
'🤑',
|
||||
'🤠',
|
||||
'😈',
|
||||
'👿',
|
||||
'👹',
|
||||
'👺',
|
||||
'🤡',
|
||||
'💩',
|
||||
'👻',
|
||||
'💀',
|
||||
'☠️',
|
||||
'👽',
|
||||
'👾',
|
||||
'🤖',
|
||||
'🎃',
|
||||
'😺',
|
||||
'😸',
|
||||
'😹',
|
||||
'😻',
|
||||
'😼',
|
||||
'😽',
|
||||
'🙀',
|
||||
'😿',
|
||||
'😾',
|
||||
];
|
||||
|
||||
export const GESTURES = [
|
||||
'👋',
|
||||
'🤚',
|
||||
'🖐',
|
||||
'✋',
|
||||
'🖖',
|
||||
'👌',
|
||||
'🤌',
|
||||
'🤏',
|
||||
'✌️',
|
||||
'🤞',
|
||||
'🤟',
|
||||
'🤘',
|
||||
'🤙',
|
||||
'👈',
|
||||
'👉',
|
||||
'👆',
|
||||
'🖕',
|
||||
'👇',
|
||||
'☝️',
|
||||
'👍',
|
||||
'👎',
|
||||
'✊',
|
||||
'👊',
|
||||
'🤛',
|
||||
'🤜',
|
||||
'👏',
|
||||
'🙌',
|
||||
'👐',
|
||||
'🤲',
|
||||
'🤝',
|
||||
'🙏',
|
||||
'✍️',
|
||||
'💅',
|
||||
'🤳',
|
||||
'💪',
|
||||
'🦾',
|
||||
'🦵',
|
||||
'🦿',
|
||||
'🦶',
|
||||
'👣',
|
||||
'👂',
|
||||
'🦻',
|
||||
'👃',
|
||||
'🫀',
|
||||
'🫁',
|
||||
'🧠',
|
||||
'🦷',
|
||||
'🦴',
|
||||
'👀',
|
||||
'👁',
|
||||
'👅',
|
||||
'👄',
|
||||
'💋',
|
||||
'🩸',
|
||||
];
|
||||
|
||||
export const SYMBOLS = [
|
||||
'⭕',
|
||||
'✅',
|
||||
'❎',
|
||||
'✳️',
|
||||
'✴️',
|
||||
'❇️',
|
||||
'#️⃣',
|
||||
'*️⃣',
|
||||
'0️⃣',
|
||||
'1️⃣',
|
||||
'2️⃣',
|
||||
'3️⃣',
|
||||
'4️⃣',
|
||||
'5️⃣',
|
||||
'6️⃣',
|
||||
'7️⃣',
|
||||
'8️⃣',
|
||||
'9️⃣',
|
||||
'🔟',
|
||||
'⛔',
|
||||
'🚫',
|
||||
'🚳',
|
||||
'🚭',
|
||||
'🚯',
|
||||
'🚱',
|
||||
'🚷',
|
||||
'📵',
|
||||
'🔞',
|
||||
'☢️',
|
||||
'☣️',
|
||||
'↩️',
|
||||
'↪️',
|
||||
'⤴️',
|
||||
'⤵️',
|
||||
'🔃',
|
||||
'🔄',
|
||||
'♈',
|
||||
'♉',
|
||||
'♊',
|
||||
'♋',
|
||||
'♌',
|
||||
'♍',
|
||||
'♎',
|
||||
'♏',
|
||||
'♐',
|
||||
'♑',
|
||||
'♒',
|
||||
'♓',
|
||||
'⛎',
|
||||
'🛐',
|
||||
'⚛️',
|
||||
'🕉️',
|
||||
'🕉',
|
||||
'✡️',
|
||||
'☸️',
|
||||
'☯️',
|
||||
'✝️',
|
||||
'☦️',
|
||||
'☪️',
|
||||
'☮️',
|
||||
'🕎',
|
||||
'🔯',
|
||||
'🔀',
|
||||
'🔁',
|
||||
'🔂',
|
||||
'⏩',
|
||||
'⏭️',
|
||||
'⏭',
|
||||
'⏯️',
|
||||
'⏯',
|
||||
'⏪',
|
||||
'⏮️',
|
||||
'⏮',
|
||||
'🔼',
|
||||
'⏫',
|
||||
'🔽',
|
||||
'⏬',
|
||||
'⏸️',
|
||||
'⏸',
|
||||
'⏹️',
|
||||
'⏹',
|
||||
'⏺️',
|
||||
'⏺',
|
||||
'⏏️',
|
||||
'🎦',
|
||||
'📶',
|
||||
'📳',
|
||||
'📴',
|
||||
'🏧',
|
||||
'🚮',
|
||||
'🚰',
|
||||
'♿',
|
||||
'🚹',
|
||||
'🚺',
|
||||
'🚻',
|
||||
'🚼',
|
||||
'🚾',
|
||||
'🛂',
|
||||
'🛃',
|
||||
'🛄',
|
||||
'🛅',
|
||||
'🚸',
|
||||
];
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
|||
.wrap {
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.listWrap {
|
||||
display: flex;
|
||||
width: 320px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { Popover, Typography } from '@douyinfe/semi-ui';
|
||||
import { EXPRESSIONES, GESTURES, SYMBOLS } from './constants';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const LIST = [
|
||||
{
|
||||
title: '符号',
|
||||
data: SYMBOLS,
|
||||
},
|
||||
{
|
||||
title: '表情',
|
||||
data: EXPRESSIONES,
|
||||
},
|
||||
{
|
||||
title: '手势',
|
||||
data: GESTURES,
|
||||
},
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
onSelectEmoji: (arg: string) => void;
|
||||
}
|
||||
|
||||
export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
|
||||
return (
|
||||
<Popover
|
||||
showArrow
|
||||
zIndex={10000}
|
||||
trigger="click"
|
||||
position="bottomLeft"
|
||||
content={
|
||||
<div className={styles.wrap}>
|
||||
{LIST.map((item, index) => {
|
||||
return (
|
||||
<div key={item.title} className={styles.sectionWrap}>
|
||||
<Title heading={6} style={{ margin: `${index === 0 ? 0 : 16}px 0 6px` }}>
|
||||
{item.title}
|
||||
</Title>
|
||||
<ul className={styles.listWrap}>
|
||||
{(item.data || []).map((ex) => (
|
||||
<li key={ex} onClick={() => onSelectEmoji(ex)}>
|
||||
{ex}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span>{children}</span>
|
||||
</Popover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconDrawBoard: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-icon="StyleSetOutlined"
|
||||
>
|
||||
<path
|
||||
d="M21.957 2.13a1 1 0 0 0-1.406.147l-9.11 11.25a6.632 6.632 0 0 0-1.367 2.969.221.221 0 0 0 .302.244 6.632 6.632 0 0 0 2.62-1.955l9.109-11.25a1 1 0 0 0-.148-1.406Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M17.008 3.665a13.454 13.454 0 0 0-5.06-.984l-.024.004-.538.011-.51.03c-1.191.091-2.37.343-3.51.75a12.305 12.305 0 0 0-3.754 2.142c-1.096.922-1.96 1.99-2.568 3.176a8.435 8.435 0 0 0-.96 3.885c0 1.335.324 2.63.962 3.848.608 1.157 1.474 2.195 2.573 3.083a12.303 12.303 0 0 0 3.755 2.049c1.444.494 2.981.745 4.563.745l.545-.01.525-.029a14.43 14.43 0 0 0 1.57-.203l.722-.148.196-.06c.514-.186.96-.566 1.253-1.083a2.87 2.87 0 0 0 .26-2.178l-.09-.349-.03-.218a2.301 2.301 0 0 1 .357-1.454c.357-.544.93-.868 1.538-.871h1.768l.204-.007c1.614-.113 2.91-1.56 3.007-3.365l.006-.22-.006-.24-.05-.432-.067-.404a8.844 8.844 0 0 0-1.236-3.08 10.13 10.13 0 0 0-.802-1.096l-1.199 1.48c.154.2.298.406.43.617.483.76.81 1.563.974 2.393l.06.358.032.276.003.086-.007.22-.021.169c-.12.724-.604 1.301-1.19 1.38l-.138.01h-1.77l-.247.008c-1.14.079-2.192.702-2.847 1.7a4.145 4.145 0 0 0-.574 3.156l.068.273.037.13.028.154a.993.993 0 0 1-.118.588.58.58 0 0 1-.248.247l-.07.021-.67.134-.549.085a12.6 12.6 0 0 1-1.657.11 12.18 12.18 0 0 1-3.961-.647 10.426 10.426 0 0 1-3.19-1.734c-.9-.729-1.606-1.57-2.096-2.5a6.38 6.38 0 0 1-.75-2.984c0-1.037.254-2.06.755-3.034.495-.963 1.206-1.839 2.11-2.6A10.494 10.494 0 0 1 7.99 5.235a11.42 11.42 0 0 1 3.41-.677l.538-.01.496.01c1.153.048 2.281.264 3.338.633l1.236-1.526Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M6.875 14.466a1.377 1.377 0 0 0-1.374-1.374 1.375 1.375 0 0 0 0 2.747c.758 0 1.374-.616 1.374-1.373ZM8.124 9.47a1.375 1.375 0 0 0-2.748 0 1.374 1.374 0 1 0 2.748 0Zm5.246-1.874a1.374 1.374 0 1 0-2.747-.001 1.374 1.374 0 0 0 2.747 0Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -49,3 +49,4 @@ export * from './IconSub';
|
|||
export * from './IconSup';
|
||||
export * from './IconGlobe';
|
||||
export * from './IconCountdown';
|
||||
export * from './IconDrawBoard';
|
||||
|
|
|
@ -31,6 +31,18 @@ export const Banner = Node.create({
|
|||
};
|
||||
},
|
||||
},
|
||||
emoji: {
|
||||
default: '✅',
|
||||
},
|
||||
textColor: {
|
||||
default: '#d83931',
|
||||
},
|
||||
borderColor: {
|
||||
default: '#fbbfbc',
|
||||
},
|
||||
backgroundColor: {
|
||||
default: '#fef1f1',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
.colorWrap {
|
||||
margin-top: 16px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
margin-top: 8px;
|
||||
|
||||
.color {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,37 @@
|
|||
import { Editor } from '@tiptap/core';
|
||||
import { Space, Button } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconTickCircle, IconAlertTriangle, IconClear, IconInfoCircle } from '@douyinfe/semi-icons';
|
||||
import { Space, Button, Popover, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { IconDrawBoard } from 'components/icons';
|
||||
import { BubbleMenu } from '../../views/bubble-menu';
|
||||
import { Divider } from '../../divider';
|
||||
import { Banner } from '../../extensions/banner';
|
||||
import { deleteNode } from '../../utils/delete-node';
|
||||
import styles from './bubble.module.scss';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const TEXT_COLORS = ['#d83931', '#de7802', '#dc9b04', '#2ea121', '#245bdb', '#6425d0', '#646a73'];
|
||||
const BORDER_COLORS = ['#fbbfbc', '#fed4a4', '#fff67a', '#b7edb1', '#bacefd', '#cdb2fa', '#dee0e3'];
|
||||
const BACKGROUND_COLORS = ['#fef1f1', '#feead2', '#ffc', '#d9f5d6', '#e1eaff', '#ece2fe', '#f2f3f5'];
|
||||
|
||||
export const BannerBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
const setColor = useCallback(
|
||||
(key, color) => {
|
||||
return () => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Banner.name, {
|
||||
[key]: color,
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
};
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
className={'bubble-menu'}
|
||||
|
@ -17,80 +41,53 @@ export const BannerBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
||||
>
|
||||
<Space>
|
||||
<Tooltip content="信息">
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
icon={<IconInfoCircle style={{ color: 'var(--semi-color-info)' }} />}
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Banner.name, {
|
||||
type: 'info',
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="警告">
|
||||
<Button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Banner.name, {
|
||||
type: 'warning',
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
}}
|
||||
icon={<IconAlertTriangle style={{ color: 'var(--semi-color-warning)' }} />}
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="危险">
|
||||
<Button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Banner.name, {
|
||||
type: 'danger',
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
}}
|
||||
icon={<IconClear style={{ color: 'var(--semi-color-danger)' }} />}
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="成功">
|
||||
<Button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Banner.name, {
|
||||
type: 'success',
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
}}
|
||||
icon={<IconTickCircle style={{ color: 'var(--semi-color-success)' }} />}
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
spacing={10}
|
||||
visible
|
||||
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
||||
content={
|
||||
<>
|
||||
<section className={styles.colorWrap}>
|
||||
<Text type="tertiary">字体颜色</Text>
|
||||
<div>
|
||||
{TEXT_COLORS.map((color) => (
|
||||
<div className={styles.color} style={{ color: color }} onClick={setColor('textColor', color)}>
|
||||
A
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className={styles.colorWrap}>
|
||||
<Text type="tertiary">边框颜色</Text>
|
||||
|
||||
<div>
|
||||
{BORDER_COLORS.map((color) => (
|
||||
<div
|
||||
className={styles.color}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={setColor('borderColor', color)}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className={styles.colorWrap}>
|
||||
<Text type="tertiary">背景颜色</Text>
|
||||
<div>
|
||||
{BACKGROUND_COLORS.map((color) => (
|
||||
<div
|
||||
className={styles.color}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={setColor('backgroundColor', color)}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" />
|
||||
</Popover>
|
||||
<Divider />
|
||||
|
||||
<Tooltip content="删除" hideOnClick>
|
||||
<Button
|
||||
size="small"
|
||||
|
|
|
@ -1,10 +1,34 @@
|
|||
.wrap {
|
||||
line-height: 0;
|
||||
border: 1px solid var(--node-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.innerWrap {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: 16px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 20px;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: .25em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
|
|
|
@ -1,12 +1,36 @@
|
|||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
|
||||
import { Popover } from '@douyinfe/semi-ui';
|
||||
import cls from 'classnames';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { EmojiPicker } from 'components/emoji-picker';
|
||||
import styles from './index.module.scss';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
export const BannerWrapper = ({ node, updateAttributes }) => {
|
||||
const { emoji, textColor, borderColor, backgroundColor } = node.attrs;
|
||||
|
||||
const onSelectEmoji = useCallback((emoji) => {
|
||||
updateAttributes({ emoji });
|
||||
}, []);
|
||||
|
||||
export const BannerWrapper = ({ node }) => {
|
||||
return (
|
||||
<NodeViewWrapper id="js-bannber-container" className={cls(styles.wrap, 'render-wrapper')}>
|
||||
<SemiBanner type={node.attrs.type} description={<NodeViewContent />} closeIcon={null} fullMode={false} />
|
||||
<NodeViewWrapper id="js-bannber-container" className={cls(styles.wrap)}>
|
||||
<div
|
||||
className={cls(styles.innerWrap, 'render-wrapper')}
|
||||
style={{
|
||||
borderColor,
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<EmojiPicker onSelectEmoji={onSelectEmoji}>
|
||||
<span className={styles.icon}>{emoji || 'Icon'}</span>
|
||||
</EmojiPicker>
|
||||
<NodeViewContent
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue