mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 19:46:32 +08:00
feat: Double-link: In-line cursor handling, e.g., up, down, left,right, backspace keys
This commit is contained in:
@@ -26,15 +26,20 @@ export const DoubleLinkComponent = (props: RenderElementProps) => {
|
||||
[doubleLinkElement, navigate]
|
||||
);
|
||||
|
||||
const displayValue = doubleLinkElement.children
|
||||
.map((item: any) => item.text)
|
||||
.join('');
|
||||
|
||||
return (
|
||||
<span>
|
||||
<PagesIcon style={{ verticalAlign: 'middle', height: '20px' }} />
|
||||
<a
|
||||
{...attributes}
|
||||
style={{ cursor: 'pointer' }}
|
||||
href={`/${doubleLinkElement.workspaceId}/${doubleLinkElement.blockId}`}
|
||||
>
|
||||
<span onClick={handleClickLinkText}>{children}</span>
|
||||
<span onClick={handleClickLinkText}>
|
||||
<a {...attributes} style={{ cursor: 'pointer' }}>
|
||||
<PagesIcon
|
||||
style={{ verticalAlign: 'middle', height: '20px' }}
|
||||
/>
|
||||
<span>
|
||||
{children}
|
||||
{displayValue}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,44 +1,42 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
memo,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import isUrl from 'is-url';
|
||||
import style9 from 'style9';
|
||||
import {
|
||||
Editor,
|
||||
Transforms,
|
||||
Element as SlateElement,
|
||||
Descendant,
|
||||
Range as SlateRange,
|
||||
Node,
|
||||
} from 'slate';
|
||||
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 OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import { LinkIcon } from '@toeverything/components/icons';
|
||||
import {
|
||||
MuiTooltip as Tooltip,
|
||||
styled,
|
||||
muiTooltipClasses,
|
||||
styled,
|
||||
type MuiTooltipProps,
|
||||
} from '@toeverything/components/ui';
|
||||
import {
|
||||
getRelativeUrlForInternalPageUrl,
|
||||
isInternalPageUrl,
|
||||
} from '@toeverything/utils';
|
||||
|
||||
import isUrl from 'is-url';
|
||||
import React, {
|
||||
KeyboardEvent,
|
||||
memo,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Descendant,
|
||||
Editor,
|
||||
Element as SlateElement,
|
||||
Node,
|
||||
Range as SlateRange,
|
||||
Transforms,
|
||||
} from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import style9 from 'style9';
|
||||
import { getRandomString } from '../utils';
|
||||
import { colors } from '../../colors';
|
||||
import { LinkIcon } from '@toeverything/components/icons';
|
||||
|
||||
export type LinkElement = {
|
||||
type: 'link';
|
||||
url: string;
|
||||
@@ -47,13 +45,20 @@ export type LinkElement = {
|
||||
};
|
||||
|
||||
export const withLinks = (editor: ReactEditor) => {
|
||||
const { isInline } = editor;
|
||||
const { isInline, isVoid } = editor;
|
||||
|
||||
editor.isInline = element => {
|
||||
// @ts-ignore
|
||||
return element.type === 'link' ? true : isInline(element);
|
||||
};
|
||||
|
||||
editor.isVoid = element => {
|
||||
// @ts-ignore
|
||||
return element.type === 'link' && element.linkType === 'doubleLink'
|
||||
? true
|
||||
: isVoid(element);
|
||||
};
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
|
||||
@@ -405,11 +405,11 @@ Editor.before = function (
|
||||
if (element) {
|
||||
if (isInlineAndVoid(editor, element)) {
|
||||
// Inline entities need to be drilled out
|
||||
// target = Editor.before(editor, target);
|
||||
target = {
|
||||
path: [0, path[1] - 1],
|
||||
offset: 0,
|
||||
};
|
||||
target = Editor.before(editor, target);
|
||||
// target = {
|
||||
// path: [0, path[1] - 1],
|
||||
// offset: 0,
|
||||
// };
|
||||
} else if (editor.isInline(element) && !editor.isVoid(element)) {
|
||||
// Inline styles such as hyperlinks need to drill directly into it
|
||||
const inlineTextLength = element?.children?.[0]?.text?.length;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable max-lines */
|
||||
import {
|
||||
CustomText,
|
||||
Text,
|
||||
type SlateUtils,
|
||||
type TextProps,
|
||||
@@ -109,7 +108,7 @@ const findLowestCommonAncestor = async (
|
||||
|
||||
export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
||||
(props, ref) => {
|
||||
const { block, editor, handleBackSpace, ...otherOptions } = props;
|
||||
const { block, editor, handleEnter, ...otherOptions } = props;
|
||||
const defaultRef = useRef<ExtendedTextUtils>(null);
|
||||
// Maybe there is a better way
|
||||
const textRef =
|
||||
@@ -448,58 +447,35 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
||||
}
|
||||
};
|
||||
|
||||
const onBackspace: TextProps['handleBackSpace'] = async props => {
|
||||
const { isCollAndStart, splitContents, selection } = props;
|
||||
if (!isCollAndStart) {
|
||||
const {
|
||||
contentBeforeSelection,
|
||||
contentAfterSelection,
|
||||
contentSelection,
|
||||
} = splitContents;
|
||||
const { selectionStart, selectionEnd } = selection;
|
||||
let hasDeleteDoubleLink = false;
|
||||
|
||||
const beforeContent = [...contentBeforeSelection.content];
|
||||
const lastItem = beforeContent[beforeContent.length - 1];
|
||||
if (
|
||||
'linkType' in lastItem &&
|
||||
lastItem.linkType === 'doubleLink'
|
||||
) {
|
||||
if (
|
||||
selectionStart === selectionEnd ||
|
||||
selectionStart.offset
|
||||
) {
|
||||
beforeContent.splice(beforeContent.length - 1, 1);
|
||||
hasDeleteDoubleLink = true;
|
||||
const onTextEnter: TextProps['handleEnter'] = async props => {
|
||||
const { splitContents } = props;
|
||||
if (splitContents) {
|
||||
const { contentBeforeSelection, contentAfterSelection } =
|
||||
splitContents;
|
||||
// after[after.length - 1];
|
||||
if (!contentBeforeSelection.isEmpty) {
|
||||
const beforeSelection = contentBeforeSelection.content;
|
||||
const lastItem: any =
|
||||
beforeSelection.length > 0
|
||||
? beforeSelection[beforeSelection.length - 1]
|
||||
: null;
|
||||
if (lastItem?.linkType === 'doubleLink') {
|
||||
contentBeforeSelection.content.push({ text: '' });
|
||||
}
|
||||
}
|
||||
|
||||
const afterContent = [...contentAfterSelection.content];
|
||||
const beginItem = afterContent[0];
|
||||
const lastSel = contentSelection.content[0];
|
||||
if (
|
||||
selectionEnd.offset &&
|
||||
'linkType' in beginItem &&
|
||||
beginItem.linkType === 'doubleLink' &&
|
||||
'linkType' in lastSel &&
|
||||
lastSel.linkType === 'doubleLink'
|
||||
) {
|
||||
afterContent.splice(0, 1);
|
||||
hasDeleteDoubleLink = true;
|
||||
}
|
||||
|
||||
if (hasDeleteDoubleLink) {
|
||||
await block.setProperty('text', {
|
||||
value: [
|
||||
...beforeContent,
|
||||
...afterContent,
|
||||
] as CustomText[],
|
||||
});
|
||||
return true;
|
||||
if (!contentAfterSelection.isEmpty) {
|
||||
const afterSelection = contentAfterSelection.content;
|
||||
const firstItem: any =
|
||||
afterSelection.length > 0 ? afterSelection[0] : null;
|
||||
if (firstItem?.linkType === 'doubleLink') {
|
||||
contentAfterSelection.content.splice(0, 0, {
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (handleBackSpace && handleBackSpace(props)) || false;
|
||||
return handleEnter && handleEnter(props);
|
||||
};
|
||||
|
||||
if (!properties || !properties.text) {
|
||||
@@ -523,7 +499,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
||||
handleUndo={onUndo}
|
||||
handleRedo={onRedo}
|
||||
handleEsc={onKeyboardEsc}
|
||||
handleBackSpace={onBackspace}
|
||||
handleEnter={onTextEnter}
|
||||
{...otherOptions}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ComponentType, ReactElement } from 'react';
|
||||
|
||||
import type {
|
||||
Column,
|
||||
DefaultColumnsValue,
|
||||
@@ -10,6 +8,7 @@ import {
|
||||
MapOperation,
|
||||
} from '@toeverything/datasource/jwt';
|
||||
import { cloneDeep } from '@toeverything/utils';
|
||||
import { ComponentType, ReactElement } from 'react';
|
||||
import type { EventData } from '../block';
|
||||
import { AsyncBlock } from '../block';
|
||||
import type { Editor } from '../editor';
|
||||
@@ -113,7 +112,11 @@ export abstract class BaseView {
|
||||
// Whether the component is empty
|
||||
isEmpty(block: AsyncBlock): boolean {
|
||||
const text = block.getProperty('text');
|
||||
const result = !text?.value?.[0]?.text;
|
||||
// const result = !text?.value?.[0]?.text;
|
||||
const result =
|
||||
text?.value?.findIndex(
|
||||
(item: any) => item.text || item.children?.length
|
||||
) === -1;
|
||||
|
||||
// Assert that the text is really empty
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user