Merge branch 'develop' into feature/remove-react-fc

This commit is contained in:
DarkSky
2022-08-12 19:18:57 +08:00
committed by GitHub
64 changed files with 1953 additions and 1059 deletions
+2 -7
View File
@@ -17,6 +17,7 @@ const cleanupWorkspace = (workspace: string) =>
const requestPermission = async (workspace: string) => {
await cleanupWorkspace(workspace);
// @ts-ignore
const dirHandler = await window.showDirectoryPicker({
id: 'AFFiNE_' + workspace,
mode: 'readwrite',
@@ -46,7 +47,7 @@ const requestPermission = async (workspace: string) => {
};
export const FileSystem = (props: { onError: () => void }) => {
const onSelected = useLocalTrigger();
const [, onSelected] = useLocalTrigger();
const apiSupported = useMemo(() => {
try {
@@ -56,12 +57,6 @@ export const FileSystem = (props: { onError: () => void }) => {
}
}, []);
useEffect(() => {
if (process.env['NX_E2E']) {
onSelected();
}
}, []);
return (
<MuiButton
variant="outlined"
+1 -2
View File
@@ -21,7 +21,7 @@ export function Login() {
<MuiSnackbar
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
open={error}
message="Login failed, please check if you have permission"
message="Request File Permission failed, please check if you have permission"
/>
<MuiGrid item xs={8}>
<Error
@@ -32,7 +32,6 @@ export function Login() {
</MuiGrid>
<MuiGrid item xs={4}>
{' '}
<MuiBox
style={{
display: 'flex',
@@ -22,7 +22,7 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
? app.getScreenPoint([bounds.minX, bounds.minY])
: undefined;
const anchor = getAnchor({
const anchorEl = getAnchor({
x: point?.[0] || 0,
y: (point?.[1] || 0) + 40,
width: bounds?.width ? bounds.width * camera.zoom : 0,
@@ -100,7 +100,7 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
<Popover
trigger="click"
visible={!!point}
anchor={anchor}
anchorEl={anchorEl}
popoverDirection="none"
content={
<PopoverContainer>
@@ -124,10 +124,16 @@ export class EditorUtil extends TDShapeUtil<T, E> {
);
const activateIfEditing = useCallback(() => {
const shapes =
state.document.pages[state.appState.currentPageId].shapes;
// https://bugs.chromium.org/p/chromium/issues/detail?id=1352417
if (shapes[shape.id] != null) {
return;
}
if (editingText && editingId !== shape.id) {
app.setEditingText(shape.id);
}
}, [app, shape.id, editingText, editingId]);
}, [app, state, shape.id, editingText, editingId]);
return (
<HTMLContainer ref={ref} {...events}>
@@ -799,11 +799,11 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
<span {...attributes}>
<code
style={{
backgroundColor: 'rgba(135,131,120,0.15)',
borderRadius: '3px',
color: '#EB5757',
fontSize: '0.875em',
padding: '0.25em 0.375em',
backgroundColor: '#F2F5F9',
borderRadius: '5px',
color: '#3A4C5C',
padding: '3px 8px',
margin: '0 2px',
}}
>
{customChildren}
@@ -25,7 +25,6 @@ import { ReactEditor } from 'slate-react';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import EditIcon from '@mui/icons-material/Edit';
import LinkOffIcon from '@mui/icons-material/LinkOff';
import AttachmentIcon from '@mui/icons-material/Attachment';
import {
MuiTooltip as Tooltip,
styled,
@@ -39,7 +38,7 @@ import {
import { getRandomString } from '../utils';
import { colors } from '../../colors';
import { LinkIcon } from '@toeverything/components/icons';
export type LinkElement = {
type: 'link';
url: string;
@@ -124,7 +123,7 @@ const LinkStyledTooltip = styled(({ className, ...props }: MuiTooltipProps) => (
))(({ theme }) => ({
[`& .${muiTooltipClasses.tooltip}`]: {
backgroundColor: '#fff',
color: '#4C6275',
color: '#3E6FDB',
boxShadow: theme.affine.shadows.shadow1,
fontSize: '14px',
},
@@ -419,8 +418,8 @@ export const LinkModal = memo((props: LinkModalProps) => {
}}
>
<div className={styles('linkModalContainerIcon')}>
<AttachmentIcon
style={{ color: colors.Gray04, fontSize: 16 }}
<LinkIcon
style={{ fontSize: '16px', marginTop: '2px' }}
/>
</div>
<input
@@ -505,8 +504,10 @@ const LinkModalContainer = styled('div')(({ theme }) => ({
const styles = style9.create({
linkModalContainerIcon: {
display: 'flex',
width: '16px',
margin: '0 16px 0 4px',
color: '#4C6275',
},
linkModalContainerInput: {
flex: '1',
@@ -517,6 +518,7 @@ const styles = style9.create({
'::-webkit-input-placeholder': {
color: '#98acbd',
},
color: '#4C6275',
},
linkMask: {
position: 'fixed',
@@ -905,6 +905,19 @@ class SlateUtils {
);
}
public insertNodes(
nodes: SlateNode | Array<SlateNode>,
options?: Parameters<typeof Transforms.insertNodes>[2]
) {
Transforms.insertNodes(this.editor, nodes, {
...options,
});
}
public getNodeByPath(path: Path) {
Editor.node(this.editor, path);
}
public getStartSelection() {
return {
anchor: this.getStart(),
@@ -40,8 +40,9 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
(todoValue.length === 1 && !todoValue[0]['text'])
);
};
const BulletLeft = styled('div')(() => ({
const BulletLeft = styled('div')(({ theme }) => ({
height: '22px',
color: theme.affine.typography.body1.color,
}));
export const BulletView = ({ block, editor }: CreateView) => {
// block.remove();
@@ -52,6 +52,8 @@ import {
BlockPendantProvider,
} from '@toeverything/components/editor-core';
import { copyToClipboard } from '@toeverything/utils';
import { DuplicateIcon } from '@toeverything/components/icons';
interface CreateCodeView extends CreateView {
style9?: StyleWithAtRules;
containerClassName?: string;
@@ -117,10 +119,21 @@ const CodeBlock = styled('div')(({ theme }) => ({
justifyContent: 'space-between',
},
'.copy-block': {
padding: '6px 10px',
padding: '0px 10px',
backgroundColor: '#fff',
height: '32px',
display: 'flex',
width: '90px',
justifyContent: 'center',
alignItems: 'center',
color: '#4C6275',
fontSize: '14px',
borderRadius: theme.affine.shape.borderRadius,
cursor: 'pointer',
svg: {
marginRight: '4px',
display: 'block',
},
},
'.cm-focused': {
outline: 'none !important',
@@ -187,7 +200,7 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
</div>
<div>
<div className="copy-block" onClick={copyCode}>
Copy
<DuplicateIcon></DuplicateIcon>Copy
</div>
</div>
</div>
@@ -261,7 +261,7 @@ const GridContainer = styled('div')<{
display: 'flex',
alignItems: 'stretch',
borderRadius: '10px',
border: '1px solid #FFF',
border: '1px solid transparent',
minWidth: `${gridItemMinWidth}%`,
[`&:hover .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
borderColor: theme.affine.palette.borderColor,
@@ -113,13 +113,18 @@ export const TextView = ({
block.id,
'end'
);
const value = [
...preNode.getProperty('text').value,
...block.getProperty('text').value,
];
await preNode.setProperty('text', {
value,
});
if (
block.getProperty('text').value[0] &&
block.getProperty('text').value[0]?.text !== ''
) {
const value = [
...preNode.getProperty('text').value,
...block.getProperty('text').value,
];
await preNode.setProperty('text', {
value,
});
}
await preNode.append(...children);
await block.remove();
editor.suspend(false);
@@ -5,6 +5,7 @@ export const List = styled('div')(({ theme }) => ({
'.checkBoxContainer': {
marginRight: '4px',
lineHeight: theme.affine.typography.body1.lineHeight,
color: theme.affine.typography.body1.color,
},
'.textContainer': {
flex: 1,
@@ -42,16 +42,19 @@ const TextBlockContainer = styled(Text)(({ theme }) => ({
fontFamily: theme.affine.typography.body1.fontFamily,
color: theme.affine.typography.body1.color,
letterSpacing: '0.1px',
a: {
color: '#3e6fdb',
},
}));
const findSlice = (arr: string[], p: string, q: string) => {
let should_include = false;
let shouldInclude = false;
return arr.filter(block => {
if (block === p || block === q) {
should_include = !should_include;
shouldInclude = !shouldInclude;
return true;
} else {
return should_include;
return shouldInclude;
}
});
};
@@ -115,11 +118,8 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
(ref as MutableRefObject<ExtendedTextUtils>) || defaultRef;
const properties = block.getProperties();
// const [is_select, set_is_select] = useState<boolean>();
// useOnSelect(block.id, (is_select: boolean) => {
// set_is_select(is_select);
// });
const on_text_view_set_selection = (selection: Range | Point) => {
const onTextViewSetSelection = (selection: Range | Point) => {
if (selection instanceof Point) {
//do some thing
} else {
@@ -128,18 +128,18 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
};
// block = await editor.commands.blockCommands.createNextBlock(block.id,)
const on_text_view_active = useCallback(
const onTextViewActive = useCallback(
(point: CursorTypes) => {
// TODO code to be optimized
if (textRef.current) {
const end_selection = textRef.current.getEndSelection();
const start_selection = textRef.current.getStartSelection();
const endSelection = textRef.current.getEndSelection();
const startSelection = textRef.current.getStartSelection();
if (point === 'start') {
textRef.current.setSelection(start_selection);
textRef.current.setSelection(startSelection);
return;
}
if (point === 'end') {
textRef.current.setSelection(end_selection);
textRef.current.setSelection(endSelection);
return;
}
try {
@@ -154,24 +154,24 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
} else {
blockTop = blockDomStyle.top + 5;
}
const end_position = ReactEditor.toDOMRange(
const endPosition = ReactEditor.toDOMRange(
textRef.current.editor,
end_selection
endSelection
)
.getClientRects()
.item(0);
const start_position = ReactEditor.toDOMRange(
const startPosition = ReactEditor.toDOMRange(
textRef.current.editor,
start_selection
startSelection
)
.getClientRects()
.item(0);
if (end_position.left <= point.x) {
textRef.current.setSelection(end_selection);
if (endPosition.left <= point.x) {
textRef.current.setSelection(endSelection);
return;
}
if (start_position.left >= point.x) {
textRef.current.setSelection(start_selection);
if (startPosition.left >= point.x) {
textRef.current.setSelection(startSelection);
return;
}
let range: globalThis.Range;
@@ -189,7 +189,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
range = document.createRange();
range.setStart(caret.offsetNode, caret.offset);
}
const slate_rang = ReactEditor.toSlateRange(
const slateRang = ReactEditor.toSlateRange(
textRef.current.editor,
range,
{
@@ -197,19 +197,19 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
suppressThrow: true,
}
);
textRef.current.setSelection(slate_rang);
textRef.current.setSelection(slateRang);
}
} catch (e) {
console.log('e: ', e);
textRef.current.setSelection(end_selection);
textRef.current.setSelection(endSelection);
}
}
},
[textRef]
);
useOnSelectActive(block.id, on_text_view_active);
useOnSelectSetSelection<'Range'>(block.id, on_text_view_set_selection);
useOnSelectActive(block.id, onTextViewActive);
useOnSelectSetSelection<'Range'>(block.id, onTextViewSetSelection);
useEffect(() => {
if (textRef.current) {
@@ -235,17 +235,17 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
(block.id === lastSelectNodeId && type === 'Range') ||
(type === 'Range' && info)
) {
on_text_view_active('end');
onTextViewActive('end');
} else {
on_text_view_active('start');
onTextViewActive('start');
}
}
} catch (e) {
console.warn('error occured in set active in initialization');
}
}, [block.id, editor.selectionManager, on_text_view_active, textRef]);
}, [block.id, editor.selectionManager, onTextViewActive, textRef]);
const on_text_change: TextProps['handleChange'] = async (
const onTextChange: TextProps['handleChange'] = async (
value,
textStyle
) => {
@@ -266,39 +266,34 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
});
}
};
const get_now_and_pre_rang_position = () => {
window.getSelection().getRangeAt(0);
// const now_range =
// editor.selectionManager.currentSelectInfo?.browserSelection.getRangeAt(
// 0
// );
const now_range = window.getSelection().getRangeAt(0);
let pre_position = null;
const now_position = now_range.getClientRects().item(0);
const getNowAndPreRangPosition = () => {
const nowRange = window.getSelection().getRangeAt(0);
let prePosition = null;
const nowPosition = nowRange.getClientRects().item(0);
try {
if (now_range.startOffset !== 0) {
const pre_rang = document.createRange();
pre_rang.setStart(
now_range.startContainer,
now_range.startOffset + 1
if (nowRange.startOffset !== 0) {
const preRang = document.createRange();
preRang.setStart(
nowRange.startContainer,
nowRange.startOffset + 1
);
pre_rang.setEnd(
now_range.endContainer,
now_range.endOffset + 1
preRang.setEnd(
nowRange.endContainer,
nowRange.endOffset + 1
);
pre_position = pre_rang.getClientRects().item(0);
prePosition = preRang.getClientRects().item(0);
}
} catch (e) {
// console.log(e);
}
return { nowPosition: now_position, prePosition: pre_position };
return { nowPosition: nowPosition, prePosition: prePosition };
};
const onKeyboardUp = (event: React.KeyboardEvent<Element>) => {
// if default event is prevented do noting
// if U want to disable up/down/enter use capture event for preventing
if (!event.isDefaultPrevented()) {
const positions = get_now_and_pre_rang_position();
const positions = getNowAndPreRangPosition();
const prePosition = positions.prePosition;
const nowPosition = positions.nowPosition;
if (prePosition) {
@@ -339,7 +334,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
// editor.selectionManager.activeNextNode(block.id, 'start');
// return;
if (!event.isDefaultPrevented()) {
const positions = get_now_and_pre_rang_position();
const positions = getNowAndPreRangPosition();
const prePosition = positions.prePosition;
const nowPosition = positions.nowPosition;
// Create the last element range of slate_editor
@@ -395,7 +390,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
return false;
}
};
const on_select_all = () => {
const onSelectAll = () => {
const isSelectAll =
textRef.current.isEmpty() || textRef.current.isSelectAll();
if (isSelectAll) {
@@ -405,22 +400,20 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
return false;
};
const on_undo = () => {
const onUndo = () => {
editor.undo();
};
const on_redo = () => {
const onRedo = () => {
editor.redo();
};
const on_keyboard_esc = () => {
const onKeyboardEsc = () => {
if (editor.selectionManager.getSelectedNodesIds().length === 0) {
const active_node_id =
const activeNodeId =
editor.selectionManager.getActivatedNodeId();
if (active_node_id) {
editor.selectionManager.setSelectedNodesIds([
active_node_id,
]);
if (activeNodeId) {
editor.selectionManager.setSelectedNodesIds([activeNodeId]);
ReactEditor.blur(textRef.current.editor);
}
} else {
@@ -428,7 +421,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
}
};
const on_shift_click = async (e: MouseEvent) => {
const onShiftClick = async (e: MouseEvent) => {
if (e.shiftKey) {
const activeId = editor.selectionManager.getActivatedNodeId();
if (activeId === block.id) {
@@ -477,16 +470,16 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
className={`${otherOptions.className}`}
currentValue={properties.text.value}
textStyle={properties.textStyle}
handleChange={on_text_change}
handleChange={onTextChange}
handleUp={onKeyboardUp}
handleDown={onKeyboardDown}
handleLeft={onKeyboardLeft}
handleRight={onKeyboardRight}
handleSelectAll={on_select_all}
handleMouseDown={on_shift_click}
handleUndo={on_undo}
handleRedo={on_redo}
handleEsc={on_keyboard_esc}
handleSelectAll={onSelectAll}
handleMouseDown={onShiftClick}
handleUndo={onUndo}
handleRedo={onRedo}
handleEsc={onKeyboardEsc}
{...otherOptions}
/>
);
@@ -27,7 +27,7 @@ const defaultConfig: WithChildrenConfig = {
const TreeView = forwardRef<
HTMLDivElement,
{ lastItem?: boolean } & ComponentPropsWithRef<'div'>
>(({ lastItem, children, onClick, ...restProps }, ref) => {
>(({ lastItem = false, children, onClick, ...restProps }, ref) => {
return (
<TreeWrapper ref={ref} {...restProps}>
<StyledTreeView>
@@ -155,8 +155,11 @@ const Wrapper = styled('div')({ display: 'flex', flexDirection: 'column' });
const Children = Wrapper;
const TREE_COLOR = '#D5DFE6';
// TODO determine the position of the horizontal line by the type of the item
const ITEM_POINT_HEIGHT = '12.5px'; // '50%'
// adjust left and right margins of the the tree line
const TREE_LINE_LEFT_OFFSET = '-16px';
// determine the position of the horizontal line by the type of the item
const TREE_LINE_TOP_OFFSET = '20px'; // '50%'
const TREE_LINE_WIDTH = '12px';
const TreeWrapper = styled('div')({
position: 'relative',
@@ -164,7 +167,7 @@ const TreeWrapper = styled('div')({
const StyledTreeView = styled('div')({
position: 'absolute',
left: '-21px',
left: TREE_LINE_LEFT_OFFSET,
height: '100%',
});
@@ -183,7 +186,7 @@ const Line = styled('div')({
const VerticalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
width: '1px',
height: last ? ITEM_POINT_HEIGHT : '100%',
height: last ? TREE_LINE_TOP_OFFSET : '100%',
paddingTop: 0,
paddingBottom: 0,
transform: 'translate(-50%, 0)',
@@ -192,11 +195,11 @@ const VerticalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
}));
const HorizontalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
width: '16px',
width: TREE_LINE_WIDTH,
height: '1px',
paddingLeft: 0,
paddingRight: 0,
top: ITEM_POINT_HEIGHT,
top: TREE_LINE_TOP_OFFSET,
transform: 'translate(0, -50%)',
opacity: last ? 0 : 'unset',
}));
@@ -204,7 +207,8 @@ const HorizontalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
const Collapsed = styled('div')({
cursor: 'pointer',
display: 'inline-block',
color: '#B9CAD5',
color: '#98ACBD',
padding: '8px',
});
const LastItemRadius = styled('div')({
@@ -212,9 +216,9 @@ const LastItemRadius = styled('div')({
position: 'absolute',
left: '-0.5px',
top: 0,
height: ITEM_POINT_HEIGHT,
height: TREE_LINE_TOP_OFFSET,
bottom: '50%',
width: '16px',
width: TREE_LINE_WIDTH,
borderWidth: '1px',
borderStyle: 'solid',
borderLeftColor: TREE_COLOR,
@@ -152,6 +152,7 @@ export const RenderRoot = ({
};
const onDrop = (event: React.DragEvent<Element>) => {
event.preventDefault();
editor.dragDropManager.handlerEditorDrop(event);
editor.getHooks().onRootNodeDrop(event);
};
@@ -233,7 +233,8 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
startPointRef.current &&
endPointRef.current &&
scrollManager.scrollContainer &&
scrollContainerRect.current
scrollContainerRect.current &&
mouseType.current === 'down'
) {
const xSign = DIRECTION_VALUE_MAP[direction[0]] || 0;
const ySign = DIRECTION_VALUE_MAP[direction[1]] || 0;
@@ -4,7 +4,11 @@ import {
Popover,
type PopoverProps,
PopperHandler,
Tag,
type PopperProps,
} from '@toeverything/components/ui';
import { TagsIcon } from '@toeverything/components/icons';
import { CreatePendantPanel } from './pendant-operation-panel';
import { IconButton } from './StyledComponent';
import { AsyncBlock } from '../editor';
@@ -13,17 +17,21 @@ type Props = {
block: AsyncBlock;
onSure?: () => void;
iconStyle?: CSSProperties;
useAddIcon?: boolean;
} & Omit<PopoverProps, 'content'>;
export const AddPendantPopover = ({
block,
onSure,
iconStyle,
useAddIcon = true,
...popoverProps
}: Props) => {
const popoverHandlerRef = useRef<PopperHandler>();
const popperRef = useRef<any>();
return (
<Popover
ref={popoverHandlerRef}
popperHandlerRef={popoverHandlerRef}
popperRef={popperRef}
content={
<CreatePendantPanel
block={block}
@@ -31,6 +39,9 @@ export const AddPendantPopover = ({
popoverHandlerRef.current?.setVisible(false);
onSure?.();
}}
onTypeChange={() => {
popperRef.current?.update?.();
}}
/>
}
placement="bottom-start"
@@ -38,9 +49,25 @@ export const AddPendantPopover = ({
style={{ padding: 0 }}
{...popoverProps}
>
<IconButton style={{ marginRight: 12, ...iconStyle }}>
<Add sx={{ fontSize: 14 }} />
</IconButton>
{useAddIcon ? (
<IconButton style={{ marginRight: 12, ...iconStyle }}>
<Add sx={{ fontSize: 14 }} />
</IconButton>
) : (
<Tag
style={{
background: '#F5F7F8',
color: '#98ACBD',
marginRight: 12,
marginBottom: 8,
}}
startElement={
<TagsIcon style={{ fontSize: 14, marginRight: 4 }} />
}
>
Tag App
</Tag>
)}
</Popover>
);
};
@@ -98,8 +98,8 @@ export const pendantConfig: { [key: string]: PendantConfig } = {
},
[PendantTypes.Status]: {
iconName: IconNames.STATUS,
background: ['#C5FBE0', '#FFF5AB', '#FFCECE', '#E3DEFF'],
color: ['#05683D', '#896406', '#AF1212', '#511AAB'],
background: ['#FFCECE', '#FFF5AB', '#C5FBE0', '#E3DEFF'],
color: ['#AF1212', '#896406', '#05683D', '#511AAB'],
},
[PendantTypes.Select]: {
iconName: IconNames.SINGLE_SELECT,
@@ -91,7 +91,7 @@ export const PendantHistoryPanel = ({
return (
<Popover
key={item.id}
ref={ref => {
popperHandlerRef={ref => {
popoverHandlerRef.current[item.id] = ref;
}}
placement="bottom-start"
@@ -11,6 +11,7 @@ export type ModifyPanelProps = {
iconConfig?: PendantConfig;
isStatusSelect?: boolean;
property?: RecastMetaProperty;
onTypeChange?: (type: PendantTypes) => void;
};
export type ModifyPanelContentProps = {
@@ -24,9 +24,11 @@ import { useOnCreateSure } from './hooks';
export const CreatePendantPanel = ({
block,
onSure,
onTypeChange,
}: {
block: AsyncBlock;
onSure?: () => void;
onTypeChange?: (option: PendantOptions) => void;
}) => {
const [selectedOption, setSelectedOption] = useState<PendantOptions>();
const [fieldName, setFieldName] = useState<string>('');
@@ -37,6 +39,10 @@ export const CreatePendantPanel = ({
setFieldName(generateRandomFieldName(selectedOption.type));
}, [selectedOption]);
useEffect(() => {
onTypeChange?.(selectedOption);
}, [selectedOption, onTypeChange]);
return (
<StyledPopoverWrapper>
<StyledOperationTitle>Add Field</StyledOperationTitle>
@@ -17,7 +17,7 @@ export const PendantPopover = (
const popoverHandlerRef = useRef<PopperHandler>();
return (
<Popover
ref={popoverHandlerRef}
popperHandlerRef={popoverHandlerRef}
pointerEnterDelay={300}
pointerLeaveDelay={200}
placement="bottom-start"
@@ -26,13 +26,13 @@ export const PendantPopover = (
block={block}
endElement={
<AddPendantPopover
container={popoverProps.container}
block={block}
onSure={() => {
popoverHandlerRef.current?.setVisible(false);
}}
offset={[0, -30]}
trigger="click"
useAddIcon={false}
/>
}
onClose={() => {
@@ -56,7 +56,7 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
return (
<Popover
ref={ref => {
popperHandlerRef={ref => {
popoverHandlerRef.current[id] = ref;
}}
container={blockRenderContainerRef.current}
@@ -107,7 +107,7 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
iconStyle={{ marginTop: 4 }}
trigger="click"
// trigger={isKanbanView ? 'hover' : 'click'}
container={blockRenderContainerRef.current}
// container={blockRenderContainerRef.current}
/>
</div>
</MuiFade>
@@ -88,7 +88,7 @@ export const generateInitialOptions = (
) => {
if (type === PendantTypes.Status) {
return [
generateBasicOption({ index: 0, iconConfig, name: 'No Started' }),
generateBasicOption({ index: 0, iconConfig, name: 'Not Started' }),
generateBasicOption({
index: 1,
iconConfig,
@@ -3,7 +3,13 @@ import type {
SlateUtils,
TextAlignOptions,
} from '@toeverything/components/common';
import { Point, Selection as SlateSelection } from 'slate';
import {
BaseRange,
Node,
Path,
Point,
Selection as SlateSelection,
} from 'slate';
import { Editor } from '../editor';
type TextUtilsFunctions =
@@ -28,7 +34,10 @@ type TextUtilsFunctions =
| 'removeSelection'
| 'insertReference'
| 'isCollapsed'
| 'blur';
| 'blur'
| 'setSelection'
| 'insertNodes'
| 'getNodeByPath';
type ExtendedTextUtils = SlateUtils & {
setLinkModalVisible: (visible: boolean) => void;
@@ -193,6 +202,59 @@ export class BlockHelper {
return '';
}
/**
*
* set selection of a text input
* @param {string} blockId
* @param {BaseRange} selection
* @return {*}
* @memberof BlockHelper
*/
public setSelection(blockId: string, selection: BaseRange) {
const text_utils = this._blockTextUtilsMap[blockId];
if (text_utils) {
return text_utils.setSelection(selection);
}
console.warn('Could find the block text utils');
}
/**
*
* insert nodes in text
* @param {string} blockId
* @param {Array<Node>} nodes
* @param {Parameters<TextUtils['insertNodes']>[1]} options
* @return {*}
* @memberof BlockHelper
*/
public insertNodes(
blockId: string,
nodes: Array<Node>,
options?: Parameters<TextUtils['insertNodes']>[1]
) {
const text_utils = this._blockTextUtilsMap[blockId];
if (text_utils) {
return text_utils.insertNodes(nodes, options);
}
console.warn('Could find the block text utils');
}
/**
*
* get text(slate node) by path
* @param {string} blockId
* @param {Path} path
* @return {*}
* @memberof BlockHelper
*/
public getNodeByPath(blockId: string, path: Path) {
const text_utils = this._blockTextUtilsMap[blockId];
if (text_utils) {
return text_utils.getNodeByPath(path);
}
console.warn('Could find the block text utils');
}
public transformPoint(
blockId: string,
...restArgs: Parameters<TextUtils['transformPoint']>
@@ -14,403 +14,126 @@ import {
services,
} from '@toeverything/datasource/db-service';
import { MarkdownParser } from './markdown-parse';
import { shouldHandlerContinue } from './utils';
import { Paste } from './paste';
// todo needs to be a switch
const support_markdown_paste = true;
enum ClipboardAction {
COPY = 'copy',
CUT = 'cut',
PASTE = 'paste',
}
class BrowserClipboard {
private event_target: Element;
private hooks: HooksRunner;
private editor: Editor;
private clipboard_parse: ClipboardParse;
private markdown_parse: MarkdownParser;
private static optimal_mime_type: string[] = [
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
OFFICE_CLIPBOARD_MIMETYPE.HTML,
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
];
//TODO: need to consider the cursor position after inserting the children
class BrowserClipboard {
private _eventTarget: Element;
private _hooks: HooksRunner;
private _editor: Editor;
private _clipboardParse: ClipboardParse;
private _markdownParse: MarkdownParser;
private _paste: Paste;
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
this.event_target = eventTarget;
this.hooks = hooks;
this.editor = editor;
this.clipboard_parse = new ClipboardParse(editor);
this.markdown_parse = new MarkdownParser();
this.initialize();
this._eventTarget = eventTarget;
this._hooks = hooks;
this._editor = editor;
this._clipboardParse = new ClipboardParse(editor);
this._markdownParse = new MarkdownParser();
this._paste = new Paste(
editor,
this._clipboardParse,
this._markdownParse
);
this._initialize();
}
public getClipboardParse() {
return this.clipboard_parse;
return this._clipboardParse;
}
private initialize() {
this.handle_copy = this.handle_copy.bind(this);
this.handle_cut = this.handle_cut.bind(this);
this.handle_paste = this.handle_paste.bind(this);
private _initialize() {
this._handleCopy = this._handleCopy.bind(this);
this._handleCut = this._handleCut.bind(this);
document.addEventListener(ClipboardAction.COPY, this.handle_copy);
document.addEventListener(ClipboardAction.CUT, this.handle_cut);
document.addEventListener(ClipboardAction.PASTE, this.handle_paste);
this.event_target.addEventListener(
ClipboardAction.COPY,
this.handle_copy
);
this.event_target.addEventListener(
ClipboardAction.CUT,
this.handle_cut
);
this.event_target.addEventListener(
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
document.addEventListener(ClipboardAction.CUT, this._handleCut);
document.addEventListener(
ClipboardAction.PASTE,
this.handle_paste
this._paste.handlePaste
);
}
private handle_copy(e: Event) {
//@ts-ignore
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
return;
}
this.dispatch_clipboard_event(
this._eventTarget.addEventListener(
ClipboardAction.COPY,
e as ClipboardEvent
this._handleCopy
);
this._eventTarget.addEventListener(
ClipboardAction.CUT,
this._handleCut
);
this._eventTarget.addEventListener(
ClipboardAction.PASTE,
this._paste.handlePaste
);
}
private handle_cut(e: Event) {
//@ts-ignore
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
return;
}
this.dispatch_clipboard_event(ClipboardAction.CUT, e as ClipboardEvent);
}
private handle_paste(e: Event) {
//@ts-ignore TODO should be handled more scientifically here, whether to trigger the paste time, also need some whitelist mechanism
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
private _handleCopy(e: Event) {
if (!shouldHandlerContinue(e, this._editor)) {
return;
}
const clipboardData = (e as ClipboardEvent).clipboardData;
const isPureFile = this.is_pure_file_in_clipboard(clipboardData);
if (!isPureFile) {
this.paste_content(clipboardData);
} else {
this.paste_file(clipboardData);
}
// this.editor.selectionManager
// .getSelectInfo()
// .then(selectionInfo => console.log(selectionInfo));
e.stopPropagation();
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
}
private paste_content(clipboardData: any) {
const originClip: { data: any; type: any } = this.getOptimalClip(
clipboardData
) as { data: any; type: any };
const originTextClipData = clipboardData.getData(
OFFICE_CLIPBOARD_MIMETYPE.TEXT
);
let clipData = originClip['data'];
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
clipData = this.excape_html(clipData);
}
switch (originClip['type']) {
/** Protocol paste */
case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED:
this.fire_paste_edit_action(clipData);
break;
case OFFICE_CLIPBOARD_MIMETYPE.HTML:
this.paste_html(clipData, originTextClipData);
break;
case OFFICE_CLIPBOARD_MIMETYPE.TEXT:
this.paste_text(clipData, originTextClipData);
break;
default:
break;
}
}
private paste_html(clipData: any, originTextClipData: any) {
if (support_markdown_paste) {
const has_markdown =
this.markdown_parse.checkIfTextContainsMd(originTextClipData);
if (has_markdown) {
try {
const convertedDataObj =
this.markdown_parse.md2Html(originTextClipData);
if (convertedDataObj.isConverted) {
clipData = convertedDataObj.text;
}
} catch (e) {
console.error(e);
clipData = originTextClipData;
}
}
}
const blocks = this.clipboard_parse.html2blocks(clipData);
this.insert_blocks(blocks);
}
private paste_text(clipData: any, originTextClipData: any) {
const blocks = this.clipboard_parse.text2blocks(clipData);
this.insert_blocks(blocks);
}
private async paste_file(clipboardData: any) {
const file = this.get_image_file(clipboardData);
if (file) {
const result = await services.api.file.create({
workspace: this.editor.workspace,
file: file,
});
const block_info: ClipBlockInfo = {
type: 'image',
properties: {
image: {
value: result.id,
name: file.name,
size: file.size,
type: file.type,
},
},
children: [] as ClipBlockInfo[],
};
this.insert_blocks([block_info]);
}
}
private get_image_file(clipboardData: any) {
const files = clipboardData.files;
if (files && files[0] && files[0].type.indexOf('image') > -1) {
return files[0];
}
return;
}
private excape_html(data: any, onlySpace?: any) {
if (!onlySpace) {
// TODO:
// data = string.htmlEscape(data);
// data = data.replace(/\n/g, '<br>');
}
// data = data.replace(/ /g, '&nbsp;');
// data = data.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
return data;
}
public getOptimalClip(clipboardData: any) {
const mimeTypeArr = BrowserClipboard.optimal_mime_type;
for (let i = 0; i < mimeTypeArr.length; i++) {
const data =
clipboardData[mimeTypeArr[i]] ||
clipboardData.getData(mimeTypeArr[i]);
if (data) {
return {
type: mimeTypeArr[i],
data: data,
};
}
}
return '';
}
private is_pure_file_in_clipboard(clipboardData: DataTransfer) {
const types = clipboardData.types;
const res =
(types.length === 1 && types[0] === 'Files') ||
(types.length === 2 &&
(types.includes('text/plain') || types.includes('text/html')) &&
types.includes('Files'));
return res;
}
private async fire_paste_edit_action(clipboardData: any) {
const clip_info: InnerClipInfo = JSON.parse(clipboardData);
clip_info && this.insert_blocks(clip_info.data, clip_info.select);
}
private can_edit_text(type: BlockFlavorKeys) {
return (
type === Protocol.Block.Type.page ||
type === Protocol.Block.Type.text ||
type === Protocol.Block.Type.heading1 ||
type === Protocol.Block.Type.heading2 ||
type === Protocol.Block.Type.heading3 ||
type === Protocol.Block.Type.quote ||
type === Protocol.Block.Type.todo ||
type === Protocol.Block.Type.code ||
type === Protocol.Block.Type.callout ||
type === Protocol.Block.Type.numbered ||
type === Protocol.Block.Type.bullet
);
}
// TODO: cursor positioning problem
private async insert_blocks(blocks: ClipBlockInfo[], select?: SelectInfo) {
if (blocks.length === 0) {
private _handleCut(e: Event) {
if (!shouldHandlerContinue(e, this._editor)) {
return;
}
const cur_select_info =
await this.editor.selectionManager.getSelectInfo();
if (cur_select_info.blocks.length === 0) {
return;
}
let begin_index = 0;
const cur_node_id =
cur_select_info.blocks[cur_select_info.blocks.length - 1].blockId;
let cur_block = await this.editor.getBlockById(cur_node_id);
const block_view = this.editor.getView(cur_block.type);
if (
cur_select_info.type === 'Range' &&
cur_block.type === 'text' &&
block_view.isEmpty(cur_block)
) {
cur_block.setType(blocks[0].type);
cur_block.setProperties(blocks[0].properties);
this.paste_children(cur_block, blocks[0].children);
begin_index = 1;
} else if (
select?.type === 'Range' &&
cur_select_info.type === 'Range' &&
this.can_edit_text(cur_block.type) &&
this.can_edit_text(blocks[0].type)
) {
if (
cur_select_info.blocks.length > 0 &&
cur_select_info.blocks[0].startInfo
) {
const start_info = cur_select_info.blocks[0].startInfo;
const end_info = cur_select_info.blocks[0].endInfo;
const cur_text_value = cur_block.getProperty('text').value;
const pre_cur_text_value = cur_text_value.slice(
0,
start_info.arrayIndex
);
const last_cur_text_value = cur_text_value.slice(
end_info.arrayIndex + 1
);
const pre_text = cur_text_value[
start_info.arrayIndex
].text.substring(0, start_info.offset);
const last_text = cur_text_value[
end_info.arrayIndex
].text.substring(end_info.offset);
let last_block: ClipBlockInfo = blocks[blocks.length - 1];
if (!this.can_edit_text(last_block.type)) {
last_block = { type: 'text', children: [] };
blocks.push(last_block);
}
const last_values = last_block.properties?.text?.value;
last_text && last_values.push({ text: last_text });
last_values.push(...last_cur_text_value);
last_block.properties = {
text: { value: last_values },
};
const insert_info = blocks[0].properties.text;
pre_text && pre_cur_text_value.push({ text: pre_text });
pre_cur_text_value.push(...insert_info.value);
this.editor.blockHelper.setBlockBlur(cur_node_id);
setTimeout(async () => {
const cur_block = await this.editor.getBlockById(
cur_node_id
);
cur_block.setProperties({
text: { value: pre_cur_text_value },
});
this.paste_children(cur_block, blocks[0].children);
}, 0);
begin_index = 1;
}
}
for (let i = begin_index; i < blocks.length; i++) {
const next_block = await this.editor.createBlock(blocks[i].type);
next_block.setProperties(blocks[i].properties);
if (cur_block.type === 'page') {
cur_block.prepend(next_block);
} else {
cur_block.after(next_block);
}
this.paste_children(next_block, blocks[i].children);
cur_block = next_block;
}
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
}
private async paste_children(parent: AsyncBlock, children: any[]) {
for (let i = 0; i < children.length; i++) {
const next_block = await this.editor.createBlock(children[i].type);
next_block.setProperties(children[i].properties);
parent.append(next_block);
await this.paste_children(next_block, children[i].children);
}
}
private pre_copy_cut(action: ClipboardAction, e: ClipboardEvent) {
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
switch (action) {
case ClipboardAction.COPY:
this.hooks.beforeCopy(e);
this._hooks.beforeCopy(e);
break;
case ClipboardAction.CUT:
this.hooks.beforeCut(e);
this._hooks.beforeCut(e);
break;
}
}
private dispatch_clipboard_event(
private _dispatchClipboardEvent(
action: ClipboardAction,
e: ClipboardEvent
) {
this.pre_copy_cut(action, e);
this._preCopyCut(action, e);
}
dispose() {
document.removeEventListener(ClipboardAction.COPY, this.handle_copy);
document.removeEventListener(ClipboardAction.CUT, this.handle_cut);
document.removeEventListener(ClipboardAction.PASTE, this.handle_paste);
this.event_target.removeEventListener(
ClipboardAction.COPY,
this.handle_copy
);
this.event_target.removeEventListener(
ClipboardAction.CUT,
this.handle_cut
);
this.event_target.removeEventListener(
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
document.removeEventListener(
ClipboardAction.PASTE,
this.handle_paste
this._paste.handlePaste
);
this.clipboard_parse.dispose();
this.clipboard_parse = null;
this.event_target = null;
this.hooks = null;
this.editor = null;
this._eventTarget.removeEventListener(
ClipboardAction.COPY,
this._handleCopy
);
this._eventTarget.removeEventListener(
ClipboardAction.CUT,
this._handleCut
);
this._eventTarget.removeEventListener(
ClipboardAction.PASTE,
this._paste.handlePaste
);
this._clipboardParse.dispose();
this._clipboardParse = null;
this._eventTarget = null;
this._hooks = null;
this._editor = null;
}
}
@@ -115,10 +115,8 @@ export default class ClipboardParse {
const block_utils = this.editor.getView(
ClipboardParse.block_types[i]
);
const blocks =
block_utils &&
block_utils.html2block &&
block_utils.html2block(el, this.parse_dom);
const blocks = block_utils?.html2block?.(el, this.parse_dom);
if (blocks) {
return blocks;
}
@@ -46,7 +46,7 @@ class ClipboardPopulator {
}
// TODO: is not compatible with safari
const success = this.copy_to_cliboard_from_pc(clips);
const success = this._copyToClipboardFromPc(clips);
if (!success) {
// This way, not compatible with firefox
const clipboardData = e.clipboardData;
@@ -65,7 +65,7 @@ class ClipboardPopulator {
}
};
private copy_to_cliboard_from_pc(clips: any[]) {
private _copyToClipboardFromPc(clips: any[]) {
let success = false;
const tempElem = document.createElement('textarea');
tempElem.value = 'temp';
@@ -96,56 +96,55 @@ class ClipboardPopulator {
return success;
}
private async get_clip_block_info(selBlock: SelectBlock) {
private async _getClipBlockInfo(selBlock: SelectBlock) {
const block = await this._editor.getBlockById(selBlock.blockId);
const block_view = this._editor.getView(block.type);
assert(block_view);
const block_info: ClipBlockInfo = {
const blockView = this._editor.getView(block.type);
assert(blockView);
const blockInfo: ClipBlockInfo = {
type: block.type,
properties: block_view.getSelProperties(block, selBlock),
properties: blockView.getSelProperties(block, selBlock),
children: [] as any[],
};
for (let i = 0; i < selBlock.children.length; i++) {
const child_info = await this.get_clip_block_info(
const childInfo = await this._getClipBlockInfo(
selBlock.children[i]
);
block_info.children.push(child_info);
blockInfo.children.push(childInfo);
}
return block_info;
return blockInfo;
}
private async get_inner_clip(): Promise<InnerClipInfo> {
private async _getInnerClip(): Promise<InnerClipInfo> {
const clips: ClipBlockInfo[] = [];
const select_info: SelectInfo =
const selectInfo: SelectInfo =
await this._selectionManager.getSelectInfo();
for (let i = 0; i < select_info.blocks.length; i++) {
const sel_block = select_info.blocks[i];
const clip_block_info = await this.get_clip_block_info(sel_block);
clips.push(clip_block_info);
for (let i = 0; i < selectInfo.blocks.length; i++) {
const selBlock = selectInfo.blocks[i];
const clipBlockInfo = await this._getClipBlockInfo(selBlock);
clips.push(clipBlockInfo);
}
const clipInfo: InnerClipInfo = {
select: select_info,
return {
select: selectInfo,
data: clips,
};
return clipInfo;
}
async getClips() {
const clips: any[] = [];
const inner_clip = await this.get_inner_clip();
const innerClip = await this._getInnerClip();
clips.push(
new Clip(
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
JSON.stringify(inner_clip)
JSON.stringify(innerClip)
)
);
const html_clip = await this._clipboardParse.generateHtml();
html_clip &&
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, html_clip));
const htmlClip = await this._clipboardParse.generateHtml();
htmlClip &&
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip));
return clips;
}
@@ -0,0 +1,455 @@
/* eslint-disable max-lines */
import {
OFFICE_CLIPBOARD_MIMETYPE,
InnerClipInfo,
ClipBlockInfo,
} from './types';
import { Editor } from '../editor';
import { AsyncBlock } from '../block';
import ClipboardParse from './clipboard-parse';
import { SelectInfo } from '../selection';
import {
Protocol,
BlockFlavorKeys,
services,
} from '@toeverything/datasource/db-service';
import { MarkdownParser } from './markdown-parse';
import { shouldHandlerContinue } from './utils';
const SUPPORT_MARKDOWN_PASTE = true;
type TextValueItem = {
text: string;
[key: string]: any;
};
export class Paste {
private _editor: Editor;
private _markdownParse: MarkdownParser;
private _clipboardParse: ClipboardParse;
constructor(
editor: Editor,
clipboardParse: ClipboardParse,
markdownParse: MarkdownParser
) {
this._markdownParse = markdownParse;
this._clipboardParse = clipboardParse;
this._editor = editor;
this.handlePaste = this.handlePaste.bind(this);
}
private static _optimalMimeType: string[] = [
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
OFFICE_CLIPBOARD_MIMETYPE.HTML,
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
];
public handlePaste(e: Event) {
if (!shouldHandlerContinue(e, this._editor)) {
return;
}
e.stopPropagation();
const clipboardData = (e as ClipboardEvent).clipboardData;
const isPureFile = Paste._isPureFileInClipboard(clipboardData);
if (isPureFile) {
this._pasteFile(clipboardData);
} else {
this._pasteContent(clipboardData);
}
}
public getOptimalClip(clipboardData: any) {
const mimeTypeArr = Paste._optimalMimeType;
for (let i = 0; i < mimeTypeArr.length; i++) {
const data =
clipboardData[mimeTypeArr[i]] ||
clipboardData.getData(mimeTypeArr[i]);
if (data) {
return {
type: mimeTypeArr[i],
data: data,
};
}
}
return '';
}
private _pasteContent(clipboardData: any) {
const originClip: { data: any; type: any } = this.getOptimalClip(
clipboardData
) as { data: any; type: any };
const originTextClipData = clipboardData.getData(
OFFICE_CLIPBOARD_MIMETYPE.TEXT
);
let clipData = originClip['data'];
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
clipData = Paste._excapeHtml(clipData);
}
switch (originClip['type']) {
/** Protocol paste */
case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED:
this._firePasteEditAction(clipData);
break;
case OFFICE_CLIPBOARD_MIMETYPE.HTML:
this._pasteHtml(clipData, originTextClipData);
break;
case OFFICE_CLIPBOARD_MIMETYPE.TEXT:
this._pasteText(clipData, originTextClipData);
break;
default:
break;
}
}
private async _firePasteEditAction(clipboardData: any) {
const clipInfo: InnerClipInfo = JSON.parse(clipboardData);
clipInfo && this._insertBlocks(clipInfo.data, clipInfo.select);
}
private async _pasteFile(clipboardData: any) {
const file = Paste._getImageFile(clipboardData);
if (file) {
const result = await services.api.file.create({
workspace: this._editor.workspace,
file: file,
});
const blockInfo: ClipBlockInfo = {
type: 'image',
properties: {
image: {
value: result.id,
name: file.name,
size: file.size,
type: file.type,
},
},
children: [] as ClipBlockInfo[],
};
await this._insertBlocks([blockInfo]);
}
}
private static _isPureFileInClipboard(clipboardData: DataTransfer) {
const types = clipboardData.types;
return (
(types.length === 1 && types[0] === 'Files') ||
(types.length === 2 &&
(types.includes('text/plain') || types.includes('text/html')) &&
types.includes('Files'))
);
}
private static _isTextEditBlock(type: BlockFlavorKeys) {
return (
type === Protocol.Block.Type.page ||
type === Protocol.Block.Type.text ||
type === Protocol.Block.Type.heading1 ||
type === Protocol.Block.Type.heading2 ||
type === Protocol.Block.Type.heading3 ||
type === Protocol.Block.Type.quote ||
type === Protocol.Block.Type.todo ||
type === Protocol.Block.Type.code ||
type === Protocol.Block.Type.callout ||
type === Protocol.Block.Type.numbered ||
type === Protocol.Block.Type.bullet
);
}
private async _insertBlocks(
blocks: ClipBlockInfo[],
pasteSelect?: SelectInfo
) {
if (blocks.length === 0) {
return;
}
const currentSelectInfo =
await this._editor.selectionManager.getSelectInfo();
// When the selection is in one of the blocks, select?.type === 'Range'
// Currently the selection does not support cross-blocking, so this case is not considered
if (currentSelectInfo.type === 'Range') {
// 当 currentSelectInfo.type === 'Range' 时,光标选中的block必然只有一个
const selectedBlock = await this._editor.getBlockById(
currentSelectInfo.blocks[0].blockId
);
const isSelectedBlockEdit = Paste._isTextEditBlock(
selectedBlock.type
);
if (isSelectedBlockEdit) {
const shouldSplitBlock =
blocks.length > 1 ||
!Paste._isTextEditBlock(blocks[0].type);
const pureText = !shouldSplitBlock
? blocks[0].properties.text.value
: [{ text: '' }];
const { startInfo, endInfo } = currentSelectInfo.blocks[0];
// Text content of the selected current editable block
const currentTextValue =
selectedBlock.getProperty('text').value;
// When the cursor selection spans different styles of text
if (startInfo?.arrayIndex !== endInfo?.arrayIndex) {
if (shouldSplitBlock) {
// TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes"
const newTextValue = currentTextValue.reduce(
(
newTextValue: TextValueItem[],
textStore: TextValueItem,
i: number
) => {
if (i < startInfo?.arrayIndex) {
newTextValue.push(textStore);
}
const { text, ...props } = textStore;
if (i === startInfo?.arrayIndex) {
newTextValue.push({
text: text.slice(0, startInfo?.offset),
...props,
});
}
return newTextValue;
},
[]
);
const nextTextValue = currentTextValue.reduce(
(
newTextValue: TextValueItem[],
textStore: TextValueItem,
i: number
) => {
if (i > endInfo?.arrayIndex) {
newTextValue.push(textStore);
}
const { text, ...props } = textStore;
if (i === endInfo?.arrayIndex) {
newTextValue.push({
text: text.slice(endInfo?.offset),
...props,
});
}
return newTextValue;
},
[]
);
selectedBlock.setProperties({
text: {
value: newTextValue,
},
});
const pasteBlocks = await this._createBlocks(blocks);
pasteBlocks.forEach(block => {
selectedBlock.after(block);
});
const nextBlock = await this._editor.createBlock(
selectedBlock?.type
);
nextBlock.setProperties({
text: {
value: nextTextValue,
},
});
pasteBlocks[pasteBlocks.length - 1].after(nextBlock);
this._setEndSelectToBlock(
pasteBlocks[pasteBlocks.length - 1].id
);
} else {
this._editor.blockHelper.insertNodes(
selectedBlock.id,
pureText,
{ select: true }
);
}
}
// When the cursor selection does not span different styles of text
if (startInfo?.arrayIndex === endInfo?.arrayIndex) {
if (shouldSplitBlock) {
// TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes"
const newTextValue = currentTextValue.reduce(
(
newTextValue: TextValueItem[],
textStore: TextValueItem,
i: number
) => {
if (i < startInfo?.arrayIndex) {
newTextValue.push(textStore);
}
const { text, ...props } = textStore;
if (i === startInfo?.arrayIndex) {
newTextValue.push({
text: `${text.slice(
0,
startInfo?.offset
)}`,
...props,
});
}
return newTextValue;
},
[]
);
const nextTextValue = currentTextValue.reduce(
(
nextTextValue: TextValueItem[],
textStore: TextValueItem,
i: number
) => {
if (i > endInfo?.arrayIndex) {
nextTextValue.push(textStore);
}
const { text, ...props } = textStore;
if (i === endInfo?.arrayIndex) {
nextTextValue.push({
text: `${text.slice(endInfo?.offset)}`,
...props,
});
}
return nextTextValue;
},
[]
);
selectedBlock.setProperties({
text: {
value: newTextValue,
},
});
const pasteBlocks = await this._createBlocks(blocks);
pasteBlocks.forEach((block: AsyncBlock) => {
selectedBlock.after(block);
});
const nextBlock = await this._editor.createBlock(
selectedBlock?.type
);
nextBlock.setProperties({
text: {
value: nextTextValue,
},
});
pasteBlocks[pasteBlocks.length - 1].after(nextBlock);
this._setEndSelectToBlock(
pasteBlocks[pasteBlocks.length - 1].id
);
} else {
this._editor.blockHelper.insertNodes(
selectedBlock.id,
pureText,
{ select: true }
);
}
}
} else {
const pasteBlocks = await this._createBlocks(blocks);
pasteBlocks.forEach(block => {
selectedBlock.after(block);
});
this._setEndSelectToBlock(
pasteBlocks[pasteBlocks.length - 1].id
);
}
}
if (currentSelectInfo.type === 'Block') {
const selectedBlock = await this._editor.getBlockById(
currentSelectInfo.blocks[currentSelectInfo.blocks.length - 1]
.blockId
);
const pasteBlocks = await this._createBlocks(blocks);
let groupBlock: AsyncBlock;
if (
selectedBlock?.type === 'group' ||
selectedBlock?.type === 'page'
) {
groupBlock = await this._editor.createBlock('group');
pasteBlocks.forEach(block => {
groupBlock.append(block);
});
await selectedBlock.after(groupBlock);
} else {
pasteBlocks.forEach(block => {
selectedBlock.after(block);
});
}
this._setEndSelectToBlock(pasteBlocks[pasteBlocks.length - 1].id);
}
}
private _setEndSelectToBlock(blockId: string) {
setTimeout(() => {
this._editor.selectionManager.activeNodeByNodeId(blockId, 'end');
}, 100);
}
private async _createBlocks(blocks: ClipBlockInfo[], parentId?: string) {
return Promise.all(
blocks.map(async clipBlockInfo => {
const block = await this._editor.createBlock(
clipBlockInfo.type
);
block?.setProperties(clipBlockInfo.properties);
await this._createBlocks(clipBlockInfo.children, block?.id);
return block;
})
);
}
private async _pasteHtml(clipData: any, originTextClipData: any) {
if (SUPPORT_MARKDOWN_PASTE) {
const hasMarkdown =
this._markdownParse.checkIfTextContainsMd(originTextClipData);
if (hasMarkdown) {
try {
const convertedDataObj =
this._markdownParse.md2Html(originTextClipData);
if (convertedDataObj.isConverted) {
clipData = convertedDataObj.text;
}
} catch (e) {
console.error(e);
clipData = originTextClipData;
}
}
}
const blocks = this._clipboardParse.html2blocks(clipData);
await this._insertBlocks(blocks);
}
private async _pasteText(clipData: any, originTextClipData: any) {
const blocks = this._clipboardParse.text2blocks(clipData);
await this._insertBlocks(blocks);
}
private static _getImageFile(clipboardData: any) {
const files = clipboardData.files;
if (files && files[0] && files[0].type.indexOf('image') > -1) {
return files[0];
}
return;
}
private static _excapeHtml(data: any, onlySpace?: any) {
if (!onlySpace) {
// TODO:
// data = string.htmlEscape(data);
// data = data.replace(/\n/g, '<br>');
}
// data = data.replace(/ /g, '&nbsp;');
// data = data.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
return data;
}
}
@@ -0,0 +1,14 @@
import { Editor } from '../editor';
export const shouldHandlerContinue = (event: Event, editor: Editor) => {
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
if (event.defaultPrevented) {
return false;
}
if (filterNodes.includes((event.target as HTMLElement)?.tagName)) {
return false;
}
return editor.selectionManager.currentSelectInfo.type !== 'None';
};
@@ -32,11 +32,11 @@ export class BlockCommands {
) {
const block = await this._editor.getBlockById(blockId);
if (block) {
const next_block = await this._editor.createBlock(type);
if (next_block) {
await block.after(next_block);
this._editor.selectionManager.activeNodeByNodeId(next_block.id);
return next_block;
const nextBlock = await this._editor.createBlock(type);
if (nextBlock) {
await block.after(nextBlock);
this._editor.selectionManager.activeNodeByNodeId(nextBlock.id);
return nextBlock;
}
}
return undefined;
@@ -101,73 +101,70 @@ export class DragDropManager {
if (this._blockDragDirection !== BlockDropPlacement.none) {
const blockId = event.dataTransfer.getData(this._blockIdKey);
if (!(await this._canBeDrop(event))) return;
if (this._blockDragDirection === BlockDropPlacement.bottom) {
this._editor.commands.blockCommands.moveBlockAfter(
blockId,
this._blockDragTargetId
);
}
if (
[BlockDropPlacement.left, BlockDropPlacement.right].includes(
this._blockDragDirection
)
) {
const dropType =
this._blockDragDirection === BlockDropPlacement.left
? GridDropType.left
: GridDropType.right;
// if target is a grid item create grid item
if (targetBlock.type !== Protocol.Block.Type.gridItem) {
await this._editor.commands.blockCommands.createLayoutBlock(
switch (this._blockDragDirection) {
case BlockDropPlacement.bottom: {
this._editor.commands.blockCommands.moveBlockAfter(
blockId,
this._blockDragTargetId,
dropType
);
} else {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
this._blockDragTargetId,
dropType
this._blockDragTargetId
);
break;
}
}
if (
[
BlockDropPlacement.outerLeft,
BlockDropPlacement.outerRight,
].includes(this._blockDragDirection)
) {
if (targetBlock.type !== Protocol.Block.Type.grid) {
await this._editor.commands.blockCommands.createLayoutBlock(
blockId,
this._blockDragTargetId,
this._blockDragDirection ===
BlockDropPlacement.outerLeft
case BlockDropPlacement.left:
case BlockDropPlacement.right: {
const dropType =
this._blockDragDirection === BlockDropPlacement.left
? GridDropType.left
: GridDropType.right
);
}
if (targetBlock.type === Protocol.Block.Type.grid) {
const gridItems = await targetBlock.children();
if (
BlockDropPlacement.outerRight ===
this._blockDragDirection
) {
: GridDropType.right;
// if target is a grid item create grid item
if (targetBlock.type !== Protocol.Block.Type.gridItem) {
await this._editor.commands.blockCommands.createLayoutBlock(
blockId,
this._blockDragTargetId,
dropType
);
} else {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
gridItems[gridItems.length - 1].id
this._blockDragTargetId,
dropType
);
}
if (
BlockDropPlacement.outerLeft ===
this._blockDragDirection
) {
await this._editor.commands.blockCommands.moveInNewGridItem(
break;
}
case BlockDropPlacement.outerLeft:
case BlockDropPlacement.outerRight: {
if (targetBlock.type !== Protocol.Block.Type.grid) {
await this._editor.commands.blockCommands.createLayoutBlock(
blockId,
gridItems[0].id,
GridDropType.right
this._blockDragTargetId,
this._blockDragDirection ===
BlockDropPlacement.outerLeft
? GridDropType.left
: GridDropType.right
);
}
if (targetBlock.type === Protocol.Block.Type.grid) {
const gridItems = await targetBlock.children();
if (
BlockDropPlacement.outerRight ===
this._blockDragDirection
) {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
gridItems[gridItems.length - 1].id
);
}
if (
BlockDropPlacement.outerLeft ===
this._blockDragDirection
) {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
gridItems[0].id,
GridDropType.right
);
}
}
}
}
}
@@ -424,7 +421,6 @@ export class DragDropManager {
}
public handlerEditorDrop(event: React.DragEvent<Element>) {
event.preventDefault();
// IMP: can not use Decorators now may use decorators is right
if (this.isEnabled()) {
if (this.isDragBlock(event)) {
@@ -207,6 +207,7 @@ export class ScrollManager {
}
private _getKeepInViewParams(blockRect: Rect) {
if (this.scrollContainer == null) return 0;
const { top, bottom } = domToRect(this._scrollContainer);
if (blockRect.top <= top + blockRect.height * 3) {
return -1;
@@ -30,6 +30,7 @@ import {
} from './types';
import { isLikeBlockListIds } from './utils';
import { Protocol } from '@toeverything/datasource/db-service';
import { Editor } from 'slate';
// IMP: maybe merge active and select into single function
export type SelectionInfo = InstanceType<
@@ -47,7 +48,7 @@ export class SelectionManager implements VirgoSelection {
private _scrollDelay = 150;
private _selectEndDelayTime = 500;
private _hasEmitEndPending = false;
private _scrollTimer: number | null = null;
/**
*
* the selection info before current
@@ -77,9 +78,6 @@ export class SelectionManager implements VirgoSelection {
type: 'None',
info: null,
};
// IMP: to delete
// @ts-ignore
window['selectionManager'] = this;
this._initWindowSelectionChangeListen();
}
@@ -683,6 +681,9 @@ export class SelectionManager implements VirgoSelection {
*/
public async activeNodeByNodeId(nodeId: string, position?: CursorTypes) {
try {
if (this._scrollTimer) {
clearTimeout(this._scrollTimer);
}
const node = await this._editor.getBlockById(nodeId);
if (node) {
this._activatedNodeId = nodeId;
@@ -691,8 +692,8 @@ export class SelectionManager implements VirgoSelection {
}
this.emit(nodeId, SelectEventTypes.active, this.lastPoint);
// TODO: Optimize the related logic after implementing the scroll bar
setTimeout(() => {
// this._editor.scrollManager.keepBlockInView(node);
this._scrollTimer = window.setTimeout(() => {
this._editor.scrollManager.keepBlockInView(node);
}, this._scrollDelay);
} else {
console.warn('Can not find node by this id');
@@ -1050,4 +1051,25 @@ export class SelectionManager implements VirgoSelection {
this._windowSelectionChangeHandler
);
}
/**
*
* move active selection to the new position
* @param {number} index
* @param {string} blockId
* @memberof SelectionManager
*/
public async moveCursor(
nowRange: any,
index: number,
blockId: string
): Promise<void> {
let preRang = document.createRange();
preRang.setStart(nowRange.startContainer, index);
preRang.setEnd(nowRange.endContainer, index);
let prePosition = preRang.getClientRects().item(0);
this.activeNodeByNodeId(
blockId,
new Point(prePosition.left, prePosition.bottom)
);
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
import { noop, Point } from '@toeverything/utils';
import { Point } from '@toeverything/utils';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEditor } from './Contexts';
import {
@@ -62,7 +62,7 @@ export const useBlock = (blockId: string) => {
return undefined;
}
let valid = true;
let offUpdate = noop;
let offUpdate: () => void | undefined = undefined;
editor.getBlockById(blockId).then(node => {
if (!valid) {
return;
@@ -79,7 +79,7 @@ export const useBlock = (blockId: string) => {
return () => {
valid = false;
offUpdate();
offUpdate?.();
};
}, [blockId, editor, requestReRender]);
@@ -1,7 +1,6 @@
import { styled } from '@toeverything/components/ui';
import { useLayoutEffect, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
// import { RenderChildren } from './RenderChildren';
import { useEditor } from '../Contexts';
import { useBlock } from '../hooks';
@@ -10,19 +9,21 @@ interface RenderBlockProps {
hasContainer?: boolean;
}
export const RenderBlock = ({
export function RenderBlock({
blockId,
hasContainer = true,
}: RenderBlockProps) => {
}: RenderBlockProps) {
const { editor, editorElement } = useEditor();
const { block } = useBlock(blockId);
const blockRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (block && blockRef.current) {
block.dom = blockRef.current;
}
});
const setRef = useCallback(
(dom: HTMLElement) => {
if (block != null && dom != null) {
block.dom = dom;
}
},
[block]
);
const blockView = useMemo(() => {
if (block?.type) {
@@ -54,17 +55,13 @@ export const RenderBlock = ({
) : null;
return hasContainer ? (
<BlockContainer
block-id={blockId}
ref={blockRef}
data-block-id={blockId}
>
<BlockContainer block-id={blockId} ref={setRef} data-block-id={blockId}>
{view}
</BlockContainer>
) : (
<> {view}</>
);
};
}
const BlockContainer = styled('div')(({ theme }) => ({
fontSize: theme.typography.body1.fontSize,
@@ -54,4 +54,5 @@ const StyledButton = styled('div')({
backgroundColor: 'transparent',
width: '100%',
height: '100%',
color: '#B9CAD5',
});
@@ -264,6 +264,7 @@ const Draggable = styled(Button)({
backgroundColor: 'transparent',
width: '16px',
height: '22px',
color: '#B9CAD5',
'& svg': {
fontSize: '20px',
marginLeft: '-2px',
@@ -93,7 +93,7 @@ export class LeftMenuPlugin extends BasePlugin {
}
};
private _onDrop = () => {
private _onDrop = (e: React.DragEvent<Element>) => {
this._lineInfo.next(undefined);
};
private _handleDragOverBlockNode = async (
@@ -0,0 +1,115 @@
/* eslint-disable filename-rules/match */
import { useCallback, useMemo, useState } from 'react';
import { IconButton, MuiSnackbar, styled } from '@toeverything/components/ui';
import { services } from '@toeverything/datasource/db-service';
import { useLocalTrigger } from '@toeverything/datasource/state';
import { CloseIcon } from '@toeverything/components/common';
const cleanupWorkspace = (workspace: string) =>
new Promise((resolve, reject) => {
const req = indexedDB.deleteDatabase(workspace);
req.addEventListener('error', e => reject(e));
req.addEventListener('blocked', e => reject(e));
req.addEventListener('upgradeneeded', e => reject(e));
req.addEventListener('success', e => resolve(e));
});
const requestPermission = async (workspace: string) => {
await cleanupWorkspace(workspace);
// @ts-ignore
const dirHandler = await window.showDirectoryPicker({
id: 'AFFiNE_' + workspace,
mode: 'readwrite',
startIn: 'documents',
});
const fileHandle = await dirHandler.getFileHandle('affine.db', {
create: true,
});
const file = await fileHandle.getFile();
const initialData = new Uint8Array(await file.arrayBuffer());
const exporter = async (contents: Uint8Array) => {
try {
const writable = await fileHandle.createWritable();
await writable.write(contents);
await writable.close();
} catch (e) {
console.log(e);
}
};
await services.api.editorBlock.setupDataExporter(
workspace,
new Uint8Array(initialData),
exporter
);
};
const StyledFileSystem = styled('div')<{ disabled?: boolean }>({
padding: '10px 12px',
fontWeight: 600,
fontSize: '14px',
color: '#3E6FDB',
textTransform: 'uppercase',
cursor: 'pointer',
'&:hover': {
background: '#F5F7F8',
borderRadius: '5px',
},
});
export const fsApiSupported = () => {
try {
return 'showOpenFilePicker' in window;
} catch (e) {
return false;
}
};
export const FileSystem = () => {
const [selected, onSelected] = useLocalTrigger();
const [error, setError] = useState(false);
const onError = useCallback(() => {
setError(true);
setTimeout(() => setError(false), 3000);
}, []);
const apiSupported = useMemo(() => fsApiSupported(), []);
if (apiSupported && !selected) {
return (
<>
<MuiSnackbar
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
open={error}
message="Request File Permission failed, please check if you have permission"
sx={{ marginTop: '3em' }}
action={
<IconButton
aria-label="close"
onClick={() => setError(false)}
>
<CloseIcon />
</IconButton>
}
/>
<StyledFileSystem
onClick={async () => {
try {
await requestPermission('AFFiNE');
onSelected();
} catch (e) {
onError();
}
}}
>
Sync to Disk
</StyledFileSystem>
</>
);
}
return null;
};
@@ -1,19 +1,36 @@
import { IconButton, styled, MuiButton } from '@toeverything/components/ui';
import { useMemo } from 'react';
import { IconButton, styled } from '@toeverything/components/ui';
import {
LogoIcon,
SideBarViewIcon,
SearchIcon,
SideBarViewCloseIcon,
} from '@toeverything/components/icons';
import { useShowSettingsSidebar } from '@toeverything/datasource/state';
import {
useShowSettingsSidebar,
useLocalTrigger,
} from '@toeverything/datasource/state';
import { CurrentPageTitle } from './Title';
import { EditorBoardSwitcher } from './EditorBoardSwitcher';
import { fsApiSupported, FileSystem } from './FileSystem';
import { CurrentPageTitle } from './Title';
export const LayoutHeader = () => {
const [isLocalWorkspace] = useLocalTrigger();
const { toggleSettingsSidebar: toggleInfoSidebar, showSettingsSidebar } =
useShowSettingsSidebar();
const warningTips = useMemo(() => {
if (!fsApiSupported()) {
return 'Your browser does not support the local storage feature, please upgrade to the latest version of Chrome or Edge browser';
} else if (!isLocalWorkspace) {
return 'You are in DEMO mode. Changes will NOT be saved unless you SYNC TO DISK';
} else {
return 'AFFiNE is under active development and the current version is UNSTABLE. Please DO NOT store information or data';
}
}, [isLocalWorkspace]);
return (
<StyledContainerForHeaderRoot>
<StyledHeaderRoot>
@@ -25,6 +42,7 @@ export const LayoutHeader = () => {
</FlexContainer>
<FlexContainer>
<StyledHelper>
<FileSystem />
<StyledShare disabled={true}>Share</StyledShare>
<div style={{ margin: '0px 12px' }}>
<IconButton
@@ -51,10 +69,7 @@ export const LayoutHeader = () => {
</StyledContainerForEditorBoardSwitcher>
</StyledHeaderRoot>
<StyledUnstableTips>
<StyledUnstableTipsText>
AFFiNE now under active development, the version is
UNSTABLE, please DO NOT store important data in this version
</StyledUnstableTipsText>
<StyledUnstableTipsText>{warningTips}</StyledUnstableTipsText>
</StyledUnstableTips>
</StyledContainerForHeaderRoot>
);
@@ -36,19 +36,13 @@ export type DndTreeProps = {
indentationWidth?: number;
collapsible?: boolean;
removable?: boolean;
showDragIndicator?: boolean;
};
/**
* Currently does not support drag and drop using the keyboard.
*/
export function DndTree(props: DndTreeProps) {
const {
indentationWidth = 20,
collapsible,
removable,
showDragIndicator,
} = props;
const { indentationWidth = 20, collapsible, removable } = props;
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
@@ -111,7 +105,6 @@ export function DndTree(props: DndTreeProps) {
: depth
}
indentationWidth={indentationWidth}
indicator={showDragIndicator}
childCount={children.length}
onCollapse={
collapsible && children.length
@@ -129,7 +122,8 @@ export function DndTree(props: DndTreeProps) {
)}
<DragOverlay
dropAnimation={dropAnimation}
style={{ marginTop: '-65px' }}
// TODO fix drag offset when the position of the page tree changes
style={{ marginTop: '-100px' }}
>
{activeId && activeItem ? (
<DndTreeItem
@@ -16,8 +16,7 @@ export function DndTreeItem({ id, depth, ...props }: DndTreeItemProps) {
isDragging,
isSorting,
listeners,
setDraggableNodeRef,
setDroppableNodeRef,
setNodeRef,
transform,
transition,
} = useSortable({ id });
@@ -29,8 +28,7 @@ export function DndTreeItem({ id, depth, ...props }: DndTreeItemProps) {
return (
<TreeItem
ref={setDraggableNodeRef}
wrapperRef={setDroppableNodeRef}
ref={setNodeRef}
pageId={id}
style={style}
depth={depth}
@@ -15,7 +15,6 @@ import {
TextLink,
TreeItemContainer,
TreeItemContent,
Wrapper,
} from './styles';
export type TreeItemProps = {
@@ -33,13 +32,10 @@ export type TreeItemProps = {
/** isDragging */
ghost?: boolean;
handleProps?: any;
indicator?: boolean;
indentationWidth: number;
onCollapse?(): void;
onRemove?(): void;
/** The ref of the outermost container is often used as droppaHTMLAttributes<HTMLLIElement>ble-node; the ref of the inner dom is often used as draggable-node */
wrapperRef?(node: HTMLLIElement): void;
} & HTMLAttributes<HTMLLIElement>;
} & HTMLAttributes<HTMLDivElement>;
export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
(
@@ -52,13 +48,10 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
ghost,
handleProps,
indentationWidth,
indicator,
collapsed,
onCollapse,
onRemove,
style,
value,
wrapperRef,
pageId,
...props
},
@@ -71,8 +64,8 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
);
return (
<Wrapper
ref={wrapperRef}
<TreeItemContainer
ref={ref}
clone={clone}
ghost={ghost}
disableSelection={disableSelection}
@@ -81,38 +74,38 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
active={pageId === page_id}
{...props}
>
<TreeItemContainer ref={ref} style={style} title={value}>
<ActionButton tabIndex={0} onClick={onCollapse}>
{childCount !== 0 ? (
collapsed ? (
<ArrowRightIcon />
) : (
<ArrowDropDownIcon />
)
) : (
<DotIcon />
)}
</ActionButton>
{childCount !== 0 ? (
collapsed ? (
<ActionButton tabIndex={0} onClick={onCollapse}>
<ArrowRightIcon />
</ActionButton>
) : (
<ActionButton tabIndex={0} onClick={onCollapse}>
<ArrowDropDownIcon />
</ActionButton>
)
) : (
<DotIcon />
)}
<TreeItemContent {...handleProps}>
<TextLink to={`/${workspace_id}/${pageId}`}>
{value}
</TextLink>
{BooleanPageTreeItemMoreActions && (
<MoreActions
workspaceId={workspace_id}
pageId={pageId}
onRemove={onRemove}
/>
)}
<TreeItemContent {...handleProps}>
<TextLink to={`/${workspace_id}/${pageId}`}>
{value}
</TextLink>
{BooleanPageTreeItemMoreActions && (
<MoreActions
workspaceId={workspace_id}
pageId={pageId}
onRemove={onRemove}
/>
)}
{/*{!clone && onRemove && <Remove onClick={onRemove} />}*/}
{clone && childCount && childCount > 1 ? (
<Counter>{childCount}</Counter>
) : null}
</TreeItemContent>
</TreeItemContainer>
</Wrapper>
{/*{!clone && onRemove && <Remove onClick={onRemove} />}*/}
{clone && childCount && childCount > 1 ? (
<Counter>{childCount}</Counter>
) : null}
</TreeItemContent>
</TreeItemContainer>
);
}
);
@@ -1,22 +1,18 @@
import { styled } from '@toeverything/components/ui';
import { Link } from 'react-router-dom';
export const TreeItemContainer = styled('div')`
box-sizing: border-box;
display: flex;
align-items: center;
color: #4c6275;
`;
export const Wrapper = styled('li')<{
export const TreeItemContainer = styled('div')<{
spacing: string;
clone?: boolean;
ghost?: boolean;
indicator?: boolean;
disableSelection?: boolean;
disableInteraction?: boolean;
active?: boolean;
}>`
display: flex;
align-items: center;
color: #4c6275;
box-sizing: border-box;
padding-left: ${({ spacing }) => spacing};
list-style: none;
@@ -26,21 +22,13 @@ export const Wrapper = styled('li')<{
${({ clone, disableSelection }) =>
(clone || disableSelection) &&
`width: 100%;
.Text,
.Count {
user-select: none;
-webkit-user-select: none;
}`}
${({ indicator }) =>
indicator &&
`width: 100%;
.Text,
.Count {
user-select: none;
-webkit-user-select: none;
}`}
`
width: 100%;
user-select: none;
opacity: 0.7;
background: transparent;
cursor: grab;
`}
${({ disableInteraction }) => disableInteraction && `pointer-events: none;`}
@@ -48,64 +36,6 @@ export const Wrapper = styled('li')<{
background: #f5f7f8;
border-radius: 5px;
}
&.clone {
display: inline-block;
padding: 0;
margin-left: 10px;
margin-top: 5px;
pointer-events: none;
${TreeItemContainer} {
padding-right: 20px;
border-radius: 4px;
box-shadow: 0px 15px 15px 0 rgba(34, 33, 81, 0.1);
}
}
&.ghost {
&.indicator {
opacity: 1;
position: relative;
z-index: 1;
margin-bottom: -1px;
${TreeItemContainer} {
position: relative;
padding: 0;
height: 8px;
border-color: #2389ff;
background-color: #56a1f8;
&:before {
position: absolute;
left: -8px;
top: -4px;
display: block;
content: '';
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid #2389ff;
}
> * {
/* Items are hidden using height and opacity to retain focus */
opacity: 0;
height: 0;
}
}
}
&:not(.indicator) {
opacity: 0.5;
}
${TreeItemContainer} > * {
box-shadow: none;
background-color: transparent;
}
}
`;
export const Counter = styled('span')`
@@ -138,7 +68,7 @@ export const ActionButton = styled('button')<{
border: none;
outline: none;
appearance: none;
background-color: transparent;
background: transparent;
-webkit-tap-highlight-color: transparent;
svg {
@@ -151,8 +81,7 @@ export const ActionButton = styled('button')<{
}
&:active {
background-color: ${({ background }) =>
background ?? 'rgba(0, 0, 0, 0.05)'};
background: ${({ background }) => background ?? 'rgba(0, 0, 0, 0.05)'};
svg {
fill: ${({ fill }) => fill ?? '#788491'};
@@ -170,9 +99,7 @@ export const TreeItemMoreActions = styled('div')`
visibility: hidden;
`;
export const TextLink = styled(Link, {
shouldForwardProp: (prop: string) => !['active'].includes(prop),
})<{ active?: boolean }>`
export const TextLink = styled(Link)`
display: flex;
align-items: center;
flex-grow: 1;
+4 -8
View File
@@ -1,6 +1,6 @@
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui';
import React, { forwardRef, type PropsWithChildren } from 'react';
import { type PopperHandler, Popper } from '../popper';
import React, { type PropsWithChildren } from 'react';
import { Popper } from '../popper';
import { PopoverContainer } from './Container';
import type { PopoverProps, PopoverDirection } from './interface';
@@ -25,15 +25,11 @@ export const placementToContainerDirection: Record<
'auto-end': 'none',
};
export const Popover = forwardRef<
PopperHandler,
PropsWithChildren<PopoverProps>
>((props, ref) => {
export const Popover = (props: PropsWithChildren<PopoverProps>) => {
const { popoverDirection, placement, content, children, style } = props;
return (
<Popper
{...props}
ref={ref}
content={
<PopoverContainer
style={style}
@@ -49,4 +45,4 @@ export const Popover = forwardRef<
{children}
</Popper>
);
});
};
+161 -165
View File
@@ -1,184 +1,180 @@
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import {
MuiPopper,
MuiClickAwayListener as ClickAwayListener,
MuiGrow as Grow,
} from '../mui';
/* eslint-disable no-restricted-imports */
import PopperUnstyled from '@mui/base/PopperUnstyled';
/* eslint-disable no-restricted-imports */
import ClickAwayListener from '@mui/base/ClickAwayListener';
/* eslint-disable no-restricted-imports */
import Grow from '@mui/material/Grow';
import { styled } from '../styled';
import { PopperProps, PopperHandler, VirtualElement } from './interface';
import { useTheme } from '../theme';
import { PopperProps, VirtualElement } from './interface';
import { PopperArrow } from './PopoverArrow';
export const Popper = forwardRef<PopperHandler, PopperProps>(
(
{
children,
content,
anchor: propsAnchor,
placement = 'top-start',
defaultVisible = false,
container,
keepMounted = false,
visible: propsVisible,
trigger = 'hover',
pointerEnterDelay = 100,
pointerLeaveDelay = 100,
onVisibleChange,
popoverStyle,
popoverClassName,
anchorStyle,
anchorClassName,
zIndex,
offset = [0, 5],
showArrow = false,
},
ref
) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
const [visible, setVisible] = useState(defaultVisible);
const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
export const Popper = ({
children,
content,
anchorEl: propsAnchorEl,
placement = 'top-start',
defaultVisible = false,
visible: propsVisible,
trigger = 'hover',
pointerEnterDelay = 100,
pointerLeaveDelay = 100,
onVisibleChange,
popoverStyle,
popoverClassName,
anchorStyle,
anchorClassName,
zIndex,
offset = [0, 5],
showArrow = false,
popperHandlerRef,
...popperProps
}: PopperProps) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
const [visible, setVisible] = useState(defaultVisible);
const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
const popperRef = useRef();
const pointerLeaveTimer = useRef<number>();
const pointerEnterTimer = useRef<number>();
const pointerLeaveTimer = useRef<number>();
const pointerEnterTimer = useRef<number>();
const visibleControlledByParent = typeof propsVisible !== 'undefined';
const isAnchorCustom = typeof propsAnchor !== 'undefined';
const hasHoverTrigger = useMemo(() => {
return (
trigger === 'hover' ||
(Array.isArray(trigger) && trigger.includes('hover'))
);
}, [trigger]);
const hasClickTrigger = useMemo(() => {
return (
trigger === 'click' ||
(Array.isArray(trigger) && trigger.includes('click'))
);
}, [trigger]);
const theme = useTheme();
const onPointerEnterHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
const onPointerLeaveHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};
useEffect(() => {
onVisibleChange?.(visible);
}, [visible, onVisibleChange]);
useImperativeHandle(ref, () => {
return {
setVisible: (visible: boolean) => {
!visibleControlledByParent && setVisible(visible);
},
};
});
const visibleControlledByParent = typeof propsVisible !== 'undefined';
const isAnchorCustom = typeof propsAnchorEl !== 'undefined';
const hasHoverTrigger = useMemo(() => {
return (
<ClickAwayListener
onClickAway={() => {
setVisible(false);
}}
>
<Container>
{isAnchorCustom ? null : (
<div
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
onClick={() => {
if (
!hasClickTrigger ||
visibleControlledByParent
) {
return;
}
setVisible(!visible);
}}
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={anchorStyle}
className={anchorClassName}
>
{children}
</div>
)}
<MuiPopper
open={
visibleControlledByParent ? propsVisible : visible
}
sx={{ zIndex: zIndex || theme.affine.zIndex.popover }}
anchorEl={isAnchorCustom ? propsAnchor : anchorEl}
placement={placement}
container={container}
keepMounted={keepMounted}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
]}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<div
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
>
{showArrow && (
<PopperArrow
placement={placement}
ref={setArrowRef}
/>
)}
{content}
</div>
</Grow>
)}
</MuiPopper>
</Container>
</ClickAwayListener>
trigger === 'hover' ||
(Array.isArray(trigger) && trigger.includes('hover'))
);
}
);
}, [trigger]);
const hasClickTrigger = useMemo(() => {
return (
trigger === 'click' ||
(Array.isArray(trigger) && trigger.includes('click'))
);
}, [trigger]);
const onPointerEnterHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
const onPointerLeaveHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};
useEffect(() => {
onVisibleChange?.(visible);
}, [visible, onVisibleChange]);
useImperativeHandle(popperHandlerRef, () => {
return {
setVisible: (visible: boolean) => {
!visibleControlledByParent && setVisible(visible);
},
};
});
return (
<ClickAwayListener
onClickAway={() => {
setVisible(false);
}}
>
<Container>
{isAnchorCustom ? null : (
<div
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
onClick={() => {
if (!hasClickTrigger || visibleControlledByParent) {
return;
}
setVisible(!visible);
}}
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={anchorStyle}
className={anchorClassName}
>
{children}
</div>
)}
<BasicStyledPopper
popperRef={popperRef}
open={visibleControlledByParent ? propsVisible : visible}
zIndex={zIndex}
anchorEl={isAnchorCustom ? propsAnchorEl : anchorEl}
placement={placement}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
]}
{...popperProps}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<div
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
>
{showArrow && (
<PopperArrow
placement={placement}
ref={setArrowRef}
/>
)}
{content}
</div>
</Grow>
)}
</BasicStyledPopper>
</Container>
</ClickAwayListener>
);
};
// The children of ClickAwayListener must be a DOM Node to judge whether the click is outside, use node.contains
const Container = styled('div')({
display: 'contents',
});
const BasicStyledPopper = styled(PopperUnstyled)<{
zIndex?: number;
}>(({ zIndex, theme }) => {
return {
zIndex: zIndex || theme.affine.zIndex.popover,
};
});
+9 -18
View File
@@ -1,6 +1,9 @@
import type { CSSProperties, ReactNode } from 'react';
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui';
import type { CSSProperties, ReactNode, Ref } from 'react';
/* eslint-disable no-restricted-imports */
import {
type PopperUnstyledProps,
type PopperPlacementType,
} from '@mui/base/PopperUnstyled';
export type VirtualElement = {
getBoundingClientRect: () => ClientRect | DOMRect;
contextElement?: Element;
@@ -21,26 +24,12 @@ export type PopperProps = {
// Popover trigger
children?: ReactNode;
// Position of Popover
placement?: PopperPlacementType;
// The popover will pop up based on the anchor position
// And if this parameter is passed, children will not be rendered
anchor?: VirtualElement | (() => VirtualElement);
// Whether the default is implicit
defaultVisible?: boolean;
// Used to manually control the visibility of the Popover
visible?: boolean;
// A HTML element or function that returns one. The container will have the portal children appended to it.
// By default, it uses the body of the top-level document object, so it's simply document.body most of the time.
container?: HTMLElement;
// Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Popper
keepMounted?: boolean;
// TODO: support focus
trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[];
@@ -71,4 +60,6 @@ export type PopperProps = {
offset?: [number, number];
showArrow?: boolean;
};
popperHandlerRef?: Ref<PopperHandler>;
} & Omit<PopperUnstyledProps, 'open' | 'ref'>;
+6 -8
View File
@@ -1,5 +1,5 @@
import { forwardRef, type PropsWithChildren, type CSSProperties } from 'react';
import { type PopperHandler, type PopperProps, Popper } from '../popper';
import { type PropsWithChildren, type CSSProperties } from 'react';
import { type PopperProps, Popper } from '../popper';
import { PopoverContainer, placementToContainerDirection } from '../popover';
import type { TooltipProps } from './interface';
import { useTheme } from '../theme';
@@ -14,17 +14,15 @@ const useTooltipStyle = (): CSSProperties => {
};
};
export const Tooltip = forwardRef<
PopperHandler,
PropsWithChildren<PopperProps & TooltipProps>
>((props, ref) => {
export const Tooltip = (
props: PropsWithChildren<PopperProps & TooltipProps>
) => {
const { content, placement = 'top-start' } = props;
const style = useTooltipStyle();
// If there is no content, hide forever
const visibleProp = content ? {} : { visible: false };
return (
<Popper
ref={ref}
{...visibleProp}
placement="top"
{...props}
@@ -39,4 +37,4 @@ export const Tooltip = forwardRef<
}
/>
);
});
};