mirror of https://github.com/fantasticit/think.git
tiptap: improve
This commit is contained in:
parent
775172a61c
commit
aac6238263
|
@ -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",
|
||||||
|
|
|
@ -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: {
|
||||||
[width, height]
|
top: true,
|
||||||
);
|
right: true,
|
||||||
|
bottom: true,
|
||||||
|
left: true,
|
||||||
|
},
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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' }}>
|
||||||
<IconDocument />
|
<Text style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<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 }}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 />,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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') };
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
.wrap {
|
.wrap {
|
||||||
margin-top: 0.75em;
|
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
|
||||||
.innerWrap {
|
.innerWrap {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.wrap {
|
.wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 0.75em;
|
|
||||||
|
|
||||||
.handleWrap {
|
.handleWrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
.wrap {
|
.wrap {
|
||||||
margin-top: 0.75em;
|
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
.itemWrap {
|
.itemWrap {
|
||||||
|
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.isActive {
|
&:hover {
|
||||||
.toolbarWrap {
|
.toolbarWrap {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue