tiptap: improve

This commit is contained in:
fantasticit 2022-04-27 21:21:24 +08:00
parent 775172a61c
commit aac6238263
27 changed files with 275 additions and 69 deletions

View File

@ -59,6 +59,7 @@
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"deep-equal": "^2.0.5", "deep-equal": "^2.0.5",
"dompurify": "^2.3.5", "dompurify": "^2.3.5",
"interactjs": "^1.10.11",
"katex": "^0.15.2", "katex": "^0.15.2",
"kity": "^2.0.4", "kity": "^2.0.4",
"lib0": "^0.2.47", "lib0": "^0.2.47",
@ -74,7 +75,6 @@
"prosemirror-tables": "^1.1.1", "prosemirror-tables": "^1.1.1",
"prosemirror-utils": "^0.9.6", "prosemirror-utils": "^0.9.6",
"prosemirror-view": "^1.23.6", "prosemirror-view": "^1.23.6",
"re-resizable": "^6.9.9",
"react": "17.0.2", "react": "17.0.2",
"react-countdown": "^2.3.2", "react-countdown": "^2.3.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",

View File

@ -1,51 +1,108 @@
import React, { useCallback, useState } from 'react'; import React, { useRef, useEffect } from 'react';
import cls from 'classnames'; import cls from 'classnames';
import { useClickOutside } from 'hooks/use-click-outside';
import interact from 'interactjs';
import styles from './style.module.scss'; import styles from './style.module.scss';
import { Resizable } from 're-resizable'; type ISize = { width: number; height: number };
interface IProps { interface IProps {
width: number; width: number;
height: number; height: number;
onChange?: (arg: { width: number; height: number }) => void; maxWidth?: number;
onChangeEnd?: (arg: { width: number; height: number }) => void; onChange?: (arg: ISize) => void;
onChangeEnd?: (arg: ISize) => void;
className?: string; className?: string;
} }
const MIN_WIDTH = 50; const MIN_WIDTH = 50;
const MIN_HEIGHT = 50; const MIN_HEIGHT = 50;
function clamp(val: number, min: number, max: number): number {
if (val < min) {
return min;
}
if (val > max) {
return max;
}
return val;
}
export const Resizeable: React.FC<IProps> = ({ export const Resizeable: React.FC<IProps> = ({
width: defaultWidth, width,
height: defaultHeight, height,
maxWidth,
className, className,
onChange, onChange,
onChangeEnd, onChangeEnd,
children, children,
}) => { }) => {
const [width, setWidth] = useState(defaultWidth); const $container = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(defaultHeight); const $topLeft = useRef<HTMLDivElement>(null);
const $topRight = useRef<HTMLDivElement>(null);
const $bottomLeft = useRef<HTMLDivElement>(null);
const $bottomRight = useRef<HTMLDivElement>(null);
const onResizeStop = useCallback( useClickOutside($container, {
(e, direction, ref, d) => { in: () => $container.current.classList.add(styles.isActive),
const nextWidth = width + d.width; out: () => $container.current.classList.remove(styles.isActive),
const nextHeight = height + d.height; });
setWidth(nextWidth);
setHeight(nextHeight); useEffect(() => {
onChangeEnd({ width: nextWidth, height: nextHeight }); interact($container.current).resizable({
edges: {
top: true,
right: true,
bottom: true,
left: true,
}, },
[width, height] listeners: {
); move: function (event) {
let { x, y } = event.target.dataset;
x = (parseFloat(x) || 0) + event.deltaRect.left;
y = (parseFloat(y) || 0) + event.deltaRect.top;
let { width, height } = event.rect;
width = clamp(width, MIN_WIDTH, maxWidth || Infinity);
height = clamp(height, MIN_HEIGHT, Infinity);
Object.assign(event.target.style, {
width: `${width}px`,
height: `${height}px`,
});
Object.assign(event.target.dataset, { x, y });
onChange && onChange({ width, height });
},
end: function (event) {
let { width, height } = event.rect;
width = clamp(width, MIN_WIDTH, maxWidth || Infinity);
height = clamp(height, MIN_HEIGHT, Infinity);
onChangeEnd && onChangeEnd({ width, height });
},
},
});
}, [maxWidth]);
useEffect(() => {
Object.assign($container.current.style, {
width: `${width}px`,
height: `${height}px`,
});
}, [width, height]);
return ( return (
<Resizable <div
size={{ width, height }} id="js-resizeable-container"
className={cls(className, styles.resizable)} className={cls(className, styles.resizable)}
minWidth={MIN_WIDTH} ref={$container}
minHeight={MIN_HEIGHT} style={{ width, height }}
onResizeStop={onResizeStop}
> >
<span className={styles.resizer + ' ' + styles.topLeft} ref={$topLeft} data-type={'topLeft'}></span>
<span className={styles.resizer + ' ' + styles.topRight} ref={$topRight} data-type={'topRight'}></span>
<span className={styles.resizer + ' ' + styles.bottomLeft} ref={$bottomLeft} data-type={'bottomLeft'}></span>
<span className={styles.resizer + ' ' + styles.bottomRight} ref={$bottomRight} data-type={'bottomRight'}></span>
{children} {children}
</Resizable> </div>
); );
}; };

View File

@ -4,6 +4,43 @@
width: 100px; width: 100px;
height: 100px; height: 100px;
max-width: 100%; max-width: 100%;
box-sizing: border-box;
.resizer {
position: absolute;
z-index: 9999;
width: 10px;
height: 10px;
background: white;
border: 3px solid #4286f4;
border-radius: 50%;
opacity: 0;
box-sizing: border-box;
}
.resizer.topLeft {
top: -5px;
left: -5px;
cursor: nwse-resize;
}
.resizer.topRight {
top: -5px;
right: -5px;
cursor: nesw-resize;
}
.resizer.bottomLeft {
bottom: -5px;
left: -5px;
cursor: nesw-resize;
}
.resizer.bottomRight {
right: -5px;
bottom: -5px;
cursor: nwse-resize;
}
&.isActive { &.isActive {
.resizer { .resizer {

View File

@ -10,10 +10,19 @@ const DEFAULT_MIND_DATA = {
version: '1.4.43', version: '1.4.43',
}; };
export interface IMindAttrs {
width?: number;
height?: number;
data?: Record<string, unknown>;
template?: string;
theme?: string;
zoom?: number;
}
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {
mind: { mind: {
setMind: (attrs?: unknown) => ReturnType; setMind: (attrs?: IMindAttrs) => ReturnType;
}; };
} }
} }
@ -28,7 +37,7 @@ export const Mind = Node.create({
addAttributes() { addAttributes() {
return { return {
width: { width: {
default: '100%', default: null,
parseHTML: getDatasetAttribute('width'), parseHTML: getDatasetAttribute('width'),
}, },
height: { height: {
@ -51,10 +60,6 @@ export const Mind = Node.create({
default: 100, default: 100,
parseHTML: getDatasetAttribute('zoom'), parseHTML: getDatasetAttribute('zoom'),
}, },
callCenterCount: {
default: 0,
parseHTML: (element) => Number(getDatasetAttribute('callcentercount')(element)),
},
}; };
}, },
@ -83,6 +88,9 @@ export const Mind = Node.create({
setMind: setMind:
(options) => (options) =>
({ tr, commands, chain, editor }) => { ({ tr, commands, chain, editor }) => {
options = options || {};
options.data = options.data || DEFAULT_MIND_DATA;
// @ts-ignore // @ts-ignore
if (tr.selection?.node?.type?.name == this.name) { if (tr.selection?.node?.type?.name == this.name) {
return commands.updateAttributes(this.name, options); return commands.updateAttributes(this.name, options);
@ -94,7 +102,7 @@ export const Mind = Node.create({
.insertContentAt(pos.before(), [ .insertContentAt(pos.before(), [
{ {
type: this.name, type: this.name,
attrs: { data: DEFAULT_MIND_DATA }, attrs: options,
}, },
]) ])
.run(); .run();

View File

@ -34,6 +34,7 @@ import { Search } from './menus/search';
import { Callout } from './menus/callout'; import { Callout } from './menus/callout';
import { Countdonw } from './menus/countdown'; import { Countdonw } from './menus/countdown';
import { DocumentChildren } from './menus/document-children';
import { DocumentReference } from './menus/document-reference'; import { DocumentReference } from './menus/document-reference';
import { Image } from './menus/image'; import { Image } from './menus/image';
import { Iframe } from './menus/iframe'; import { Iframe } from './menus/iframe';
@ -91,6 +92,7 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
<Callout editor={editor} /> <Callout editor={editor} />
<Countdonw editor={editor} /> <Countdonw editor={editor} />
<DocumentChildren editor={editor} />
<DocumentReference editor={editor} /> <DocumentReference editor={editor} />
<Image editor={editor} /> <Image editor={editor} />
<Iframe editor={editor} /> <Iframe editor={editor} />

View File

@ -1,11 +1,13 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { Button, Form, Dropdown } from '@douyinfe/semi-ui'; import { Button, Form, Dropdown } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { number } from 'lib0';
type ISize = { width: number; height: number }; type ISize = { width: number; height: number };
export const Size: React.FC<{ width: number; height: number; onOk: (arg: ISize) => void }> = ({ export const Size: React.FC<{ width: number; maxWidth?: number; height: number; onOk: (arg: ISize) => void }> = ({
width, width,
maxWidth,
height, height,
onOk, onOk,
children, children,
@ -27,7 +29,7 @@ export const Size: React.FC<{ width: number; height: number; onOk: (arg: ISize)
render={ render={
<div style={{ padding: '0 12px 12px' }}> <div style={{ padding: '0 12px 12px' }}>
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left"> <Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.InputNumber autofocus label="宽" field="width" /> <Form.InputNumber autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} />
<Form.InputNumber label="高" field="height" /> <Form.InputNumber label="高" field="height" />
</Form> </Form>
<Button size="small" type="primary" theme="solid" htmlType="submit" onClick={handleOk}> <Button size="small" type="primary" theme="solid" htmlType="submit" onClick={handleOk}>

View File

@ -0,0 +1,26 @@
import { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui';
import { IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from '../../views/bubble-menu';
import { DocumentChildren } from '../../extensions/document-children';
export const DocumentChildrenBubbleMenu = ({ editor }) => {
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
return (
<BubbleMenu
className={'bubble-menu'}
editor={editor}
pluginKey="document-children-bubble-menu"
shouldShow={() => editor.isActive(DocumentChildren.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
<Space>
<Tooltip content="删除节点" hideOnClick>
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Space>
</BubbleMenu>
);
};

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Editor } from '@tiptap/core';
import { DocumentChildrenBubbleMenu } from './bubble';
export const DocumentChildren: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return (
<>
<DocumentChildrenBubbleMenu editor={editor} />
</>
);
};

View File

@ -60,7 +60,9 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
main={ main={
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<Text style={{ display: 'flex', alignItems: 'center' }}>
<IconDocument /> <IconDocument />
</Text>
<Text <Text
ellipsis={{ showTooltip: { opts: { content: item.title, position: 'right' } } }} ellipsis={{ showTooltip: { opts: { content: item.title, position: 'right' } } }}
style={{ width: 150, paddingLeft: 6 }} style={{ width: 150, paddingLeft: 6 }}

View File

@ -6,10 +6,12 @@ import { BubbleMenu } from '../../views/bubble-menu';
import { Divider } from '../../divider'; import { Divider } from '../../divider';
import { Image } from '../../extensions/image'; import { Image } from '../../extensions/image';
import { Size } from '../_components/size'; import { Size } from '../_components/size';
import { getEditorContainerDOMSize } from '../../utils/editor';
export const ImageBubbleMenu = ({ editor }) => { export const ImageBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Image.name); const attrs = editor.getAttributes(Image.name);
const { width: currentWidth, height: currentHeight } = attrs; const { width: currentWidth, height: currentHeight } = attrs;
const { width: maxWidth } = getEditorContainerDOMSize(editor);
const [width, setWidth] = useState(currentWidth); const [width, setWidth] = useState(currentWidth);
const [height, setHeight] = useState(currentHeight); const [height, setHeight] = useState(currentHeight);
@ -91,6 +93,7 @@ export const ImageBubbleMenu = ({ editor }) => {
<Size <Size
width={width} width={width}
maxWidth={maxWidth}
height={height} height={height}
onOk={(size) => { onOk={(size) => {
editor editor

View File

@ -21,6 +21,7 @@ import { useToggle } from 'hooks/use-toggle';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache'; import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
import { isTitleActive } from '../../utils/is-active'; import { isTitleActive } from '../../utils/is-active';
import { getEditorContainerDOMSize } from '../../utils/editor';
import { createCountdown } from '../countdown/service'; import { createCountdown } from '../countdown/service';
const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3); const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3);
@ -88,7 +89,10 @@ const COMMANDS = [
{ {
icon: <IconMind />, icon: <IconMind />,
label: '思维导图', label: '思维导图',
action: (editor) => editor.chain().focus().setMind().run(), action: (editor) => {
const { width } = getEditorContainerDOMSize(editor);
editor.chain().focus().setMind({ width }).run();
},
}, },
{ {
icon: <IconMath />, icon: <IconMath />,

View File

@ -1,11 +1,25 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui'; import { Space, Button } from '@douyinfe/semi-ui';
import { IconDelete } from '@douyinfe/semi-icons'; import { IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from '../../views/bubble-menu'; import { BubbleMenu } from '../../views/bubble-menu';
import { Mind } from '../../extensions/mind'; import { Mind } from '../../extensions/mind';
import { Size } from '../_components/size';
import { Divider } from '../../divider';
import { getEditorContainerDOMSize } from '../../utils/editor';
export const MindBubbleMenu = ({ editor }) => { export const MindBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Mind.name);
const { width, height } = attrs;
const { width: maxWidth } = getEditorContainerDOMSize(editor);
const setSize = useCallback(
(size) => {
editor.chain().updateAttributes(Mind.name, size).setNodeSelection(editor.state.selection.from).focus().run();
},
[editor]
);
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]); const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
return ( return (
@ -15,8 +29,15 @@ export const MindBubbleMenu = ({ editor }) => {
pluginKey="mind-bubble-menu" pluginKey="mind-bubble-menu"
shouldShow={() => editor.isActive(Mind.name)} shouldShow={() => editor.isActive(Mind.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
> >
<Space> <Space>
<Size width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
<Tooltip content="设置宽高">
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Size>
<Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" /> <Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
</Tooltip> </Tooltip>

View File

@ -146,6 +146,7 @@
z-index: 999; z-index: 999;
pointer-events: all; pointer-events: all;
background: white; background: white;
color: #333;
outline: none; outline: none;
opacity: 1; opacity: 1;
} }

View File

@ -27,12 +27,16 @@
} }
} }
.node-image,
.node-attachment, .node-attachment,
.node-callout,
.node-countdown,
.node-iframe, .node-iframe,
.node-image,
.node-katex,
.node-mind, .node-mind,
.node-banner, .node-codeBlock,
.node-countdown { .node-documentChildren,
.node-documentReference {
margin-top: 0.75em; margin-top: 0.75em;
} }
@ -53,9 +57,6 @@
.node-codeBlock, .node-codeBlock,
.node-documentChildren, .node-documentChildren,
.node-documentReference { .node-documentReference {
display: inline-block;
max-width: 100%;
.render-wrapper { .render-wrapper {
position: relative; position: relative;
user-select: text; user-select: text;

View File

@ -0,0 +1,15 @@
import { Editor } from '@tiptap/core';
const cache = new Map();
export function getEditorContainerDOMSize(editor: Editor): { width: number } {
if (!cache.has('width')) {
cache.set('width', (editor.options.element as HTMLElement).offsetWidth);
}
if (cache.has('width') && cache.get('width') <= 0) {
cache.set('width', (editor.options.element as HTMLElement).offsetWidth);
}
return { width: cache.get('width') };
}

View File

@ -1,5 +1,4 @@
.wrap { .wrap {
margin-top: 0.75em;
line-height: 0; line-height: 0;
.innerWrap { .innerWrap {

View File

@ -1,6 +1,5 @@
.wrap { .wrap {
position: relative; position: relative;
margin-top: 0.75em;
.handleWrap { .handleWrap {
display: flex; display: flex;

View File

@ -1,8 +1,8 @@
.wrap { .wrap {
padding: 12px; padding: 12px;
margin-top: 0.75em;
border: 1px solid var(--node-border-color); border: 1px solid var(--node-border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
user-select: none;
.itemWrap { .itemWrap {
display: flex; display: flex;

View File

@ -1,5 +1,4 @@
.wrap { .wrap {
margin-top: 0.75em;
border-radius: var(--border-radius); border-radius: var(--border-radius);
.itemWrap { .itemWrap {

View File

@ -3,6 +3,7 @@ import cls from 'classnames';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Typography } from '@douyinfe/semi-ui'; import { Typography } from '@douyinfe/semi-ui';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import { getEditorContainerDOMSize } from '../../utils/editor';
import styles from './index.module.scss'; import styles from './index.module.scss';
const { Text } = Typography; const { Text } = Typography;
@ -10,6 +11,7 @@ const { Text } = Typography;
export const IframeWrapper = ({ editor, node, updateAttributes }) => { export const IframeWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { url, width, height } = node.attrs; const { url, width, height } = node.attrs;
const { width: maxWidth } = getEditorContainerDOMSize(editor);
const onResize = useCallback((size) => { const onResize = useCallback((size) => {
updateAttributes({ width: size.width, height: size.height }); updateAttributes({ width: size.width, height: size.height });
@ -39,7 +41,7 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
return ( return (
<NodeViewWrapper> <NodeViewWrapper>
{isEditable ? ( {isEditable ? (
<Resizeable height={height} width={width} onChangeEnd={onResize}> <Resizeable width={width || maxWidth} maxWidth={maxWidth} height={height} onChangeEnd={onResize}>
<div style={{ width, height, maxWidth: '100%' }}>{content}</div> <div style={{ width, height, maxWidth: '100%' }}>{content}</div>
</Resizeable> </Resizeable>
) : ( ) : (

View File

@ -7,6 +7,7 @@ import { Resizeable } from 'components/resizeable';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { uploadFile } from 'services/file'; import { uploadFile } from 'services/file';
import { extractFileExtension, extractFilename, getImageWidthHeight } from '../../utils/file'; import { extractFileExtension, extractFilename, getImageWidthHeight } from '../../utils/file';
import { getEditorContainerDOMSize } from '../../utils/editor';
import styles from './index.module.scss'; import styles from './index.module.scss';
const { Text } = Typography; const { Text } = Typography;
@ -14,6 +15,7 @@ const { Text } = Typography;
export const ImageWrapper = ({ editor, node, updateAttributes }) => { export const ImageWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { hasTrigger, error, src, alt, title, width, height, textAlign } = node.attrs; const { hasTrigger, error, src, alt, title, width, height, textAlign } = node.attrs;
const { width: maxWidth } = getEditorContainerDOMSize(editor);
const $upload = useRef<HTMLInputElement>(); const $upload = useRef<HTMLInputElement>();
const [loading, toggleLoading] = useToggle(false); const [loading, toggleLoading] = useToggle(false);
@ -80,7 +82,13 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
if (isEditable) { if (isEditable) {
return ( return (
<Resizeable className={cls('render-wrapper')} width={width} height={height} onChangeEnd={onResize}> <Resizeable
className={cls('render-wrapper')}
width={width || maxWidth}
height={height}
maxWidth={maxWidth}
onChangeEnd={onResize}
>
{img} {img}
</Resizeable> </Resizeable>
); );

View File

@ -20,7 +20,7 @@
opacity: 0; opacity: 0;
} }
&.isActive { &:hover {
.toolbarWrap { .toolbarWrap {
opacity: 1; opacity: 1;
} }

View File

@ -1,4 +1,4 @@
import { NodeViewWrapper } from '@tiptap/react'; import { NodeViewContent, NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Spin, Typography } from '@douyinfe/semi-ui'; import { Spin, Typography } from '@douyinfe/semi-ui';
@ -6,6 +6,7 @@ import deepEqual from 'deep-equal';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { clamp } from '../../utils/clamp'; import { clamp } from '../../utils/clamp';
import { getEditorContainerDOMSize } from '../../utils/editor';
import { Mind } from '../../extensions/mind'; import { Mind } from '../../extensions/mind';
import { loadKityMinder } from './kityminder'; import { loadKityMinder } from './kityminder';
import { Toolbar } from './toolbar'; import { Toolbar } from './toolbar';
@ -17,8 +18,9 @@ const { Text } = Typography;
export const MindWrapper = ({ editor, node, updateAttributes }) => { export const MindWrapper = ({ editor, node, updateAttributes }) => {
const $container = useRef(); const $container = useRef();
const $mind = useRef<any>(); const $mind = useRef<any>();
const isMindActive = editor.isActive(Mind.name);
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const isActive = editor.isActive(Mind.name);
const { width: maxWidth } = getEditorContainerDOMSize(editor);
const { data, template, theme, zoom, width, height } = node.attrs; const { data, template, theme, zoom, width, height } = node.attrs;
const [loading, toggleLoading] = useToggle(true); const [loading, toggleLoading] = useToggle(true);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
@ -202,9 +204,9 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
}, [theme]); }, [theme]);
return ( return (
<NodeViewWrapper className={cls(styles.wrap, isMindActive && styles.isActive)}> <NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
{isEditable ? ( {isEditable ? (
<Resizeable width={width} height={height} onChangeEnd={onResize}> <Resizeable width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
{content} {content}
</Resizeable> </Resizeable>
) : ( ) : (
@ -213,6 +215,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
<div className={styles.toolbarWrap}> <div className={styles.toolbarWrap}>
<Toolbar <Toolbar
isEditable={isEditable} isEditable={isEditable}
maxHeight={height * 0.8}
template={template} template={template}
theme={theme} theme={theme}
zoom={zoom} zoom={zoom}
@ -223,6 +226,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
setTheme={setTheme} setTheme={setTheme}
/> />
</div> </div>
<NodeViewContent />
</NodeViewWrapper> </NodeViewWrapper>
); );
}; };

View File

@ -2,7 +2,6 @@ export const loadKityMinder = async (): Promise<any> => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
if (window.kityminder) { if (window.kityminder) {
if (window.kityminder.Editor) { if (window.kityminder.Editor) {
console.log('无需重复');
return; return;
} }
} }

View File

@ -5,7 +5,7 @@
z-index: 1000; z-index: 1000;
display: flex; display: flex;
padding: 4px; padding: 4px;
overflow-x: auto; overflow: auto;
background-color: var(--semi-color-nav-bg); background-color: var(--semi-color-nav-bg);
border: 1px solid var(--semi-color-border); border: 1px solid var(--semi-color-border);
border-radius: 3px; border-radius: 3px;

View File

@ -11,6 +11,7 @@ const { Text } = Typography;
interface IProps { interface IProps {
isEditable: boolean; isEditable: boolean;
maxHeight: number;
zoom: number | string; zoom: number | string;
template: string; template: string;
theme: string; theme: string;
@ -23,6 +24,7 @@ interface IProps {
export const Toolbar: React.FC<IProps> = ({ export const Toolbar: React.FC<IProps> = ({
isEditable, isEditable,
maxHeight,
template, template,
theme, theme,
zoom, zoom,
@ -33,7 +35,7 @@ export const Toolbar: React.FC<IProps> = ({
setTheme, setTheme,
}) => { }) => {
return ( return (
<div className={styles.wrap}> <div className={styles.wrap} style={{ maxHeight }}>
{isEditable ? ( {isEditable ? (
<> <>
<Tooltip content="缩小" position="right"> <Tooltip content="缩小" position="right">

View File

@ -89,6 +89,7 @@ importers:
copy-to-clipboard: ^3.3.1 copy-to-clipboard: ^3.3.1
deep-equal: ^2.0.5 deep-equal: ^2.0.5
dompurify: ^2.3.5 dompurify: ^2.3.5
interactjs: ^1.10.11
katex: ^0.15.2 katex: ^0.15.2
kity: ^2.0.4 kity: ^2.0.4
lib0: ^0.2.47 lib0: ^0.2.47
@ -104,7 +105,6 @@ importers:
prosemirror-tables: ^1.1.1 prosemirror-tables: ^1.1.1
prosemirror-utils: ^0.9.6 prosemirror-utils: ^0.9.6
prosemirror-view: ^1.23.6 prosemirror-view: ^1.23.6
re-resizable: ^6.9.9
react: 17.0.2 react: 17.0.2
react-countdown: ^2.3.2 react-countdown: ^2.3.2
react-dom: 17.0.2 react-dom: 17.0.2
@ -172,6 +172,7 @@ importers:
copy-to-clipboard: 3.3.1 copy-to-clipboard: 3.3.1
deep-equal: 2.0.5 deep-equal: 2.0.5
dompurify: 2.3.5 dompurify: 2.3.5
interactjs: 1.10.11
katex: 0.15.2 katex: 0.15.2
kity: 2.0.4 kity: 2.0.4
lib0: 0.2.47 lib0: 0.2.47
@ -187,7 +188,6 @@ importers:
prosemirror-tables: 1.1.1 prosemirror-tables: 1.1.1
prosemirror-utils: 0.9.6_prosemirror-tables@1.1.1 prosemirror-utils: 0.9.6_prosemirror-tables@1.1.1
prosemirror-view: 1.23.6 prosemirror-view: 1.23.6
re-resizable: 6.9.9_react-dom@17.0.2+react@17.0.2
react: 17.0.2 react: 17.0.2
react-countdown: 2.3.2_react-dom@17.0.2+react@17.0.2 react-countdown: 2.3.2_react-dom@17.0.2+react@17.0.2
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
@ -955,6 +955,10 @@ packages:
- y-protocols - y-protocols
dev: false dev: false
/@interactjs/types/1.10.11:
resolution: {integrity: sha512-YRsVFWjL8Gkkvlx3qnjeaxW4fnibSJ9791g8BA7Pv5ANByI64WmtR1vU7A2rXcrOn8XvyCEfY0ss1s8NhZP+MA==}
dev: false
/@ioredis/commands/1.1.1: /@ioredis/commands/1.1.1:
resolution: {integrity: sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg==} resolution: {integrity: sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg==}
dev: false dev: false
@ -4464,6 +4468,12 @@ packages:
through: 2.3.8 through: 2.3.8
dev: true dev: true
/interactjs/1.10.11:
resolution: {integrity: sha512-VPUWsGAOPmrZe1YF7Fq/4AIBBZ+3FikZRS8bpzT6VsAfUuhxl/CKJY73IAiZHd3fz9p174CXErn0Qs81XEFICA==}
dependencies:
'@interactjs/types': 1.10.11
dev: false
/internal-slot/1.0.3: /internal-slot/1.0.3:
resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -6697,16 +6707,6 @@ packages:
unpipe: 1.0.0 unpipe: 1.0.0
dev: false dev: false
/re-resizable/6.9.9_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==}
peerDependencies:
react: ^16.13.1 || ^17.0.0 || ^18.0.0
react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0
dependencies:
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
dev: false
/react-countdown/2.3.2_react-dom@17.0.2+react@17.0.2: /react-countdown/2.3.2_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==} resolution: {integrity: sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==}
peerDependencies: peerDependencies: