mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { FC, useMemo } from 'react';
|
||||
|
||||
interface CheckBoxProps {
|
||||
size?: number;
|
||||
height?: number;
|
||||
checked?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const CheckBox: FC<CheckBoxProps> = ({
|
||||
size = '16px',
|
||||
height = '23px',
|
||||
checked,
|
||||
onChange,
|
||||
}) => {
|
||||
const dynamic_style = useMemo(
|
||||
() => ({
|
||||
height: {
|
||||
height,
|
||||
},
|
||||
size: {
|
||||
width: size,
|
||||
height: size,
|
||||
},
|
||||
}),
|
||||
[height, size]
|
||||
);
|
||||
return (
|
||||
<CheckBoxContainer
|
||||
style={dynamic_style.height}
|
||||
onClick={() => onChange(!checked)}
|
||||
>
|
||||
<div
|
||||
style={dynamic_style.size}
|
||||
className={`checkBox ${checked ? 'checked' : 'unChecked'}`}
|
||||
/>
|
||||
</CheckBoxContainer>
|
||||
);
|
||||
};
|
||||
const CheckBoxContainer = styled('div')(() => ({
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
'.checkBox': {
|
||||
position: 'relative',
|
||||
borderWidth: '1.5px',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
// Align border
|
||||
marginLeft: '1.5px',
|
||||
transitionDuration: '.2s',
|
||||
transitionTimingFunction: 'ease-in',
|
||||
},
|
||||
'.unChecked': {
|
||||
borderColor: '#c4c7cc',
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: '#fff',
|
||||
'&:hover': {
|
||||
boxShadow: 'inset 0 0 4px rgba(165, 124, 124, 0.2)',
|
||||
},
|
||||
},
|
||||
'.checked': {
|
||||
backgroundColor: '#B9CAD5',
|
||||
borderColor: '#B9CAD5',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '4px',
|
||||
bottom: '0px',
|
||||
left: '4px',
|
||||
right: '0px',
|
||||
width: '9px',
|
||||
height: '5px',
|
||||
borderWidth: '1.5px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: '#fff',
|
||||
borderTop: 'none',
|
||||
borderRight: 'none',
|
||||
transform: 'rotate(-45deg)',
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,164 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useRef, type FC } from 'react';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { CheckBox } from './CheckBox';
|
||||
import type { TodoAsyncBlock, TodoProperties } from './types';
|
||||
|
||||
export const defaultTodoProps: TodoProperties = {
|
||||
text: { value: [{ text: '' }] },
|
||||
};
|
||||
|
||||
const reset_todo_state = async (block: TodoAsyncBlock) => {
|
||||
await block.setProperties({
|
||||
checked: { value: false },
|
||||
collapsed: { value: false },
|
||||
});
|
||||
};
|
||||
|
||||
const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
|
||||
const todoValue = contentValue.value;
|
||||
return (
|
||||
todoValue.length === 0 ||
|
||||
(todoValue.length === 1 && !todoValue[0]['text'])
|
||||
);
|
||||
};
|
||||
|
||||
export const TodoView: FC<CreateView> = ({ block, editor }) => {
|
||||
const properties = { ...defaultTodoProps, ...block.getProperties() };
|
||||
const text_ref = useRef<ExtendedTextUtils>(null);
|
||||
|
||||
const turn_into_text_block = async () => {
|
||||
// Convert to text block
|
||||
await block.setType('text');
|
||||
await reset_todo_state(block);
|
||||
|
||||
if (!text_ref.current) {
|
||||
throw new Error(
|
||||
'Failed to set cursor position! text_ref is not exist!'
|
||||
);
|
||||
}
|
||||
const currentSelection = text_ref.current.getCurrentSelection();
|
||||
if (!currentSelection) {
|
||||
throw new Error(
|
||||
'Failed to get cursor selection! currentSelection is not exist!'
|
||||
);
|
||||
}
|
||||
// Update cursor position
|
||||
editor.selectionManager.setNodeActiveSelection(block.id, {
|
||||
type: 'Range',
|
||||
info: currentSelection,
|
||||
});
|
||||
};
|
||||
|
||||
const on_text_enter: TextProps['handleEnter'] = async props => {
|
||||
const { splitContents, isShiftKey } = props;
|
||||
if (isShiftKey) {
|
||||
return false;
|
||||
}
|
||||
const { contentBeforeSelection, contentAfterSelection } = splitContents;
|
||||
const before = [...contentBeforeSelection.content];
|
||||
const after = [...contentAfterSelection.content];
|
||||
if (todoIsEmpty({ value: before } as ContentColumnValue)) {
|
||||
await turn_into_text_block();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Move children to new block
|
||||
const children = (await block.children()).filter(
|
||||
Boolean
|
||||
) as AsyncBlock[];
|
||||
|
||||
const next_node = await editor.createBlock(Protocol.Block.Type.todo);
|
||||
if (!next_node) {
|
||||
throw new Error('Failed to create todo block');
|
||||
}
|
||||
await block.removeChildren();
|
||||
await next_node.append(...children);
|
||||
await next_node.setProperties({
|
||||
text: { value: after } as ContentColumnValue,
|
||||
});
|
||||
|
||||
await block.setProperties({
|
||||
text: { value: before } as ContentColumnValue,
|
||||
collapsed: { value: false },
|
||||
});
|
||||
await block.after(next_node);
|
||||
|
||||
editor.selectionManager.activeNodeByNodeId(next_node.id);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const on_tab: TextProps['handleTab'] = async ({ isShiftKey }) => {
|
||||
await tabBlock(block, isShiftKey);
|
||||
return true;
|
||||
};
|
||||
|
||||
const on_backspace: TextProps['handleBackSpace'] = async ({
|
||||
isCollAndStart,
|
||||
}) => {
|
||||
if (!isCollAndStart) {
|
||||
return false;
|
||||
}
|
||||
await turn_into_text_block();
|
||||
return true;
|
||||
};
|
||||
|
||||
const on_checked_change = async (checked: boolean) => {
|
||||
await block.setProperties({
|
||||
checked: { value: checked },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={properties.checked?.value ? 'checked' : ''}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
);
|
||||
};
|
||||
|
||||
const TodoBlock = styled('div')({
|
||||
display: 'flex',
|
||||
'.checkBoxContainer': {
|
||||
marginRight: '4px',
|
||||
height: '22px',
|
||||
},
|
||||
'.textContainer': {
|
||||
flex: 1,
|
||||
maxWidth: '100%',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'hidden',
|
||||
},
|
||||
'.checked': {
|
||||
color: '#B9CAD5',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
BaseView,
|
||||
getTextProperties,
|
||||
AsyncBlock,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
} from '@toeverything/framework/virgo';
|
||||
// import type { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
|
||||
import { TodoView, defaultTodoProps } from './TodoView';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
|
||||
export class TodoBlock extends BaseView {
|
||||
type = Protocol.Block.Type.todo;
|
||||
// View = withTreeViewChildren((props: CreateView) => <TodoView {...props} />);
|
||||
View = withTreeViewChildren(TodoView);
|
||||
// //
|
||||
// override ChildrenView = WithTreeViewChildren;
|
||||
|
||||
override async onCreate(block: TodoAsyncBlock) {
|
||||
if (!block.getProperty('text')) {
|
||||
await block.setProperties(defaultTodoProps);
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: TodoAsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'UL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
|
||||
texts[0].text = texts[0].text.substring('[ ] '.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ul><li>[ ] ${content}</li></ul>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AsyncBlock } from '@toeverything/framework/virgo';
|
||||
import type {
|
||||
ContentColumnValue,
|
||||
BooleanColumnValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
|
||||
export interface TodoProperties {
|
||||
text: ContentColumnValue;
|
||||
checked?: BooleanColumnValue;
|
||||
collapsed?: BooleanColumnValue;
|
||||
}
|
||||
|
||||
export class TodoAsyncBlock extends AsyncBlock {
|
||||
override setProperties(
|
||||
properties: Partial<TodoProperties>
|
||||
): Promise<boolean> {
|
||||
return super.setProperties(properties);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user