mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 11:36:25 +08:00
Merge branch 'develop' into feature/remove-react-fc
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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 = {
|
||||
|
||||
+6
@@ -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, ' ');
|
||||
// data = data.replace(/\t/g, ' ');
|
||||
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, ' ');
|
||||
// data = data.replace(/\t/g, ' ');
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
@@ -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<
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user