mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support adding cover to document (#4814)
This commit is contained in:
parent
5daf9d23f5
commit
c0210a5778
@ -2,7 +2,8 @@ import { Op } from 'quill-delta';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { Element } from 'slate';
|
||||
import { ViewIconTypePB, ViewLayoutPB } from '@/services/backend';
|
||||
import { YXmlText } from 'yjs/dist/src/types/YXmlText';
|
||||
import { PageCover } from '$app_reducers/pages/slice';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export interface EditorNode {
|
||||
id: string;
|
||||
@ -162,14 +163,22 @@ export interface MentionPage {
|
||||
}
|
||||
|
||||
export interface EditorProps {
|
||||
id: string;
|
||||
sharedType?: YXmlText;
|
||||
title?: string;
|
||||
cover?: PageCover;
|
||||
onTitleChange?: (title: string) => void;
|
||||
onCoverChange?: (cover?: PageCover) => void;
|
||||
showTitle?: boolean;
|
||||
id: string;
|
||||
disableFocus?: boolean;
|
||||
}
|
||||
|
||||
export interface LocalEditorProps {
|
||||
disableFocus?: boolean;
|
||||
sharedType: Y.XmlText;
|
||||
id: string;
|
||||
caretColor?: string;
|
||||
}
|
||||
|
||||
export enum EditorNodeType {
|
||||
Text = 'text',
|
||||
Paragraph = 'paragraph',
|
||||
@ -221,7 +230,9 @@ export enum MentionType {
|
||||
|
||||
export interface Mention {
|
||||
// inline page ref id
|
||||
page?: string;
|
||||
page_id?: string;
|
||||
// reminder date ref id
|
||||
date?: string;
|
||||
|
||||
type: MentionType;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5C1 1.67157 1.67157 1 2.5 1ZM2.5 2C2.22386 2 2 2.22386 2 2.5V8.3636L3.6818 6.6818C3.76809 6.59551 3.88572 6.54797 4.00774 6.55007C4.12975 6.55216 4.24568 6.60372 4.32895 6.69293L7.87355 10.4901L10.6818 7.6818C10.8575 7.50607 11.1425 7.50607 11.3182 7.6818L13 9.3636V2.5C13 2.22386 12.7761 2 12.5 2H2.5ZM2 12.5V9.6364L3.98887 7.64753L7.5311 11.4421L8.94113 13H2.5C2.22386 13 2 12.7761 2 12.5ZM12.5 13H10.155L8.48336 11.153L11 8.6364L13 10.6364V12.5C13 12.7761 12.7761 13 12.5 13ZM6.64922 5.5C6.64922 5.03013 7.03013 4.64922 7.5 4.64922C7.96987 4.64922 8.35078 5.03013 8.35078 5.5C8.35078 5.96987 7.96987 6.35078 7.5 6.35078C7.03013 6.35078 6.64922 5.96987 6.64922 5.5ZM7.5 3.74922C6.53307 3.74922 5.74922 4.53307 5.74922 5.5C5.74922 6.46693 6.53307 7.25078 7.5 7.25078C8.46693 7.25078 9.25078 6.46693 9.25078 5.5C9.25078 4.53307 8.46693 3.74922 7.5 3.74922Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.5" y="3" width="13" height="10" rx="1.5" stroke="#333333"/>
|
||||
<circle cx="5.5" cy="6.5" r="1" stroke="#333333"/>
|
||||
<path d="M5 13L10.112 8.45603C10.4211 8.18126 10.8674 8.12513 11.235 8.31482L14.5 10" stroke="#333333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 342 B |
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export function Colors() {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export default Colors;
|
@ -1,13 +1,22 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { pattern } from '$app/utils/open_url';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => void; onEscape?: () => void }) {
|
||||
const urlPattern = /^https?:\/\/.+/;
|
||||
|
||||
export function EmbedLink({
|
||||
onDone,
|
||||
onEscape,
|
||||
defaultLink,
|
||||
}: {
|
||||
defaultLink?: string;
|
||||
onDone?: (value: string) => void;
|
||||
onEscape?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
const [value, setValue] = useState(defaultLink ?? '');
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleChange = useCallback(
|
||||
@ -15,7 +24,7 @@ export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => vo
|
||||
const value = e.target.value;
|
||||
|
||||
setValue(value);
|
||||
setError(!pattern.test(value));
|
||||
setError(!urlPattern.test(value));
|
||||
},
|
||||
[setValue, setError]
|
||||
);
|
||||
|
@ -0,0 +1,123 @@
|
||||
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||
import Popover, { PopoverProps } from '@mui/material/Popover';
|
||||
import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs';
|
||||
import SwipeableViews from 'react-swipeable-views';
|
||||
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
||||
|
||||
export enum TAB_KEY {
|
||||
Colors = 'colors',
|
||||
UPLOAD = 'upload',
|
||||
EMBED_LINK = 'embed_link',
|
||||
UNSPLASH = 'unsplash',
|
||||
}
|
||||
|
||||
export type TabOption = {
|
||||
key: TAB_KEY;
|
||||
label: string;
|
||||
Component: React.ComponentType<{
|
||||
onDone?: (value: string) => void;
|
||||
onEscape?: () => void;
|
||||
}>;
|
||||
onDone?: (value: string) => void;
|
||||
};
|
||||
|
||||
export function UploadTabs({
|
||||
tabOptions,
|
||||
popoverProps,
|
||||
containerStyle,
|
||||
}: {
|
||||
containerStyle?: React.CSSProperties;
|
||||
tabOptions: TabOption[];
|
||||
popoverProps?: PopoverProps;
|
||||
}) {
|
||||
const [tabValue, setTabValue] = useState<TAB_KEY>(() => {
|
||||
return tabOptions[0].key;
|
||||
});
|
||||
|
||||
const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => {
|
||||
setTabValue(newValue as TAB_KEY);
|
||||
}, []);
|
||||
|
||||
const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
popoverProps?.onClose?.({}, 'escapeKeyDown');
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTabValue((prev) => {
|
||||
const currentIndex = tabOptions.findIndex((tab) => tab.key === prev);
|
||||
let nextIndex = currentIndex + 1;
|
||||
|
||||
if (e.shiftKey) {
|
||||
nextIndex = currentIndex - 1;
|
||||
}
|
||||
|
||||
return tabOptions[nextIndex % tabOptions.length]?.key ?? tabOptions[0].key;
|
||||
});
|
||||
}
|
||||
},
|
||||
[popoverProps, tabOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
{...popoverProps}
|
||||
{...PopoverCommonProps}
|
||||
open={popoverProps?.open ?? false}
|
||||
disableAutoFocus={false}
|
||||
onKeyDown={onKeyDown}
|
||||
PaperProps={{
|
||||
style: {
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={containerStyle} className={'flex flex-col gap-4 overflow-hidden'}>
|
||||
<ViewTabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
scrollButtons={false}
|
||||
variant='scrollable'
|
||||
allowScrollButtonsMobile
|
||||
className={'min-h-[38px] border-b border-line-divider px-2'}
|
||||
>
|
||||
{tabOptions.map((tab) => {
|
||||
const { key, label } = tab;
|
||||
|
||||
return <ViewTab key={key} iconPosition='start' color='inherit' label={label} value={key} />;
|
||||
})}
|
||||
</ViewTabs>
|
||||
|
||||
<div className={'h-full w-full flex-1 overflow-y-auto overflow-x-hidden'}>
|
||||
<SwipeableViews
|
||||
slideStyle={{
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
}}
|
||||
axis={'x'}
|
||||
index={selectedIndex}
|
||||
>
|
||||
{tabOptions.map((tab, index) => {
|
||||
const { key, Component, onDone } = tab;
|
||||
|
||||
return (
|
||||
<TabPanel className={'flex h-full w-full flex-col'} key={key} index={index} value={selectedIndex}>
|
||||
<Component onDone={onDone} onEscape={() => popoverProps?.onClose?.({}, 'escapeKeyDown')} />
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</SwipeableViews>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export * from './Unsplash';
|
||||
export * from './UploadImage';
|
||||
export * from './EmbedLink';
|
||||
export * from './Colors';
|
||||
export * from './UploadTabs';
|
||||
|
@ -1,33 +1,52 @@
|
||||
import ViewIconGroup from '$app/components/_shared/view_title/ViewIconGroup';
|
||||
import { PageIcon } from '$app_reducers/pages/slice';
|
||||
import { PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||
import ViewIcon from '$app/components/_shared/view_title/ViewIcon';
|
||||
import { ViewCover } from '$app/components/_shared/view_title/cover';
|
||||
|
||||
function ViewBanner({
|
||||
icon,
|
||||
hover,
|
||||
onUpdateIcon,
|
||||
showCover,
|
||||
cover,
|
||||
onUpdateCover,
|
||||
}: {
|
||||
icon?: PageIcon;
|
||||
hover: boolean;
|
||||
onUpdateIcon: (icon: string) => void;
|
||||
showCover: boolean;
|
||||
cover?: PageCover;
|
||||
onUpdateCover?: (cover?: PageCover) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: icon ? 'flex' : 'none',
|
||||
}}
|
||||
>
|
||||
<ViewIcon onUpdateIcon={onUpdateIcon} icon={icon} />
|
||||
<div className={'view-banner flex w-full flex-col'}>
|
||||
{showCover && cover && <ViewCover cover={cover} onUpdateCover={onUpdateCover} />}
|
||||
|
||||
<div className={'relative min-h-[65px] px-16 pt-4'}>
|
||||
<div
|
||||
style={{
|
||||
display: icon ? 'flex' : 'none',
|
||||
position: cover ? 'absolute' : 'relative',
|
||||
bottom: cover ? '24px' : 'auto',
|
||||
}}
|
||||
>
|
||||
<ViewIcon onUpdateIcon={onUpdateIcon} icon={icon} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
opacity: hover ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<ViewIconGroup
|
||||
icon={icon}
|
||||
onUpdateIcon={onUpdateIcon}
|
||||
showCover={showCover}
|
||||
cover={cover}
|
||||
onUpdateCover={onUpdateCover}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
opacity: hover ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<ViewIconGroup icon={icon} onUpdateIcon={onUpdateIcon} />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon
|
||||
if (!icon) return null;
|
||||
return (
|
||||
<>
|
||||
<div className={`-ml-2 flex rounded p-2 hover:bg-content-blue-50`}>
|
||||
<div className={`view-icon -ml-2 flex rounded p-2`}>
|
||||
<div onClick={onOpen} className={'h-full w-full cursor-pointer rounded text-6xl'}>
|
||||
{icon.value}
|
||||
</div>
|
||||
|
@ -1,31 +1,42 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PageIcon } from '$app_reducers/pages/slice';
|
||||
import { CoverType, PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||
import React, { useCallback } from 'react';
|
||||
import { randomEmoji } from '$app/utils/emoji';
|
||||
import { EmojiEmotionsOutlined } from '@mui/icons-material';
|
||||
import Button from '@mui/material/Button';
|
||||
import { ReactComponent as ImageIcon } from '$app/assets/image.svg';
|
||||
import { ImageType } from '$app/application/document/document.types';
|
||||
|
||||
interface Props {
|
||||
icon?: PageIcon;
|
||||
// onUpdateCover: (coverType: CoverType, cover: string) => void;
|
||||
onUpdateIcon: (icon: string) => void;
|
||||
showCover: boolean;
|
||||
cover?: PageCover;
|
||||
onUpdateCover?: (cover: PageCover) => void;
|
||||
}
|
||||
function ViewIconGroup({ icon, onUpdateIcon }: Props) {
|
||||
|
||||
const defaultCover = {
|
||||
cover_selection_type: CoverType.Asset,
|
||||
cover_selection: 'app_flowy_abstract_cover_2.jpeg',
|
||||
image_type: ImageType.Internal,
|
||||
};
|
||||
|
||||
function ViewIconGroup({ icon, onUpdateIcon, showCover, cover, onUpdateCover }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showAddIcon = !icon?.value;
|
||||
|
||||
const showAddCover = !cover && showCover;
|
||||
|
||||
const onAddIcon = useCallback(() => {
|
||||
const emoji = randomEmoji();
|
||||
|
||||
onUpdateIcon(emoji);
|
||||
}, [onUpdateIcon]);
|
||||
|
||||
// const onAddCover = useCallback(() => {
|
||||
// const color = randomColor();
|
||||
//
|
||||
// onUpdateCover(CoverType.Color, color);
|
||||
// }, []);
|
||||
const onAddCover = useCallback(() => {
|
||||
onUpdateCover?.(defaultCover);
|
||||
}, [onUpdateCover]);
|
||||
|
||||
return (
|
||||
<div className={'flex items-center py-2'}>
|
||||
@ -34,11 +45,11 @@ function ViewIconGroup({ icon, onUpdateIcon }: Props) {
|
||||
{t('document.plugins.cover.addIcon')}
|
||||
</Button>
|
||||
)}
|
||||
{/*{showAddCover && (*/}
|
||||
{/* <Button onClick={onAddCover} color={'inherit'} startIcon={<ImageOutlined />}>*/}
|
||||
{/* {t('document.plugins.cover.addCover')}*/}
|
||||
{/* </Button>*/}
|
||||
{/*)}*/}
|
||||
{showAddCover && (
|
||||
<Button onClick={onAddCover} color={'inherit'} startIcon={<ImageIcon />}>
|
||||
{t('document.plugins.cover.addCover')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import ViewBanner from '$app/components/_shared/view_title/ViewBanner';
|
||||
import { Page, PageIcon } from '$app_reducers/pages/slice';
|
||||
import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||
import { ViewIconTypePB } from '@/services/backend';
|
||||
import ViewTitleInput from '$app/components/_shared/view_title/ViewTitleInput';
|
||||
|
||||
@ -9,9 +9,20 @@ interface Props {
|
||||
showTitle?: boolean;
|
||||
onTitleChange?: (title: string) => void;
|
||||
onUpdateIcon?: (icon: PageIcon) => void;
|
||||
forceHover?: boolean;
|
||||
showCover?: boolean;
|
||||
onUpdateCover?: (cover?: PageCover) => void;
|
||||
}
|
||||
|
||||
function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpdateIconProp }: Props) {
|
||||
function ViewTitle({
|
||||
view,
|
||||
forceHover = false,
|
||||
onTitleChange,
|
||||
showTitle = true,
|
||||
onUpdateIcon: onUpdateIconProp,
|
||||
showCover = false,
|
||||
onUpdateCover,
|
||||
}: Props) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [icon, setIcon] = useState<PageIcon | undefined>(view.icon);
|
||||
|
||||
@ -38,7 +49,14 @@ function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpda
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<ViewBanner icon={icon} hover={hover} onUpdateIcon={onUpdateIcon} />
|
||||
<ViewBanner
|
||||
showCover={showCover}
|
||||
cover={view.cover}
|
||||
icon={icon}
|
||||
hover={hover || forceHover}
|
||||
onUpdateIcon={onUpdateIcon}
|
||||
onUpdateCover={onUpdateCover}
|
||||
/>
|
||||
{showTitle && (
|
||||
<div className='relative'>
|
||||
<ViewTitleInput value={view.name} onChange={onTitleChange} />
|
||||
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { colorMap } from '$app/utils/color';
|
||||
|
||||
const colors = Object.entries(colorMap);
|
||||
|
||||
function Colors({ onDone }: { onDone?: (value: string) => void }) {
|
||||
return (
|
||||
<div className={'flex flex-wrap justify-center gap-2 p-2 pb-6'}>
|
||||
{colors.map(([name, value]) => (
|
||||
<div
|
||||
key={name}
|
||||
className={'h-9 w-9 cursor-pointer rounded rounded-full'}
|
||||
style={{ backgroundColor: value }}
|
||||
onClick={() => onDone?.(name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Colors;
|
@ -0,0 +1,91 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { CoverType, PageCover } from '$app_reducers/pages/slice';
|
||||
import { PopoverOrigin } from '@mui/material/Popover';
|
||||
import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY } from '$app/components/_shared/image_upload';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Colors from '$app/components/_shared/view_title/cover/Colors';
|
||||
import { ImageType } from '$app/application/document/document.types';
|
||||
|
||||
const initialOrigin: {
|
||||
anchorOrigin: PopoverOrigin;
|
||||
transformOrigin: PopoverOrigin;
|
||||
} = {
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
function CoverPopover({
|
||||
anchorEl,
|
||||
open,
|
||||
onClose,
|
||||
onUpdateCover,
|
||||
}: {
|
||||
anchorEl: HTMLElement | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onUpdateCover?: (cover?: PageCover) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tabOptions: TabOption[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: t('document.plugins.cover.colors'),
|
||||
key: TAB_KEY.Colors,
|
||||
Component: Colors,
|
||||
onDone: (value: string) => {
|
||||
onUpdateCover?.({
|
||||
cover_selection_type: CoverType.Color,
|
||||
cover_selection: value,
|
||||
image_type: ImageType.Internal,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('document.imageBlock.embedLink.label'),
|
||||
key: TAB_KEY.EMBED_LINK,
|
||||
Component: EmbedLink,
|
||||
onDone: (value: string) => {
|
||||
onUpdateCover?.({
|
||||
cover_selection_type: CoverType.Image,
|
||||
cover_selection: value,
|
||||
image_type: ImageType.External,
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: TAB_KEY.UNSPLASH,
|
||||
label: t('document.imageBlock.unsplash.label'),
|
||||
Component: Unsplash,
|
||||
onDone: (value: string) => {
|
||||
onUpdateCover?.({
|
||||
cover_selection_type: CoverType.Image,
|
||||
cover_selection: value,
|
||||
image_type: ImageType.External,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [onClose, onUpdateCover, t]);
|
||||
|
||||
return (
|
||||
<UploadTabs
|
||||
popoverProps={{
|
||||
anchorEl,
|
||||
open,
|
||||
onClose,
|
||||
...initialOrigin,
|
||||
}}
|
||||
containerStyle={{ width: 433, maxHeight: 300 }}
|
||||
tabOptions={tabOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default CoverPopover;
|
@ -0,0 +1,66 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { CoverType, PageCover } from '$app_reducers/pages/slice';
|
||||
import { renderColor } from '$app/utils/color';
|
||||
import ViewCoverActions from '$app/components/_shared/view_title/cover/ViewCoverActions';
|
||||
import CoverPopover from '$app/components/_shared/view_title/cover/CoverPopover';
|
||||
import DefaultImage from '$app/assets/images/default_cover.jpg';
|
||||
|
||||
export function ViewCover({ cover, onUpdateCover }: { cover: PageCover; onUpdateCover?: (cover?: PageCover) => void }) {
|
||||
const { cover_selection_type: type, cover_selection: value = '' } = useMemo(() => cover || {}, [cover]);
|
||||
const [showAction, setShowAction] = useState(false);
|
||||
const actionRef = useRef<HTMLDivElement>(null);
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
|
||||
const renderCoverColor = useCallback((color: string) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: renderColor(color),
|
||||
}}
|
||||
className={`h-full w-full`}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderCoverImage = useCallback((url: string) => {
|
||||
return <img draggable={false} src={url} alt={''} className={'h-full w-full object-cover'} />;
|
||||
}, []);
|
||||
|
||||
const handleRemoveCover = useCallback(() => {
|
||||
onUpdateCover?.(null);
|
||||
}, [onUpdateCover]);
|
||||
|
||||
const handleClickChange = useCallback(() => {
|
||||
setShowPopover(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
setShowAction(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowAction(false);
|
||||
}}
|
||||
className={'relative flex h-[255px] w-full'}
|
||||
>
|
||||
{type === CoverType.Asset ? renderCoverImage(DefaultImage) : null}
|
||||
{type === CoverType.Color ? renderCoverColor(value) : null}
|
||||
{type === CoverType.Image ? renderCoverImage(value) : null}
|
||||
<ViewCoverActions
|
||||
show={showAction}
|
||||
ref={actionRef}
|
||||
onRemove={handleRemoveCover}
|
||||
onClickChange={handleClickChange}
|
||||
/>
|
||||
{showPopover && (
|
||||
<CoverPopover
|
||||
open={showPopover}
|
||||
onClose={() => setShowPopover(false)}
|
||||
anchorEl={actionRef.current}
|
||||
onUpdateCover={onUpdateCover}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg';
|
||||
|
||||
function ViewCoverActions(
|
||||
{ show, onRemove, onClickChange }: { show: boolean; onRemove: () => void; onClickChange: () => void },
|
||||
ref: React.ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div ref={ref} className={`absolute ${show ? 'flex' : 'opacity-0'} bottom-4 right-10 items-center space-x-2 p-2`}>
|
||||
<div className={'flex items-center space-x-2'}>
|
||||
<Button
|
||||
onClick={onClickChange}
|
||||
className={'min-w-0 p-1.5'}
|
||||
size={'small'}
|
||||
variant={'contained'}
|
||||
color={'inherit'}
|
||||
>
|
||||
{t('document.plugins.cover.changeCover')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'contained'}
|
||||
size={'small'}
|
||||
className={'min-h-0 min-w-0 p-1.5'}
|
||||
sx={{
|
||||
'.MuiButton-startIcon': {
|
||||
marginRight: '0px',
|
||||
},
|
||||
}}
|
||||
onClick={onRemove}
|
||||
color={'inherit'}
|
||||
startIcon={<DeleteIcon />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(ViewCoverActions);
|
@ -0,0 +1 @@
|
||||
export * from './ViewCover';
|
@ -1,12 +1,14 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Editor from '$app/components/editor/Editor';
|
||||
import { DocumentHeader } from 'src/appflowy_app/components/document/document_header';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||
import { PageCover } from '$app_reducers/pages/slice';
|
||||
|
||||
export function Document({ id }: { id: string }) {
|
||||
const page = useAppSelector((state) => state.pages.pageMap[id]);
|
||||
|
||||
const [cover, setCover] = useState<PageCover | undefined>(undefined);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onTitleChange = useCallback(
|
||||
@ -21,12 +23,29 @@ export function Document({ id }: { id: string }) {
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const view = useMemo(() => {
|
||||
return {
|
||||
...page,
|
||||
cover,
|
||||
};
|
||||
}, [page, cover]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setCover(undefined);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (!page) return null;
|
||||
|
||||
return (
|
||||
<div className={'relative'}>
|
||||
<DocumentHeader page={page} />
|
||||
<Editor id={id} onTitleChange={onTitleChange} title={page.name} />
|
||||
<div className={'relative w-full'}>
|
||||
<DocumentHeader onUpdateCover={setCover} page={view} />
|
||||
<div className={'flex w-full justify-center'}>
|
||||
<div className={'max-w-screen w-[964px] min-w-0'}>
|
||||
<Editor id={id} cover={cover} onCoverChange={setCover} onTitleChange={onTitleChange} title={page.name} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { Page, PageIcon } from '$app_reducers/pages/slice';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||
import ViewTitle from '$app/components/_shared/view_title/ViewTitle';
|
||||
import { updatePageIcon } from '$app/application/folder/page.service';
|
||||
|
||||
interface DocumentHeaderProps {
|
||||
page: Page;
|
||||
onUpdateCover: (cover?: PageCover) => void;
|
||||
}
|
||||
|
||||
export function DocumentHeader({ page }: DocumentHeaderProps) {
|
||||
export function DocumentHeader({ page, onUpdateCover }: DocumentHeaderProps) {
|
||||
const pageId = page.id;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [forceHover, setForceHover] = useState(false);
|
||||
const onUpdateIcon = useCallback(
|
||||
async (icon: PageIcon) => {
|
||||
await updatePageIcon(pageId, icon.value ? icon : undefined);
|
||||
@ -17,10 +20,39 @@ export function DocumentHeader({ page }: DocumentHeaderProps) {
|
||||
[pageId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const parent = ref.current?.parentElement;
|
||||
|
||||
if (!parent) return;
|
||||
|
||||
const documentDom = parent.querySelector('.appflowy-editor') as HTMLElement;
|
||||
|
||||
if (!documentDom) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const isMoveInTitle = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-title'));
|
||||
const isMoveInHeader = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-header'));
|
||||
|
||||
setForceHover(isMoveInTitle || isMoveInHeader);
|
||||
};
|
||||
|
||||
documentDom.addEventListener('mousemove', handleMouseMove);
|
||||
return () => {
|
||||
documentDom.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!page) return null;
|
||||
return (
|
||||
<div className={'document-header select-none px-16 pt-4'}>
|
||||
<ViewTitle showTitle={false} onUpdateIcon={onUpdateIcon} view={page} />
|
||||
<div ref={ref} className={'document-header select-none'}>
|
||||
<ViewTitle
|
||||
showCover
|
||||
showTitle={false}
|
||||
forceHover={forceHover}
|
||||
onUpdateCover={onUpdateCover}
|
||||
onUpdateIcon={onUpdateIcon}
|
||||
view={page}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -49,6 +49,12 @@ export function wrapFormula(editor: ReactEditor, formula?: string) {
|
||||
Transforms.insertNodes(editor, formulaElement, {
|
||||
select: true,
|
||||
});
|
||||
|
||||
const path = editor.selection?.anchor.path;
|
||||
|
||||
if (path) {
|
||||
editor.select(path);
|
||||
}
|
||||
}
|
||||
|
||||
export function unwrapFormula(editor: ReactEditor) {
|
||||
|
@ -254,17 +254,23 @@ export const CustomEditor = {
|
||||
},
|
||||
|
||||
insertMention(editor: ReactEditor, mention: Mention) {
|
||||
const mentionElement = {
|
||||
type: EditorInlineNodeType.Mention,
|
||||
children: [{ text: '@' }],
|
||||
data: {
|
||||
...mention,
|
||||
const mentionElement = [
|
||||
{
|
||||
type: EditorInlineNodeType.Mention,
|
||||
children: [{ text: '$' }],
|
||||
data: {
|
||||
...mention,
|
||||
},
|
||||
},
|
||||
};
|
||||
];
|
||||
|
||||
Transforms.insertNodes(editor, mentionElement, {
|
||||
select: true,
|
||||
});
|
||||
|
||||
editor.collapse({
|
||||
edge: 'end',
|
||||
});
|
||||
},
|
||||
|
||||
toggleTodo(editor: ReactEditor, node: TodoListNode) {
|
||||
|
@ -9,10 +9,8 @@ export const Callout = memo(
|
||||
<div contentEditable={false} className={'absolute w-full select-none px-2 pt-[15px]'}>
|
||||
<CalloutIcon node={node} />
|
||||
</div>
|
||||
<div {...attributes} ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}>
|
||||
<div
|
||||
className={`flex w-full flex-col rounded border border-solid border-line-divider bg-content-blue-50 py-2 pl-10`}
|
||||
>
|
||||
<div ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}>
|
||||
<div {...attributes} className={`flex w-full flex-col rounded bg-content-blue-50 py-2 pl-10`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -24,9 +24,9 @@ export const ImageBlock = memo(
|
||||
onClick={() => {
|
||||
if (!selected) onFocusNode();
|
||||
}}
|
||||
className={`${className} image-block relative w-full cursor-pointer py-1`}
|
||||
className={`${className} image-block relative w-full cursor-pointer py-1`}
|
||||
>
|
||||
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
||||
<div ref={ref} className={'absolute left-0 top-0 h-full w-full select-none caret-transparent'}>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
|
@ -39,7 +39,7 @@ function ImageEmpty({
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
'flex h-[48px] w-full cursor-pointer items-center gap-[10px] bg-content-blue-50 px-4 text-text-caption'
|
||||
'flex h-[48px] w-full cursor-pointer select-none items-center gap-[10px] bg-content-blue-50 px-4 text-text-caption'
|
||||
}
|
||||
>
|
||||
<ImageIcon />
|
||||
|
@ -1,20 +1,13 @@
|
||||
import React, { useCallback, useMemo, SyntheticEvent, useState } from 'react';
|
||||
import Popover, { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||
import React, { useMemo } from 'react';
|
||||
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||
import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks';
|
||||
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
||||
import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EmbedLink, Unsplash } from '$app/components/_shared/image_upload';
|
||||
import SwipeableViews from 'react-swipeable-views';
|
||||
import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY } from '$app/components/_shared/image_upload';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { ImageNode, ImageType } from '$app/application/document/document.types';
|
||||
|
||||
enum TAB_KEY {
|
||||
UPLOAD = 'upload',
|
||||
EMBED_LINK = 'embed_link',
|
||||
UNSPLASH = 'unsplash',
|
||||
}
|
||||
const initialOrigin: {
|
||||
transformOrigin: PopoverOrigin;
|
||||
anchorOrigin: PopoverOrigin;
|
||||
@ -53,7 +46,7 @@ function UploadPopover({
|
||||
open,
|
||||
});
|
||||
|
||||
const tabOptions = useMemo(() => {
|
||||
const tabOptions: TabOption[] = useMemo(() => {
|
||||
return [
|
||||
// {
|
||||
// label: t('button.upload'),
|
||||
@ -87,102 +80,25 @@ function UploadPopover({
|
||||
];
|
||||
}, [editor, node, onClose, t]);
|
||||
|
||||
const [tabValue, setTabValue] = useState<TAB_KEY>(tabOptions[0].key);
|
||||
|
||||
const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => {
|
||||
setTabValue(newValue as TAB_KEY);
|
||||
}, []);
|
||||
|
||||
const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTabValue((prev) => {
|
||||
const currentIndex = tabOptions.findIndex((tab) => tab.key === prev);
|
||||
const nextIndex = (currentIndex + 1) % tabOptions.length;
|
||||
|
||||
return tabOptions[nextIndex]?.key ?? tabOptions[0].key;
|
||||
});
|
||||
}
|
||||
},
|
||||
[onClose, tabOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
{...PopoverCommonProps}
|
||||
disableAutoFocus={false}
|
||||
open={open && isEntered}
|
||||
anchorEl={anchorEl}
|
||||
transformOrigin={transformOrigin}
|
||||
anchorOrigin={anchorOrigin}
|
||||
onClose={onClose}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
PaperProps={{
|
||||
style: {
|
||||
padding: 0,
|
||||
<UploadTabs
|
||||
popoverProps={{
|
||||
anchorEl,
|
||||
open: open && isEntered,
|
||||
onClose,
|
||||
transformOrigin,
|
||||
anchorOrigin,
|
||||
onMouseDown: (e) => {
|
||||
e.stopPropagation();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: paperWidth,
|
||||
maxHeight: paperHeight,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
className={'flex flex-col gap-4'}
|
||||
>
|
||||
<ViewTabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
scrollButtons={false}
|
||||
variant='scrollable'
|
||||
allowScrollButtonsMobile
|
||||
className={'min-h-[38px] border-b border-line-divider px-2'}
|
||||
>
|
||||
{tabOptions.map((tab) => {
|
||||
const { key, label } = tab;
|
||||
|
||||
return <ViewTab key={key} iconPosition='start' color='inherit' label={label} value={key} />;
|
||||
})}
|
||||
</ViewTabs>
|
||||
|
||||
<div className={'h-full w-full flex-1 overflow-y-auto overflow-x-hidden'}>
|
||||
<SwipeableViews
|
||||
slideStyle={{
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
}}
|
||||
axis={'x'}
|
||||
index={selectedIndex}
|
||||
>
|
||||
{tabOptions.map((tab, index) => {
|
||||
const { key, Component, onDone } = tab;
|
||||
|
||||
return (
|
||||
<TabPanel className={'flex h-full w-full flex-col'} key={key} index={index} value={selectedIndex}>
|
||||
<Component onDone={onDone} onEscape={onClose} />
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</SwipeableViews>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
containerStyle={{
|
||||
maxWidth: paperWidth,
|
||||
maxHeight: paperHeight,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
tabOptions={tabOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { EditorElementProps, PageNode } from '$app/application/document/document
|
||||
export const Page = memo(
|
||||
forwardRef<HTMLDivElement, EditorElementProps<PageNode>>(({ node: _, children, ...attributes }, ref) => {
|
||||
const className = useMemo(() => {
|
||||
return `${attributes.className ?? ''} pb-3 text-4xl font-bold`;
|
||||
return `${attributes.className ?? ''} document-title pb-3 text-4xl font-bold`;
|
||||
}, [attributes.className]);
|
||||
|
||||
return (
|
||||
|
@ -5,62 +5,87 @@ import { EditorProps } from '$app/application/document/document.types';
|
||||
import { Provider } from '$app/components/editor/provider';
|
||||
import { YXmlText } from 'yjs/dist/src/types/YXmlText';
|
||||
import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
export const CollaborativeEditor = memo(({ id, title, showTitle = true, onTitleChange, disableFocus }: EditorProps) => {
|
||||
const [sharedType, setSharedType] = useState<YXmlText | null>(null);
|
||||
const provider = useMemo(() => {
|
||||
setSharedType(null);
|
||||
return new Provider(id, showTitle);
|
||||
}, [id, showTitle]);
|
||||
export const CollaborativeEditor = memo(
|
||||
({ id, title, cover, showTitle = true, onTitleChange, onCoverChange, ...props }: EditorProps) => {
|
||||
const [sharedType, setSharedType] = useState<YXmlText | null>(null);
|
||||
const provider = useMemo(() => {
|
||||
setSharedType(null);
|
||||
return new Provider(id, showTitle);
|
||||
}, [id, showTitle]);
|
||||
|
||||
const root = useMemo(() => {
|
||||
if (!showTitle || !sharedType || !sharedType.doc) return null;
|
||||
const root = useMemo(() => {
|
||||
if (!showTitle || !sharedType || !sharedType.doc) return null;
|
||||
|
||||
return getYTarget(sharedType?.doc, [0]);
|
||||
}, [sharedType, showTitle]);
|
||||
return getYTarget(sharedType?.doc, [0]);
|
||||
}, [sharedType, showTitle]);
|
||||
|
||||
const rootText = useMemo(() => {
|
||||
if (!root) return null;
|
||||
return getInsertTarget(root, [0]);
|
||||
}, [root]);
|
||||
const rootText = useMemo(() => {
|
||||
if (!root) return null;
|
||||
return getInsertTarget(root, [0]);
|
||||
}, [root]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootText || rootText.toString() === title) return;
|
||||
useEffect(() => {
|
||||
if (!rootText || rootText.toString() === title) return;
|
||||
|
||||
if (rootText.length > 0) {
|
||||
rootText.delete(0, rootText.length);
|
||||
if (rootText.length > 0) {
|
||||
rootText.delete(0, rootText.length);
|
||||
}
|
||||
|
||||
rootText.insert(0, title || '');
|
||||
}, [title, rootText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!root) return;
|
||||
|
||||
const originalCover = root.getAttribute('data')?.cover;
|
||||
|
||||
if (cover === undefined) return;
|
||||
if (isEqual(originalCover, cover)) return;
|
||||
root.setAttribute('data', { cover: cover ? cover : undefined });
|
||||
}, [cover, root]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!root) return;
|
||||
const rootId = root.getAttribute('blockId');
|
||||
|
||||
if (!rootId) return;
|
||||
|
||||
const getCover = () => {
|
||||
const data = root.getAttribute('data');
|
||||
|
||||
onCoverChange?.(data?.cover);
|
||||
};
|
||||
|
||||
getCover();
|
||||
const onChange = () => {
|
||||
onTitleChange?.(root.toString());
|
||||
getCover();
|
||||
};
|
||||
|
||||
root.observeDeep(onChange);
|
||||
return () => root.unobserveDeep(onChange);
|
||||
}, [onTitleChange, root, onCoverChange]);
|
||||
|
||||
useEffect(() => {
|
||||
provider.connect();
|
||||
const handleConnected = () => {
|
||||
setSharedType(provider.sharedType);
|
||||
};
|
||||
|
||||
provider.on('ready', handleConnected);
|
||||
return () => {
|
||||
setSharedType(null);
|
||||
provider.off('ready', handleConnected);
|
||||
provider.disconnect();
|
||||
};
|
||||
}, [provider]);
|
||||
|
||||
if (!sharedType || id !== provider.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
rootText.insert(0, title || '');
|
||||
}, [title, rootText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!root) return;
|
||||
const onChange = () => {
|
||||
onTitleChange?.(root.toString());
|
||||
};
|
||||
|
||||
root.observeDeep(onChange);
|
||||
return () => root.unobserveDeep(onChange);
|
||||
}, [onTitleChange, root]);
|
||||
|
||||
useEffect(() => {
|
||||
provider.connect();
|
||||
const handleConnected = () => {
|
||||
setSharedType(provider.sharedType);
|
||||
};
|
||||
|
||||
provider.on('ready', handleConnected);
|
||||
return () => {
|
||||
setSharedType(null);
|
||||
provider.off('ready', handleConnected);
|
||||
provider.disconnect();
|
||||
};
|
||||
}, [provider]);
|
||||
|
||||
if (!sharedType || id !== provider.id) {
|
||||
return null;
|
||||
return <Editor sharedType={sharedType} id={id} {...props} />;
|
||||
}
|
||||
|
||||
return <Editor sharedType={sharedType} id={id} disableFocus={disableFocus} />;
|
||||
});
|
||||
);
|
||||
|
@ -11,7 +11,6 @@ import { useShortcuts } from 'src/appflowy_app/components/editor/plugins/shortcu
|
||||
import { BlockActionsToolbar } from '$app/components/editor/components/tools/block_actions';
|
||||
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import * as Y from 'yjs';
|
||||
import { NodeEntry } from 'slate';
|
||||
import {
|
||||
DecorateStateProvider,
|
||||
@ -22,8 +21,9 @@ import {
|
||||
} from '$app/components/editor/stores';
|
||||
import CommandPanel from '../tools/command_panel/CommandPanel';
|
||||
import { EditorBlockStateProvider } from '$app/components/editor/stores/block';
|
||||
import { LocalEditorProps } from '$app/application/document/document.types';
|
||||
|
||||
function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: string; disableFocus?: boolean }) {
|
||||
function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: LocalEditorProps) {
|
||||
const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType);
|
||||
const decorateCodeHighlight = useDecorateCodeHighlight(editor);
|
||||
const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor);
|
||||
@ -74,7 +74,10 @@ function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: strin
|
||||
disableFocus={disableFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
decorate={decorate}
|
||||
className={'px-16 caret-text-title outline-none focus:outline-none'}
|
||||
style={{
|
||||
caretColor,
|
||||
}}
|
||||
className={`px-16 outline-none focus:outline-none`}
|
||||
/>
|
||||
<CommandPanel />
|
||||
<div onClick={handleOnClickEnd} className={'relative bottom-0 left-0 h-10 w-full cursor-text'} />
|
||||
|
@ -27,6 +27,7 @@ import { Text as TextComponent } from '../blocks/text';
|
||||
import { Page } from '../blocks/page';
|
||||
import { useElementState } from '$app/components/editor/components/editor/Element.hooks';
|
||||
import UnSupportBlock from '$app/components/editor/components/blocks/_shared/unSupportBlock';
|
||||
import { renderColor } from '$app/utils/color';
|
||||
|
||||
function Element({ element, attributes, children }: RenderElementProps) {
|
||||
const node = element;
|
||||
@ -98,8 +99,8 @@ function Element({ element, attributes, children }: RenderElementProps) {
|
||||
const data = (node.data as BlockData) || {};
|
||||
|
||||
return {
|
||||
backgroundColor: data.bg_color,
|
||||
color: data.font_color,
|
||||
backgroundColor: data.bg_color ? renderColor(data.bg_color) : undefined,
|
||||
color: data.font_color ? renderColor(data.font_color) : undefined,
|
||||
};
|
||||
}, [node.data]);
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { RenderLeafProps } from 'slate-react';
|
||||
import { Link } from '$app/components/editor/components/inline_nodes/link';
|
||||
import { renderColor } from '$app/utils/color';
|
||||
|
||||
export function Leaf({ attributes, children, leaf }: RenderLeafProps) {
|
||||
let newChildren = children;
|
||||
@ -39,11 +40,11 @@ export function Leaf({ attributes, children, leaf }: RenderLeafProps) {
|
||||
const style: CSSProperties = {};
|
||||
|
||||
if (leaf.font_color) {
|
||||
style['color'] = leaf.font_color.replace('0x', '#');
|
||||
style['color'] = renderColor(leaf.font_color);
|
||||
}
|
||||
|
||||
if (leaf.bg_color) {
|
||||
style['backgroundColor'] = leaf.bg_color.replace('0x', '#');
|
||||
style['backgroundColor'] = renderColor(leaf.bg_color);
|
||||
}
|
||||
|
||||
if (leaf.href) {
|
||||
|
@ -3,10 +3,10 @@ import React from 'react';
|
||||
// Put this at the start and end of an inline component to work around this Chromium bug:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
|
||||
|
||||
export const InlineChromiumBugfix = () => (
|
||||
export const InlineChromiumBugfix = ({ className }: { className?: string }) => (
|
||||
<span
|
||||
contentEditable={false}
|
||||
className={'absolute caret-transparent'}
|
||||
className={`absolute caret-transparent ${className ?? ''}`}
|
||||
style={{
|
||||
fontSize: 0,
|
||||
}}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { forwardRef, memo, useCallback, MouseEvent, useRef } from 'react';
|
||||
import React, { forwardRef, memo, useCallback, MouseEvent, useRef, useEffect } from 'react';
|
||||
import { ReactEditor, useSelected, useSlate } from 'slate-react';
|
||||
import { Transforms } from 'slate';
|
||||
import { Editor, Range, Transforms } from 'slate';
|
||||
import { EditorElementProps, FormulaNode } from '$app/application/document/document.types';
|
||||
import FormulaLeaf from '$app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf';
|
||||
import FormulaEditPopover from '$app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover';
|
||||
@ -18,17 +18,27 @@ export const InlineFormula = memo(
|
||||
const selected = useSelected();
|
||||
const open = Boolean(popoverOpen && selected);
|
||||
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && isCollapsed && !open) {
|
||||
const afterPoint = editor.selection ? editor.after(editor.selection) : undefined;
|
||||
|
||||
const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined;
|
||||
|
||||
if (afterStart) {
|
||||
editor.select(afterStart);
|
||||
}
|
||||
}
|
||||
}, [editor, isCollapsed, selected, open]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLSpanElement>) => {
|
||||
const target = e.currentTarget;
|
||||
const path = getNodePath(editor, target);
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
Transforms.select(editor, path);
|
||||
if (editor.selection) {
|
||||
setRange(editor.selection);
|
||||
openPopover();
|
||||
}
|
||||
setRange(path);
|
||||
openPopover();
|
||||
},
|
||||
[editor, openPopover, setRange]
|
||||
);
|
||||
@ -103,9 +113,9 @@ export const InlineFormula = memo(
|
||||
selected ? 'selected' : ''
|
||||
}`}
|
||||
>
|
||||
<InlineChromiumBugfix />
|
||||
<InlineChromiumBugfix className={'left-0'} />
|
||||
<FormulaLeaf formula={formula}>{children}</FormulaLeaf>
|
||||
<InlineChromiumBugfix />
|
||||
<InlineChromiumBugfix className={'right-0'} />
|
||||
</span>
|
||||
{open && (
|
||||
<FormulaEditPopover
|
||||
|
@ -7,13 +7,12 @@ import { InlineChromiumBugfix } from '$app/components/editor/components/inline_n
|
||||
export const Mention = memo(
|
||||
forwardRef<HTMLSpanElement, EditorElementProps<MentionNode>>(({ node, children, ...attributes }, ref) => {
|
||||
return (
|
||||
<>
|
||||
<span {...attributes} contentEditable={false} ref={ref}>
|
||||
<InlineChromiumBugfix />
|
||||
<MentionLeaf mention={node.data}>{children}</MentionLeaf>
|
||||
<InlineChromiumBugfix />
|
||||
</span>
|
||||
</>
|
||||
<span {...attributes} contentEditable={false} className={`relative cursor-pointer`} ref={ref}>
|
||||
<InlineChromiumBugfix className={'left-0'} />
|
||||
<span className={'absolute right-0 top-0 h-full w-0 opacity-0'}>{children}</span>
|
||||
<MentionLeaf mention={node.data} />
|
||||
<InlineChromiumBugfix className={'right-0'} />
|
||||
</span>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
@ -5,23 +5,44 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { pageTypeMap } from '$app_reducers/pages/slice';
|
||||
import { getPage } from '$app/application/folder/page.service';
|
||||
import { useSelected } from 'slate-react';
|
||||
import { useSelected, useSlate } from 'slate-react';
|
||||
import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg';
|
||||
import { notify } from 'src/appflowy_app/components/_shared/notify';
|
||||
import { subscribeNotifications } from '$app/application/notification';
|
||||
import { FolderNotification } from '@/services/backend';
|
||||
import { Editor, Range } from 'slate';
|
||||
|
||||
export function MentionLeaf({ children, mention }: { mention: Mention; children: React.ReactNode }) {
|
||||
export function MentionLeaf({ mention }: { mention: Mention }) {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState<MentionPage | null>(null);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
const editor = useSlate();
|
||||
const selected = useSelected();
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && isCollapsed && page) {
|
||||
const afterPoint = editor.selection ? editor.after(editor.selection) : undefined;
|
||||
|
||||
const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined;
|
||||
|
||||
if (afterStart) {
|
||||
editor.select(afterStart);
|
||||
}
|
||||
}
|
||||
}, [editor, isCollapsed, selected, page]);
|
||||
|
||||
const loadPage = useCallback(async () => {
|
||||
setError(true);
|
||||
if (!mention.page) return;
|
||||
// keep old field for backward compatibility
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const pageId = mention.page_id ?? mention.page;
|
||||
|
||||
if (!pageId) return;
|
||||
try {
|
||||
const page = await getPage(mention.page);
|
||||
const page = await getPage(pageId);
|
||||
|
||||
setPage(page);
|
||||
setError(false);
|
||||
@ -29,7 +50,7 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children:
|
||||
setPage(null);
|
||||
setError(true);
|
||||
}
|
||||
}, [mention.page]);
|
||||
}, [mention]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPage();
|
||||
@ -94,31 +115,27 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children:
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<span className={'relative'}>
|
||||
<span
|
||||
className={'relative mx-1 inline-flex cursor-pointer items-center hover:rounded hover:bg-content-blue-100'}
|
||||
onClick={openPage}
|
||||
style={{
|
||||
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
||||
}}
|
||||
>
|
||||
{page && (
|
||||
<span
|
||||
className={`mention-inline mx-1 inline-flex select-none items-center gap-1`}
|
||||
onClick={openPage}
|
||||
contentEditable={false}
|
||||
style={{
|
||||
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
||||
}}
|
||||
>
|
||||
{error ? (
|
||||
<>
|
||||
<EyeClose />
|
||||
<span className={'mr-0.5 text-text-caption underline'}>{t('document.mention.deleted')}</span>
|
||||
</>
|
||||
) : (
|
||||
page && (
|
||||
<>
|
||||
<span className={'text-sx absolute left-0.5'}>{page.icon?.value || <DocumentSvg />}</span>
|
||||
<span className={'ml-6 mr-0.5 underline'}>{page.name || t('document.title.placeholder')}</span>
|
||||
{page.icon?.value || <DocumentSvg />}
|
||||
<span className={'mr-1 underline'}>{page.name || t('document.title.placeholder')}</span>
|
||||
</>
|
||||
)}
|
||||
{error && (
|
||||
<>
|
||||
<span className={'text-sx absolute left-0.5'}>
|
||||
<EyeClose />
|
||||
</span>
|
||||
<span className={'ml-6 mr-0.5 text-text-caption underline'}>{t('document.mention.deleted')}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className={'invisible'}>{children}</span>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import KeyboardNavigation, {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TitleOutlined } from '@mui/icons-material';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
import { ColorEnum, renderColor } from '$app/utils/color';
|
||||
|
||||
export interface ColorPickerProps {
|
||||
onChange?: (format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => void;
|
||||
@ -39,8 +40,8 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: backgroundColor ?? 'transparent',
|
||||
color: color === '' ? 'var(--text-title)' : color,
|
||||
backgroundColor: backgroundColor ? renderColor(backgroundColor) : 'transparent',
|
||||
color: color === '' ? 'var(--text-title)' : renderColor(color),
|
||||
}}
|
||||
className={'flex h-5 w-5 items-center justify-center rounded border border-line-divider'}
|
||||
>
|
||||
@ -118,40 +119,40 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro
|
||||
content: renderColorItem(t('editor.backgroundColorDefault'), '', ''),
|
||||
},
|
||||
{
|
||||
key: `bg-gray-rgba(161,161,159,0.61)`,
|
||||
content: renderColorItem(t('editor.backgroundColorGray'), '', 'rgba(161,161,159,0.61)'),
|
||||
key: `bg-lime-${ColorEnum.Lime}`,
|
||||
content: renderColorItem(t('editor.backgroundColorLime'), '', ColorEnum.Lime),
|
||||
},
|
||||
{
|
||||
key: `bg-brown-rgba(178,93,37,0.65)`,
|
||||
content: renderColorItem(t('editor.backgroundColorBrown'), '', 'rgba(178,93,37,0.65)'),
|
||||
key: `bg-aqua-${ColorEnum.Aqua}`,
|
||||
content: renderColorItem(t('editor.backgroundColorAqua'), '', ColorEnum.Aqua),
|
||||
},
|
||||
{
|
||||
key: `bg-orange-rgba(248,156,71,0.65)`,
|
||||
content: renderColorItem(t('editor.backgroundColorOrange'), '', 'rgba(248,156,71,0.65)'),
|
||||
key: `bg-orange-${ColorEnum.Orange}`,
|
||||
content: renderColorItem(t('editor.backgroundColorOrange'), '', ColorEnum.Orange),
|
||||
},
|
||||
{
|
||||
key: `bg-yellow-rgba(229,197,137,0.6)`,
|
||||
content: renderColorItem(t('editor.backgroundColorYellow'), '', 'rgba(229,197,137,0.6)'),
|
||||
key: `bg-yellow-${ColorEnum.Yellow}`,
|
||||
content: renderColorItem(t('editor.backgroundColorYellow'), '', ColorEnum.Yellow),
|
||||
},
|
||||
{
|
||||
key: `bg-green-rgba(124,189,111,0.65)`,
|
||||
content: renderColorItem(t('editor.backgroundColorGreen'), '', 'rgba(124,189,111,0.65)'),
|
||||
key: `bg-green-${ColorEnum.Green}`,
|
||||
content: renderColorItem(t('editor.backgroundColorGreen'), '', ColorEnum.Green),
|
||||
},
|
||||
{
|
||||
key: `bg-blue-rgba(100,174,199,0.71)`,
|
||||
content: renderColorItem(t('editor.backgroundColorBlue'), '', 'rgba(100,174,199,0.71)'),
|
||||
key: `bg-blue-${ColorEnum.Blue}`,
|
||||
content: renderColorItem(t('editor.backgroundColorBlue'), '', ColorEnum.Blue),
|
||||
},
|
||||
{
|
||||
key: `bg-purple-rgba(182,114,234,0.63)`,
|
||||
content: renderColorItem(t('editor.backgroundColorPurple'), '', 'rgba(182,114,234,0.63)'),
|
||||
key: `bg-purple-${ColorEnum.Purple}`,
|
||||
content: renderColorItem(t('editor.backgroundColorPurple'), '', ColorEnum.Purple),
|
||||
},
|
||||
{
|
||||
key: `bg-pink-rgba(238,142,179,0.6)`,
|
||||
content: renderColorItem(t('editor.backgroundColorPink'), '', 'rgba(238,142,179,0.6)'),
|
||||
key: `bg-pink-${ColorEnum.Pink}`,
|
||||
content: renderColorItem(t('editor.backgroundColorPink'), '', ColorEnum.Pink),
|
||||
},
|
||||
{
|
||||
key: `bg-red-rgba(238,88,98,0.64)`,
|
||||
content: renderColorItem(t('editor.backgroundColorRed'), '', 'rgba(238,88,98,0.64)'),
|
||||
key: `bg-red-${ColorEnum.LightPink}`,
|
||||
content: renderColorItem(t('editor.backgroundColorRed'), '', ColorEnum.LightPink),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -26,6 +26,7 @@ export const canSetColorBlocks: EditorNodeType[] = [
|
||||
EditorNodeType.NumberedListBlock,
|
||||
EditorNodeType.ToggleListBlock,
|
||||
EditorNodeType.QuoteBlock,
|
||||
EditorNodeType.CalloutBlock,
|
||||
];
|
||||
|
||||
export function BlockOperationMenu({
|
||||
|
@ -52,7 +52,8 @@ export function useMentionPanel({
|
||||
|
||||
closePanel(true);
|
||||
CustomEditor.insertMention(editor, {
|
||||
page: id,
|
||||
page_id: id,
|
||||
type: MentionType.PageRef,
|
||||
});
|
||||
},
|
||||
[closePanel, editor]
|
||||
|
@ -17,7 +17,6 @@ import { ReactComponent as GridIcon } from '$app/assets/grid.svg';
|
||||
import { ReactComponent as ImageIcon } from '$app/assets/image.svg';
|
||||
import { DataObjectOutlined, FunctionsOutlined, HorizontalRuleOutlined, MenuBookOutlined } from '@mui/icons-material';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { randomEmoji } from '$app/utils/emoji';
|
||||
import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||
import { YjsEditor } from '@slate-yjs/core';
|
||||
import { useEditorBlockDispatch } from '$app/components/editor/stores/block';
|
||||
@ -133,7 +132,7 @@ export function useSlashCommandPanel({
|
||||
|
||||
if (nodeType === EditorNodeType.CalloutBlock) {
|
||||
Object.assign(data, {
|
||||
icon: randomEmoji(),
|
||||
icon: '📌',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,8 @@ export function Formula() {
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const selection = editor.selection;
|
||||
|
||||
if (!selection) return;
|
||||
|
||||
setRange(selection);
|
||||
|
@ -83,7 +83,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
}
|
||||
|
||||
|
||||
.text-content {
|
||||
.text-content, [data-dark-mode="true"] .text-content {
|
||||
&.empty-content {
|
||||
@apply min-w-[1px];
|
||||
span {
|
||||
@ -94,7 +94,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
}
|
||||
}
|
||||
|
||||
.text-element:has(.text-placeholder), .divider-node {
|
||||
.text-element:has(.text-placeholder), .divider-node, [data-dark-mode="true"] .text-element:has(.text-placeholder), [data-dark-mode="true"] .divider-node {
|
||||
::selection {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
@ -151,8 +151,14 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
}
|
||||
}
|
||||
|
||||
.image-block, .math-equation-block {
|
||||
.image-block, .math-equation-block, [data-dark-mode="true"] .image-block, [data-dark-mode="true"] .math-equation-block {
|
||||
::selection {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.mention-inline {
|
||||
&:hover {
|
||||
@apply bg-fill-list-active rounded;
|
||||
}
|
||||
}
|
@ -28,8 +28,11 @@ export class Provider extends EventEmitter {
|
||||
sharedType.applyDelta(delta);
|
||||
|
||||
const rootId = this.dataClient.rootId as string;
|
||||
const root = delta[0].insert as Y.XmlText;
|
||||
const data = root.getAttribute('data');
|
||||
|
||||
sharedType.setAttribute('blockId', rootId);
|
||||
sharedType.setAttribute('data', data);
|
||||
|
||||
this.sharedType = sharedType;
|
||||
this.sharedType?.observeDeep(this.onChange);
|
||||
|
@ -7,76 +7,90 @@ export function generateId() {
|
||||
return nanoid(10);
|
||||
}
|
||||
|
||||
export function transformToInlineElement(op: Op): Element | null {
|
||||
export function transformToInlineElement(op: Op): Element[] {
|
||||
const attributes = op.attributes;
|
||||
|
||||
if (!attributes) return null;
|
||||
if (!attributes) return [];
|
||||
const { formula, mention, ...attrs } = attributes;
|
||||
|
||||
if (formula) {
|
||||
return {
|
||||
type: EditorInlineNodeType.Formula,
|
||||
data: formula,
|
||||
children: [
|
||||
{
|
||||
text: op.insert as string,
|
||||
...attrs,
|
||||
},
|
||||
],
|
||||
};
|
||||
const texts = (op.insert as string).split('');
|
||||
|
||||
return texts.map((text) => {
|
||||
return {
|
||||
type: EditorInlineNodeType.Formula,
|
||||
data: formula,
|
||||
children: [
|
||||
{
|
||||
text,
|
||||
...attrs,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (mention) {
|
||||
return {
|
||||
type: EditorInlineNodeType.Mention,
|
||||
children: [
|
||||
{
|
||||
text: op.insert as string,
|
||||
...attrs,
|
||||
const texts = (op.insert as string).split('');
|
||||
|
||||
return texts.map((text) => {
|
||||
return {
|
||||
type: EditorInlineNodeType.Mention,
|
||||
children: [
|
||||
{
|
||||
text,
|
||||
...attrs,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
...(mention as Mention),
|
||||
},
|
||||
],
|
||||
data: {
|
||||
...(mention as Mention),
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getInlinesWithDelta(delta?: Op[]): (Text | Element)[] {
|
||||
return delta && delta.length > 0
|
||||
? delta.map((op) => {
|
||||
const matchInline = transformToInlineElement(op);
|
||||
const newDelta: (Text | Element)[] = [];
|
||||
|
||||
if (matchInline) {
|
||||
return matchInline;
|
||||
}
|
||||
if (!delta || !delta.length)
|
||||
return [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
];
|
||||
|
||||
if (op.attributes) {
|
||||
if ('font_color' in op.attributes && op.attributes['font_color'] === '') {
|
||||
delete op.attributes['font_color'];
|
||||
}
|
||||
delta.forEach((op) => {
|
||||
const matchInlines = transformToInlineElement(op);
|
||||
|
||||
if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') {
|
||||
delete op.attributes['bg_color'];
|
||||
}
|
||||
if (matchInlines.length > 0) {
|
||||
newDelta.push(...matchInlines);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('code' in op.attributes && !op.attributes['code']) {
|
||||
delete op.attributes['code'];
|
||||
}
|
||||
}
|
||||
if (op.attributes) {
|
||||
if ('font_color' in op.attributes && op.attributes['font_color'] === '') {
|
||||
delete op.attributes['font_color'];
|
||||
}
|
||||
|
||||
return {
|
||||
text: op.insert as string,
|
||||
...op.attributes,
|
||||
};
|
||||
})
|
||||
: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
];
|
||||
if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') {
|
||||
delete op.attributes['bg_color'];
|
||||
}
|
||||
|
||||
if ('code' in op.attributes && !op.attributes['code']) {
|
||||
delete op.attributes['code'];
|
||||
}
|
||||
}
|
||||
|
||||
newDelta.push({
|
||||
text: op.insert as string,
|
||||
...op.attributes,
|
||||
});
|
||||
});
|
||||
|
||||
return newDelta;
|
||||
}
|
||||
|
||||
export function convertToSlateValue(data: EditorData, includeRoot: boolean): Element[] {
|
||||
|
@ -1,12 +1,31 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.appflowy-scroll-container {
|
||||
&::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.workspaces {
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.MuiPopover-root, .MuiPaper-root {
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.view-icon {
|
||||
&:hover {
|
||||
background-color: rgba(156, 156, 156, 0.20);
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { ViewIconTypePB, ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { ImageType } from '$app/application/document/document.types';
|
||||
import { Nullable } from 'unsplash-js/dist/helpers/typescript';
|
||||
|
||||
export const pageTypeMap = {
|
||||
[ViewLayoutPB.Document]: 'document',
|
||||
@ -14,6 +16,7 @@ export interface Page {
|
||||
name: string;
|
||||
layout: ViewLayoutPB;
|
||||
icon?: PageIcon;
|
||||
cover?: PageCover;
|
||||
}
|
||||
|
||||
export interface PageIcon {
|
||||
@ -21,6 +24,17 @@ export interface PageIcon {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export enum CoverType {
|
||||
Color = 'CoverType.color',
|
||||
Image = 'CoverType.file',
|
||||
Asset = 'CoverType.asset',
|
||||
}
|
||||
export type PageCover = Nullable<{
|
||||
image_type?: ImageType;
|
||||
cover_selection_type?: CoverType;
|
||||
cover_selection?: string;
|
||||
}>;
|
||||
|
||||
export function parserViewPBToPage(view: ViewPB): Page {
|
||||
const icon = view.icon;
|
||||
|
||||
|
31
frontend/appflowy_tauri/src/appflowy_app/utils/color.ts
Normal file
31
frontend/appflowy_tauri/src/appflowy_app/utils/color.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export enum ColorEnum {
|
||||
Purple = 'appflowy_them_color_tint1',
|
||||
Pink = 'appflowy_them_color_tint2',
|
||||
LightPink = 'appflowy_them_color_tint3',
|
||||
Orange = 'appflowy_them_color_tint4',
|
||||
Yellow = 'appflowy_them_color_tint5',
|
||||
Lime = 'appflowy_them_color_tint6',
|
||||
Green = 'appflowy_them_color_tint7',
|
||||
Aqua = 'appflowy_them_color_tint8',
|
||||
Blue = 'appflowy_them_color_tint9',
|
||||
}
|
||||
|
||||
export const colorMap = {
|
||||
[ColorEnum.Purple]: 'var(--tint-purple)',
|
||||
[ColorEnum.Pink]: 'var(--tint-pink)',
|
||||
[ColorEnum.LightPink]: 'var(--tint-red)',
|
||||
[ColorEnum.Orange]: 'var(--tint-orange)',
|
||||
[ColorEnum.Yellow]: 'var(--tint-yellow)',
|
||||
[ColorEnum.Lime]: 'var(--tint-lime)',
|
||||
[ColorEnum.Green]: 'var(--tint-green)',
|
||||
[ColorEnum.Aqua]: 'var(--tint-aqua)',
|
||||
[ColorEnum.Blue]: 'var(--tint-blue)',
|
||||
};
|
||||
|
||||
export function renderColor(color: string) {
|
||||
if (colorMap[color as ColorEnum]) {
|
||||
return colorMap[color as ColorEnum];
|
||||
}
|
||||
|
||||
return color.replace('0x', '#');
|
||||
}
|
@ -39,12 +39,21 @@ export const getDesignTokens = (isDark: boolean): ThemeOptions => {
|
||||
styleOverrides: {
|
||||
contained: {
|
||||
color: 'var(--content-on-fill)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
},
|
||||
containedPrimary: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--fill-default)',
|
||||
},
|
||||
},
|
||||
containedInherit: {
|
||||
color: 'var(--text-title)',
|
||||
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.4)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--bg-body)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButtonBase: {
|
||||
|
@ -8,13 +8,7 @@ function DocumentPage() {
|
||||
const documentId = params.id;
|
||||
|
||||
if (!documentId) return null;
|
||||
return (
|
||||
<div className={'flex w-full justify-center'}>
|
||||
<div className={'max-w-screen w-[964px] min-w-0'}>
|
||||
<Document id={documentId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Document id={documentId} />;
|
||||
}
|
||||
|
||||
export default DocumentPage;
|
||||
|
@ -92,7 +92,7 @@
|
||||
--fill-active: #e0f8ff;
|
||||
--fill-list-hover: #e0f8ff;
|
||||
--fill-list-active: #edeef2;
|
||||
--content-blue-400: #00bcf0;
|
||||
--content-blue-400: rgb(0, 188, 240);
|
||||
--content-blue-300: #52d1f4;
|
||||
--content-blue-600: #009fd1;
|
||||
--content-blue-100: #e0f8ff;
|
||||
@ -111,7 +111,7 @@
|
||||
--function-info: #00bcf0;
|
||||
--tint-purple: #e8e0ff;
|
||||
--tint-pink: #ffe7ee;
|
||||
--tint-red: #ffe7ee;
|
||||
--tint-red: #ffdddd;
|
||||
--tint-lime: #f5ffdc;
|
||||
--tint-green: #ddffd6;
|
||||
--tint-aqua: #defff1;
|
||||
|
@ -134,7 +134,7 @@
|
||||
"type": "color"
|
||||
},
|
||||
"red": {
|
||||
"value": "#ffe7ee",
|
||||
"value": "#ffdddd",
|
||||
"type": "color"
|
||||
}
|
||||
}
|
||||
|
@ -1239,6 +1239,8 @@
|
||||
"backgroundColorPurple": "Purple background",
|
||||
"backgroundColorPink": "Pink background",
|
||||
"backgroundColorRed": "Red background",
|
||||
"backgroundColorLime": "Lime background",
|
||||
"backgroundColorAqua": "Aqua background",
|
||||
"done": "Done",
|
||||
"cancel": "Cancel",
|
||||
"tint1": "Tint 1",
|
||||
|
Loading…
Reference in New Issue
Block a user