Initial TrueGrowth source import
This commit is contained in:
12
packages/react-text/.babelrc
Normal file
12
packages/react-text/.babelrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
18
packages/react-text/.eslintrc.json
Normal file
18
packages/react-text/.eslintrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/react-text/README.md
Normal file
7
packages/react-text/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# react-text
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test react-text` to execute the unit tests via [Vitest](https://vitest.dev/).
|
||||
23
packages/react-text/package.json
Normal file
23
packages/react-text/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@plait-board/react-text",
|
||||
"version": "0.2.1",
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"private": false,
|
||||
"dependencies": {
|
||||
"slate": "^0.116.0",
|
||||
"slate-dom": "^0.116.0",
|
||||
"slate-history": "^0.115.0",
|
||||
"slate-react": "^0.116.0",
|
||||
"@plait/text-plugins": "^0.84.0",
|
||||
"@plait/common": "^0.84.0",
|
||||
"is-hotkey": "^0.2.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.mjs",
|
||||
"require": "./index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/react-text/project.json
Normal file
17
packages/react-text/project.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "react-text",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/react-text/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"// targets": "to see all targets run: nx show project react-text --web",
|
||||
"targets": {
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "tsc -p tsconfig.lib.json --noEmit",
|
||||
"cwd": "packages/react-text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
packages/react-text/src/custom-types.ts
Normal file
35
packages/react-text/src/custom-types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { BaseEditor, BaseRange, Range, Element } from 'slate';
|
||||
import { ReactEditor, RenderElementProps } from 'slate-react';
|
||||
import { HistoryEditor } from 'slate-history';
|
||||
import { CustomElement, CustomText as PlaitCustomText } from '@plait/common';
|
||||
|
||||
// 扩展 CustomText 类型,添加缺失的文本属性
|
||||
export interface ExtendedCustomText extends PlaitCustomText {
|
||||
'font-family'?: string;
|
||||
'font-weight'?: string;
|
||||
'text-shadow'?: string;
|
||||
'text-gradient'?: string;
|
||||
'line-height'?: string;
|
||||
'letter-spacing'?: string;
|
||||
}
|
||||
|
||||
export type CustomEditor = BaseEditor &
|
||||
ReactEditor &
|
||||
HistoryEditor & {
|
||||
nodeToDecorations?: Map<Element, Range[]>;
|
||||
};
|
||||
|
||||
export type RenderElementPropsFor<T> = RenderElementProps & {
|
||||
element: T;
|
||||
};
|
||||
|
||||
declare module 'slate' {
|
||||
interface CustomTypes {
|
||||
Editor: CustomEditor;
|
||||
Element: CustomElement;
|
||||
Text: ExtendedCustomText;
|
||||
Range: BaseRange & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
}
|
||||
3
packages/react-text/src/index.ts
Normal file
3
packages/react-text/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './text';
|
||||
export * from './custom-types';
|
||||
export { setSearchHighlightQuery } from './search-highlight';
|
||||
2
packages/react-text/src/plugins/index.ts
Normal file
2
packages/react-text/src/plugins/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './with-text';
|
||||
export * from './with-link';
|
||||
65
packages/react-text/src/plugins/with-link.tsx
Normal file
65
packages/react-text/src/plugins/with-link.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { CustomElement, LinkElement } from '@plait/common';
|
||||
import { CustomEditor, RenderElementPropsFor } from '../custom-types';
|
||||
import { isUrl, LinkEditor } from '@plait/text-plugins';
|
||||
|
||||
// Put this at the start and end of an inline component to work around this Chromium bug:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
|
||||
export const InlineChromiumBugfix = () => (
|
||||
<span contentEditable={false} style={{ fontSize: 0 }}>
|
||||
{String.fromCodePoint(160) /* Non-breaking space */}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const LinkComponent = ({
|
||||
attributes,
|
||||
children,
|
||||
element,
|
||||
}: RenderElementPropsFor<LinkElement>) => {
|
||||
return (
|
||||
<a
|
||||
{...attributes}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
cursor: 'inherit',
|
||||
}}
|
||||
data-url={element.url}
|
||||
className="plait-board-link"
|
||||
>
|
||||
<InlineChromiumBugfix />
|
||||
{children}
|
||||
<InlineChromiumBugfix />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const withInlineLink = (editor: CustomEditor) => {
|
||||
const { insertData, insertText, isInline } = editor;
|
||||
|
||||
editor.isInline = (element: CustomElement) => {
|
||||
return (
|
||||
((element as LinkElement).type &&
|
||||
['link'].includes((element as LinkElement).type)) ||
|
||||
isInline(element)
|
||||
);
|
||||
};
|
||||
|
||||
editor.insertText = (text) => {
|
||||
if (text && isUrl(text)) {
|
||||
LinkEditor.wrapLink(editor, text, text);
|
||||
} else {
|
||||
insertText(text);
|
||||
}
|
||||
};
|
||||
|
||||
editor.insertData = (data) => {
|
||||
const text = data.getData('text/plain');
|
||||
|
||||
if (text && isUrl(text)) {
|
||||
LinkEditor.wrapLink(editor, text, text);
|
||||
} else {
|
||||
insertData(data);
|
||||
}
|
||||
};
|
||||
|
||||
return editor;
|
||||
};
|
||||
30
packages/react-text/src/plugins/with-text.ts
Normal file
30
packages/react-text/src/plugins/with-text.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
export const withText = <T extends ReactEditor>(editor: T) => {
|
||||
const e = editor as T;
|
||||
const { insertData } = e;
|
||||
|
||||
e.insertBreak = () => {
|
||||
editor.insertText('\n');
|
||||
};
|
||||
|
||||
e.insertSoftBreak = () => {
|
||||
editor.insertText('\n');
|
||||
};
|
||||
|
||||
e.insertData = (data: DataTransfer) => {
|
||||
let text = data.getData('text/plain');
|
||||
const plaitData = data.getData(`application/x-slate-fragment`);
|
||||
if (!plaitData && text) {
|
||||
if (text.endsWith('\n')) {
|
||||
text = text.substring(0, text.length - 1);
|
||||
}
|
||||
text = text.trim().replace(/\t+/g, ' ');
|
||||
e.insertText(text);
|
||||
return;
|
||||
}
|
||||
insertData(data);
|
||||
};
|
||||
|
||||
return e;
|
||||
};
|
||||
27
packages/react-text/src/search-highlight.ts
Normal file
27
packages/react-text/src/search-highlight.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
let searchQuery = '';
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshot() {
|
||||
return searchQuery;
|
||||
}
|
||||
|
||||
/** 设置搜索高亮关键词(供搜索组件调用) */
|
||||
export function setSearchHighlightQuery(query: string) {
|
||||
if (searchQuery === query) return;
|
||||
searchQuery = query;
|
||||
listeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
/** 订阅搜索高亮关键词(供 Text 组件内部使用) */
|
||||
export function useSearchHighlightQuery(): string {
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
}
|
||||
40
packages/react-text/src/styles/index.scss
Normal file
40
packages/react-text/src/styles/index.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
.plait-board-container {
|
||||
.text {
|
||||
foreignObject {
|
||||
overflow: hidden;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plait-text-container {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
display: inline-block;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans', 'Noto Sans CJK SC', 'Microsoft Yahei', 'Hiragino Sans GB', Arial, sans-serif;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.slate-editable-container {
|
||||
outline: none;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
& [data-slate-node='element'] {
|
||||
user-select: none;
|
||||
}
|
||||
&[contenteditable="true"] {
|
||||
cursor: text;
|
||||
& [data-slate-node='element'] {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
packages/react-text/src/text.spec.tsx
Normal file
64
packages/react-text/src/text.spec.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { Text } from './text';
|
||||
import type { ParagraphElement } from '@plait/common';
|
||||
import type { CustomEditor } from './custom-types';
|
||||
|
||||
describe('Text', () => {
|
||||
it('should render successfully', () => {
|
||||
const ele: ParagraphElement = {
|
||||
children: [{ text: '' }],
|
||||
type: 'paragraph',
|
||||
};
|
||||
const { baseElement } = render(<Text text={ele} />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('clears stale selection paths when text changes', async () => {
|
||||
let editor: CustomEditor | undefined;
|
||||
const longText: ParagraphElement = {
|
||||
children: Array.from({ length: 14 }, (_, index) => ({
|
||||
text: `line-${index}`,
|
||||
})),
|
||||
type: 'paragraph',
|
||||
};
|
||||
const shortText: ParagraphElement = {
|
||||
children: [{ text: 'short' }],
|
||||
type: 'paragraph',
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<Text
|
||||
text={longText}
|
||||
afterInit={(initializedEditor) => {
|
||||
editor = initializedEditor as CustomEditor;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editor).toBeDefined();
|
||||
});
|
||||
|
||||
editor!.selection = {
|
||||
anchor: { path: [0, 13], offset: 0 },
|
||||
focus: { path: [0, 13], offset: 0 },
|
||||
};
|
||||
editor!.history.undos.push([
|
||||
{
|
||||
type: 'remove_node',
|
||||
path: [0, 13],
|
||||
node: { text: 'line-13' },
|
||||
},
|
||||
]);
|
||||
|
||||
rerender(<Text text={shortText} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editor!.selection).toBeNull();
|
||||
});
|
||||
expect(editor!.history.undos).toHaveLength(0);
|
||||
expect(() => editor!.undo()).not.toThrow();
|
||||
});
|
||||
});
|
||||
306
packages/react-text/src/text.tsx
Normal file
306
packages/react-text/src/text.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
createEditor,
|
||||
type Descendant,
|
||||
Range,
|
||||
Transforms,
|
||||
Text as SlateText,
|
||||
Node as SlateNode,
|
||||
type NodeEntry,
|
||||
type BaseRange,
|
||||
} from 'slate';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import {
|
||||
Editable,
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
Slate,
|
||||
withReact,
|
||||
} from 'slate-react';
|
||||
import {
|
||||
type CustomElement,
|
||||
type CustomText,
|
||||
type LinkElement,
|
||||
type ParagraphElement,
|
||||
type TextProps,
|
||||
} from '@plait/common';
|
||||
import React, { useMemo, useCallback, useEffect, CSSProperties } from 'react';
|
||||
import { withHistory } from 'slate-history';
|
||||
import { withText } from './plugins/with-text';
|
||||
import { type CustomEditor, RenderElementPropsFor } from './custom-types';
|
||||
import { useSearchHighlightQuery } from './search-highlight';
|
||||
|
||||
import './styles/index.scss';
|
||||
import { LinkComponent, withInlineLink } from './plugins/with-link';
|
||||
|
||||
export type TextComponentProps = TextProps;
|
||||
|
||||
export const Text: React.FC<TextComponentProps> = (
|
||||
props: TextComponentProps
|
||||
) => {
|
||||
const { text, readonly, onChange, onComposition, afterInit } = props;
|
||||
|
||||
const searchQuery = useSearchHighlightQuery();
|
||||
|
||||
const renderLeaf = useCallback(
|
||||
(props: RenderLeafProps) => <Leaf {...props} />,
|
||||
[]
|
||||
);
|
||||
|
||||
const decorate = useCallback(
|
||||
([node, path]: NodeEntry): (BaseRange & Record<string, unknown>)[] => {
|
||||
if (!searchQuery || !SlateText.isText(node)) return [];
|
||||
|
||||
const ranges: (BaseRange & Record<string, unknown>)[] = [];
|
||||
const { text: nodeText } = node;
|
||||
const escaped = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(escaped, 'gi');
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(nodeText)) !== null) {
|
||||
ranges.push({
|
||||
anchor: { path, offset: match.index },
|
||||
focus: { path, offset: match.index + match[0].length },
|
||||
searchHighlight: true,
|
||||
} as BaseRange & Record<string, unknown>);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
},
|
||||
[searchQuery]
|
||||
);
|
||||
|
||||
const initialValue: Descendant[] = [text];
|
||||
|
||||
const editor = useMemo(() => {
|
||||
const editor = withInlineLink(
|
||||
withText(withHistory(withReact(createEditor())))
|
||||
);
|
||||
afterInit && afterInit(editor);
|
||||
return editor;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (text === editor.children[0]) {
|
||||
return;
|
||||
}
|
||||
editor.children = [text];
|
||||
resetEditorHistory(editor);
|
||||
ensureValidSelection(editor);
|
||||
editor.onChange();
|
||||
}, [text, editor]);
|
||||
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
|
||||
const { selection } = editor;
|
||||
|
||||
// Default left/right behavior is unit:'character'.
|
||||
// This fails to distinguish between two cursor positions, such as
|
||||
// <inline>foo<cursor/></inline> vs <inline>foo</inline><cursor/>.
|
||||
// Here we modify the behavior to unit:'offset'.
|
||||
// This lets the user step into and out of the inline without stepping over characters.
|
||||
// You may wish to customize this further to only use unit:'offset' in specific cases.
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const { nativeEvent } = event;
|
||||
if (isKeyHotkey('left', nativeEvent)) {
|
||||
event.preventDefault();
|
||||
Transforms.move(editor, { unit: 'offset', reverse: true });
|
||||
return;
|
||||
}
|
||||
if (isKeyHotkey('right', nativeEvent)) {
|
||||
event.preventDefault();
|
||||
Transforms.move(editor, { unit: 'offset' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
initialValue={initialValue}
|
||||
onChange={(value: Descendant[]) => {
|
||||
onChange &&
|
||||
onChange({
|
||||
newText: editor.children[0] as ParagraphElement,
|
||||
operations: editor.operations,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Editable
|
||||
className="slate-editable-container plait-text-container"
|
||||
decorate={decorate}
|
||||
renderElement={(props) => <Element {...props} />}
|
||||
renderLeaf={renderLeaf}
|
||||
readOnly={readonly === undefined ? true : readonly}
|
||||
onCompositionStart={(event) => {
|
||||
if (onComposition) {
|
||||
onComposition(event as unknown as CompositionEvent);
|
||||
}
|
||||
}}
|
||||
onCompositionUpdate={(event) => {
|
||||
if (onComposition) {
|
||||
onComposition(event as unknown as CompositionEvent);
|
||||
}
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
if (onComposition) {
|
||||
onComposition(event as unknown as CompositionEvent);
|
||||
}
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</Slate>
|
||||
);
|
||||
};
|
||||
|
||||
const resetEditorHistory = (editor: CustomEditor) => {
|
||||
editor.history.undos = [];
|
||||
editor.history.redos = [];
|
||||
};
|
||||
|
||||
const ensureValidSelection = (editor: CustomEditor) => {
|
||||
const { selection } = editor;
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
SlateNode.has(editor, selection.anchor.path) &&
|
||||
SlateNode.has(editor, selection.focus.path)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
Transforms.deselect(editor);
|
||||
};
|
||||
|
||||
const Element = (props: RenderElementProps) => {
|
||||
const { attributes, children, element } = props as RenderElementPropsFor<
|
||||
CustomElement & { type: string }
|
||||
>;
|
||||
switch (element.type) {
|
||||
case 'link':
|
||||
return (
|
||||
<LinkComponent {...(props as RenderElementPropsFor<LinkElement>)} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<ParagraphComponent
|
||||
{...(props as RenderElementPropsFor<ParagraphElement>)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const ParagraphComponent = ({
|
||||
attributes,
|
||||
children,
|
||||
element,
|
||||
}: RenderElementPropsFor<ParagraphElement>) => {
|
||||
const style = { textAlign: element.align } as CSSProperties;
|
||||
return (
|
||||
<div style={style} {...attributes}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Leaf: React.FC<RenderLeafProps> = ({ children, leaf, attributes }) => {
|
||||
if ((leaf as CustomText).bold) {
|
||||
children = <strong>{children}</strong>;
|
||||
}
|
||||
|
||||
if ((leaf as CustomText).code) {
|
||||
children = <code>{children}</code>;
|
||||
}
|
||||
|
||||
if ((leaf as CustomText).italic) {
|
||||
children = <em>{children}</em>;
|
||||
}
|
||||
|
||||
// 下划线和删除线统一通过 CSS 实现,以支持自定义样式和颜色
|
||||
// 不再使用 <u> 和 <s> 标签,避免 DOM 结构变化导致的渲染问题
|
||||
|
||||
// 上标支持
|
||||
if ((leaf as any).superscript) {
|
||||
children = <sup>{children}</sup>;
|
||||
}
|
||||
|
||||
// 下标支持
|
||||
if ((leaf as any).subscript) {
|
||||
children = <sub>{children}</sub>;
|
||||
}
|
||||
|
||||
const customText = leaf as CustomText;
|
||||
const fontSize = customText['font-size'];
|
||||
|
||||
// 扩展的自定义样式
|
||||
const fontFamily = (leaf as any)['font-family'];
|
||||
const fontWeight = (leaf as any)['font-weight'];
|
||||
const textShadow = (leaf as any)['text-shadow'];
|
||||
const textGradient = (leaf as any)['text-gradient'];
|
||||
const lineHeight = (leaf as any)['line-height'];
|
||||
const letterSpacing = (leaf as any)['letter-spacing'];
|
||||
const backgroundColor = (leaf as any)['background-color'];
|
||||
const textTransform = (leaf as any)['text-transform'];
|
||||
const textStroke = (leaf as any)['text-stroke'];
|
||||
const textStrokeColor = (leaf as any)['text-stroke-color'];
|
||||
const textDecorationStyle = (leaf as any)['text-decoration-style'];
|
||||
const textDecorationColor = (leaf as any)['text-decoration-color'];
|
||||
|
||||
const style: CSSProperties = {
|
||||
color: customText.color,
|
||||
fontSize: fontSize ? `${fontSize}px` : undefined,
|
||||
// 自定义文本效果样式
|
||||
fontFamily: fontFamily || undefined,
|
||||
fontWeight: fontWeight || undefined,
|
||||
textShadow: textShadow || undefined,
|
||||
lineHeight: lineHeight || undefined,
|
||||
letterSpacing: letterSpacing || undefined,
|
||||
// 新增样式
|
||||
backgroundColor: backgroundColor || undefined,
|
||||
textTransform: textTransform || undefined,
|
||||
};
|
||||
|
||||
// 文字描边
|
||||
if (textStroke) {
|
||||
(style as any).WebkitTextStroke = `${textStroke}px ${textStrokeColor || '#000000'}`;
|
||||
}
|
||||
|
||||
// 下划线/删除线 - 统一使用 CSS 实现
|
||||
const isUnderlined = (leaf as CustomText).underlined;
|
||||
const isStrikethrough = (leaf as any).strikethrough;
|
||||
|
||||
if (isUnderlined || isStrikethrough) {
|
||||
const decorations: string[] = [];
|
||||
if (isUnderlined) decorations.push('underline');
|
||||
if (isStrikethrough) decorations.push('line-through');
|
||||
|
||||
style.textDecoration = decorations.join(' ');
|
||||
if (textDecorationStyle) {
|
||||
style.textDecorationStyle = textDecorationStyle as any;
|
||||
}
|
||||
if (textDecorationColor) {
|
||||
style.textDecorationColor = textDecorationColor;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有文字渐变,设置渐变相关样式
|
||||
if (textGradient) {
|
||||
style.backgroundImage = textGradient;
|
||||
style.backgroundClip = 'text';
|
||||
style.WebkitBackgroundClip = 'text';
|
||||
style.WebkitTextFillColor = 'transparent';
|
||||
// 清除可能冲突的 color
|
||||
style.color = undefined;
|
||||
}
|
||||
|
||||
// 搜索高亮(覆盖用户背景色,确保可见性)
|
||||
if ((leaf as any).searchHighlight) {
|
||||
style.backgroundColor = 'rgba(255, 235, 59, 0.6)';
|
||||
style.borderRadius = '2px';
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={style} {...attributes}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
24
packages/react-text/tsconfig.json
Normal file
24
packages/react-text/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
23
packages/react-text/tsconfig.lib.json
Normal file
23
packages/react-text/tsconfig.lib.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"node",
|
||||
"@nx/react/typings/cssmodule.d.ts",
|
||||
"@nx/react/typings/image.d.ts",
|
||||
"vite/client"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx"
|
||||
],
|
||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
19
packages/react-text/tsconfig.spec.json
Normal file
19
packages/react-text/tsconfig.spec.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
, "src/text.tsx" ]
|
||||
}
|
||||
55
packages/react-text/vite.config.ts
Normal file
55
packages/react-text/vite.config.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import * as path from 'path';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/packages/react-text',
|
||||
|
||||
plugins: [
|
||||
react(),
|
||||
nxViteTsPaths(),
|
||||
dts({
|
||||
entryRoot: 'src',
|
||||
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
|
||||
}),
|
||||
],
|
||||
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: '../../dist/react-text',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: 'src/index.ts',
|
||||
name: 'react-text',
|
||||
fileName: 'index',
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ['es', 'cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime', 'slate', 'slate-react', 'slate-history', 'is-hotkey', '@plait/text-plugins', '@plait/common'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@plait': path.resolve(__dirname, 'src'), // 根据项目结构调整路径
|
||||
'react-text': path.resolve(__dirname, 'packages/react-text/src'), // 配置 lib 包的别名
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user