mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support editor format text color and bg color (#3061)
This commit is contained in:
parent
915ce02157
commit
a885170869
@ -46,6 +46,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-calendar": "^4.1.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-i18next": "^12.2.0",
|
||||
@ -72,6 +73,7 @@
|
||||
"@types/quill": "^2.0.10",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-beautiful-dnd": "^13.1.3",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
|
@ -88,6 +88,9 @@ dependencies:
|
||||
react-calendar:
|
||||
specifier: ^4.1.0
|
||||
version: 4.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||
react-color:
|
||||
specifier: ^2.19.3
|
||||
version: 2.19.3(react@18.2.0)
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
@ -162,6 +165,9 @@ devDependencies:
|
||||
'@types/react-beautiful-dnd':
|
||||
specifier: ^13.1.3
|
||||
version: 13.1.4
|
||||
'@types/react-color':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/react-dom':
|
||||
specifier: ^18.0.6
|
||||
version: 18.2.4
|
||||
@ -960,6 +966,14 @@ packages:
|
||||
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
|
||||
dev: true
|
||||
|
||||
/@icons/material@0.2.4(react@18.2.0):
|
||||
resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@istanbuljs/load-nyc-config@1.1.0:
|
||||
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
||||
engines: {node: '>=8'}
|
||||
@ -1701,6 +1715,13 @@ packages:
|
||||
'@types/react': 18.2.6
|
||||
dev: true
|
||||
|
||||
/@types/react-color@3.0.6:
|
||||
resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
'@types/reactcss': 1.2.6
|
||||
dev: true
|
||||
|
||||
/@types/react-dom@18.2.4:
|
||||
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
|
||||
dependencies:
|
||||
@ -1747,6 +1768,12 @@ packages:
|
||||
'@types/scheduler': 0.16.3
|
||||
csstype: 3.1.2
|
||||
|
||||
/@types/reactcss@1.2.6:
|
||||
resolution: {integrity: sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
dev: true
|
||||
|
||||
/@types/scheduler@0.16.3:
|
||||
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
||||
|
||||
@ -3975,6 +4002,10 @@ packages:
|
||||
p-locate: 5.0.0
|
||||
dev: true
|
||||
|
||||
/lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
dev: false
|
||||
|
||||
/lodash.clonedeep@4.5.0:
|
||||
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||
|
||||
@ -4035,6 +4066,10 @@ packages:
|
||||
tmpl: 1.0.5
|
||||
dev: false
|
||||
|
||||
/material-colors@1.2.6:
|
||||
resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==}
|
||||
dev: false
|
||||
|
||||
/memoize-one@5.2.1:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
dev: false
|
||||
@ -4593,6 +4628,21 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-color@2.19.3(react@18.2.0):
|
||||
resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
dependencies:
|
||||
'@icons/material': 0.2.4(react@18.2.0)
|
||||
lodash: 4.17.21
|
||||
lodash-es: 4.17.21
|
||||
material-colors: 1.2.6
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
reactcss: 1.2.3(react@18.2.0)
|
||||
tinycolor2: 1.6.0
|
||||
dev: false
|
||||
|
||||
/react-dom@18.2.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||
peerDependencies:
|
||||
@ -4770,6 +4820,15 @@ packages:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/reactcss@1.2.3(react@18.2.0):
|
||||
resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
dependencies:
|
||||
lodash: 4.17.21
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/read-cache@1.0.0:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
dependencies:
|
||||
@ -5230,7 +5289,6 @@ packages:
|
||||
|
||||
/tinycolor2@1.6.0:
|
||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
dev: true
|
||||
|
||||
/tmpl@1.0.5:
|
||||
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
||||
|
@ -40,9 +40,6 @@ function BlockDraggable(
|
||||
data-draggable-type={type}
|
||||
onMouseDown={getAnchorEl ? undefined : onDragStart}
|
||||
className={`relative ${className || ''}`}
|
||||
style={{
|
||||
opacity: isDragging ? 0.7 : 1,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{
|
||||
|
@ -143,7 +143,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
||||
return (
|
||||
<BlockMenuTurnInto
|
||||
key={option.key}
|
||||
lable={option.title}
|
||||
label={option.title}
|
||||
onHovered={() => {
|
||||
setHovered(BlockMenuOption.TurnInto);
|
||||
setSubMenuOpened(true);
|
||||
|
@ -9,14 +9,14 @@ function BlockMenuTurnInto({
|
||||
onHovered,
|
||||
isHovered,
|
||||
menuOpened,
|
||||
lable,
|
||||
label,
|
||||
}: {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
onHovered: (e: MouseEvent) => void;
|
||||
isHovered: boolean;
|
||||
menuOpened: boolean;
|
||||
lable?: string;
|
||||
label?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>();
|
||||
@ -39,7 +39,7 @@ function BlockMenuTurnInto({
|
||||
<>
|
||||
<MenuItem
|
||||
ref={ref}
|
||||
title={lable}
|
||||
title={label}
|
||||
isHovered={isHovered}
|
||||
icon={<Transform />}
|
||||
extra={<ArrowRight />}
|
||||
@ -60,7 +60,10 @@ function BlockMenuTurnInto({
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
onOk={() => onClose()}
|
||||
onClose={() => {
|
||||
setAnchorPosition(undefined);
|
||||
}}
|
||||
anchorReference={'anchorPosition'}
|
||||
anchorPosition={anchorPosition}
|
||||
transformOrigin={{
|
||||
|
@ -9,9 +9,9 @@ import { getNode } from '$app/utils/document/node';
|
||||
import { get } from '$app/utils/tool';
|
||||
|
||||
const headingBlockTopOffset: Record<number, string> = {
|
||||
1: '1.65rem',
|
||||
2: '1.3rem',
|
||||
3: '0.25rem',
|
||||
1: '0.4rem',
|
||||
2: '0.2rem',
|
||||
3: '0.15rem',
|
||||
};
|
||||
|
||||
export function useBlockSideToolbar(id: string) {
|
||||
@ -87,35 +87,6 @@ export function useBlockSideToolbar(id: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function getNodeIdByPoint(x: number, y: number) {
|
||||
const viewportNodes = document.querySelectorAll('[data-block-id]');
|
||||
let node: {
|
||||
el: Element;
|
||||
rect: DOMRect;
|
||||
} | null = null;
|
||||
|
||||
viewportNodes.forEach((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
if (rect.x + rect.width - 1 >= x && rect.y + rect.height - 1 >= y && rect.y <= y) {
|
||||
if (!node || rect.y > node.rect.y) {
|
||||
node = {
|
||||
el,
|
||||
rect,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
return node
|
||||
? (
|
||||
node as {
|
||||
el: Element;
|
||||
rect: DOMRect;
|
||||
}
|
||||
).el.getAttribute('data-block-id')
|
||||
: null;
|
||||
}
|
||||
|
||||
const transformOrigin: PopoverOrigin = {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
|
@ -29,7 +29,7 @@ export default function BlockSideToolbar({ id }: { id: string }) {
|
||||
opacity: show ? 1 : 0,
|
||||
top: topOffset,
|
||||
}}
|
||||
className='absolute left-[-50px] inline-flex transition-opacity duration-100'
|
||||
className='absolute left-[-50px] inline-flex'
|
||||
>
|
||||
{/** Add Block below */}
|
||||
<Tooltip disableInteractive={true} title={t('blockActions.addBelowTooltip')} placement={'top-start'}>
|
||||
|
@ -292,7 +292,7 @@ function BlockSlashMenu({
|
||||
<div ref={ref} className={'min-h-0 flex-1 overflow-y-auto overflow-x-hidden'}>
|
||||
{Object.entries(optionsByGroup).map(([group, options]) => (
|
||||
<div key={group}>
|
||||
<div className={'text-shade-3 px-2 py-2 text-sm'}>{group}</div>
|
||||
<div className={'px-2 py-2 text-sm text-text-caption'}>{group}</div>
|
||||
<div>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
|
@ -90,7 +90,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
||||
{...props}
|
||||
ref={ref}
|
||||
data-block-id={node.id}
|
||||
className={`pt-[0.5px] ${className}`}
|
||||
className={className}
|
||||
>
|
||||
{renderBlock()}
|
||||
<BlockOverlay id={id} />
|
||||
|
@ -9,6 +9,8 @@ export const defaultTextActionItems = [
|
||||
TextAction.Strikethrough,
|
||||
TextAction.Code,
|
||||
TextAction.Equation,
|
||||
TextAction.TextColor,
|
||||
TextAction.Highlight,
|
||||
];
|
||||
const groupKeys = {
|
||||
comment: [],
|
||||
@ -21,11 +23,20 @@ const groupKeys = {
|
||||
TextAction.Equation,
|
||||
],
|
||||
link: [TextAction.Link],
|
||||
color: [TextAction.TextColor, TextAction.Highlight],
|
||||
turn: [TextAction.Turn],
|
||||
};
|
||||
|
||||
export const multiLineTextActionProps: TextActionMenuProps = {
|
||||
customItems: [TextAction.Bold, TextAction.Italic, TextAction.Underline, TextAction.Strikethrough, TextAction.Code],
|
||||
customItems: [
|
||||
TextAction.Bold,
|
||||
TextAction.Italic,
|
||||
TextAction.Underline,
|
||||
TextAction.Strikethrough,
|
||||
TextAction.Code,
|
||||
TextAction.TextColor,
|
||||
TextAction.Highlight,
|
||||
],
|
||||
};
|
||||
export const multiLineTextActionGroups = [groupKeys.format];
|
||||
export const textActionGroups = [groupKeys.turn, groupKeys.format, groupKeys.link];
|
||||
export const multiLineTextActionGroups = [groupKeys.format, groupKeys.color];
|
||||
export const textActionGroups = [groupKeys.turn, groupKeys.format, groupKeys.color, groupKeys.link];
|
||||
|
@ -18,7 +18,7 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
|
||||
style={{
|
||||
opacity: 0,
|
||||
}}
|
||||
className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-fill-toolbar leading-tight text-content-on-fill shadow-md transition-opacity duration-100'
|
||||
className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-fill-toolbar leading-tight text-content-on-fill shadow-md'
|
||||
onMouseDown={(e) => {
|
||||
// prevent toolbar from taking focus away from editor
|
||||
e.preventDefault();
|
||||
|
@ -0,0 +1,101 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ColorPicker from '$app/components/document/TextActionMenu/menu/ColorPicker';
|
||||
import { FormatColorFill, FormatColorText } from '@mui/icons-material';
|
||||
import { TextAction } from '$app/interfaces/document';
|
||||
|
||||
function BgColorPicker() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getColorIcon = useCallback((color: string) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
className={'rounded border border-line-divider p-0.5'}
|
||||
>
|
||||
<FormatColorText />
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
const colors = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: t('colors.default'),
|
||||
key: 'default',
|
||||
color: 'transparent',
|
||||
},
|
||||
{
|
||||
name: t('colors.custom'),
|
||||
key: 'custom',
|
||||
color: 'transparent',
|
||||
},
|
||||
{
|
||||
key: 'gray',
|
||||
name: t('colors.gray'),
|
||||
color: '#78909c',
|
||||
},
|
||||
{
|
||||
key: 'brown',
|
||||
name: t('colors.brown'),
|
||||
color: '#8d6e63',
|
||||
},
|
||||
{
|
||||
key: 'orange',
|
||||
name: t('colors.orange'),
|
||||
color: '#ff9100',
|
||||
},
|
||||
{
|
||||
key: 'yellow',
|
||||
name: t('colors.yellow'),
|
||||
color: '#ffd600',
|
||||
},
|
||||
{
|
||||
key: 'green',
|
||||
name: t('colors.green'),
|
||||
color: '#00e676',
|
||||
},
|
||||
{
|
||||
key: 'blue',
|
||||
name: t('colors.blue'),
|
||||
color: '#448aff',
|
||||
},
|
||||
{
|
||||
key: 'purple',
|
||||
name: t('colors.purple'),
|
||||
color: '#e040fb',
|
||||
},
|
||||
{
|
||||
key: 'pink',
|
||||
name: t('colors.pink'),
|
||||
color: '#ff4081',
|
||||
},
|
||||
{
|
||||
key: 'red',
|
||||
name: t('colors.red'),
|
||||
color: '#ff5252',
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
getColorIcon={getColorIcon}
|
||||
icon={
|
||||
<FormatColorFill
|
||||
sx={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
colors={colors}
|
||||
format={TextAction.Highlight}
|
||||
label={t('toolbar.highlight')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BgColorPicker;
|
@ -0,0 +1,197 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { List } from '@mui/material';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useBindArrowKey } from '$app/components/document/_shared/useBindArrowKey';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { formatThunk, getFormatValuesThunk } from '$app_reducers/document/async-actions/format';
|
||||
import { TextAction } from '$app/interfaces/document';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import CustomColorPicker from '$app/components/document/TextActionMenu/menu/CustomColorPicker';
|
||||
|
||||
export interface ColorItem {
|
||||
name: string;
|
||||
key: string;
|
||||
color: string;
|
||||
}
|
||||
function ColorPicker({
|
||||
label,
|
||||
format,
|
||||
colors,
|
||||
icon,
|
||||
getColorIcon,
|
||||
}: {
|
||||
format: TextAction;
|
||||
label: string;
|
||||
colors: ColorItem[];
|
||||
icon: React.ReactNode;
|
||||
getColorIcon: (color: string) => React.ReactNode;
|
||||
}) {
|
||||
const { controller, docId } = useSubscribeDocument();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [anchorPosition, setAnchorPosition] = useState<
|
||||
| {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
const open = Boolean(anchorPosition);
|
||||
const dispatch = useAppDispatch();
|
||||
const [customPickerAnchorPosition, setCustomPickerAnchorPosition] = useState<
|
||||
| {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
const customOpened = Boolean(customPickerAnchorPosition);
|
||||
const [selectOption, setSelectOption] = useState<string | null>(null);
|
||||
const [activeColor, setActiveColor] = useState<string | null>(null);
|
||||
|
||||
const openCustomColorPicker = useCallback(() => {
|
||||
const target = document.querySelector('.color-item-custom') as Element;
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
setCustomPickerAnchorPosition({
|
||||
left: rect.left + rect.width + 10,
|
||||
top: rect.top,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectOption === 'custom') {
|
||||
openCustomColorPicker();
|
||||
} else {
|
||||
setCustomPickerAnchorPosition(undefined);
|
||||
}
|
||||
}, [selectOption, openCustomColorPicker]);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
const rect = ref.current?.getBoundingClientRect();
|
||||
|
||||
if (!rect) return;
|
||||
setAnchorPosition({
|
||||
left: rect.left,
|
||||
top: rect.top + rect.height + 10,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadActiveColor = useCallback(async () => {
|
||||
const { payload: formatValues } = (await dispatch(getFormatValuesThunk({ format, docId }))) as {
|
||||
payload: Record<string, (boolean | string | undefined)[]>;
|
||||
};
|
||||
const multiLines = Object.keys(formatValues).length > 1;
|
||||
const firstKey = Object.keys(formatValues)[0];
|
||||
const firstValue = formatValues[firstKey].find((item) => item);
|
||||
|
||||
setActiveColor(multiLines ? null : String(firstValue));
|
||||
}, [dispatch, docId, format]);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await loadActiveColor();
|
||||
})();
|
||||
}, [loadActiveColor]);
|
||||
|
||||
const formatColor = useCallback(
|
||||
async (color: string | null) => {
|
||||
await dispatch(formatThunk({ format, value: color, controller }));
|
||||
setAnchorPosition(undefined);
|
||||
await loadActiveColor();
|
||||
},
|
||||
[format, controller, dispatch, loadActiveColor]
|
||||
);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (selectOption === 'custom') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectOption === 'default') {
|
||||
await formatColor(null);
|
||||
} else {
|
||||
const item = colors.find((color) => color.key === selectOption);
|
||||
|
||||
await formatColor(item?.color || null);
|
||||
}
|
||||
}, [selectOption, formatColor, colors]);
|
||||
|
||||
useBindArrowKey({
|
||||
options: colors.map((item) => item.key),
|
||||
onChange: (key) => {
|
||||
setSelectOption(key);
|
||||
},
|
||||
selectOption,
|
||||
onEnter: () => onClick(),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={ref}
|
||||
className={'cursor-pointer px-1.5 hover:text-fill-hover'}
|
||||
onClick={onOpen}
|
||||
style={{
|
||||
color: activeColor || undefined,
|
||||
}}
|
||||
>
|
||||
<Tooltip placement={'top-start'} disableInteractive title={label}>
|
||||
<div>{icon}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Popover
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
disableAutoFocus={true}
|
||||
disableRestoreFocus={true}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={open}
|
||||
anchorReference={'anchorPosition'}
|
||||
anchorPosition={anchorPosition}
|
||||
onClose={() => setAnchorPosition(undefined)}
|
||||
>
|
||||
<List>
|
||||
<div className={'w-[200px] px-4 py-2 uppercase text-text-caption'}>{label}</div>
|
||||
{colors.map((item) => (
|
||||
<MenuItem
|
||||
className={`color-item-${item.key}`}
|
||||
key={item.key}
|
||||
onMouseEnter={() => {
|
||||
setSelectOption(item.key);
|
||||
}}
|
||||
style={{
|
||||
padding: '4px',
|
||||
}}
|
||||
selected={selectOption === item.key}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={'flex items-center'}>
|
||||
{getColorIcon(item.color)}
|
||||
<div className={'ml-2'}>{item.name}</div>
|
||||
</div>
|
||||
{item.key === 'custom' && (
|
||||
<CustomColorPicker
|
||||
open={customOpened}
|
||||
onChange={formatColor}
|
||||
anchorPosition={customPickerAnchorPosition}
|
||||
onClose={() => {
|
||||
setCustomPickerAnchorPosition(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</List>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColorPicker;
|
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { RGBColor, SketchPicker } from 'react-color';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Divider } from '@mui/material';
|
||||
|
||||
function CustomColorPicker({
|
||||
onChange,
|
||||
open,
|
||||
onClose,
|
||||
anchorPosition,
|
||||
}: {
|
||||
open: boolean;
|
||||
onChange: (color: string) => void;
|
||||
anchorPosition?: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [color, setColor] = useState<RGBColor | undefined>();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
disableAutoFocus={true}
|
||||
disableRestoreFocus={true}
|
||||
sx={{
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
className: 'p-2',
|
||||
}}
|
||||
open={open}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
anchorReference={'anchorPosition'}
|
||||
anchorPosition={anchorPosition}
|
||||
onClose={onClose}
|
||||
>
|
||||
<SketchPicker
|
||||
onChange={(color, event) => {
|
||||
setColor(color.rgb);
|
||||
}}
|
||||
color={color}
|
||||
/>
|
||||
<Divider />
|
||||
<div className={'z-10 flex justify-end bg-bg-body px-2 pt-2'}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(`rgba(${color?.r}, ${color?.g}, ${color?.b}, ${color?.a})`);
|
||||
}}
|
||||
variant={'contained'}
|
||||
>
|
||||
{t('button.done')}
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomColorPicker;
|
@ -29,7 +29,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
||||
const { node: focusNode } = useSubscribeNode(focusId);
|
||||
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
const color = useMemo(() => (isActive ? 'text-content-on-fill-hover' : ''), [isActive]);
|
||||
const color = useMemo(() => (isActive ? 'text-fill-hover' : ''), [isActive]);
|
||||
|
||||
const isFormatActive = useCallback(async () => {
|
||||
if (!focusNode) return false;
|
||||
@ -125,22 +125,18 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
||||
return <StrikethroughSOutlined sx={iconSize} />;
|
||||
case TextAction.Link:
|
||||
return (
|
||||
<div className={'flex items-center justify-center px-1 text-[0.8rem]'}>
|
||||
<LinkIcon
|
||||
sx={{
|
||||
fontSize: '1.2rem',
|
||||
marginRight: '0.25rem',
|
||||
}}
|
||||
/>
|
||||
<div className={'underline'}>{t('toolbar.link')}</div>
|
||||
</div>
|
||||
<LinkIcon
|
||||
sx={{
|
||||
fontSize: '1.2rem',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case TextAction.Equation:
|
||||
return <Functions sx={iconSize} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [icon, t]);
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<ToolbarTooltip title={formatTooltips[format]}>
|
||||
|
@ -0,0 +1,97 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextAction } from '$app/interfaces/document';
|
||||
import ColorPicker from '$app/components/document/TextActionMenu/menu/ColorPicker';
|
||||
import { FormatColorText } from '@mui/icons-material';
|
||||
|
||||
function TextColorPicker() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getColorIcon = useCallback((color: string) => {
|
||||
return (
|
||||
<div className={'rounded border border-line-divider p-0.5'}>
|
||||
<FormatColorText style={{ color }} />
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const colors = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: t('colors.default'),
|
||||
key: 'default',
|
||||
color: 'var(--text-title)',
|
||||
},
|
||||
{
|
||||
name: t('colors.custom'),
|
||||
key: 'custom',
|
||||
color: 'var(--text-title)',
|
||||
},
|
||||
{
|
||||
key: 'gray',
|
||||
name: t('colors.gray'),
|
||||
color: '#546e7a',
|
||||
},
|
||||
{
|
||||
key: 'brown',
|
||||
name: t('colors.brown'),
|
||||
color: '#795548',
|
||||
},
|
||||
{
|
||||
key: 'orange',
|
||||
name: t('colors.orange'),
|
||||
color: '#ff5722',
|
||||
},
|
||||
{
|
||||
key: 'yellow',
|
||||
name: t('colors.yellow'),
|
||||
color: '#ffff00',
|
||||
},
|
||||
{
|
||||
key: 'green',
|
||||
name: t('colors.green'),
|
||||
color: '#4caf50',
|
||||
},
|
||||
{
|
||||
key: 'blue',
|
||||
name: t('colors.blue'),
|
||||
color: '#0d47a1',
|
||||
},
|
||||
{
|
||||
key: 'purple',
|
||||
name: t('colors.purple'),
|
||||
color: '#9c27b0',
|
||||
},
|
||||
{
|
||||
key: 'pink',
|
||||
name: t('colors.pink'),
|
||||
color: '#d81b60',
|
||||
},
|
||||
{
|
||||
key: 'red',
|
||||
name: t('colors.red'),
|
||||
color: '#b71c1c',
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
icon={
|
||||
<FormatColorText
|
||||
sx={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
getColorIcon={getColorIcon}
|
||||
colors={colors}
|
||||
format={TextAction.TextColor}
|
||||
label={t('toolbar.color')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextColorPicker;
|
@ -3,6 +3,8 @@ import React, { useCallback } from 'react';
|
||||
import TurnIntoSelect from '$app/components/document/TextActionMenu/menu/TurnIntoSelect';
|
||||
import FormatButton from '$app/components/document/TextActionMenu/menu/FormatButton';
|
||||
import { useTextActionMenu } from '$app/components/document/TextActionMenu/menu/index.hooks';
|
||||
import TextColorPicker from '$app/components/document/TextActionMenu/menu/TextColorPicker';
|
||||
import BgColorPicker from '$app/components/document/TextActionMenu/menu/BgColorPicker';
|
||||
|
||||
function TextActionMenuList() {
|
||||
const { groupItems, isSingleLine, focusId } = useTextActionMenu();
|
||||
@ -19,6 +21,10 @@ function TextActionMenuList() {
|
||||
case TextAction.Code:
|
||||
case TextAction.Equation:
|
||||
return <FormatButton format={action} icon={action} />;
|
||||
case TextAction.TextColor:
|
||||
return <TextColorPicker />;
|
||||
case TextAction.Highlight:
|
||||
return <BgColorPicker />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export default function VirtualizedList({
|
||||
const id = childIds[virtualRow.index];
|
||||
|
||||
return (
|
||||
<div key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
|
||||
<div className={'pt-[0.5px]'} key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
|
||||
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
||||
{renderNode(id)}
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { forwardRef, MouseEvent, useMemo } from 'react';
|
||||
import { ListItemButton } from '@mui/material';
|
||||
import { MenuItem as MuiMenuItem } from '@mui/material';
|
||||
|
||||
const MenuItem = forwardRef(function (
|
||||
{
|
||||
@ -34,14 +34,16 @@ const MenuItem = forwardRef(function (
|
||||
|
||||
return (
|
||||
<div className={className} ref={ref} id={id}>
|
||||
<ListItemButton
|
||||
<MuiMenuItem
|
||||
sx={{
|
||||
borderRadius: '4px',
|
||||
padding: '4px 8px',
|
||||
fontSize: 14,
|
||||
}}
|
||||
selected={isHovered}
|
||||
onMouseEnter={(e) => onHover?.(e)}
|
||||
onMouseEnter={(e) => {
|
||||
onHover?.(e);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -53,7 +55,7 @@ const MenuItem = forwardRef(function (
|
||||
width: imgSize.width,
|
||||
height: imgSize.height,
|
||||
}}
|
||||
className={`mr-2 flex items-center justify-center rounded border border-shade-5`}
|
||||
className={`mr-2 flex items-center justify-center rounded border border-line-divider`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
@ -61,7 +63,7 @@ const MenuItem = forwardRef(function (
|
||||
<div className={'text-sm'}>{title}</div>
|
||||
{desc && (
|
||||
<div
|
||||
className={'font-normal text-shade-4'}
|
||||
className={'font-normal text-text-caption'}
|
||||
style={{
|
||||
fontSize: '0.85em',
|
||||
fontWeight: 300,
|
||||
@ -72,7 +74,7 @@ const MenuItem = forwardRef(function (
|
||||
)}
|
||||
</div>
|
||||
<div>{extra}</div>
|
||||
</ListItemButton>
|
||||
</MuiMenuItem>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -21,6 +21,8 @@ interface Attributes {
|
||||
link_placeholder?: string;
|
||||
temporary?: boolean;
|
||||
formula?: string;
|
||||
font_color?: string;
|
||||
bg_color?: string;
|
||||
}
|
||||
interface TextLeafProps extends RenderLeafProps {
|
||||
leaf: BaseText & Attributes;
|
||||
@ -122,7 +124,15 @@ const TextLeaf = (props: TextLeafProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<span ref={ref} {...customAttributes} className={className.join(' ')}>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: leaf.bg_color,
|
||||
color: leaf.font_color,
|
||||
}}
|
||||
ref={ref}
|
||||
{...customAttributes}
|
||||
className={className.join(' ')}
|
||||
>
|
||||
{newChildren}
|
||||
</span>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
function ToolbarTooltip({ title, children }: { children: JSX.Element; title?: string }) {
|
||||
return (
|
||||
<Tooltip
|
||||
disableInteractive
|
||||
slotProps={{ tooltip: { style: { background: 'var(--bg-tips)', borderRadius: 8 } } }}
|
||||
title={title}
|
||||
placement='top-start'
|
||||
|
@ -31,10 +31,12 @@ interface Option {
|
||||
const TurnIntoPopover = ({
|
||||
id,
|
||||
onClose,
|
||||
onOk,
|
||||
...props
|
||||
}: {
|
||||
id: string;
|
||||
onClose?: () => void;
|
||||
onOk?: () => void;
|
||||
} & PopoverProps) => {
|
||||
const { node } = useSubscribeNode(id);
|
||||
const { turnIntoHeading, turnIntoBlock } = useTurnInto({ node, onClose });
|
||||
@ -142,8 +144,9 @@ const TurnIntoPopover = ({
|
||||
const isSelected = getSelected(option);
|
||||
|
||||
option.onClick ? option.onClick(option.type, isSelected) : turnIntoBlock(option.type, isSelected);
|
||||
onOk?.();
|
||||
},
|
||||
[getSelected, turnIntoBlock]
|
||||
[onOk, getSelected, turnIntoBlock]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Keyboard } from '$app/constants/document/keyboard';
|
||||
|
||||
export const useBindArrowKey = ({
|
||||
options,
|
||||
onLeft,
|
||||
onRight,
|
||||
onEnter,
|
||||
onChange,
|
||||
selectOption,
|
||||
}: {
|
||||
options: string[];
|
||||
onLeft?: () => void;
|
||||
onRight?: () => void;
|
||||
onEnter?: () => void;
|
||||
onChange?: (key: string) => void;
|
||||
selectOption?: string | null;
|
||||
}) => {
|
||||
const onUp = useCallback(() => {
|
||||
const getSelected = () => {
|
||||
const index = options.findIndex((item) => item === selectOption);
|
||||
|
||||
if (index === -1) return options[0];
|
||||
const length = options.length;
|
||||
|
||||
return options[(index + length - 1) % length];
|
||||
};
|
||||
|
||||
onChange?.(getSelected());
|
||||
}, [onChange, options, selectOption]);
|
||||
|
||||
const onDown = useCallback(() => {
|
||||
const getSelected = () => {
|
||||
const index = options.findIndex((item) => item === selectOption);
|
||||
|
||||
if (index === -1) return options[0];
|
||||
const length = options.length;
|
||||
|
||||
return options[(index + 1) % length];
|
||||
};
|
||||
|
||||
onChange?.(getSelected());
|
||||
}, [onChange, options, selectOption]);
|
||||
|
||||
const handleArrowKey = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (
|
||||
[Keyboard.keys.UP, Keyboard.keys.DOWN, Keyboard.keys.LEFT, Keyboard.keys.RIGHT, Keyboard.keys.ENTER].includes(
|
||||
e.key
|
||||
)
|
||||
) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.key === Keyboard.keys.UP) {
|
||||
onUp();
|
||||
} else if (e.key === Keyboard.keys.DOWN) {
|
||||
onDown();
|
||||
} else if (e.key === Keyboard.keys.LEFT) {
|
||||
onLeft?.();
|
||||
} else if (e.key === Keyboard.keys.RIGHT) {
|
||||
onRight?.();
|
||||
} else if (e.key === Keyboard.keys.ENTER) {
|
||||
onEnter?.();
|
||||
}
|
||||
},
|
||||
[onDown, onEnter, onLeft, onRight, onUp]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleArrowKey, true);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleArrowKey, true);
|
||||
};
|
||||
}, [handleArrowKey]);
|
||||
};
|
@ -235,6 +235,8 @@ export enum TextAction {
|
||||
Code = 'code',
|
||||
Equation = 'formula',
|
||||
Link = 'href',
|
||||
TextColor = 'font_color',
|
||||
Highlight = 'bg_color',
|
||||
}
|
||||
export interface TextActionMenuProps {
|
||||
/**
|
||||
|
@ -5,6 +5,35 @@ import { DocumentController } from '$app/stores/effects/document/document_contro
|
||||
import Delta from 'quill-delta';
|
||||
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||
|
||||
type FormatValues = Record<string, (boolean | string | undefined)[]>;
|
||||
|
||||
export const getFormatValuesThunk = createAsyncThunk(
|
||||
'document/getFormatValues',
|
||||
({ docId, format }: { docId: string; format: TextAction }, thunkAPI) => {
|
||||
const { getState } = thunkAPI;
|
||||
const state = getState() as RootState;
|
||||
const document = state[DOCUMENT_NAME][docId];
|
||||
const documentRange = state[RANGE_NAME][docId];
|
||||
const { ranges } = documentRange;
|
||||
const mapAttrs = (delta: Delta, format: TextAction) => {
|
||||
return delta.ops.map((op) => op.attributes?.[format] as boolean | string | undefined);
|
||||
};
|
||||
|
||||
const formatValues: FormatValues = {};
|
||||
|
||||
Object.entries(ranges).forEach(([id, range]) => {
|
||||
const node = document.nodes[id];
|
||||
const delta = new Delta(node.data?.delta);
|
||||
const index = range?.index || 0;
|
||||
const length = range?.length || 0;
|
||||
const rangeDelta = delta.slice(index, index + length);
|
||||
|
||||
formatValues[id] = mapAttrs(rangeDelta, format);
|
||||
});
|
||||
return formatValues;
|
||||
}
|
||||
);
|
||||
|
||||
export const getFormatActiveThunk = createAsyncThunk<
|
||||
boolean,
|
||||
{
|
||||
@ -12,30 +41,22 @@ export const getFormatActiveThunk = createAsyncThunk<
|
||||
docId: string;
|
||||
}
|
||||
>('document/getFormatActive', async ({ format, docId }, thunkAPI) => {
|
||||
const { getState } = thunkAPI;
|
||||
const state = getState() as RootState;
|
||||
const document = state[DOCUMENT_NAME][docId];
|
||||
const documentRange = state[RANGE_NAME][docId];
|
||||
const { ranges } = documentRange;
|
||||
const match = (delta: Delta, format: TextAction) => {
|
||||
return delta.ops.every((op) => op.attributes?.[format]);
|
||||
const { dispatch } = thunkAPI;
|
||||
const { payload } = (await dispatch(getFormatValuesThunk({ docId, format }))) as {
|
||||
payload: FormatValues;
|
||||
};
|
||||
|
||||
return Object.entries(ranges).every(([id, range]) => {
|
||||
const node = document.nodes[id];
|
||||
const delta = new Delta(node.data?.delta);
|
||||
const index = range?.index || 0;
|
||||
const length = range?.length || 0;
|
||||
const rangeDelta = delta.slice(index, index + length);
|
||||
|
||||
return match(rangeDelta, format);
|
||||
return Object.values(payload).every((values) => {
|
||||
return values.every((value) => {
|
||||
return value !== undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export const toggleFormatThunk = createAsyncThunk(
|
||||
'document/toggleFormat',
|
||||
async (payload: { format: TextAction; controller: DocumentController; isActive?: boolean }, thunkAPI) => {
|
||||
const { getState, dispatch } = thunkAPI;
|
||||
const { dispatch } = thunkAPI;
|
||||
const { format, controller } = payload;
|
||||
const docId = controller.documentId;
|
||||
let isActive = payload.isActive;
|
||||
@ -51,38 +72,30 @@ export const toggleFormatThunk = createAsyncThunk(
|
||||
isActive = !!active;
|
||||
}
|
||||
|
||||
const formatValue = isActive ? undefined : true;
|
||||
const formatValue = isActive ? null : true;
|
||||
await dispatch(formatThunk({ format, value: formatValue, controller }));
|
||||
}
|
||||
);
|
||||
|
||||
export const formatThunk = createAsyncThunk(
|
||||
'document/format',
|
||||
async (payload: { format: TextAction; value: string | boolean | null; controller: DocumentController }, thunkAPI) => {
|
||||
const { getState } = thunkAPI;
|
||||
const { format, controller, value } = payload;
|
||||
const docId = controller.documentId;
|
||||
const state = getState() as RootState;
|
||||
const document = state[DOCUMENT_NAME][docId];
|
||||
const documentRange = state[RANGE_NAME][docId];
|
||||
const { ranges } = documentRange;
|
||||
|
||||
const toggle = (delta: Delta, format: TextAction, value: string | boolean | undefined) => {
|
||||
const newOps = delta.ops.map((op) => {
|
||||
const attributes = {
|
||||
...op.attributes,
|
||||
[format]: value,
|
||||
};
|
||||
|
||||
return {
|
||||
insert: op.insert,
|
||||
attributes: attributes,
|
||||
};
|
||||
});
|
||||
|
||||
return new Delta(newOps);
|
||||
};
|
||||
|
||||
const actions = Object.entries(ranges).map(([id, range]) => {
|
||||
const node = document.nodes[id];
|
||||
const delta = new Delta(node.data?.delta);
|
||||
const index = range?.index || 0;
|
||||
const length = range?.length || 0;
|
||||
const beforeDelta = delta.slice(0, index);
|
||||
const afterDelta = delta.slice(index + length);
|
||||
const rangeDelta = delta.slice(index, index + length);
|
||||
const toggleFormatDelta = toggle(rangeDelta, format, formatValue);
|
||||
const newDelta = beforeDelta.concat(toggleFormatDelta).concat(afterDelta);
|
||||
const diffDelta: Delta = new Delta();
|
||||
diffDelta.retain(index).retain(length, { [format]: value });
|
||||
const newDelta = delta.compose(diffDelta);
|
||||
|
||||
return controller.getUpdateAction({
|
||||
...node,
|
||||
|
@ -47,4 +47,19 @@ th {
|
||||
span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
@apply text-text-placeholder;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.sketch-picker {
|
||||
background-color: var(--bg-body) !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.sketch-picker .flexbox-fix {
|
||||
border-color: var(--line-divider) !important;
|
||||
}
|
||||
.sketch-picker [id^='rc-editable-input'] {
|
||||
background-color: var(--bg-body) !important;
|
||||
border-color: var(--line-divider) !important;
|
||||
color: var(--text-title) !important;
|
||||
box-shadow: var(--line-border) 0px 0px 0px 1px inset !important;
|
||||
}
|
@ -608,5 +608,18 @@
|
||||
"views": {
|
||||
"deleteContentTitle": "Are you sure want to delete the {pageType}?",
|
||||
"deleteContentCaption": "if you delete this {pageType}, you can restore it from the trash."
|
||||
},
|
||||
"colors": {
|
||||
"custom": "Custom",
|
||||
"default": "Default",
|
||||
"red": "Red",
|
||||
"orange": "Orange",
|
||||
"yellow": "Yellow",
|
||||
"green": "Green",
|
||||
"blue": "Blue",
|
||||
"purple": "Purple",
|
||||
"pink": "Pink",
|
||||
"brown": "Brown",
|
||||
"gray": "Gray"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user