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]
|
[doubleLinkElement, navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const displayValue = doubleLinkElement.children
|
||||||
|
.map((item: any) => item.text)
|
||||||
|
.join('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span onClick={handleClickLinkText}>
|
||||||
<PagesIcon style={{ verticalAlign: 'middle', height: '20px' }} />
|
<a {...attributes} style={{ cursor: 'pointer' }}>
|
||||||
<a
|
<PagesIcon
|
||||||
{...attributes}
|
style={{ verticalAlign: 'middle', height: '20px' }}
|
||||||
style={{ cursor: 'pointer' }}
|
/>
|
||||||
href={`/${doubleLinkElement.workspaceId}/${doubleLinkElement.blockId}`}
|
<span>
|
||||||
>
|
{children}
|
||||||
<span onClick={handleClickLinkText}>{children}</span>
|
{displayValue}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</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 EditIcon from '@mui/icons-material/Edit';
|
||||||
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
||||||
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||||
|
import { LinkIcon } from '@toeverything/components/icons';
|
||||||
import {
|
import {
|
||||||
MuiTooltip as Tooltip,
|
MuiTooltip as Tooltip,
|
||||||
styled,
|
|
||||||
muiTooltipClasses,
|
muiTooltipClasses,
|
||||||
|
styled,
|
||||||
type MuiTooltipProps,
|
type MuiTooltipProps,
|
||||||
} from '@toeverything/components/ui';
|
} from '@toeverything/components/ui';
|
||||||
import {
|
import {
|
||||||
getRelativeUrlForInternalPageUrl,
|
getRelativeUrlForInternalPageUrl,
|
||||||
isInternalPageUrl,
|
isInternalPageUrl,
|
||||||
} from '@toeverything/utils';
|
} 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 { getRandomString } from '../utils';
|
||||||
import { colors } from '../../colors';
|
|
||||||
import { LinkIcon } from '@toeverything/components/icons';
|
|
||||||
export type LinkElement = {
|
export type LinkElement = {
|
||||||
type: 'link';
|
type: 'link';
|
||||||
url: string;
|
url: string;
|
||||||
@@ -47,13 +45,20 @@ export type LinkElement = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const withLinks = (editor: ReactEditor) => {
|
export const withLinks = (editor: ReactEditor) => {
|
||||||
const { isInline } = editor;
|
const { isInline, isVoid } = editor;
|
||||||
|
|
||||||
editor.isInline = element => {
|
editor.isInline = element => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return element.type === 'link' ? true : isInline(element);
|
return element.type === 'link' ? true : isInline(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
editor.isVoid = element => {
|
||||||
|
// @ts-ignore
|
||||||
|
return element.type === 'link' && element.linkType === 'doubleLink'
|
||||||
|
? true
|
||||||
|
: isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -405,11 +405,11 @@ Editor.before = function (
|
|||||||
if (element) {
|
if (element) {
|
||||||
if (isInlineAndVoid(editor, element)) {
|
if (isInlineAndVoid(editor, element)) {
|
||||||
// Inline entities need to be drilled out
|
// Inline entities need to be drilled out
|
||||||
// target = Editor.before(editor, target);
|
target = Editor.before(editor, target);
|
||||||
target = {
|
// target = {
|
||||||
path: [0, path[1] - 1],
|
// path: [0, path[1] - 1],
|
||||||
offset: 0,
|
// offset: 0,
|
||||||
};
|
// };
|
||||||
} else if (editor.isInline(element) && !editor.isVoid(element)) {
|
} else if (editor.isInline(element) && !editor.isVoid(element)) {
|
||||||
// Inline styles such as hyperlinks need to drill directly into it
|
// Inline styles such as hyperlinks need to drill directly into it
|
||||||
const inlineTextLength = element?.children?.[0]?.text?.length;
|
const inlineTextLength = element?.children?.[0]?.text?.length;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import {
|
import {
|
||||||
CustomText,
|
|
||||||
Text,
|
Text,
|
||||||
type SlateUtils,
|
type SlateUtils,
|
||||||
type TextProps,
|
type TextProps,
|
||||||
@@ -109,7 +108,7 @@ const findLowestCommonAncestor = async (
|
|||||||
|
|
||||||
export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const { block, editor, handleBackSpace, ...otherOptions } = props;
|
const { block, editor, handleEnter, ...otherOptions } = props;
|
||||||
const defaultRef = useRef<ExtendedTextUtils>(null);
|
const defaultRef = useRef<ExtendedTextUtils>(null);
|
||||||
// Maybe there is a better way
|
// Maybe there is a better way
|
||||||
const textRef =
|
const textRef =
|
||||||
@@ -448,58 +447,35 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBackspace: TextProps['handleBackSpace'] = async props => {
|
const onTextEnter: TextProps['handleEnter'] = async props => {
|
||||||
const { isCollAndStart, splitContents, selection } = props;
|
const { splitContents } = props;
|
||||||
if (!isCollAndStart) {
|
if (splitContents) {
|
||||||
const {
|
const { contentBeforeSelection, contentAfterSelection } =
|
||||||
contentBeforeSelection,
|
splitContents;
|
||||||
contentAfterSelection,
|
// after[after.length - 1];
|
||||||
contentSelection,
|
if (!contentBeforeSelection.isEmpty) {
|
||||||
} = splitContents;
|
const beforeSelection = contentBeforeSelection.content;
|
||||||
const { selectionStart, selectionEnd } = selection;
|
const lastItem: any =
|
||||||
let hasDeleteDoubleLink = false;
|
beforeSelection.length > 0
|
||||||
|
? beforeSelection[beforeSelection.length - 1]
|
||||||
const beforeContent = [...contentBeforeSelection.content];
|
: null;
|
||||||
const lastItem = beforeContent[beforeContent.length - 1];
|
if (lastItem?.linkType === 'doubleLink') {
|
||||||
if (
|
contentBeforeSelection.content.push({ text: '' });
|
||||||
'linkType' in lastItem &&
|
|
||||||
lastItem.linkType === 'doubleLink'
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
selectionStart === selectionEnd ||
|
|
||||||
selectionStart.offset
|
|
||||||
) {
|
|
||||||
beforeContent.splice(beforeContent.length - 1, 1);
|
|
||||||
hasDeleteDoubleLink = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!contentAfterSelection.isEmpty) {
|
||||||
const afterContent = [...contentAfterSelection.content];
|
const afterSelection = contentAfterSelection.content;
|
||||||
const beginItem = afterContent[0];
|
const firstItem: any =
|
||||||
const lastSel = contentSelection.content[0];
|
afterSelection.length > 0 ? afterSelection[0] : null;
|
||||||
if (
|
if (firstItem?.linkType === 'doubleLink') {
|
||||||
selectionEnd.offset &&
|
contentAfterSelection.content.splice(0, 0, {
|
||||||
'linkType' in beginItem &&
|
text: '',
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (handleBackSpace && handleBackSpace(props)) || false;
|
return handleEnter && handleEnter(props);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!properties || !properties.text) {
|
if (!properties || !properties.text) {
|
||||||
@@ -523,7 +499,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
handleUndo={onUndo}
|
handleUndo={onUndo}
|
||||||
handleRedo={onRedo}
|
handleRedo={onRedo}
|
||||||
handleEsc={onKeyboardEsc}
|
handleEsc={onKeyboardEsc}
|
||||||
handleBackSpace={onBackspace}
|
handleEnter={onTextEnter}
|
||||||
{...otherOptions}
|
{...otherOptions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ComponentType, ReactElement } from 'react';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Column,
|
Column,
|
||||||
DefaultColumnsValue,
|
DefaultColumnsValue,
|
||||||
@@ -10,6 +8,7 @@ import {
|
|||||||
MapOperation,
|
MapOperation,
|
||||||
} from '@toeverything/datasource/jwt';
|
} from '@toeverything/datasource/jwt';
|
||||||
import { cloneDeep } from '@toeverything/utils';
|
import { cloneDeep } from '@toeverything/utils';
|
||||||
|
import { ComponentType, ReactElement } from 'react';
|
||||||
import type { EventData } from '../block';
|
import type { EventData } from '../block';
|
||||||
import { AsyncBlock } from '../block';
|
import { AsyncBlock } from '../block';
|
||||||
import type { Editor } from '../editor';
|
import type { Editor } from '../editor';
|
||||||
@@ -113,7 +112,11 @@ export abstract class BaseView {
|
|||||||
// Whether the component is empty
|
// Whether the component is empty
|
||||||
isEmpty(block: AsyncBlock): boolean {
|
isEmpty(block: AsyncBlock): boolean {
|
||||||
const text = block.getProperty('text');
|
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
|
// Assert that the text is really empty
|
||||||
if (
|
if (
|
||||||
|
|||||||
Reference in New Issue
Block a user