Skip to content

Commit d2ccec7

Browse files
authored
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
1 parent 69469e9 commit d2ccec7

File tree

90 files changed

+1454
-1246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+1454
-1246
lines changed

frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export interface MathEquationNode extends Element {
100100

101101
export interface FormulaNode extends Element {
102102
type: EditorInlineNodeType.Formula;
103-
data: boolean;
103+
data: string;
104104
}
105105

106106
export interface MentionNode extends Element {
@@ -173,51 +173,15 @@ export interface EditorElementProps<T = Element> extends HTMLAttributes<HTMLDivE
173173
node: T;
174174
}
175175

176-
export interface EditorInlineAttributes {
177-
bold?: boolean;
178-
italic?: boolean;
179-
underline?: boolean;
180-
strikethrough?: boolean;
181-
font_color?: string;
182-
bg_color?: string;
183-
href?: string;
184-
code?: boolean;
185-
formula?: boolean;
186-
prism_token?: string;
187-
mention?: {
188-
type: string;
189-
// inline page ref id
190-
page?: string;
191-
// reminder date ref id
192-
date?: string;
193-
};
194-
}
195-
196176
export enum EditorMarkFormat {
197177
Bold = 'bold',
198178
Italic = 'italic',
199179
Underline = 'underline',
200180
StrikeThrough = 'strikethrough',
201181
Code = 'code',
202-
Formula = 'formula',
203-
}
204-
205-
export enum EditorStyleFormat {
206-
FontColor = 'font_color',
207-
BackgroundColor = 'bg_color',
208182
Href = 'href',
209-
}
210-
211-
export enum EditorTurnFormat {
212-
Paragraph = 'paragraph',
213-
Heading1 = 'heading1', // 'heading1' is a special format, it's not a slate node type, but a slate node type's data
214-
Heading2 = 'heading2',
215-
Heading3 = 'heading3',
216-
TodoList = 'todo_list',
217-
BulletedList = 'bulleted_list',
218-
NumberedList = 'numbered_list',
219-
Quote = 'quote',
220-
ToggleList = 'toggle_list',
183+
FontColor = 'font_color',
184+
BgColor = 'bg_color',
221185
}
222186

223187
export enum MentionType {

frontend/appflowy_tauri/src/appflowy_app/components/_shared/ButtonPopoverList/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions,
5353
option.onClick();
5454
handleClose();
5555
}}
56+
className={'flex items-center gap-1 rounded-none px-2 text-xs font-medium'}
5657
>
57-
<span className={'mr-2'}>{option.icon}</span>
58+
<span className={'text-base'}>{option.icon}</span>
5859
<span>{option.label}</span>
5960
</MenuItem>
6061
))}

frontend/appflowy_tauri/src/appflowy_app/components/_shared/EmojiPicker/EmojiPicker.hooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { init, FrequentlyUsed, getEmojiDataFromNative, Store } from 'emoji-mart'
55
import { PopoverProps } from '@mui/material/Popover';
66
import { PopoverOrigin } from '@mui/material/Popover/Popover';
77
import { useVirtualizer } from '@tanstack/react-virtual';
8-
import { chunkArray } from '$app/utils/tool';
8+
import chunk from 'lodash-es/chunk';
99

1010
export const EMOJI_SIZE = 32;
1111

@@ -154,7 +154,7 @@ export function getRowsWithCategories(emojiCategories: EmojiCategory[], rowSize:
154154
id: category.id,
155155
type: 'category',
156156
});
157-
chunkArray(category.emojis, rowSize).forEach((chunk, index) => {
157+
chunk(category.emojis, rowSize).forEach((chunk, index) => {
158158
rows.push({
159159
type: 'emojis',
160160
emojis: chunk,

frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { updatePageName } from '$app_reducers/pages/async_actions';
66

77
export const DatabaseTitle = () => {
88
const viewId = useViewId();
9-
const pageName = useAppSelector((state) => state.pages.pageMap[viewId].name);
9+
const pageName = useAppSelector((state) => state.pages.pageMap[viewId]?.name || '');
1010
const dispatch = useAppDispatch();
1111

1212
const handleInput = useCallback<FormEventHandler>(

frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const TextCell: FC<TextCellProps> = ({ placeholder, cell }) => {
4040

4141
return (
4242
<>
43-
<CellText className={`min-h-[36px] w-full`} ref={cellRef} onClick={handleClick}>
43+
<CellText className={`min-h-[36px] w-full cursor-text`} ref={cellRef} onClick={handleClick}>
4444
{content}
4545
</CellText>
4646
<Suspense>

frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Button, Tooltip } from '@mui/material';
22
import { DragEventHandler, FC, HTMLAttributes, memo, useCallback, useEffect, useMemo, useState } from 'react';
3-
import { throttle } from '$app/utils/tool';
43
import { useViewId } from '$app/hooks';
54
import { DragItem, DropPosition, DragType, useDraggable, useDroppable, ScrollDirection } from '../../_shared';
65
import { fieldService, Field } from '../../application';
@@ -9,6 +8,7 @@ import GridResizer from '$app/components/database/grid/GridField/GridResizer';
98
import GridFieldMenu from '$app/components/database/grid/GridField/GridFieldMenu';
109
import { areEqual } from 'react-window';
1110
import { useOpenMenu } from '$app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks';
11+
import throttle from 'lodash-es/throttle';
1212

1313
export interface GridFieldProps extends HTMLAttributes<HTMLDivElement> {
1414
field: Field;
@@ -88,9 +88,9 @@ export const GridField: FC<GridFieldProps> = memo(
8888

8989
const [menuAnchorPosition, setMenuAnchorPosition] = useState<
9090
| {
91-
top: number;
92-
left: number;
93-
}
91+
top: number;
92+
left: number;
93+
}
9494
| undefined
9595
>(undefined);
9696

@@ -164,8 +164,9 @@ export const GridField: FC<GridFieldProps> = memo(
164164
/>
165165
{isOver && (
166166
<div
167-
className={`absolute bottom-0 top-0 z-10 w-0.5 bg-blue-500 ${dropPosition === DropPosition.Before ? 'left-[-1px]' : 'left-full'
168-
}`}
167+
className={`absolute bottom-0 top-0 z-10 w-0.5 bg-blue-500 ${
168+
dropPosition === DropPosition.Before ? 'left-[-1px]' : 'left-full'
169+
}`}
169170
/>
170171
)}
171172
<GridResizer field={field} onWidthChange={resizeColumnWidth} />

frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridNewRow/GridNewRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function GridNewRow({ index, groupId, getContainerRef }: Props) {
4949
toggleCssProperty(false);
5050
}}
5151
onClick={handleClick}
52-
className={'grid-new-row flex grow'}
52+
className={'grid-new-row flex grow cursor-pointer'}
5353
>
5454
<span
5555
style={{

frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import { ReactEditor } from 'slate-react';
2-
import { Editor, Element as SlateElement, Range, Transforms } from 'slate';
3-
import { EditorInlineNodeType } from '$app/application/document/document.types';
2+
import { Editor, Element as SlateElement, NodeEntry, Range, Transforms } from 'slate';
3+
import { EditorInlineNodeType, FormulaNode } from '$app/application/document/document.types';
44

5-
export function insertFormula(editor: ReactEditor, formula?: string) {
5+
export function insertFormula(editor: ReactEditor) {
66
if (editor.selection) {
7-
wrapFormula(editor, formula);
7+
wrapFormula(editor);
88
}
99
}
1010

1111
export function updateFormula(editor: ReactEditor, formula: string) {
1212
if (isFormulaActive(editor)) {
1313
Transforms.delete(editor);
14-
insertFormula(editor, formula);
14+
wrapFormula(editor, formula);
15+
}
16+
}
17+
18+
export function deleteFormula(editor: ReactEditor) {
19+
if (isFormulaActive(editor)) {
20+
Transforms.delete(editor);
1521
}
1622
}
1723

@@ -21,32 +27,55 @@ export function wrapFormula(editor: ReactEditor, formula?: string) {
2127
}
2228

2329
const { selection } = editor;
30+
31+
if (!selection) return;
2432
const isCollapsed = selection && Range.isCollapsed(selection);
2533

34+
const data = formula || editor.string(selection);
2635
const formulaElement = {
2736
type: EditorInlineNodeType.Formula,
28-
data: true,
29-
children: isCollapsed
30-
? [
31-
{
32-
text: formula || '',
33-
},
34-
]
35-
: [],
37+
data,
38+
children: [
39+
{
40+
text: '$',
41+
},
42+
],
3643
};
3744

38-
if (isCollapsed) {
39-
Transforms.insertNodes(editor, formulaElement);
40-
} else {
41-
Transforms.wrapNodes(editor, formulaElement, { split: true });
42-
Transforms.collapse(editor, { edge: 'end' });
45+
if (!isCollapsed) {
46+
Transforms.delete(editor);
4347
}
48+
49+
Transforms.insertNodes(editor, formulaElement, {
50+
select: true,
51+
});
4452
}
4553

4654
export function unwrapFormula(editor: ReactEditor) {
47-
Transforms.unwrapNodes(editor, {
55+
const [match] = Editor.nodes(editor, {
4856
match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === EditorInlineNodeType.Formula,
4957
});
58+
59+
if (!match) return;
60+
61+
const [node, path] = match as NodeEntry<FormulaNode>;
62+
const formula = node.data;
63+
const range = Editor.range(editor, match[1]);
64+
const beforePoint = Editor.before(editor, path, { unit: 'character' });
65+
66+
Transforms.select(editor, range);
67+
Transforms.delete(editor);
68+
69+
Transforms.insertText(editor, formula);
70+
71+
if (!beforePoint) return;
72+
Transforms.select(editor, {
73+
anchor: beforePoint,
74+
focus: {
75+
...beforePoint,
76+
offset: beforePoint.offset + formula.length,
77+
},
78+
});
5079
}
5180

5281
export function isFormulaActive(editor: ReactEditor) {

frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { ReactEditor } from 'slate-react';
22
import { Editor, Element, Node, NodeEntry, Point, Range, Transforms, Location } from 'slate';
33
import { LIST_TYPES, tabBackward, tabForward } from '$app/components/editor/command/tab';
44
import { isMarkActive, removeMarks, toggleMark } from '$app/components/editor/command/mark';
5-
import { insertFormula, isFormulaActive, unwrapFormula, updateFormula } from '$app/components/editor/command/formula';
5+
import {
6+
deleteFormula,
7+
insertFormula,
8+
isFormulaActive,
9+
unwrapFormula,
10+
updateFormula,
11+
} from '$app/components/editor/command/formula';
612
import {
713
EditorInlineNodeType,
814
EditorNodeType,
@@ -82,13 +88,12 @@ export const CustomEditor = {
8288
isMarkActive,
8389
isFormulaActive,
8490
updateFormula,
85-
toggleInlineElement: (editor: ReactEditor, format: EditorInlineNodeType) => {
86-
if (format === EditorInlineNodeType.Formula) {
87-
if (isFormulaActive(editor)) {
88-
unwrapFormula(editor);
89-
} else {
90-
insertFormula(editor);
91-
}
91+
deleteFormula,
92+
toggleFormula: (editor: ReactEditor) => {
93+
if (isFormulaActive(editor)) {
94+
unwrapFormula(editor);
95+
} else {
96+
insertFormula(editor);
9297
}
9398
},
9499

@@ -102,6 +107,47 @@ export const CustomEditor = {
102107
return !!match;
103108
},
104109

110+
toggleAlign(editor: ReactEditor, format: string) {
111+
const matchNodes = Array.from(
112+
Editor.nodes(editor, {
113+
match: (n) => Element.isElement(n) && n.blockId !== undefined,
114+
})
115+
);
116+
117+
if (!matchNodes) return;
118+
119+
matchNodes.forEach((match) => {
120+
const [node] = match as NodeEntry<
121+
Element & {
122+
data: {
123+
align?: string;
124+
};
125+
}
126+
>;
127+
const path = ReactEditor.findPath(editor, node);
128+
129+
const data = (node.data as { align?: string }) || {};
130+
const newProperties = {
131+
data: {
132+
...data,
133+
align: data.align === format ? undefined : format,
134+
},
135+
} as Partial<Element>;
136+
137+
Transforms.setNodes(editor, newProperties, { at: path });
138+
});
139+
},
140+
141+
getAlign(editor: ReactEditor) {
142+
const match = CustomEditor.getBlock(editor);
143+
144+
if (!match) return undefined;
145+
146+
const [node] = match as NodeEntry<Element>;
147+
148+
return (node.data as { align?: string })?.align;
149+
},
150+
105151
insertMention(editor: ReactEditor, mention: Mention) {
106152
const mentionElement = {
107153
type: EditorInlineNodeType.Mention,
@@ -111,8 +157,9 @@ export const CustomEditor = {
111157
},
112158
};
113159

114-
Transforms.insertNodes(editor, mentionElement);
115-
Transforms.move(editor);
160+
Transforms.insertNodes(editor, mentionElement, {
161+
select: true,
162+
});
116163
},
117164

118165
toggleTodo(editor: ReactEditor, node: TodoListNode) {

0 commit comments

Comments
 (0)