tiptap: improve banner

pull/29/head
fantasticit 2022-04-25 13:45:15 +08:00
parent 367800c24f
commit 4147f3821a
11 changed files with 5345 additions and 81 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -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>
}
/>
);
};

View File

@ -49,3 +49,4 @@ export * from './IconSub';
export * from './IconSup';
export * from './IconGlobe';
export * from './IconCountdown';
export * from './IconDrawBoard';

View File

@ -31,6 +31,18 @@ export const Banner = Node.create({
};
},
},
emoji: {
default: '✅',
},
textColor: {
default: '#d83931',
},
borderColor: {
default: '#fbbfbc',
},
backgroundColor: {
default: '#fef1f1',
},
};
},

View File

@ -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;
}
}
}
}

View File

@ -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"

View File

@ -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 {

View File

@ -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>
);
};