mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support block align (#4219)
* fix: mention bugs * feat: support to align block range * fix: inline formula bugs * fix: adjust UI of color picker * fix: tab bugs
This commit is contained in:
parent
69469e9989
commit
d2ccec79e4
@ -100,7 +100,7 @@ export interface MathEquationNode extends Element {
|
||||
|
||||
export interface FormulaNode extends Element {
|
||||
type: EditorInlineNodeType.Formula;
|
||||
data: boolean;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface MentionNode extends Element {
|
||||
@ -173,51 +173,15 @@ export interface EditorElementProps<T = Element> extends HTMLAttributes<HTMLDivE
|
||||
node: T;
|
||||
}
|
||||
|
||||
export interface EditorInlineAttributes {
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
underline?: boolean;
|
||||
strikethrough?: boolean;
|
||||
font_color?: string;
|
||||
bg_color?: string;
|
||||
href?: string;
|
||||
code?: boolean;
|
||||
formula?: boolean;
|
||||
prism_token?: string;
|
||||
mention?: {
|
||||
type: string;
|
||||
// inline page ref id
|
||||
page?: string;
|
||||
// reminder date ref id
|
||||
date?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export enum EditorMarkFormat {
|
||||
Bold = 'bold',
|
||||
Italic = 'italic',
|
||||
Underline = 'underline',
|
||||
StrikeThrough = 'strikethrough',
|
||||
Code = 'code',
|
||||
Formula = 'formula',
|
||||
}
|
||||
|
||||
export enum EditorStyleFormat {
|
||||
FontColor = 'font_color',
|
||||
BackgroundColor = 'bg_color',
|
||||
Href = 'href',
|
||||
}
|
||||
|
||||
export enum EditorTurnFormat {
|
||||
Paragraph = 'paragraph',
|
||||
Heading1 = 'heading1', // 'heading1' is a special format, it's not a slate node type, but a slate node type's data
|
||||
Heading2 = 'heading2',
|
||||
Heading3 = 'heading3',
|
||||
TodoList = 'todo_list',
|
||||
BulletedList = 'bulleted_list',
|
||||
NumberedList = 'numbered_list',
|
||||
Quote = 'quote',
|
||||
ToggleList = 'toggle_list',
|
||||
FontColor = 'font_color',
|
||||
BgColor = 'bg_color',
|
||||
}
|
||||
|
||||
export enum MentionType {
|
||||
|
@ -53,8 +53,9 @@ function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions,
|
||||
option.onClick();
|
||||
handleClose();
|
||||
}}
|
||||
className={'flex items-center gap-1 rounded-none px-2 text-xs font-medium'}
|
||||
>
|
||||
<span className={'mr-2'}>{option.icon}</span>
|
||||
<span className={'text-base'}>{option.icon}</span>
|
||||
<span>{option.label}</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
@ -5,7 +5,7 @@ import { init, FrequentlyUsed, getEmojiDataFromNative, Store } from 'emoji-mart'
|
||||
import { PopoverProps } from '@mui/material/Popover';
|
||||
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { chunkArray } from '$app/utils/tool';
|
||||
import chunk from 'lodash-es/chunk';
|
||||
|
||||
export const EMOJI_SIZE = 32;
|
||||
|
||||
@ -154,7 +154,7 @@ export function getRowsWithCategories(emojiCategories: EmojiCategory[], rowSize:
|
||||
id: category.id,
|
||||
type: 'category',
|
||||
});
|
||||
chunkArray(category.emojis, rowSize).forEach((chunk, index) => {
|
||||
chunk(category.emojis, rowSize).forEach((chunk, index) => {
|
||||
rows.push({
|
||||
type: 'emojis',
|
||||
emojis: chunk,
|
||||
|
@ -6,7 +6,7 @@ import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||
|
||||
export const DatabaseTitle = () => {
|
||||
const viewId = useViewId();
|
||||
const pageName = useAppSelector((state) => state.pages.pageMap[viewId].name);
|
||||
const pageName = useAppSelector((state) => state.pages.pageMap[viewId]?.name || '');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleInput = useCallback<FormEventHandler>(
|
||||
|
@ -40,7 +40,7 @@ export const TextCell: FC<TextCellProps> = ({ placeholder, cell }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CellText className={`min-h-[36px] w-full`} ref={cellRef} onClick={handleClick}>
|
||||
<CellText className={`min-h-[36px] w-full cursor-text`} ref={cellRef} onClick={handleClick}>
|
||||
{content}
|
||||
</CellText>
|
||||
<Suspense>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Button, Tooltip } from '@mui/material';
|
||||
import { DragEventHandler, FC, HTMLAttributes, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { throttle } from '$app/utils/tool';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { DragItem, DropPosition, DragType, useDraggable, useDroppable, ScrollDirection } from '../../_shared';
|
||||
import { fieldService, Field } from '../../application';
|
||||
@ -9,6 +8,7 @@ import GridResizer from '$app/components/database/grid/GridField/GridResizer';
|
||||
import GridFieldMenu from '$app/components/database/grid/GridField/GridFieldMenu';
|
||||
import { areEqual } from 'react-window';
|
||||
import { useOpenMenu } from '$app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
|
||||
export interface GridFieldProps extends HTMLAttributes<HTMLDivElement> {
|
||||
field: Field;
|
||||
@ -88,9 +88,9 @@ export const GridField: FC<GridFieldProps> = memo(
|
||||
|
||||
const [menuAnchorPosition, setMenuAnchorPosition] = useState<
|
||||
| {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
@ -164,8 +164,9 @@ export const GridField: FC<GridFieldProps> = memo(
|
||||
/>
|
||||
{isOver && (
|
||||
<div
|
||||
className={`absolute bottom-0 top-0 z-10 w-0.5 bg-blue-500 ${dropPosition === DropPosition.Before ? 'left-[-1px]' : 'left-full'
|
||||
}`}
|
||||
className={`absolute bottom-0 top-0 z-10 w-0.5 bg-blue-500 ${
|
||||
dropPosition === DropPosition.Before ? 'left-[-1px]' : 'left-full'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<GridResizer field={field} onWidthChange={resizeColumnWidth} />
|
||||
|
@ -49,7 +49,7 @@ function GridNewRow({ index, groupId, getContainerRef }: Props) {
|
||||
toggleCssProperty(false);
|
||||
}}
|
||||
onClick={handleClick}
|
||||
className={'grid-new-row flex grow'}
|
||||
className={'grid-new-row flex grow cursor-pointer'}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
|
@ -1,17 +1,23 @@
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Editor, Element as SlateElement, Range, Transforms } from 'slate';
|
||||
import { EditorInlineNodeType } from '$app/application/document/document.types';
|
||||
import { Editor, Element as SlateElement, NodeEntry, Range, Transforms } from 'slate';
|
||||
import { EditorInlineNodeType, FormulaNode } from '$app/application/document/document.types';
|
||||
|
||||
export function insertFormula(editor: ReactEditor, formula?: string) {
|
||||
export function insertFormula(editor: ReactEditor) {
|
||||
if (editor.selection) {
|
||||
wrapFormula(editor, formula);
|
||||
wrapFormula(editor);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateFormula(editor: ReactEditor, formula: string) {
|
||||
if (isFormulaActive(editor)) {
|
||||
Transforms.delete(editor);
|
||||
insertFormula(editor, formula);
|
||||
wrapFormula(editor, formula);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteFormula(editor: ReactEditor) {
|
||||
if (isFormulaActive(editor)) {
|
||||
Transforms.delete(editor);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,32 +27,55 @@ export function wrapFormula(editor: ReactEditor, formula?: string) {
|
||||
}
|
||||
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return;
|
||||
const isCollapsed = selection && Range.isCollapsed(selection);
|
||||
|
||||
const data = formula || editor.string(selection);
|
||||
const formulaElement = {
|
||||
type: EditorInlineNodeType.Formula,
|
||||
data: true,
|
||||
children: isCollapsed
|
||||
? [
|
||||
{
|
||||
text: formula || '',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
data,
|
||||
children: [
|
||||
{
|
||||
text: '$',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (isCollapsed) {
|
||||
Transforms.insertNodes(editor, formulaElement);
|
||||
} else {
|
||||
Transforms.wrapNodes(editor, formulaElement, { split: true });
|
||||
Transforms.collapse(editor, { edge: 'end' });
|
||||
if (!isCollapsed) {
|
||||
Transforms.delete(editor);
|
||||
}
|
||||
|
||||
Transforms.insertNodes(editor, formulaElement, {
|
||||
select: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function unwrapFormula(editor: ReactEditor) {
|
||||
Transforms.unwrapNodes(editor, {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === EditorInlineNodeType.Formula,
|
||||
});
|
||||
|
||||
if (!match) return;
|
||||
|
||||
const [node, path] = match as NodeEntry<FormulaNode>;
|
||||
const formula = node.data;
|
||||
const range = Editor.range(editor, match[1]);
|
||||
const beforePoint = Editor.before(editor, path, { unit: 'character' });
|
||||
|
||||
Transforms.select(editor, range);
|
||||
Transforms.delete(editor);
|
||||
|
||||
Transforms.insertText(editor, formula);
|
||||
|
||||
if (!beforePoint) return;
|
||||
Transforms.select(editor, {
|
||||
anchor: beforePoint,
|
||||
focus: {
|
||||
...beforePoint,
|
||||
offset: beforePoint.offset + formula.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function isFormulaActive(editor: ReactEditor) {
|
||||
|
@ -2,7 +2,13 @@ import { ReactEditor } from 'slate-react';
|
||||
import { Editor, Element, Node, NodeEntry, Point, Range, Transforms, Location } from 'slate';
|
||||
import { LIST_TYPES, tabBackward, tabForward } from '$app/components/editor/command/tab';
|
||||
import { isMarkActive, removeMarks, toggleMark } from '$app/components/editor/command/mark';
|
||||
import { insertFormula, isFormulaActive, unwrapFormula, updateFormula } from '$app/components/editor/command/formula';
|
||||
import {
|
||||
deleteFormula,
|
||||
insertFormula,
|
||||
isFormulaActive,
|
||||
unwrapFormula,
|
||||
updateFormula,
|
||||
} from '$app/components/editor/command/formula';
|
||||
import {
|
||||
EditorInlineNodeType,
|
||||
EditorNodeType,
|
||||
@ -82,13 +88,12 @@ export const CustomEditor = {
|
||||
isMarkActive,
|
||||
isFormulaActive,
|
||||
updateFormula,
|
||||
toggleInlineElement: (editor: ReactEditor, format: EditorInlineNodeType) => {
|
||||
if (format === EditorInlineNodeType.Formula) {
|
||||
if (isFormulaActive(editor)) {
|
||||
unwrapFormula(editor);
|
||||
} else {
|
||||
insertFormula(editor);
|
||||
}
|
||||
deleteFormula,
|
||||
toggleFormula: (editor: ReactEditor) => {
|
||||
if (isFormulaActive(editor)) {
|
||||
unwrapFormula(editor);
|
||||
} else {
|
||||
insertFormula(editor);
|
||||
}
|
||||
},
|
||||
|
||||
@ -102,6 +107,47 @@ export const CustomEditor = {
|
||||
return !!match;
|
||||
},
|
||||
|
||||
toggleAlign(editor: ReactEditor, format: string) {
|
||||
const matchNodes = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
match: (n) => Element.isElement(n) && n.blockId !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
if (!matchNodes) return;
|
||||
|
||||
matchNodes.forEach((match) => {
|
||||
const [node] = match as NodeEntry<
|
||||
Element & {
|
||||
data: {
|
||||
align?: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
const path = ReactEditor.findPath(editor, node);
|
||||
|
||||
const data = (node.data as { align?: string }) || {};
|
||||
const newProperties = {
|
||||
data: {
|
||||
...data,
|
||||
align: data.align === format ? undefined : format,
|
||||
},
|
||||
} as Partial<Element>;
|
||||
|
||||
Transforms.setNodes(editor, newProperties, { at: path });
|
||||
});
|
||||
},
|
||||
|
||||
getAlign(editor: ReactEditor) {
|
||||
const match = CustomEditor.getBlock(editor);
|
||||
|
||||
if (!match) return undefined;
|
||||
|
||||
const [node] = match as NodeEntry<Element>;
|
||||
|
||||
return (node.data as { align?: string })?.align;
|
||||
},
|
||||
|
||||
insertMention(editor: ReactEditor, mention: Mention) {
|
||||
const mentionElement = {
|
||||
type: EditorInlineNodeType.Mention,
|
||||
@ -111,8 +157,9 @@ export const CustomEditor = {
|
||||
},
|
||||
};
|
||||
|
||||
Transforms.insertNodes(editor, mentionElement);
|
||||
Transforms.move(editor);
|
||||
Transforms.insertNodes(editor, mentionElement, {
|
||||
select: true,
|
||||
});
|
||||
},
|
||||
|
||||
toggleTodo(editor: ReactEditor, node: TodoListNode) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Editor } from 'slate';
|
||||
import { Editor, Text, Range } from 'slate';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
export function toggleMark(
|
||||
editor: ReactEditor,
|
||||
mark: {
|
||||
key: string;
|
||||
key: EditorMarkFormat;
|
||||
value: string | boolean;
|
||||
}
|
||||
) {
|
||||
@ -13,13 +14,55 @@ export function toggleMark(
|
||||
const isActive = isMarkActive(editor, key);
|
||||
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, key);
|
||||
Editor.removeMark(editor, key as string);
|
||||
} else {
|
||||
Editor.addMark(editor, key, value);
|
||||
Editor.addMark(editor, key as string, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function isMarkActive(editor: ReactEditor, format: string) {
|
||||
export function isMarkActive(editor: ReactEditor, format: EditorMarkFormat) {
|
||||
const selection = editor.selection;
|
||||
|
||||
if (!selection) return false;
|
||||
|
||||
const isExpanded = Range.isExpanded(selection);
|
||||
|
||||
if (isExpanded) {
|
||||
let anchor = Range.start(selection);
|
||||
const focus = Range.end(selection);
|
||||
const isEnd = Editor.isEnd(editor, anchor, anchor.path);
|
||||
|
||||
if (isEnd) {
|
||||
const after = Editor.after(editor, anchor);
|
||||
|
||||
if (after) {
|
||||
anchor = after;
|
||||
}
|
||||
}
|
||||
|
||||
const matches = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
match: Text.isText,
|
||||
at: {
|
||||
anchor,
|
||||
focus,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return matches.every((match) => {
|
||||
const [node] = match;
|
||||
|
||||
const { text, ...attributes } = node;
|
||||
|
||||
if (!text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!attributes[format];
|
||||
});
|
||||
}
|
||||
|
||||
const marks = Editor.marks(editor) as Record<string, string | boolean> | null;
|
||||
|
||||
return marks ? !!marks[format] : false;
|
||||
|
@ -65,13 +65,13 @@ export function tabForward(editor: ReactEditor) {
|
||||
to: toPath,
|
||||
});
|
||||
|
||||
node.children.forEach((child, index) => {
|
||||
if (index === 0) return;
|
||||
const length = node.children.length;
|
||||
|
||||
for (let i = length - 1; i > 0; i--) {
|
||||
editor.liftNodes({
|
||||
at: [...toPath, index],
|
||||
at: [...toPath, i],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function tabBackward(editor: ReactEditor) {
|
||||
@ -81,6 +81,8 @@ export function tabBackward(editor: ReactEditor) {
|
||||
|
||||
const [node, path] = match as NodeEntry<Element & { level: number }>;
|
||||
|
||||
const depth = path.length;
|
||||
|
||||
if (node.type === EditorNodeType.Page) return;
|
||||
if (node.type !== EditorNodeType.Paragraph) {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
@ -89,6 +91,7 @@ export function tabBackward(editor: ReactEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth === 1) return;
|
||||
editor.liftNodes({
|
||||
at: path,
|
||||
});
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React, { CSSProperties, useMemo } from 'react';
|
||||
import { ReactEditor, useSelected, useSlateStatic } from 'slate-react';
|
||||
import { Editor, Element } from 'slate';
|
||||
import { Editor, Element, Range } from 'slate';
|
||||
import { EditorNodeType, HeadingNode } from '$app/application/document/document.types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function PlaceholderContent({ node, ...attributes }: { node: Element; className?: string; style?: CSSProperties }) {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const selected = useSelected();
|
||||
const selected = useSelected() && !!editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
const block = useMemo(() => {
|
||||
const path = ReactEditor.findPath(editor, node);
|
||||
const match = Editor.above(editor, {
|
||||
@ -21,7 +22,7 @@ function PlaceholderContent({ node, ...attributes }: { node: Element; className?
|
||||
}, [editor, node]);
|
||||
|
||||
const className = useMemo(() => {
|
||||
return `pointer-events-none absolute left-0.5 top-0 whitespace-nowrap text-text-placeholder ${
|
||||
return `pointer-events-none select-none mx-1 absolute left-0.5 min-h-[26px] top-0 whitespace-nowrap text-text-placeholder ${
|
||||
attributes.className ?? ''
|
||||
}`;
|
||||
}, [attributes.className]);
|
||||
|
@ -0,0 +1,12 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
export const UnSupportBlock = forwardRef<HTMLDivElement, void>((_, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Alert className={'h-[48px] w-full'} title={'Unsupported block'} severity={'error'} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default UnSupportBlock;
|
@ -6,10 +6,10 @@ export const BulletedList = memo(
|
||||
({ node: _, children, className, ...attributes }, ref) => {
|
||||
return (
|
||||
<>
|
||||
<span contentEditable={false} className={'absolute flex w-6 select-none justify-center font-medium'}>
|
||||
<span contentEditable={false} className={'absolute flex w-6 select-none justify-center pt-[3px] font-medium'}>
|
||||
•
|
||||
</span>
|
||||
<div ref={ref} {...attributes} className={`${className} ml-6`}>
|
||||
<div ref={ref} {...attributes} className={`${className} pl-6`}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
|
@ -18,13 +18,13 @@ export const GridBlock = memo(
|
||||
}, [blockId, selectedBlockContext]);
|
||||
|
||||
return (
|
||||
<div {...attributes} onClick={onClick} className={`${className} relative my-2`}>
|
||||
<div {...attributes} onClick={onClick} className={`${className} relative my-2 w-full`}>
|
||||
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
contentEditable={false}
|
||||
className='flex h-[400px] overflow-hidden border-b border-t border-line-divider bg-bg-body py-3 caret-text-title'
|
||||
className='flex h-[400px] w-full overflow-hidden border-b border-t border-line-divider bg-bg-body py-3 caret-text-title'
|
||||
>
|
||||
{viewId ? <GridView viewId={viewId} /> : <DatabaseEmpty node={node} />}
|
||||
</div>
|
||||
|
@ -5,8 +5,8 @@ export const DividerNode = memo(
|
||||
forwardRef<HTMLDivElement, EditorElementProps<DividerNodeType>>(
|
||||
({ node: _node, children: children, className, ...attributes }, ref) => {
|
||||
return (
|
||||
<div {...attributes} className={`${className} relative`}>
|
||||
<div contentEditable={false} className={'w-full py-2.5 text-line-divider'}>
|
||||
<div {...attributes} className={`${className} relative w-full`}>
|
||||
<div contentEditable={false} className={'w-full py-3 text-line-divider'}>
|
||||
<hr />
|
||||
</div>
|
||||
<div ref={ref} className={`absolute h-full w-full caret-transparent`}>
|
||||
|
@ -7,7 +7,7 @@ export const Heading = memo(
|
||||
const level = node.data.level;
|
||||
const fontSizeCssProperty = getHeadingCssProperty(level);
|
||||
|
||||
const className = `${attributes.className ?? ''} font-bold ${fontSizeCssProperty}`;
|
||||
const className = `${attributes.className ?? ''} ${fontSizeCssProperty}`;
|
||||
|
||||
return (
|
||||
<div {...attributes} ref={ref} className={className}>
|
||||
|
@ -20,7 +20,7 @@ export const MathEquation = memo(
|
||||
onClick={(e) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
}}
|
||||
className={`${className} relative my-2 cursor-pointer`}
|
||||
className={`${className} relative my-2 w-full cursor-pointer`}
|
||||
>
|
||||
<div
|
||||
contentEditable={false}
|
||||
|
@ -34,10 +34,10 @@ export const NumberedList = memo(
|
||||
|
||||
return (
|
||||
<>
|
||||
<span contentEditable={false} className={'absolute flex w-6 select-none justify-center font-medium'}>
|
||||
<span contentEditable={false} className={'absolute flex w-6 select-none justify-center pt-[3px] font-medium'}>
|
||||
{index}.
|
||||
</span>
|
||||
<div ref={ref} {...attributes} className={`${className} ml-6`}>
|
||||
<div ref={ref} {...attributes} className={`${className} pl-6`}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
|
@ -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 ?? ''} text-4xl font-bold`;
|
||||
return `${attributes.className ?? ''} pb-3 min-h-[52px] text-4xl font-bold`;
|
||||
}, [attributes.className]);
|
||||
|
||||
return (
|
||||
|
@ -4,12 +4,18 @@ import { EditorElementProps, TextNode } from '$app/application/document/document
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
|
||||
export const Text = memo(
|
||||
forwardRef<HTMLDivElement, EditorElementProps<TextNode>>(({ node, children, ...attributes }, ref) => {
|
||||
forwardRef<HTMLDivElement, EditorElementProps<TextNode>>(({ node, children, className, ...attributes }, ref) => {
|
||||
const editor = useSlateStatic();
|
||||
const isEmpty = editor.isEmpty(node);
|
||||
|
||||
return (
|
||||
<div ref={ref} {...attributes} className={`text-element mx-1 ${!isEmpty ? 'flex' : ''} relative h-full`}>
|
||||
<div
|
||||
ref={ref}
|
||||
{...attributes}
|
||||
className={`text-element min-h-[26px] px-1 ${!isEmpty ? 'flex items-center' : 'select-none leading-[26px]'} ${
|
||||
className ?? ''
|
||||
} relative h-full`}
|
||||
>
|
||||
<Placeholder isEmpty={isEmpty} node={node} />
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@ export const TodoList = memo(
|
||||
data-playwright-selected={false}
|
||||
contentEditable={false}
|
||||
onClick={toggleTodo}
|
||||
className='absolute cursor-pointer select-none text-xl text-fill-default'
|
||||
className='absolute cursor-pointer select-none pt-[3px] text-xl text-fill-default'
|
||||
>
|
||||
{checked ? <CheckboxCheckSvg /> : <CheckboxUncheckSvg />}
|
||||
</span>
|
||||
|
@ -21,7 +21,7 @@ export const ToggleList = memo(
|
||||
data-playwright-selected={false}
|
||||
contentEditable={false}
|
||||
onClick={toggleToggleList}
|
||||
className='absolute cursor-pointer select-none text-xl hover:text-fill-default'
|
||||
className='absolute cursor-pointer select-none pt-[3px] text-xl hover:text-fill-default'
|
||||
>
|
||||
{collapsed ? <RightSvg /> : <RightSvg className={'rotate-90 transform'} />}
|
||||
</span>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EditorNodeType, CodeNode } from '$app/application/document/document.types';
|
||||
|
||||
import { createEditor, NodeEntry, BaseRange, Editor, Element } from 'slate';
|
||||
import { createEditor, NodeEntry, BaseRange, Editor, Element, Range } from 'slate';
|
||||
import { ReactEditor, withReact } from 'slate-react';
|
||||
import { withBlockPlugins } from '$app/components/editor/plugins/withBlockPlugins';
|
||||
import { decorateCode } from '$app/components/editor/components/blocks/code/utils';
|
||||
@ -11,6 +11,7 @@ import { withYjs, YjsEditor, withYHistory } from '@slate-yjs/core';
|
||||
import * as Y from 'yjs';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { proxySet, subscribeKey } from 'valtio/utils';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
export function useEditor(sharedType: Y.XmlText) {
|
||||
const editor = useMemo(() => {
|
||||
@ -57,7 +58,7 @@ export function useEditor(sharedType: Y.XmlText) {
|
||||
};
|
||||
}
|
||||
|
||||
export function useDecorate(editor: ReactEditor) {
|
||||
export function useDecorateCodeHighlight(editor: ReactEditor) {
|
||||
return useCallback(
|
||||
(entry: NodeEntry): BaseRange[] => {
|
||||
const path = entry[1];
|
||||
@ -80,9 +81,19 @@ export function useDecorate(editor: ReactEditor) {
|
||||
|
||||
export function useEditorState(editor: ReactEditor) {
|
||||
const selectedBlocks = useMemo(() => proxySet([]), []);
|
||||
const decorateState = useMemo(
|
||||
() =>
|
||||
proxySet<{
|
||||
range: BaseRange;
|
||||
class_name: string;
|
||||
}>([]),
|
||||
[]
|
||||
);
|
||||
|
||||
const [selectedLength, setSelectedLength] = useState(0);
|
||||
|
||||
const ranges = useSnapshot(decorateState);
|
||||
|
||||
subscribeKey(selectedBlocks, 'size', (v) => setSelectedLength(v));
|
||||
|
||||
useEffect(() => {
|
||||
@ -127,11 +138,44 @@ export function useEditorState(editor: ReactEditor) {
|
||||
};
|
||||
}, [editor, selectedBlocks, selectedLength]);
|
||||
|
||||
const decorate = useCallback(
|
||||
([, path]: NodeEntry): BaseRange[] => {
|
||||
const highlightRanges: (Range & {
|
||||
class_name: string;
|
||||
})[] = [];
|
||||
|
||||
ranges.forEach((state) => {
|
||||
const intersection = Range.intersection(state.range, Editor.range(editor, path));
|
||||
|
||||
if (intersection) {
|
||||
highlightRanges.push({
|
||||
...intersection,
|
||||
class_name: state.class_name,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return highlightRanges;
|
||||
},
|
||||
[editor, ranges]
|
||||
);
|
||||
|
||||
return {
|
||||
selectedBlocks,
|
||||
decorate,
|
||||
decorateState,
|
||||
};
|
||||
}
|
||||
|
||||
export const EditorSelectedBlockContext = createContext<Set<string>>(new Set());
|
||||
|
||||
export const EditorSelectedBlockProvider = EditorSelectedBlockContext.Provider;
|
||||
|
||||
export const DecorateStateContext = createContext<
|
||||
Set<{
|
||||
range: BaseRange;
|
||||
class_name: string;
|
||||
}>
|
||||
>(new Set());
|
||||
|
||||
export const DecorateStateProvider = DecorateStateContext.Provider;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import {
|
||||
DecorateStateProvider,
|
||||
EditorSelectedBlockProvider,
|
||||
useDecorate,
|
||||
useDecorateCodeHighlight,
|
||||
useEditor,
|
||||
useEditorState,
|
||||
} from '$app/components/editor/components/editor/Editor.hooks';
|
||||
@ -14,12 +15,23 @@ import { SlashCommandPanel } from '$app/components/editor/components/tools/comma
|
||||
import { MentionPanel } from '$app/components/editor/components/tools/command_panel/mention_panel';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import * as Y from 'yjs';
|
||||
import { NodeEntry } from 'slate';
|
||||
|
||||
function Editor({ sharedType }: { sharedType: Y.XmlText; id: string }) {
|
||||
const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType);
|
||||
const decorate = useDecorate(editor);
|
||||
const decorateCodeHighlight = useDecorateCodeHighlight(editor);
|
||||
const { onDOMBeforeInput, onKeyDown: onShortcutsKeyDown } = useShortcuts(editor);
|
||||
const { selectedBlocks } = useEditorState(editor);
|
||||
const { selectedBlocks, decorate: decorateCustomRange, decorateState } = useEditorState(editor);
|
||||
|
||||
const decorate = useCallback(
|
||||
(entry: NodeEntry) => {
|
||||
const codeRanges = decorateCodeHighlight(entry);
|
||||
const customRanges = decorateCustomRange(entry);
|
||||
|
||||
return [...codeRanges, ...customRanges];
|
||||
},
|
||||
[decorateCodeHighlight, decorateCustomRange]
|
||||
);
|
||||
|
||||
if (editor.sharedRoot.length === 0) {
|
||||
return <CircularProgress className='m-auto' />;
|
||||
@ -27,20 +39,22 @@ function Editor({ sharedType }: { sharedType: Y.XmlText; id: string }) {
|
||||
|
||||
return (
|
||||
<EditorSelectedBlockProvider value={selectedBlocks}>
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<SelectionToolbar />
|
||||
<BlockActionsToolbar />
|
||||
<CustomEditable
|
||||
{...props}
|
||||
onDOMBeforeInput={onDOMBeforeInput}
|
||||
onKeyDown={onShortcutsKeyDown}
|
||||
decorate={decorate}
|
||||
className={'px-16 caret-text-title outline-none focus:outline-none'}
|
||||
/>
|
||||
<SlashCommandPanel />
|
||||
<MentionPanel />
|
||||
<div onClick={handleOnClickEnd} className={'relative bottom-0 left-0 h-10 w-full cursor-text'} />
|
||||
</Slate>
|
||||
<DecorateStateProvider value={decorateState}>
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<SelectionToolbar />
|
||||
<BlockActionsToolbar />
|
||||
<CustomEditable
|
||||
{...props}
|
||||
onDOMBeforeInput={onDOMBeforeInput}
|
||||
onKeyDown={onShortcutsKeyDown}
|
||||
decorate={decorate}
|
||||
className={'px-16 caret-text-title outline-none focus:outline-none'}
|
||||
/>
|
||||
<SlashCommandPanel />
|
||||
<MentionPanel />
|
||||
<div onClick={handleOnClickEnd} className={'relative bottom-0 left-0 h-10 w-full cursor-text'} />
|
||||
</Slate>
|
||||
</DecorateStateProvider>
|
||||
</EditorSelectedBlockProvider>
|
||||
);
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import { MathEquation } from '$app/components/editor/components/blocks/math_equa
|
||||
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';
|
||||
|
||||
function Element({ element, attributes, children }: RenderElementProps) {
|
||||
const node = element;
|
||||
@ -68,15 +69,24 @@ function Element({ element, attributes, children }: RenderElementProps) {
|
||||
case EditorNodeType.EquationBlock:
|
||||
return MathEquation;
|
||||
default:
|
||||
return Paragraph;
|
||||
return UnSupportBlock;
|
||||
}
|
||||
}, [node.type]) as FC<EditorElementProps & HTMLAttributes<HTMLElement>>;
|
||||
|
||||
const { isSelected } = useElementState(node);
|
||||
|
||||
const className = useMemo(() => {
|
||||
return `block-element my-1 flex rounded ${isSelected ? 'bg-content-blue-100' : ''}`;
|
||||
}, [isSelected]);
|
||||
const align =
|
||||
(
|
||||
node.data as {
|
||||
align: 'left' | 'center' | 'right';
|
||||
}
|
||||
)?.align || 'left';
|
||||
|
||||
return `block-element flex rounded ${isSelected ? 'bg-content-blue-100' : ''} ${
|
||||
align ? `block-align-${align}` : ''
|
||||
}`;
|
||||
}, [isSelected, node.data]);
|
||||
|
||||
const style = useMemo(() => {
|
||||
const data = (node.data as BlockData) || {};
|
||||
|
@ -5,11 +5,11 @@ import { Link } from '$app/components/editor/components/marks';
|
||||
export function Leaf({ attributes, children, leaf }: RenderLeafProps) {
|
||||
let newChildren = children;
|
||||
|
||||
const classList = [leaf.prism_token, leaf.prism_token && 'token'].filter(Boolean);
|
||||
const classList = [leaf.prism_token, leaf.prism_token && 'token', leaf.class_name].filter(Boolean);
|
||||
|
||||
if (leaf.code) {
|
||||
newChildren = (
|
||||
<code className={'mx-0.5 rounded-sm bg-gray-300 bg-opacity-50 px-1 text-xs font-normal text-[#EB5757]'}>
|
||||
<code className={'bg-gray-300 bg-opacity-50 text-xs font-normal tracking-wider text-[#EB5757]'}>
|
||||
{newChildren}
|
||||
</code>
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ import React from 'react';
|
||||
export const InlineChromiumBugfix = () => (
|
||||
<span
|
||||
contentEditable={false}
|
||||
className={'absolute caret-transparent'}
|
||||
style={{
|
||||
fontSize: 0,
|
||||
}}
|
||||
|
@ -46,6 +46,11 @@ function FormulaEditPopover({
|
||||
placeholder={'E = mc^2'}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
fullWidth={true}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onDone(text);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={'ml-2'}>
|
||||
<Button size={'small'} variant={'text'} onClick={() => onDone(text)}>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import KatexMath from '$app/components/_shared/KatexMath';
|
||||
|
||||
function FormulaLeaf({ text, children }: { text: string; children: React.ReactNode }) {
|
||||
function FormulaLeaf({ formula, children }: { formula: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<span className={'relative'}>
|
||||
<KatexMath latex={text || ''} isInline />
|
||||
<KatexMath latex={formula || ''} isInline />
|
||||
<span className={'absolute left-0 right-0 h-0 w-0 opacity-0'}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
|
@ -1,19 +1,17 @@
|
||||
import React, { forwardRef, memo, useCallback, MouseEvent, useRef, useEffect, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Text, Transforms } from 'slate';
|
||||
import { ReactEditor, useSelected, useSlate } from 'slate-react';
|
||||
import { Transforms, Range, Editor } from 'slate';
|
||||
import { EditorElementProps, FormulaNode } from '$app/application/document/document.types';
|
||||
import FormulaLeaf from '$app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf';
|
||||
import { InlineChromiumBugfix } from '$app/components/editor/components/inline_nodes/InlineChromiumBugfix';
|
||||
import FormulaEditPopover from '$app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover';
|
||||
import { getNodePath, moveCursorToNodeEnd } from '$app/components/editor/components/editor/utils';
|
||||
import { useElementFocused } from '$app/components/editor/components/inline_nodes/useElementFocused';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
|
||||
export const InlineFormula = memo(
|
||||
forwardRef<HTMLSpanElement, EditorElementProps<FormulaNode>>(({ node, children, ...attributes }, ref) => {
|
||||
const editor = useSlate();
|
||||
const text = (node.children[0] as Text).text;
|
||||
const focused = useElementFocused(node);
|
||||
const formula = node.data;
|
||||
const selected = useSelected();
|
||||
|
||||
const anchor = useRef<HTMLSpanElement | null>(null);
|
||||
const [openEditPopover, setOpenEditPopover] = useState<boolean>(false);
|
||||
@ -31,12 +29,19 @@ export const InlineFormula = memo(
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (focused) {
|
||||
setOpenEditPopover(true);
|
||||
const selection = editor.selection;
|
||||
|
||||
if (selected && selection) {
|
||||
const path = ReactEditor.findPath(editor, node);
|
||||
const nodeRange = Editor.range(editor, path);
|
||||
|
||||
if (Range.includes(nodeRange, selection.anchor) && Range.includes(nodeRange, selection.focus)) {
|
||||
setOpenEditPopover(true);
|
||||
}
|
||||
} else {
|
||||
setOpenEditPopover(false);
|
||||
}
|
||||
}, [focused]);
|
||||
}, [editor, node, selected]);
|
||||
|
||||
const handleEditPopoverClose = useCallback(() => {
|
||||
setOpenEditPopover(false);
|
||||
@ -64,25 +69,35 @@ export const InlineFormula = memo(
|
||||
contentEditable={false}
|
||||
onDoubleClick={handleClick}
|
||||
onClick={handleClick}
|
||||
className={`relative rounded px-1 py-0.5 text-xs ${focused ? 'bg-fill-list-active' : ''}`}
|
||||
data-playwright-selected={focused}
|
||||
className={`relative rounded px-1 py-0.5 text-xs ${selected ? 'bg-fill-list-active' : ''}`}
|
||||
data-playwright-selected={selected}
|
||||
>
|
||||
<InlineChromiumBugfix />
|
||||
<FormulaLeaf text={text}>{children}</FormulaLeaf>
|
||||
<InlineChromiumBugfix />
|
||||
<FormulaLeaf formula={formula}>{children}</FormulaLeaf>
|
||||
</span>
|
||||
{openEditPopover && (
|
||||
<FormulaEditPopover
|
||||
defaultText={text}
|
||||
onDone={(formula) => {
|
||||
if (anchor.current === null) return;
|
||||
defaultText={formula}
|
||||
onDone={(newFormula) => {
|
||||
if (anchor.current === null || newFormula === formula) return;
|
||||
const path = getNodePath(editor, anchor.current);
|
||||
|
||||
// select the node before updating the formula
|
||||
Transforms.select(editor, path);
|
||||
CustomEditor.updateFormula(editor, formula);
|
||||
if (newFormula === '') {
|
||||
const point = editor.before(path);
|
||||
|
||||
handleEditPopoverClose();
|
||||
CustomEditor.deleteFormula(editor);
|
||||
setOpenEditPopover(false);
|
||||
if (point) {
|
||||
ReactEditor.focus(editor);
|
||||
editor.select(point);
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
CustomEditor.updateFormula(editor, newFormula);
|
||||
handleEditPopoverClose();
|
||||
}
|
||||
}}
|
||||
anchorEl={anchor.current}
|
||||
open={openEditPopover}
|
||||
|
@ -2,25 +2,15 @@ import React, { forwardRef, memo } from 'react';
|
||||
import { EditorElementProps, MentionNode } from '$app/application/document/document.types';
|
||||
|
||||
import MentionLeaf from '$app/components/editor/components/inline_nodes/mention/MentionLeaf';
|
||||
import { useElementFocused } from '$app/components/editor/components/inline_nodes/useElementFocused';
|
||||
|
||||
export const Mention = memo(
|
||||
forwardRef<HTMLSpanElement, EditorElementProps<MentionNode>>(({ node, children, ...attributes }, ref) => {
|
||||
const focused = useElementFocused(node);
|
||||
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
data-playwright-selected={focused}
|
||||
contentEditable={false}
|
||||
className={`${attributes.className ?? ''} text-sx relative rounded px-1 hover:bg-content-blue-100`}
|
||||
ref={ref}
|
||||
style={{
|
||||
backgroundColor: focused ? 'var(--content-blue-100)' : undefined,
|
||||
}}
|
||||
>
|
||||
<MentionLeaf mention={node.data}>{children}</MentionLeaf>
|
||||
</span>
|
||||
<>
|
||||
<span {...attributes} contentEditable={false} ref={ref}>
|
||||
<MentionLeaf mention={node.data}>{children}</MentionLeaf>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
@ -29,15 +29,18 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children:
|
||||
}, [navigate, page]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={'relative'}>
|
||||
{page && (
|
||||
<span className={'inline-flex cursor-pointer items-center'} onClick={openPage}>
|
||||
<span className={'mr-1 inline-flex items-center'}>{page.icon?.value || <DocumentSvg />}</span>
|
||||
<span className={'text-sx underline'}>{page.name || t('document.title.placeholder')}</span>
|
||||
<span
|
||||
className={'relative inline-flex cursor-pointer items-center hover:bg-content-blue-100'}
|
||||
onClick={openPage}
|
||||
>
|
||||
<span className={'text-sx absolute left-0'}>{page.icon?.value || <DocumentSvg />}</span>
|
||||
<span className={'ml-6 underline'}>{page.name || t('document.title.placeholder')}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className={'absolute left-0 right-0 h-0 w-0 opacity-0'}>{children}</span>
|
||||
</>
|
||||
<span className={'invisible'}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { ReactEditor, useSelected, useSlate } from 'slate-react';
|
||||
import { useMemo } from 'react';
|
||||
import { Range, Text, Element } from 'slate';
|
||||
|
||||
export function useElementFocused(node: Element) {
|
||||
const editor = useSlate();
|
||||
const text = (node.children[0] as Text).text;
|
||||
const selected = useSelected();
|
||||
|
||||
const focused = useMemo(() => {
|
||||
if (!selected) return false;
|
||||
const selection = editor.selection;
|
||||
|
||||
if (!selection) return false;
|
||||
const path = ReactEditor.findPath(editor, node);
|
||||
const range = { anchor: { path, offset: 0 }, focus: { path, offset: text.length } } as Range;
|
||||
|
||||
return Range.includes(range, selection);
|
||||
}, [editor, selected, node, text.length]);
|
||||
|
||||
return focused;
|
||||
}
|
@ -5,7 +5,6 @@ import { PopoverCommonProps } from '$app/components/editor/components/tools/popo
|
||||
import Button from '@mui/material/Button';
|
||||
import { getNodePath } from '$app/components/editor/components/editor/utils';
|
||||
import { addMark, BasePoint, Editor, Transforms, removeMark } from 'slate';
|
||||
import { EditorStyleFormat } from '$app/application/document/document.types';
|
||||
import { useSlate } from 'slate-react';
|
||||
import { ReactComponent as RemoveSvg } from '$app/assets/delete.svg';
|
||||
import { ReactComponent as LinkSvg } from '$app/assets/link.svg';
|
||||
@ -13,6 +12,7 @@ import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
|
||||
import { open as openWindow } from '@tauri-apps/api/shell';
|
||||
import { OutlinedInput } from '@mui/material';
|
||||
import { notify } from '$app/components/editor/components/tools/notify';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
const pattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w.-]*)*\/?$/;
|
||||
|
||||
@ -37,10 +37,11 @@ export function LinkEditPopover({
|
||||
|
||||
// select the node before updating the formula
|
||||
Transforms.select(editor, path);
|
||||
|
||||
if (link === '') {
|
||||
removeMark(editor, EditorStyleFormat.Href);
|
||||
removeMark(editor, EditorMarkFormat.Href);
|
||||
} else {
|
||||
addMark(editor, EditorStyleFormat.Href, link);
|
||||
addMark(editor, EditorMarkFormat.Href, link);
|
||||
}
|
||||
|
||||
onClose();
|
||||
@ -54,7 +55,8 @@ export function LinkEditPopover({
|
||||
|
||||
// select the node before updating the formula
|
||||
Transforms.select(editor, path);
|
||||
editor.removeMark(EditorStyleFormat.Href);
|
||||
|
||||
editor.removeMark(EditorMarkFormat.Href);
|
||||
|
||||
onClose(beforePathEnd);
|
||||
}, [editor, anchorEl, onClose]);
|
||||
|
@ -2,10 +2,13 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomColorPicker } from '$app/components/editor/components/tools/_shared/CustomColorPicker';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { MenuItem, MenuList } from '@mui/material';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg';
|
||||
import { IconButton, MenuItem, MenuList } from '@mui/material';
|
||||
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { PopoverNoBackdropProps } from '$app/components/editor/components/tools/popover';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { ThemeMode } from '$app_reducers/current-user/slice';
|
||||
|
||||
export interface ColorPickerProps {
|
||||
onFocus?: () => void;
|
||||
@ -14,63 +17,49 @@ export interface ColorPickerProps {
|
||||
color?: string;
|
||||
onChange?: (color: string) => void;
|
||||
}
|
||||
export function ColorPicker({ onFocus, onBlur, label, color, onChange }: ColorPickerProps) {
|
||||
export function ColorPicker({ onFocus, onBlur, label, color = '', onChange }: ColorPickerProps) {
|
||||
const { t } = useTranslation();
|
||||
const colors = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'default',
|
||||
name: t('colors.default'),
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
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]
|
||||
);
|
||||
const isDark = useAppSelector((state) => state.currentUser?.userSetting?.themeMode === ThemeMode.Dark);
|
||||
const [selectedColor, setSelectedColor] = useState(color);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedColor(color);
|
||||
}, [color]);
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setSelectedColor(color);
|
||||
onChange?.(color);
|
||||
};
|
||||
|
||||
const colors = useMemo(() => {
|
||||
return !isDark
|
||||
? [
|
||||
'#e8e0ff',
|
||||
'#ffd6e8',
|
||||
'#f5d0ff',
|
||||
'#e1fbff',
|
||||
'#ffebcc',
|
||||
'#fff7cc',
|
||||
'#e8ffcc',
|
||||
'#e8f4ff',
|
||||
'#fff2cd',
|
||||
'#d9d9d9',
|
||||
'#f0f0f0',
|
||||
]
|
||||
: [
|
||||
'#4D4078',
|
||||
'#7B2CBF',
|
||||
'#FFB800',
|
||||
'#00B800',
|
||||
'#00B8FF',
|
||||
'#007BFF',
|
||||
'#B800FF',
|
||||
'#FF00B8',
|
||||
'#FF0000',
|
||||
'#FF6C00',
|
||||
'#FFD800',
|
||||
];
|
||||
}, [isDark]);
|
||||
|
||||
const [openCustomColorPicker, setOpenCustomColorPicker] = useState(false);
|
||||
|
||||
@ -97,16 +86,16 @@ export function ColorPicker({ onFocus, onBlur, label, color, onChange }: ColorPi
|
||||
onMouseLeave={() => {
|
||||
setOpenCustomColorPicker(false);
|
||||
}}
|
||||
className={'flex px-2 py-1'}
|
||||
className={'mx-2 mb-2 flex px-2.5 py-1'}
|
||||
ref={customItemRef}
|
||||
>
|
||||
<div className={'flex-1'}>{t('colors.custom')}</div>
|
||||
<div className={'flex-1 text-xs'}>{t('colors.custom')}</div>
|
||||
<MoreSvg className={'h-4 w-4'} />
|
||||
{openCustomColorPicker && (
|
||||
<CustomColorPicker
|
||||
anchorEl={customItemRef.current}
|
||||
open={openCustomColorPicker}
|
||||
onColorChange={onChange}
|
||||
onColorChange={handleColorChange}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
{...PopoverNoBackdropProps}
|
||||
onClose={() => {
|
||||
@ -127,34 +116,39 @@ export function ColorPicker({ onFocus, onBlur, label, color, onChange }: ColorPi
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
{colors.map((c) => {
|
||||
return (
|
||||
<MenuItem
|
||||
className={'flex px-2 py-1'}
|
||||
key={c.key}
|
||||
<div className={'flex flex-grow flex-wrap items-center gap-1 px-3 py-0'}>
|
||||
<Tooltip title={t('colors.default')}>
|
||||
<IconButton
|
||||
className={'rounded-full'}
|
||||
onClick={() => {
|
||||
onChange?.(c.color);
|
||||
handleColorChange('');
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={'mr-2 flex h-4 w-4 items-center justify-center rounded-full border-2 p-0.5'}
|
||||
<DeleteSvg />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{colors.map((c) => {
|
||||
return (
|
||||
<IconButton
|
||||
key={c}
|
||||
onClick={() => {
|
||||
handleColorChange(c);
|
||||
}}
|
||||
className={'flex h-6 w-6 cursor-pointer items-center justify-center rounded-full p-1'}
|
||||
style={{
|
||||
borderColor: c.color,
|
||||
backgroundColor: c === selectedColor ? 'var(--content-blue-100)' : undefined,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={'h-2 w-2 rounded-full'}
|
||||
className={'h-full w-full rounded-full'}
|
||||
style={{
|
||||
backgroundColor: c.color,
|
||||
backgroundColor: c,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex-1'}>{c.name}</div>
|
||||
|
||||
{color && color === c.color && <SelectCheckSvg />}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</MenuList>
|
||||
</div>
|
||||
);
|
||||
|
@ -4,11 +4,11 @@ import { Element } from 'slate';
|
||||
import AddBlockBelow from '$app/components/editor/components/tools/block_actions/AddBlockBelow';
|
||||
import BlockMenu from '$app/components/editor/components/tools/block_actions/BlockMenu';
|
||||
|
||||
export function BlockActions({ node }: { node?: Element }) {
|
||||
export function BlockActions({ node, setMenuVisible }: { node?: Element; setMenuVisible: (visible: boolean) => void }) {
|
||||
return (
|
||||
<>
|
||||
<AddBlockBelow node={node} />
|
||||
<BlockMenu node={node} />
|
||||
<BlockMenu setMenuVisible={setMenuVisible} node={node} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { EditorNodeType } from '$app/application/document/document.types';
|
||||
export function useBlockActionsToolbar(ref: React.RefObject<HTMLDivElement>) {
|
||||
const editor = useSlate();
|
||||
const [node, setNode] = useState<Element | null>(null);
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
@ -69,15 +70,22 @@ export function useBlockActionsToolbar(ref: React.RefObject<HTMLDivElement>) {
|
||||
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
dom.addEventListener('mousemove', handleMouseMove);
|
||||
dom.parentElement?.addEventListener('mouseleave', handleMouseLeave);
|
||||
if (!menuVisible) {
|
||||
dom.addEventListener('mousemove', handleMouseMove);
|
||||
dom.parentElement?.addEventListener('mouseleave', handleMouseLeave);
|
||||
} else {
|
||||
dom.removeEventListener('mousemove', handleMouseMove);
|
||||
dom.parentElement?.removeEventListener('mouseleave', handleMouseLeave);
|
||||
}
|
||||
|
||||
return () => {
|
||||
dom.removeEventListener('mousemove', handleMouseMove);
|
||||
dom.parentElement?.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}, [editor, ref]);
|
||||
}, [editor, ref, menuVisible]);
|
||||
|
||||
return {
|
||||
setMenuVisible,
|
||||
node: node?.type === EditorNodeType.Page ? null : node,
|
||||
};
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { getBlockCssProperty } from '$app/components/editor/components/tools/blo
|
||||
export function BlockActionsToolbar() {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { node } = useBlockActionsToolbar(ref);
|
||||
const { node, setMenuVisible } = useBlockActionsToolbar(ref);
|
||||
|
||||
const cssProperty = node && getBlockCssProperty(node);
|
||||
|
||||
@ -15,7 +15,7 @@ export function BlockActionsToolbar() {
|
||||
<div
|
||||
ref={ref}
|
||||
contentEditable={false}
|
||||
className={`block-actions ${cssProperty} absolute z-10 flex w-[64px] flex-grow transform items-center justify-end px-1 opacity-0 transition-opacity`}
|
||||
className={`block-actions ${cssProperty} absolute z-10 flex min-h-[26px] w-[64px] flex-grow transform items-center justify-end px-1 opacity-0 transition-opacity`}
|
||||
onMouseDown={(e) => {
|
||||
// prevent toolbar from taking focus away from editor
|
||||
e.preventDefault();
|
||||
@ -26,8 +26,8 @@ export function BlockActionsToolbar() {
|
||||
}}
|
||||
>
|
||||
{/* Ensure the toolbar in middle */}
|
||||
<div className={'invisible'}>0</div>
|
||||
{<BlockActions node={node || undefined} />}
|
||||
<div className={`invisible`}>$</div>
|
||||
{<BlockActions setMenuVisible={setMenuVisible} node={node || undefined} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useContext, useRef, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as DragSvg } from '$app/assets/drag.svg';
|
||||
@ -6,13 +6,20 @@ import BlockOperationMenu from '$app/components/editor/components/tools/block_ac
|
||||
import { Element } from 'slate';
|
||||
import { EditorSelectedBlockContext } from '$app/components/editor/components/editor/Editor.hooks';
|
||||
|
||||
function BlockMenu({ node }: { node?: Element }) {
|
||||
function BlockMenu({ node, setMenuVisible }: { node?: Element; setMenuVisible: (visible: boolean) => void }) {
|
||||
const dragBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [selectedNode, setSelectedNode] = useState<Element>();
|
||||
const selectedBlockContext = useContext(EditorSelectedBlockContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (openMenu) {
|
||||
setMenuVisible(true);
|
||||
} else {
|
||||
setMenuVisible(false);
|
||||
}
|
||||
}, [openMenu, setMenuVisible]);
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
@ -1,35 +1,15 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Popover, { PopoverProps } from '@mui/material/Popover';
|
||||
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
|
||||
import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, MenuProps, Menu } from '@mui/material';
|
||||
import { Button, Divider } from '@mui/material';
|
||||
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
||||
import { Element } from 'slate';
|
||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
|
||||
import { FontColorPicker, BgColorPicker } from '$app/components/editor/components/tools/_shared';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useBlockMenuKeyDown } from '$app/components/editor/components/tools/block_actions/BlockMenu.hooks';
|
||||
|
||||
enum SubMenuType {
|
||||
TextColor = 'textColor',
|
||||
BackgroundColor = 'backgroundColor',
|
||||
}
|
||||
|
||||
const subMenuProps: Partial<MenuProps> = {
|
||||
anchorOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
};
|
||||
import { Color } from './color';
|
||||
|
||||
export function BlockOperationMenu({
|
||||
node,
|
||||
@ -37,7 +17,6 @@ export function BlockOperationMenu({
|
||||
}: {
|
||||
node: Element;
|
||||
} & PopoverProps) {
|
||||
const optionsRef = React.useRef<HTMLDivElement>(null);
|
||||
const editor = useSlateStatic();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -57,14 +36,6 @@ export function BlockOperationMenu({
|
||||
const { onKeyDown } = useBlockMenuKeyDown({
|
||||
onClose: handleClose,
|
||||
});
|
||||
const [subMenuType, setSubMenuType] = useState<null | SubMenuType>(null);
|
||||
|
||||
const subMenuAnchorEl = useMemo(() => {
|
||||
if (!subMenuType) return null;
|
||||
return optionsRef.current?.querySelector(`[data-submenu-type="${subMenuType}"]`);
|
||||
}, [subMenuType]);
|
||||
|
||||
const subMenuOpen = Boolean(subMenuAnchorEl);
|
||||
|
||||
const operationOptions = useMemo(
|
||||
() => [
|
||||
@ -88,51 +59,6 @@ export function BlockOperationMenu({
|
||||
[editor, node, handleClose, t]
|
||||
);
|
||||
|
||||
const colorOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
type: SubMenuType.TextColor,
|
||||
text: t('editor.textColor'),
|
||||
onClick: () => {
|
||||
setSubMenuType(SubMenuType.TextColor);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: SubMenuType.BackgroundColor,
|
||||
text: t('editor.backgroundColor'),
|
||||
onClick: () => {
|
||||
setSubMenuType(SubMenuType.BackgroundColor);
|
||||
},
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const subMenuContent = useMemo(() => {
|
||||
switch (subMenuType) {
|
||||
case SubMenuType.TextColor:
|
||||
return (
|
||||
<FontColorPicker
|
||||
onChange={(color) => {
|
||||
CustomEditor.setBlockColor(editor, node, { font_color: color });
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case SubMenuType.BackgroundColor:
|
||||
return (
|
||||
<BgColorPicker
|
||||
onChange={(color) => {
|
||||
CustomEditor.setBlockColor(editor, node, { bg_color: color });
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [editor, node, handleClose, subMenuType]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
{...PopoverCommonProps}
|
||||
@ -157,34 +83,17 @@ export function BlockOperationMenu({
|
||||
))}
|
||||
</div>
|
||||
<Divider className={'my-1'} />
|
||||
<div ref={optionsRef} className={'flex flex-col p-2'}>
|
||||
<Typography variant={'body2'} className={'mb-1 text-text-caption'}>
|
||||
{t('editor.color')}
|
||||
</Typography>
|
||||
{colorOptions.map((option, index) => (
|
||||
<Button
|
||||
data-submenu-type={option.type}
|
||||
color={'inherit'}
|
||||
onClick={option.onClick}
|
||||
size={'small'}
|
||||
endIcon={<MoreSvg />}
|
||||
className={'w-full justify-between'}
|
||||
key={index}
|
||||
>
|
||||
<div className={'flex-1 text-left'}>{option.text}</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Menu
|
||||
container={optionsRef.current}
|
||||
{...PopoverCommonProps}
|
||||
{...subMenuProps}
|
||||
open={subMenuOpen}
|
||||
anchorEl={subMenuAnchorEl}
|
||||
onClose={() => setSubMenuType(null)}
|
||||
>
|
||||
{subMenuContent}
|
||||
</Menu>
|
||||
<Color
|
||||
node={
|
||||
node as Element & {
|
||||
data?: {
|
||||
font_color?: string;
|
||||
bg_color?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,100 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Element } from 'slate';
|
||||
import { Button, Popover } from '@mui/material';
|
||||
import ColorLensOutlinedIcon from '@mui/icons-material/ColorLensOutlined';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BgColorPicker, FontColorPicker } from '$app/components/editor/components/tools/_shared';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { PopoverNoBackdropProps } from '$app/components/editor/components/tools/popover';
|
||||
|
||||
export function Color({
|
||||
node,
|
||||
onClose,
|
||||
}: {
|
||||
node: Element & {
|
||||
data?: {
|
||||
font_color?: string;
|
||||
bg_color?: string;
|
||||
};
|
||||
};
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const editor = useSlateStatic();
|
||||
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const fontColor = useMemo(() => {
|
||||
return node.data?.font_color;
|
||||
}, [node]);
|
||||
|
||||
const bgColor = useMemo(() => {
|
||||
return node.data?.bg_color;
|
||||
}, [node]);
|
||||
|
||||
const onColorChange = useCallback(
|
||||
(format: 'font_color' | 'bg_color', color: string) => {
|
||||
CustomEditor.setBlockColor(editor, node, {
|
||||
[format]: color,
|
||||
});
|
||||
onClose?.();
|
||||
},
|
||||
[editor, node, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
color={'inherit'}
|
||||
onMouseEnter={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
endIcon={<MoreSvg />}
|
||||
startIcon={<ColorLensOutlinedIcon />}
|
||||
size={'small'}
|
||||
className={'mx-2 my-1 justify-start'}
|
||||
>
|
||||
{t('editor.color')}
|
||||
{open && (
|
||||
<Popover
|
||||
{...PopoverNoBackdropProps}
|
||||
anchorOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
PaperProps={{
|
||||
...PopoverNoBackdropProps.PaperProps,
|
||||
className: 'w-[200px] max-h-[360px] overflow-x-hidden overflow-y-auto',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={ref.current}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className={'flex flex-col'}>
|
||||
<FontColorPicker
|
||||
onChange={(color) => {
|
||||
onColorChange('font_color', color);
|
||||
}}
|
||||
color={fontColor}
|
||||
/>
|
||||
<BgColorPicker
|
||||
onChange={(color) => {
|
||||
onColorChange('bg_color', color);
|
||||
}}
|
||||
color={bgColor}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './Color';
|
@ -18,7 +18,7 @@ export function useMentionPanel({
|
||||
|
||||
const onClick = useCallback(
|
||||
(type: MentionType, mention: Mention) => {
|
||||
closePanel(false);
|
||||
closePanel(true);
|
||||
CustomEditor.insertMention(editor, mention);
|
||||
},
|
||||
[closePanel, editor]
|
||||
|
@ -1,485 +0,0 @@
|
||||
import {
|
||||
EditorInlineAttributes,
|
||||
EditorInlineNodeType,
|
||||
EditorMarkFormat,
|
||||
EditorNodeType,
|
||||
EditorStyleFormat,
|
||||
EditorTurnFormat,
|
||||
HeadingNode,
|
||||
} from '$app/application/document/document.types';
|
||||
|
||||
import { ReactComponent as BoldSvg } from '$app/assets/bold.svg';
|
||||
import { ReactComponent as UnderlineSvg } from '$app/assets/underline.svg';
|
||||
import { ReactComponent as StrikeThroughSvg } from '$app/assets/strikethrough.svg';
|
||||
import { ReactComponent as ItalicSvg } from '$app/assets/italic.svg';
|
||||
import { ReactComponent as CodeSvg } from '$app/assets/inline-code.svg';
|
||||
import { ReactComponent as Heading1Svg } from '$app/assets/h1.svg';
|
||||
import { ReactComponent as Heading2Svg } from '$app/assets/h2.svg';
|
||||
import { ReactComponent as Heading3Svg } from '$app/assets/h3.svg';
|
||||
import { ReactComponent as ParagraphSvg } from '$app/assets/text.svg';
|
||||
import { ReactComponent as TodoListSvg } from '$app/assets/todo-list.svg';
|
||||
import { ReactComponent as QuoteSvg } from '$app/assets/quote.svg';
|
||||
import { ReactComponent as ToggleListSvg } from '$app/assets/show-menu.svg';
|
||||
import { ReactComponent as NumberedListSvg } from '$app/assets/numbers.svg';
|
||||
import { ReactComponent as BulletedListSvg } from '$app/assets/list.svg';
|
||||
import { ReactComponent as LinkSvg } from '$app/assets/link.svg';
|
||||
|
||||
import FormatColorFillIcon from '@mui/icons-material/FormatColorFill';
|
||||
import FormatColorTextIcon from '@mui/icons-material/FormatColorText';
|
||||
import Functions from '@mui/icons-material/Functions';
|
||||
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { getBlock } from '$app/components/editor/plugins/utils';
|
||||
import { FontColorPicker, BgColorPicker } from '$app/components/editor/components/tools/_shared';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { addMark, Editor } from 'slate';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
|
||||
const markFormatActions = [
|
||||
EditorMarkFormat.Underline,
|
||||
EditorMarkFormat.Bold,
|
||||
EditorMarkFormat.Italic,
|
||||
EditorMarkFormat.StrikeThrough,
|
||||
EditorMarkFormat.Code,
|
||||
EditorMarkFormat.Formula,
|
||||
];
|
||||
|
||||
const styleFormatActions = [EditorStyleFormat.Href, EditorStyleFormat.FontColor, EditorStyleFormat.BackgroundColor];
|
||||
|
||||
const textFormatActions = [
|
||||
EditorTurnFormat.Paragraph,
|
||||
EditorTurnFormat.Heading1,
|
||||
EditorTurnFormat.Heading2,
|
||||
EditorTurnFormat.Heading3,
|
||||
];
|
||||
|
||||
const blockFormatActions = [
|
||||
EditorTurnFormat.TodoList,
|
||||
EditorTurnFormat.Quote,
|
||||
EditorTurnFormat.ToggleList,
|
||||
EditorTurnFormat.NumberedList,
|
||||
EditorTurnFormat.BulletedList,
|
||||
];
|
||||
|
||||
export interface SelectionAction {
|
||||
format: EditorMarkFormat | EditorTurnFormat | EditorStyleFormat;
|
||||
Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
text: string;
|
||||
isActive: () => boolean;
|
||||
onClick: ((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void) | (() => void);
|
||||
alwaysInSingleLine?: boolean;
|
||||
}
|
||||
|
||||
export function useSelectionMarkFormatActions(editor: ReactEditor) {
|
||||
const { t } = useTranslation();
|
||||
const formatMark = useCallback(
|
||||
(format: EditorMarkFormat) => {
|
||||
CustomEditor.toggleMark(editor, {
|
||||
key: format,
|
||||
value: true,
|
||||
});
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
const isFormatActive = useCallback(
|
||||
(format: EditorMarkFormat) => {
|
||||
return CustomEditor.isMarkActive(editor, format);
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const map = {
|
||||
[EditorMarkFormat.Bold]: {
|
||||
format: EditorMarkFormat.Bold,
|
||||
Icon: BoldSvg,
|
||||
text: t('editor.bold'),
|
||||
isActive: () => {
|
||||
return isFormatActive(EditorMarkFormat.Bold);
|
||||
},
|
||||
onClick: () => {
|
||||
formatMark(EditorMarkFormat.Bold);
|
||||
},
|
||||
},
|
||||
[EditorMarkFormat.Italic]: {
|
||||
format: EditorMarkFormat.Italic,
|
||||
Icon: ItalicSvg,
|
||||
text: t('editor.italic'),
|
||||
isActive: () => {
|
||||
return isFormatActive(EditorMarkFormat.Italic);
|
||||
},
|
||||
onClick: () => {
|
||||
formatMark(EditorMarkFormat.Italic);
|
||||
},
|
||||
},
|
||||
[EditorMarkFormat.Underline]: {
|
||||
format: EditorMarkFormat.Underline,
|
||||
Icon: UnderlineSvg,
|
||||
text: t('editor.underline'),
|
||||
isActive: () => {
|
||||
return isFormatActive(EditorMarkFormat.Underline);
|
||||
},
|
||||
onClick: () => {
|
||||
formatMark(EditorMarkFormat.Underline);
|
||||
},
|
||||
},
|
||||
[EditorMarkFormat.StrikeThrough]: {
|
||||
format: EditorMarkFormat.StrikeThrough,
|
||||
Icon: StrikeThroughSvg,
|
||||
text: t('editor.strikethrough'),
|
||||
isActive: () => {
|
||||
return isFormatActive(EditorMarkFormat.StrikeThrough);
|
||||
},
|
||||
onClick: () => {
|
||||
formatMark(EditorMarkFormat.StrikeThrough);
|
||||
},
|
||||
},
|
||||
[EditorMarkFormat.Code]: {
|
||||
format: EditorMarkFormat.Code,
|
||||
Icon: CodeSvg,
|
||||
text: t('editor.embedCode'),
|
||||
isActive: () => {
|
||||
return isFormatActive(EditorMarkFormat.Code);
|
||||
},
|
||||
onClick: () => {
|
||||
formatMark(EditorMarkFormat.Code);
|
||||
},
|
||||
},
|
||||
[EditorMarkFormat.Formula]: {
|
||||
format: EditorMarkFormat.Formula,
|
||||
Icon: Functions,
|
||||
alwaysInSingleLine: true,
|
||||
text: t('document.plugins.createInlineMathEquation'),
|
||||
isActive: () => {
|
||||
return CustomEditor.isFormulaActive(editor);
|
||||
},
|
||||
onClick: () => {
|
||||
CustomEditor.toggleInlineElement(editor, EditorInlineNodeType.Formula);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return markFormatActions.map((format) => map[format]) as SelectionAction[];
|
||||
}, [editor, formatMark, isFormatActive, t]);
|
||||
}
|
||||
|
||||
export function useBlockFormatActionMap(editor: ReactEditor) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMemo(() => {
|
||||
const toHeading = (level: number) => {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.HeadingBlock,
|
||||
data: {
|
||||
level,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
[EditorTurnFormat.Paragraph]: {
|
||||
format: EditorTurnFormat.Paragraph,
|
||||
text: t('editor.text'),
|
||||
onClick: () => {
|
||||
const node = getBlock(editor);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.Paragraph,
|
||||
});
|
||||
},
|
||||
Icon: ParagraphSvg,
|
||||
isActive: () => {
|
||||
return CustomEditor.isBlockActive(editor, EditorTurnFormat.Paragraph);
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.Heading1]: {
|
||||
format: EditorTurnFormat.Heading1,
|
||||
text: t('editor.heading1'),
|
||||
Icon: Heading1Svg,
|
||||
onClick: () => {
|
||||
toHeading(1);
|
||||
},
|
||||
isActive: () => {
|
||||
const node = getBlock(editor) as HeadingNode;
|
||||
|
||||
if (!node) return false;
|
||||
const isBlock = CustomEditor.isBlockActive(editor, EditorNodeType.HeadingBlock);
|
||||
|
||||
return isBlock && node.data.level === 1;
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.Heading2]: {
|
||||
format: EditorTurnFormat.Heading2,
|
||||
Icon: Heading2Svg,
|
||||
text: t('editor.heading2'),
|
||||
onClick: () => {
|
||||
toHeading(2);
|
||||
},
|
||||
isActive: () => {
|
||||
const node = getBlock(editor) as HeadingNode;
|
||||
|
||||
if (!node) return false;
|
||||
const isBlock = CustomEditor.isBlockActive(editor, EditorNodeType.HeadingBlock);
|
||||
|
||||
return isBlock && node.data.level === 2;
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.Heading3]: {
|
||||
format: EditorTurnFormat.Heading3,
|
||||
Icon: Heading3Svg,
|
||||
text: t('editor.heading3'),
|
||||
onClick: () => {
|
||||
toHeading(3);
|
||||
},
|
||||
isActive: () => {
|
||||
const node = getBlock(editor) as HeadingNode;
|
||||
|
||||
if (!node) return false;
|
||||
const isBlock = CustomEditor.isBlockActive(editor, EditorNodeType.HeadingBlock);
|
||||
|
||||
return isBlock && node.data.level === 3;
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.TodoList]: {
|
||||
format: EditorTurnFormat.TodoList,
|
||||
text: t('document.plugins.todoList'),
|
||||
onClick: () => {
|
||||
const node = getBlock(editor);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.TodoListBlock,
|
||||
});
|
||||
},
|
||||
Icon: TodoListSvg,
|
||||
isActive: () => {
|
||||
const entry = CustomEditor.getBlock(editor);
|
||||
|
||||
if (!entry) return false;
|
||||
|
||||
const node = entry[0];
|
||||
|
||||
return node.type === EditorNodeType.TodoListBlock;
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.Quote]: {
|
||||
format: EditorTurnFormat.Quote,
|
||||
text: t('editor.quote'),
|
||||
onClick: () => {
|
||||
const node = getBlock(editor);
|
||||
|
||||
if (!node) return;
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.QuoteBlock,
|
||||
});
|
||||
},
|
||||
Icon: QuoteSvg,
|
||||
isActive: () => {
|
||||
const entry = CustomEditor.getBlock(editor);
|
||||
|
||||
if (!entry) return false;
|
||||
|
||||
const node = entry[0];
|
||||
|
||||
return node.type === EditorNodeType.QuoteBlock;
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.ToggleList]: {
|
||||
format: EditorTurnFormat.ToggleList,
|
||||
text: t('document.plugins.toggleList'),
|
||||
onClick: () => {
|
||||
const node = getBlock(editor);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.ToggleListBlock,
|
||||
});
|
||||
},
|
||||
Icon: ToggleListSvg,
|
||||
isActive: () => {
|
||||
const entry = CustomEditor.getBlock(editor);
|
||||
|
||||
if (!entry) return false;
|
||||
|
||||
const node = entry[0];
|
||||
|
||||
return node.type === EditorNodeType.ToggleListBlock;
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.NumberedList]: {
|
||||
format: EditorTurnFormat.NumberedList,
|
||||
text: t('document.plugins.numberedList'),
|
||||
onClick: () => {
|
||||
const node = getBlock(editor);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.NumberedListBlock,
|
||||
});
|
||||
},
|
||||
Icon: NumberedListSvg,
|
||||
isActive: () => {
|
||||
const entry = CustomEditor.getBlock(editor);
|
||||
|
||||
if (!entry) return false;
|
||||
|
||||
const node = entry[0];
|
||||
|
||||
return node.type === EditorNodeType.NumberedListBlock;
|
||||
},
|
||||
},
|
||||
[EditorTurnFormat.BulletedList]: {
|
||||
format: EditorTurnFormat.BulletedList,
|
||||
text: t('document.plugins.bulletedList'),
|
||||
onClick: () => {
|
||||
const node = getBlock(editor);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.BulletedListBlock,
|
||||
});
|
||||
},
|
||||
Icon: BulletedListSvg,
|
||||
isActive: () => {
|
||||
const entry = CustomEditor.getBlock(editor);
|
||||
|
||||
if (!entry) return false;
|
||||
|
||||
const node = entry[0];
|
||||
|
||||
return node.type === EditorNodeType.BulletedListBlock;
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [editor, t]);
|
||||
}
|
||||
|
||||
export function useSelectionTextFormatActions(editor: ReactEditor): SelectionAction[] {
|
||||
const map = useBlockFormatActionMap(editor);
|
||||
|
||||
return useMemo(() => {
|
||||
return textFormatActions.map((action) => map[action]);
|
||||
}, [map]);
|
||||
}
|
||||
|
||||
export function useBlockFormatActions(editor: ReactEditor): SelectionAction[] {
|
||||
const map = useBlockFormatActionMap(editor);
|
||||
|
||||
return useMemo(() => {
|
||||
return blockFormatActions.map((action) => map[action]);
|
||||
}, [map]);
|
||||
}
|
||||
|
||||
export function useSelectionStyleFormatActions(
|
||||
editor: ReactEditor,
|
||||
{
|
||||
onPopoverOpen,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPopoverClose,
|
||||
}: {
|
||||
onPopoverOpen: (format: EditorStyleFormat, target: HTMLButtonElement) => void;
|
||||
onPopoverClose: () => void;
|
||||
onFocus: () => void;
|
||||
onBlur: () => void;
|
||||
}
|
||||
) {
|
||||
const handleStyleChange = useCallback(
|
||||
(format: EditorStyleFormat, value: string) => {
|
||||
onPopoverClose();
|
||||
addMark(editor, format, value);
|
||||
},
|
||||
[editor, onPopoverClose]
|
||||
);
|
||||
|
||||
const subMenu = useCallback(
|
||||
(format: EditorStyleFormat) => {
|
||||
if (!editor.selection) return null;
|
||||
const entry = editor.node(editor.selection);
|
||||
const node = entry[0] as EditorInlineAttributes;
|
||||
|
||||
switch (format) {
|
||||
case EditorStyleFormat.Href:
|
||||
return null;
|
||||
case EditorStyleFormat.BackgroundColor:
|
||||
return (
|
||||
<BgColorPicker
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
color={node.bg_color}
|
||||
onChange={(color) => handleStyleChange(format, color)}
|
||||
/>
|
||||
);
|
||||
case EditorStyleFormat.FontColor:
|
||||
return (
|
||||
<FontColorPicker
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
color={node.font_color}
|
||||
onChange={(color) => handleStyleChange(format, color)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
[editor, handleStyleChange, onBlur, onFocus]
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = useMemo(() => {
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, format: EditorStyleFormat) => {
|
||||
onPopoverOpen(format, e.currentTarget);
|
||||
};
|
||||
|
||||
const map = {
|
||||
[EditorStyleFormat.Href]: {
|
||||
format: EditorStyleFormat.Href,
|
||||
Icon: LinkSvg,
|
||||
text: t('editor.link'),
|
||||
alwaysInSingleLine: true,
|
||||
isActive: () => {
|
||||
return CustomEditor.isMarkActive(editor, EditorStyleFormat.Href);
|
||||
},
|
||||
onClick: () => {
|
||||
if (!editor.selection) return;
|
||||
const text = Editor.string(editor, editor.selection);
|
||||
|
||||
addMark(editor, EditorStyleFormat.Href, text);
|
||||
},
|
||||
},
|
||||
[EditorStyleFormat.FontColor]: {
|
||||
format: EditorStyleFormat.FontColor,
|
||||
Icon: FormatColorTextIcon,
|
||||
text: t('editor.textColor'),
|
||||
isActive: () => {
|
||||
return CustomEditor.isMarkActive(editor, EditorStyleFormat.FontColor);
|
||||
},
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => handleClick(e, EditorStyleFormat.FontColor),
|
||||
},
|
||||
[EditorStyleFormat.BackgroundColor]: {
|
||||
format: EditorStyleFormat.BackgroundColor,
|
||||
Icon: FormatColorFillIcon,
|
||||
text: t('editor.backgroundColor'),
|
||||
isActive: () => {
|
||||
return CustomEditor.isMarkActive(editor, EditorStyleFormat.BackgroundColor);
|
||||
},
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>
|
||||
handleClick(e, EditorStyleFormat.BackgroundColor),
|
||||
},
|
||||
};
|
||||
|
||||
return styleFormatActions.map((format) => map[format]) as SelectionAction[];
|
||||
}, [t, editor, onPopoverOpen]);
|
||||
|
||||
return {
|
||||
options,
|
||||
handleStyleChange,
|
||||
subMenu,
|
||||
};
|
||||
}
|
@ -1,56 +1,34 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSlate } from 'slate-react';
|
||||
import { Range } from 'slate';
|
||||
import {
|
||||
SelectionAction,
|
||||
useBlockFormatActions,
|
||||
useSelectionMarkFormatActions,
|
||||
useSelectionStyleFormatActions,
|
||||
useSelectionTextFormatActions,
|
||||
} from '$app/components/editor/components/tools/selection_toolbar/SelectionActions.hooks';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { EditorStyleFormat } from '$app/application/document/document.types';
|
||||
import { PopoverPreventBlurProps } from '$app/components/editor/components/tools/popover';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { Paragraph } from '$app/components/editor/components/tools/selection_toolbar/actions/paragraph';
|
||||
import { Heading } from '$app/components/editor/components/tools/selection_toolbar/actions/heading';
|
||||
import { Divider } from '@mui/material';
|
||||
import { Bold } from '$app/components/editor/components/tools/selection_toolbar/actions/bold';
|
||||
import { Italic } from '$app/components/editor/components/tools/selection_toolbar/actions/italic';
|
||||
import { Underline } from '$app/components/editor/components/tools/selection_toolbar/actions/underline';
|
||||
import { StrikeThrough } from '$app/components/editor/components/tools/selection_toolbar/actions/strikethrough';
|
||||
import { InlineCode } from '$app/components/editor/components/tools/selection_toolbar/actions/inline_code';
|
||||
import { Formula } from '$app/components/editor/components/tools/selection_toolbar/actions/formula';
|
||||
import { TodoList } from '$app/components/editor/components/tools/selection_toolbar/actions/todo_list';
|
||||
import { Quote } from '$app/components/editor/components/tools/selection_toolbar/actions/quote';
|
||||
import { ToggleList } from '$app/components/editor/components/tools/selection_toolbar/actions/toggle_list';
|
||||
import { BulletedList } from '$app/components/editor/components/tools/selection_toolbar/actions/bulleted_list';
|
||||
import { NumberedList } from '$app/components/editor/components/tools/selection_toolbar/actions/numbered_list';
|
||||
import { Href } from '$app/components/editor/components/tools/selection_toolbar/actions/href';
|
||||
import { Align } from '$app/components/editor/components/tools/selection_toolbar/actions/align';
|
||||
import { Color } from '$app/components/editor/components/tools/selection_toolbar/actions/color';
|
||||
|
||||
function SelectionActions({
|
||||
toolbarVisible,
|
||||
storeSelection,
|
||||
restoreSelection,
|
||||
}: {
|
||||
toolbarVisible: boolean;
|
||||
storeSelection: () => void;
|
||||
restoreSelection: () => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const editor = useSlate() as ReactEditor;
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const [menuType, setMenuType] = useState<EditorStyleFormat | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handlePopoverOpen = useCallback((format: EditorStyleFormat, target: HTMLButtonElement) => {
|
||||
setAnchorEl(target);
|
||||
setMenuType(format);
|
||||
}, []);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
storeSelection();
|
||||
}, [storeSelection]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
restoreSelection();
|
||||
}, [restoreSelection]);
|
||||
|
||||
const handlePopoverClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setMenuType(null);
|
||||
handleBlur();
|
||||
}, [handleBlur]);
|
||||
|
||||
const [isMultiple, setIsMultiple] = useState(false);
|
||||
const getIsMultiple = useCallback(() => {
|
||||
const editor = useSlate();
|
||||
const isAcrossBlockSelection = useMemo(() => {
|
||||
if (!editor.selection) return false;
|
||||
const selection = editor.selection;
|
||||
const start = selection.anchor;
|
||||
@ -65,100 +43,43 @@ function SelectionActions({
|
||||
const endNode = CustomEditor.getBlock(editor, end);
|
||||
|
||||
return Boolean(startNode && endNode && startNode[0].blockId !== endNode[0].blockId);
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (toolbarVisible) {
|
||||
setIsMultiple(getIsMultiple());
|
||||
} else {
|
||||
setIsMultiple(false);
|
||||
}
|
||||
}, [editor, getIsMultiple, toolbarVisible]);
|
||||
|
||||
const markOptions = useSelectionMarkFormatActions(editor);
|
||||
const textOptions = useSelectionTextFormatActions(editor);
|
||||
const blockOptions = useBlockFormatActions(editor);
|
||||
const { options: styleOptions, subMenu: styleSubMenu } = useSelectionStyleFormatActions(editor, {
|
||||
onPopoverOpen: handlePopoverOpen,
|
||||
onPopoverClose: handlePopoverClose,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
});
|
||||
|
||||
const subMenu = useMemo(() => {
|
||||
if (!menuType) return null;
|
||||
|
||||
return styleSubMenu(menuType);
|
||||
}, [menuType, styleSubMenu]);
|
||||
|
||||
const group = useMemo(() => {
|
||||
const base = [markOptions, styleOptions];
|
||||
|
||||
if (isMultiple) {
|
||||
const filter = (option: SelectionAction) => {
|
||||
return !option.alwaysInSingleLine;
|
||||
};
|
||||
|
||||
return [markOptions.filter(filter), styleOptions.filter(filter)];
|
||||
}
|
||||
|
||||
return [textOptions, ...base, blockOptions];
|
||||
}, [markOptions, styleOptions, isMultiple, textOptions, blockOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toolbarVisible) {
|
||||
handlePopoverClose();
|
||||
}
|
||||
}, [toolbarVisible, handlePopoverClose]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editor.selection, editor]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={'flex w-fit flex-grow items-center'}>
|
||||
{group.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={index > 0 ? 'border-l border-gray-500' : ''}>
|
||||
{item.map((action) => {
|
||||
const { format, Icon, text, onClick, isActive } = action;
|
||||
|
||||
const isActivated = isActive();
|
||||
|
||||
return (
|
||||
<Tooltip placement={'top'} title={text} key={format}>
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
size={'small'}
|
||||
className={`bg-transparent px-1.5 py-0 text-bg-body hover:bg-transparent`}
|
||||
>
|
||||
<Icon
|
||||
style={{
|
||||
color: isActivated ? 'var(--fill-default)' : undefined,
|
||||
}}
|
||||
className={'h-4 w-4 text-lg text-bg-body hover:text-fill-hover'}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{open && (
|
||||
<Popover
|
||||
{...PopoverPreventBlurProps}
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handlePopoverClose}
|
||||
anchorOrigin={{
|
||||
vertical: 30,
|
||||
horizontal: 'left',
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
// prevent editor blur
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{subMenu}
|
||||
</Popover>
|
||||
<div className={'flex w-fit flex-grow items-center gap-1'}>
|
||||
{!isAcrossBlockSelection && (
|
||||
<>
|
||||
<Paragraph />
|
||||
<Heading />
|
||||
<Divider className={'opacity-40'} orientation={'vertical'} flexItem={true} />
|
||||
</>
|
||||
)}
|
||||
<Bold />
|
||||
<Italic />
|
||||
<Underline />
|
||||
<StrikeThrough />
|
||||
<InlineCode />
|
||||
{!isAcrossBlockSelection && (
|
||||
<>
|
||||
<Formula />
|
||||
<Divider className={'opacity-40'} orientation={'vertical'} flexItem={true} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isAcrossBlockSelection && (
|
||||
<>
|
||||
<TodoList />
|
||||
<Quote />
|
||||
<ToggleList />
|
||||
<BulletedList />
|
||||
<NumberedList />
|
||||
<Divider className={'opacity-40'} orientation={'vertical'} flexItem={true} />
|
||||
</>
|
||||
)}
|
||||
{!isAcrossBlockSelection && <Href />}
|
||||
<Align />
|
||||
<Color onOpen={storeSelection} onClose={restoreSelection} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { MutableRefObject, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { getSelectionPosition } from '$app/components/editor/components/tools/selection_toolbar/utils';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { DecorateStateContext } from '$app/components/editor/components/editor/Editor.hooks';
|
||||
import { BaseRange } from 'slate';
|
||||
|
||||
export function useSelectionToolbar(ref: MutableRefObject<HTMLDivElement | null>) {
|
||||
const editor = useSlate() as ReactEditor;
|
||||
@ -82,6 +84,8 @@ export function useSelectionToolbar(ref: MutableRefObject<HTMLDivElement | null>
|
||||
};
|
||||
}, [editor, recalculatePosition, ref]);
|
||||
|
||||
const decorateStateContext = useContext(DecorateStateContext);
|
||||
|
||||
const restoreSelection = useCallback(() => {
|
||||
if (!rangeRef.current) return;
|
||||
const windowSelection = window.getSelection();
|
||||
@ -90,9 +94,14 @@ export function useSelectionToolbar(ref: MutableRefObject<HTMLDivElement | null>
|
||||
windowSelection.removeAllRanges();
|
||||
windowSelection.addRange(rangeRef.current);
|
||||
rangeRef.current = null;
|
||||
}, []);
|
||||
decorateStateContext.clear();
|
||||
}, [decorateStateContext]);
|
||||
|
||||
const storeSelection = useCallback(() => {
|
||||
decorateStateContext.add({
|
||||
range: editor.selection as BaseRange,
|
||||
class_name: 'bg-content-blue-100',
|
||||
});
|
||||
const windowSelection = window.getSelection();
|
||||
|
||||
if (!windowSelection) return;
|
||||
@ -100,7 +109,7 @@ export function useSelectionToolbar(ref: MutableRefObject<HTMLDivElement | null>
|
||||
if (windowSelection.rangeCount === 0) return;
|
||||
|
||||
rangeRef.current = windowSelection.getRangeAt(0);
|
||||
}, []);
|
||||
}, [decorateStateContext, editor.selection]);
|
||||
|
||||
return {
|
||||
visible,
|
||||
|
@ -5,13 +5,13 @@ import SelectionActions from '$app/components/editor/components/tools/selection_
|
||||
export const SelectionToolbar = memo(() => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { visible, ...toolbarProps } = useSelectionToolbar(ref);
|
||||
const { visible, restoreSelection, storeSelection } = useSelectionToolbar(ref);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={
|
||||
'selection-toolbar pointer-events-none absolute z-[100] flex w-fit flex-grow transform items-center rounded-lg bg-[var(--fill-toolbar)] p-2 opacity-0 shadow-lg transition-opacity'
|
||||
'selection-toolbar pointer-events-none absolute z-[100] flex min-h-[37px] w-fit flex-grow transform items-center rounded-lg bg-[var(--fill-toolbar)] px-2 opacity-0 shadow-lg transition-opacity'
|
||||
}
|
||||
onMouseDown={(e) => {
|
||||
// prevent toolbar from taking focus away from editor
|
||||
@ -21,7 +21,11 @@ export const SelectionToolbar = memo(() => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<SelectionActions {...toolbarProps} toolbarVisible={visible} />
|
||||
{visible ? (
|
||||
<SelectionActions storeSelection={storeSelection} restoreSelection={restoreSelection} />
|
||||
) : (
|
||||
<div className={'w-[541px]'} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,32 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
|
||||
import { Tooltip } from '@mui/material';
|
||||
|
||||
const ActionButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{
|
||||
tooltip: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
children: React.ReactNode;
|
||||
active?: boolean;
|
||||
} & IconButtonProps
|
||||
>(({ tooltip, onClick, children, active, className, ...props }, ref) => {
|
||||
return (
|
||||
<Tooltip placement={'top'} title={tooltip}>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
size={'small'}
|
||||
style={{
|
||||
color: active ? 'var(--fill-default)' : undefined,
|
||||
}}
|
||||
{...props}
|
||||
className={`${className ?? ''} bg-transparent px-1 py-2 text-bg-body hover:bg-transparent hover:text-fill-hover`}
|
||||
>
|
||||
{children}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
export default ActionButton;
|
@ -0,0 +1,96 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { ReactComponent as AlignLeftSvg } from '$app/assets/align-left.svg';
|
||||
import { ReactComponent as AlignCenterSvg } from '$app/assets/align-center.svg';
|
||||
import { ReactComponent as AlignRightSvg } from '$app/assets/align-right.svg';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
|
||||
export function Align() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const align = CustomEditor.getAlign(editor);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const Icon = useMemo(() => {
|
||||
switch (align) {
|
||||
case 'left':
|
||||
return AlignLeftSvg;
|
||||
case 'center':
|
||||
return AlignCenterSvg;
|
||||
case 'right':
|
||||
return AlignRightSvg;
|
||||
default:
|
||||
return AlignLeftSvg;
|
||||
}
|
||||
}, [align]);
|
||||
|
||||
const toggleAlign = useCallback(
|
||||
(align: string) => {
|
||||
return () => {
|
||||
CustomEditor.toggleAlign(editor, align);
|
||||
};
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const getAlignIcon = useCallback((key: string) => {
|
||||
switch (key) {
|
||||
case 'left':
|
||||
return <AlignLeftSvg />;
|
||||
case 'center':
|
||||
return <AlignCenterSvg />;
|
||||
case 'right':
|
||||
return <AlignRightSvg />;
|
||||
default:
|
||||
return <AlignLeftSvg />;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement={'bottom'}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onOpen={handleOpen}
|
||||
classes={{ tooltip: 'bg-fill-toolbar' }}
|
||||
title={
|
||||
<div className={'flex items-center justify-center'}>
|
||||
{['left', 'center', 'right'].map((key) => {
|
||||
return (
|
||||
<IconButton
|
||||
key={key}
|
||||
className={'text-content-on-fill hover:bg-transparent hover:text-content-blue-400'}
|
||||
onClick={toggleAlign(key)}
|
||||
disabled={align === key}
|
||||
>
|
||||
{getAlignIcon(key)}
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ActionButton active={!!align} tooltip={t('document.plugins.optionAction.align')}>
|
||||
<div className={'flex items-center'}>
|
||||
<Icon />
|
||||
<MoreSvg className={'rotate-90 transform'} />
|
||||
</div>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default Align;
|
@ -0,0 +1 @@
|
||||
export * from './Align';
|
@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { ReactComponent as BoldSvg } from '$app/assets/bold.svg';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
export function Bold() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Bold);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.toggleMark(editor, {
|
||||
key: EditorMarkFormat.Bold,
|
||||
value: true,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} active={isActivated} tooltip={t('editor.bold')}>
|
||||
<BoldSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Bold;
|
@ -0,0 +1 @@
|
||||
export * from './Bold';
|
@ -0,0 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { EditorNodeType } from '$app/application/document/document.types';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { ReactComponent as BulletedListSvg } from '$app/assets/list.svg';
|
||||
|
||||
export function BulletedList() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.BulletedListBlock);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.BulletedListBlock,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton active={isActivated} onClick={onClick} tooltip={t('document.plugins.bulletedList')}>
|
||||
<BulletedListSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default BulletedList;
|
@ -0,0 +1 @@
|
||||
export * from './BulletedList';
|
@ -0,0 +1,95 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import ColorLensOutlinedIcon from '@mui/icons-material/ColorLensOutlined';
|
||||
import { addMark } from 'slate';
|
||||
import { Popover } from '@mui/material';
|
||||
import { BgColorPicker, FontColorPicker } from '$app/components/editor/components/tools/_shared';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
import { PopoverNoBackdropProps } from '$app/components/editor/components/tools/popover';
|
||||
|
||||
export function Color({ onClose, onOpen }: { onClose?: () => void; onOpen?: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
onOpen?.();
|
||||
} else {
|
||||
onClose?.();
|
||||
}
|
||||
}, [onClose, onOpen, open]);
|
||||
const isActivated =
|
||||
CustomEditor.isMarkActive(editor, EditorMarkFormat.FontColor) ||
|
||||
CustomEditor.isMarkActive(editor, EditorMarkFormat.BgColor);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => {
|
||||
addMark(editor, format, color);
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const fontColor = useMemo(() => {
|
||||
return editor.getMarks()?.[EditorMarkFormat.FontColor];
|
||||
}, [editor]);
|
||||
|
||||
const bgColor = useMemo(() => {
|
||||
return editor.getMarks()?.[EditorMarkFormat.BgColor];
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
ref={ref}
|
||||
onMouseEnter={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
tooltip={t('editor.color')}
|
||||
active={isActivated}
|
||||
>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<ColorLensOutlinedIcon className={'w-[14px]'} />
|
||||
<MoreSvg className={'rotate-90 transform'} />
|
||||
</div>
|
||||
{open && (
|
||||
<Popover
|
||||
{...PopoverNoBackdropProps}
|
||||
open={open}
|
||||
anchorEl={ref.current}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
...PopoverNoBackdropProps.PaperProps,
|
||||
className: 'w-[200px] max-h-[360px] overflow-x-hidden overflow-y-auto',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
disableAutoFocus={false}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FontColorPicker color={fontColor} onChange={(color) => handleChange(EditorMarkFormat.FontColor, color)} />
|
||||
<BgColorPicker color={bgColor} onChange={(color) => handleChange(EditorMarkFormat.BgColor, color)} />
|
||||
</Popover>
|
||||
)}
|
||||
</ActionButton>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './Color';
|
@ -0,0 +1,24 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import Functions from '@mui/icons-material/Functions';
|
||||
|
||||
export function Formula() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isFormulaActive(editor);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.toggleFormula(editor);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} active={isActivated} tooltip={t('document.plugins.createInlineMathEquation')}>
|
||||
<Functions className={'w-[14px]'} />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Formula;
|
@ -0,0 +1 @@
|
||||
export * from './Formula';
|
@ -0,0 +1,56 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { ReactComponent as Heading1Svg } from '$app/assets/h1.svg';
|
||||
import { ReactComponent as Heading2Svg } from '$app/assets/h2.svg';
|
||||
import { ReactComponent as Heading3Svg } from '$app/assets/h3.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { EditorNodeType, HeadingNode } from '$app/application/document/document.types';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { getBlock } from '$app/components/editor/plugins/utils';
|
||||
|
||||
export function Heading() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const toHeading = useCallback(
|
||||
(level: number) => {
|
||||
return () => {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.HeadingBlock,
|
||||
data: {
|
||||
level,
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const isActivated = useCallback(
|
||||
(level: number) => {
|
||||
const node = getBlock(editor) as HeadingNode;
|
||||
|
||||
if (!node) return false;
|
||||
const isBlock = CustomEditor.isBlockActive(editor, EditorNodeType.HeadingBlock);
|
||||
|
||||
return isBlock && node.data.level === level;
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex items-center justify-center'}>
|
||||
<ActionButton active={isActivated(1)} tooltip={t('editor.heading1')} onClick={toHeading(1)}>
|
||||
<Heading1Svg />
|
||||
</ActionButton>
|
||||
<ActionButton active={isActivated(2)} tooltip={t('editor.heading2')} onClick={toHeading(2)}>
|
||||
<Heading2Svg />
|
||||
</ActionButton>
|
||||
<ActionButton active={isActivated(3)} tooltip={t('editor.heading3')} onClick={toHeading(3)}>
|
||||
<Heading3Svg />
|
||||
</ActionButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Heading;
|
@ -0,0 +1 @@
|
||||
export * from './Heading';
|
@ -0,0 +1,32 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { ReactComponent as LinkSvg } from '$app/assets/link.svg';
|
||||
import { Editor } from 'slate';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
export function Href() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Href);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!editor.selection) return;
|
||||
const text = Editor.string(editor, editor.selection);
|
||||
|
||||
CustomEditor.toggleMark(editor, {
|
||||
key: EditorMarkFormat.Href,
|
||||
value: text,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} active={isActivated} tooltip={t('editor.link')}>
|
||||
<LinkSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Href;
|
@ -0,0 +1 @@
|
||||
export * from './Href';
|
@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { ReactComponent as CodeSvg } from '$app/assets/inline-code.svg';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
export function InlineCode() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Code);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.toggleMark(editor, {
|
||||
key: EditorMarkFormat.Code,
|
||||
value: true,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} active={isActivated} tooltip={t('editor.embedCode')}>
|
||||
<CodeSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default InlineCode;
|
@ -0,0 +1 @@
|
||||
export * from './InlineCode';
|
@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { ReactComponent as ItalicSvg } from '$app/assets/italic.svg';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
export function Italic() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Italic);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.toggleMark(editor, {
|
||||
key: EditorMarkFormat.Italic,
|
||||
value: true,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} active={isActivated} tooltip={t('editor.italic')}>
|
||||
<ItalicSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Italic;
|
@ -0,0 +1 @@
|
||||
export * from './Italic';
|
@ -0,0 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { EditorNodeType } from '$app/application/document/document.types';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { ReactComponent as NumberedListSvg } from '$app/assets/numbers.svg';
|
||||
|
||||
export function NumberedList() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.NumberedListBlock);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.NumberedListBlock,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton active={isActivated} onClick={onClick} tooltip={t('document.plugins.numberedList')}>
|
||||
<NumberedListSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default NumberedList;
|
@ -0,0 +1 @@
|
||||
export * from './NumberedList';
|
@ -0,0 +1,33 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getBlock } from '$app/components/editor/plugins/utils';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { EditorNodeType } from '$app/application/document/document.types';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { ReactComponent as ParagraphSvg } from '$app/assets/text.svg';
|
||||
|
||||
export function Paragraph() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
const node = getBlock(editor);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.Paragraph,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
const isActive = CustomEditor.isBlockActive(editor, EditorNodeType.Paragraph);
|
||||
|
||||
return (
|
||||
<ActionButton active={isActive} onClick={onClick} tooltip={t('editor.text')}>
|
||||
<ParagraphSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Paragraph;
|
@ -0,0 +1 @@
|
||||
export * from './Paragraph';
|
@ -0,0 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { EditorNodeType } from '$app/application/document/document.types';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { ReactComponent as QuoteSvg } from '$app/assets/quote.svg';
|
||||
|
||||
export function Quote() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.QuoteBlock);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.QuoteBlock,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton active={isActivated} onClick={onClick} tooltip={t('editor.quote')}>
|
||||
<QuoteSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Quote;
|
@ -0,0 +1 @@
|
||||
export * from './Quote';
|
@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { ReactComponent as StrikeThroughSvg } from '$app/assets/strikethrough.svg';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
export function StrikeThrough() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.StrikeThrough);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.toggleMark(editor, {
|
||||
key: EditorMarkFormat.StrikeThrough,
|
||||
value: true,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} active={isActivated} tooltip={t('editor.strikethrough')}>
|
||||
<StrikeThroughSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default StrikeThrough;
|
@ -0,0 +1 @@
|
||||
export * from './StrikeThrough';
|
@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { EditorNodeType } from '$app/application/document/document.types';
|
||||
import { ReactComponent as TodoListSvg } from '$app/assets/todo-list.svg';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function TodoList() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
|
||||
const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.TodoListBlock);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.TodoListBlock,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton active={isActivated} onClick={onClick} tooltip={t('document.plugins.todoList')}>
|
||||
<TodoListSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default TodoList;
|
@ -0,0 +1 @@
|
||||
export * from './TodoList';
|
@ -0,0 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { EditorNodeType } from '$app/application/document/document.types';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { ReactComponent as ToggleListSvg } from '$app/assets/show-menu.svg';
|
||||
|
||||
export function ToggleList() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.ToggleListBlock);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.turnToBlock(editor, {
|
||||
type: EditorNodeType.ToggleListBlock,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton active={isActivated} onClick={onClick} tooltip={t('document.plugins.toggleList')}>
|
||||
<ToggleListSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToggleList;
|
@ -0,0 +1 @@
|
||||
export * from './ToggleList';
|
@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { CustomEditor } from '$app/components/editor/command';
|
||||
import { ReactComponent as UnderlineSvg } from '$app/assets/underline.svg';
|
||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||
|
||||
export function Underline() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlateStatic();
|
||||
const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Underline);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
CustomEditor.toggleMark(editor, {
|
||||
key: EditorMarkFormat.Underline,
|
||||
value: true,
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} active={isActivated} tooltip={t('editor.underline')}>
|
||||
<UnderlineSvg />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Underline;
|
@ -0,0 +1 @@
|
||||
export * from './Underline';
|
@ -5,11 +5,11 @@ import { CustomEditor } from '$app/components/editor/command';
|
||||
export function getHeadingCssProperty(level: number) {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return 'text-3xl pt-4 pb-2';
|
||||
return 'text-3xl py-[16px] font-bold';
|
||||
case 2:
|
||||
return 'text-2xl pt-3 pb-2';
|
||||
return 'text-2xl py-[12px] font-bold';
|
||||
case 3:
|
||||
return 'text-xl pt-2 pb-2';
|
||||
return 'text-xl py-[8px] font-bold';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -11,12 +11,12 @@ export function transformToInlineElement(op: Op): Element | null {
|
||||
const attributes = op.attributes;
|
||||
|
||||
if (!attributes) return null;
|
||||
const isFormula = attributes.formula;
|
||||
const formula = attributes.formula as string;
|
||||
|
||||
if (isFormula) {
|
||||
if (formula) {
|
||||
return {
|
||||
type: EditorInlineNodeType.Formula,
|
||||
data: true,
|
||||
data: formula,
|
||||
children: [
|
||||
{
|
||||
text: op.insert as string,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import ButtonPopoverList from '$app/components/_shared/ButtonPopoverList';
|
||||
import { IconButton } from '@mui/material';
|
||||
import AddSvg from '$app/components/_shared/svg/AddSvg';
|
||||
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
|
||||
import { GridSvg } from '$app/components/_shared/svg/GridSvg';
|
||||
import { BoardSvg } from '$app/components/_shared/svg/BoardSvg';
|
||||
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
|
||||
import { ReactComponent as GridSvg } from '$app/assets/grid.svg';
|
||||
import { ReactComponent as BoardSvg } from '$app/assets/board.svg';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
|
||||
function AddButton({ isVisible, onAddPage }: { isVisible: boolean; onAddPage: (layout: ViewLayoutPB) => void }) {
|
||||
@ -67,7 +67,7 @@ function AddButton({ isVisible, onAddPage }: { isVisible: boolean; onAddPage: (l
|
||||
popoverOptions={options}
|
||||
isVisible={isVisible}
|
||||
>
|
||||
<IconButton className={'mr-2 h-6 w-6'}>
|
||||
<IconButton size={'small'}>
|
||||
<AddSvg />
|
||||
</IconButton>
|
||||
</ButtonPopoverList>
|
||||
|
@ -2,10 +2,12 @@ import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconButton } from '@mui/material';
|
||||
import ButtonPopoverList from '../../_shared/ButtonPopoverList';
|
||||
import { MoreHoriz } from '@mui/icons-material';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { CopySvg } from '$app/components/_shared/svg/CopySvg';
|
||||
import { EditSvg } from '$app/components/_shared/svg/EditSvg';
|
||||
|
||||
import { ReactComponent as DetailsSvg } from '$app/assets/details.svg';
|
||||
import { ReactComponent as EditSvg } from '$app/assets/edit.svg';
|
||||
import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
|
||||
import { ReactComponent as TrashSvg } from '$app/assets/delete.svg';
|
||||
|
||||
import RenameDialog from './RenameDialog';
|
||||
import { Page } from '$app_reducers/pages/slice';
|
||||
import DeleteDialog from '$app/components/layout/NestedPage/DeleteDialog';
|
||||
@ -83,8 +85,8 @@ function MoreButton({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton className={'h-6 w-6'}>
|
||||
<MoreHoriz />
|
||||
<IconButton size={'small'}>
|
||||
<DetailsSvg />
|
||||
</IconButton>
|
||||
</ButtonPopoverList>
|
||||
<RenameDialog
|
||||
|
@ -12,7 +12,7 @@ interface EditorInlineAttributes {
|
||||
code?: boolean;
|
||||
formula?: string;
|
||||
prism_token?: string;
|
||||
temporary?: string;
|
||||
class_name?: string;
|
||||
mention?: {
|
||||
type: string;
|
||||
// inline page ref id
|
||||
|
@ -14,6 +14,98 @@ export const getDesignTokens = (mode: ThemeMode): ThemeOptions => {
|
||||
textTransform: 'none',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--fill-list-hover)',
|
||||
},
|
||||
borderRadius: '4px',
|
||||
padding: '2px',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
contained: {
|
||||
color: 'var(--content-on-fill)',
|
||||
},
|
||||
containedPrimary: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--fill-default)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButtonBase: {
|
||||
defaultProps: {
|
||||
sx: {
|
||||
'&.Mui-selected:hover': {
|
||||
backgroundColor: 'var(--fill-list-active)',
|
||||
},
|
||||
},
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--fill-list-hover)',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: 'var(--fill-list-active)',
|
||||
},
|
||||
borderRadius: '4px',
|
||||
padding: '2px',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
defaultProps: {
|
||||
sx: {
|
||||
'& .MuiBackdrop-root': {
|
||||
backgroundColor: 'var(--bg-mask)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
arrow: {
|
||||
color: 'var(--bg-tips)',
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'var(--bg-tips)',
|
||||
color: 'var(--text-title)',
|
||||
fontSize: '0.85rem',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
input: {
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDivider: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderColor: 'var(--line-divider)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
mode: isDark ? 'dark' : 'light',
|
||||
primary: {
|
||||
|
@ -1,140 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export function debounce(fn: (...args: any[]) => void, delay: number) {
|
||||
let timeout: NodeJS.Timeout;
|
||||
const debounceFn = (...args: any[]) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
fn.apply(undefined, args);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
debounceFn.cancel = () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
|
||||
return debounceFn;
|
||||
}
|
||||
|
||||
export function throttle<T extends (...args: any[]) => void = (...args: any[]) => void>(
|
||||
fn: T,
|
||||
delay: number,
|
||||
immediate = true
|
||||
): T {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const run = (...args: Parameters<T>) => {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
!immediate && fn.apply(undefined, args);
|
||||
}, delay);
|
||||
immediate && fn.apply(undefined, args);
|
||||
}
|
||||
};
|
||||
|
||||
return run as T;
|
||||
}
|
||||
|
||||
export function get<T = any>(obj: any, path: string[], defaultValue?: any): T {
|
||||
let value = obj;
|
||||
|
||||
for (const prop of path) {
|
||||
if (value === undefined || typeof value !== 'object' || value[prop] === undefined) {
|
||||
return defaultValue !== undefined ? defaultValue : undefined;
|
||||
}
|
||||
|
||||
value = value[prop];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function set(obj: any, path: string[], value: any): void {
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const prop = path[i];
|
||||
|
||||
if (i === path.length - 1) {
|
||||
current[prop] = value;
|
||||
} else {
|
||||
if (!current[prop]) {
|
||||
current[prop] = {};
|
||||
}
|
||||
|
||||
current = current[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isEqual<T>(value1: T, value2: T): boolean {
|
||||
if (typeof value1 !== 'object' || value1 === null || typeof value2 !== 'object' || value2 === null) {
|
||||
return value1 === value2;
|
||||
}
|
||||
|
||||
if (Array.isArray(value1)) {
|
||||
if (!Array.isArray(value2) || value1.length !== value2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < value1.length; i++) {
|
||||
if (!isEqual(value1[i], value2[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(value1);
|
||||
const keys2 = Object.keys(value2);
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of keys1) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (!isEqual(value1[key], value2[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function clone<T>(value: T): T {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => clone(item)) as any;
|
||||
}
|
||||
|
||||
const result: any = {};
|
||||
|
||||
for (const key in value) {
|
||||
result[key] = clone(value[key]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function chunkArray<T>(array: T[], chunkSize: number) {
|
||||
const chunks = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < array.length) {
|
||||
chunks.push(array.slice(i, i + chunkSize));
|
||||
i += chunkSize;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interval that repeatedly calls the given function with a specified delay.
|
||||
*
|
||||
|
@ -1,72 +0,0 @@
|
||||
|
||||
.MuiDialog-root [class$="-MuiBackdrop-root-MuiDialog-backdrop"] {
|
||||
background-color: var(--bg-mask);
|
||||
}
|
||||
|
||||
[class$="-MuiPaper-root-MuiPopover-paper"], [class$="-MuiPaper-root-MuiDialog-paper"] {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
[class$='-MuiButtonBase-root-MuiMenuItem-root'].MuiButtonBase-root.Mui-selected {
|
||||
background-color: var(--fill-list-active);
|
||||
}
|
||||
|
||||
[class$='-MuiButtonBase-root-MuiMenuItem-root'].MuiButtonBase-root:hover, [class$='-MuiButtonBase-root-MuiMenuItem-root'].MuiButtonBase-root.Mui-selected:hover {
|
||||
background-color: var(--fill-list-active);
|
||||
}
|
||||
|
||||
.MuiList-root .Mui-focusVisible.MuiMenuItem-root:not(:hover, .Mui-selected) {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.MuiPaper-root.MuiMenu-paper.MuiPopover-paper {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.MuiButton-contained.MuiButton-containedPrimary.MuiButton-containedPrimary {
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.MuiIconButton-root.MuiIconButton-sizeMedium {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.MuiIconButton-root[class$='-MuiButtonBase-root-MuiIconButton-root']:hover {
|
||||
background: var(--fill-list-hover);
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.MuiIconButton-root {
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.MuiButton-containedPrimary.MuiButton-contained:hover {
|
||||
background: var(--fill-default);
|
||||
}
|
||||
|
||||
.MuiTooltip-tooltip {
|
||||
background: var(--bg-tips) !important;
|
||||
color: var(--text-title) !important;
|
||||
font-size: 0.85rem !important;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.MuiTooltip-arrow {
|
||||
color: var(--bg-tips) !important;
|
||||
}
|
||||
|
||||
.MuiInput-input[class$='-MuiSelect-select-MuiInputBase-input-MuiInput-input']:focus {
|
||||
background: transparent;
|
||||
}
|
||||
.MuiList-root .MuiMenuItem-root {
|
||||
border-radius: 8px;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.MuiDivider-root.MuiDivider-fullWidth {
|
||||
border-color: var(--line-divider);
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
@import './variables/index.css';
|
||||
@import './mui.css';
|
||||
|
||||
/* stop body from scrolling */
|
||||
html,
|
||||
@ -40,10 +39,35 @@ body {
|
||||
scrollbar-shadow-color: var(--bg-body);
|
||||
}
|
||||
|
||||
.block-element {
|
||||
@apply my-[1px];
|
||||
}
|
||||
|
||||
.block-element .block-element {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.block-element[data-block-type="page"] .text-element {
|
||||
min-height: 52px !important;
|
||||
line-height: 52px !important;
|
||||
}
|
||||
|
||||
.block-element[data-block-type="paragraph"] .block-element {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.block-element.block-align-left .text-element {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.block-element.block-align-right .text-element {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.block-element.block-align-center .text-element {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.block-element[data-block-type="todo_list"] .checked > .text-element {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-caption);
|
||||
|
Loading…
Reference in New Issue
Block a user