mirror of https://github.com/fantasticit/think.git
tiptap: improve resizable
This commit is contained in:
parent
bf913734ea
commit
733a4910e9
|
@ -10,6 +10,7 @@ interface IProps {
|
|||
width: number;
|
||||
height: number;
|
||||
maxWidth?: number;
|
||||
isEditable?: boolean;
|
||||
onChange?: (arg: ISize) => void;
|
||||
onChangeEnd?: (arg: ISize) => void;
|
||||
className?: string;
|
||||
|
@ -18,26 +19,30 @@ interface IProps {
|
|||
const MIN_WIDTH = 50;
|
||||
const MIN_HEIGHT = 50;
|
||||
|
||||
function clamp(val: number, min: number, max: number): number {
|
||||
function clamp(val: number, min: number, max: number): string {
|
||||
if (val < min) {
|
||||
return min;
|
||||
return '' + min;
|
||||
}
|
||||
if (val > max) {
|
||||
return max;
|
||||
return '' + max;
|
||||
}
|
||||
return val;
|
||||
return '' + val;
|
||||
}
|
||||
|
||||
export const Resizeable: React.FC<IProps> = ({
|
||||
width,
|
||||
height,
|
||||
maxWidth,
|
||||
isEditable = false,
|
||||
className,
|
||||
onChange,
|
||||
onChangeEnd,
|
||||
children,
|
||||
}) => {
|
||||
const $container = useRef<HTMLDivElement>(null);
|
||||
const $cloneNode = useRef<HTMLDivElement>(null);
|
||||
const $cloneNodeTip = useRef<HTMLDivElement>(null);
|
||||
const $placeholderNode = useRef<HTMLDivElement>(null);
|
||||
const $topLeft = useRef<HTMLDivElement>(null);
|
||||
const $topRight = useRef<HTMLDivElement>(null);
|
||||
const $bottomLeft = useRef<HTMLDivElement>(null);
|
||||
|
@ -49,6 +54,8 @@ export const Resizeable: React.FC<IProps> = ({
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditable) return;
|
||||
|
||||
interact($container.current).resizable({
|
||||
edges: {
|
||||
top: true,
|
||||
|
@ -58,19 +65,24 @@ export const Resizeable: React.FC<IProps> = ({
|
|||
},
|
||||
listeners: {
|
||||
move: function (event) {
|
||||
let { x, y } = event.target.dataset;
|
||||
x = (parseFloat(x) || 0) + event.deltaRect.left;
|
||||
y = (parseFloat(y) || 0) + event.deltaRect.top;
|
||||
const placeholderNode = $placeholderNode.current;
|
||||
Object.assign(placeholderNode.style, {
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
const cloneNode = $cloneNode.current;
|
||||
let { width, height } = event.rect;
|
||||
width = clamp(width, MIN_WIDTH, maxWidth || Infinity);
|
||||
height = clamp(height, MIN_HEIGHT, Infinity);
|
||||
|
||||
Object.assign(event.target.style, {
|
||||
width = parseInt(clamp(width, MIN_WIDTH, maxWidth || Infinity));
|
||||
height = parseInt(clamp(height, MIN_HEIGHT, Infinity));
|
||||
Object.assign(cloneNode.style, {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
zIndex: 1000,
|
||||
});
|
||||
Object.assign(event.target.dataset, { x, y });
|
||||
|
||||
const tipNode = $cloneNodeTip.current;
|
||||
tipNode.innerText = `${width}x${height}`;
|
||||
|
||||
onChange && onChange({ width, height });
|
||||
},
|
||||
end: function (event) {
|
||||
|
@ -78,11 +90,24 @@ export const Resizeable: React.FC<IProps> = ({
|
|||
width = clamp(width, MIN_WIDTH, maxWidth || Infinity);
|
||||
height = clamp(height, MIN_HEIGHT, Infinity);
|
||||
|
||||
const cloneNode = $cloneNode.current;
|
||||
Object.assign(cloneNode.style, {
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const tipNode = $cloneNodeTip.current;
|
||||
tipNode.innerText = ``;
|
||||
|
||||
const placeholderNode = $placeholderNode.current;
|
||||
Object.assign(placeholderNode.style, {
|
||||
opacity: 1,
|
||||
});
|
||||
|
||||
onChangeEnd && onChangeEnd({ width, height });
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [maxWidth]);
|
||||
}, [maxWidth, isEditable]);
|
||||
|
||||
useEffect(() => {
|
||||
Object.assign($container.current.style, {
|
||||
|
@ -98,10 +123,41 @@ export const Resizeable: React.FC<IProps> = ({
|
|||
ref={$container}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<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>
|
||||
{isEditable && (
|
||||
<>
|
||||
<div ref={$placeholderNode} className={styles.placeholderWrap} style={{ opacity: 1 }}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div ref={$cloneNode} className={styles.cloneNodeWrap} style={{ width, height, maxWidth }}>
|
||||
<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>
|
||||
<span ref={$cloneNodeTip}></span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,6 +6,17 @@
|
|||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.cloneNodeWrap {
|
||||
position: absolute;
|
||||
background-color: rgb(179 212 255 / 30%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
|
|
|
@ -72,7 +72,6 @@
|
|||
&::after {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background-color: rgb(179 212 255 / 30%);
|
||||
border: 1px solid var(--node-selected-border-color) !important;
|
||||
border-radius: var(--border-radius);
|
||||
content: '';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import cls from 'classnames';
|
||||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||
import { NodeViewWrapper } from '@tiptap/react';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { Resizeable } from 'components/resizeable';
|
||||
import { getEditorContainerDOMSize } from '../../utils/editor';
|
||||
|
@ -17,36 +17,21 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
|
|||
updateAttributes({ width: size.width, height: size.height });
|
||||
}, []);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<NodeViewContent as="div" className={cls(styles.wrap, 'render-wrapper')}>
|
||||
{url ? (
|
||||
<div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}>
|
||||
<iframe src={url}></iframe>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyWrap}>
|
||||
<Text>请设置外链地址</Text>
|
||||
</div>
|
||||
)}
|
||||
</NodeViewContent>
|
||||
),
|
||||
[url, width, height]
|
||||
);
|
||||
|
||||
if (!isEditable && !url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
{isEditable ? (
|
||||
<Resizeable width={width || maxWidth} maxWidth={maxWidth} height={height} onChangeEnd={onResize}>
|
||||
<div style={{ width, height, maxWidth: '100%' }}>{content}</div>
|
||||
</Resizeable>
|
||||
) : (
|
||||
<div style={{ width, height, maxWidth: '100%' }}>{content}</div>
|
||||
)}
|
||||
<Resizeable width={width} maxWidth={maxWidth} height={height} isEditable={isEditable} onChangeEnd={onResize}>
|
||||
<div className={cls(styles.wrap, 'render-wrapper')}>
|
||||
{url ? (
|
||||
<div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}>
|
||||
<iframe src={url}></iframe>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyWrap}>
|
||||
<Text>请设置外链地址</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Resizeable>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import cls from 'classnames';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Typography, Spin } from '@douyinfe/semi-ui';
|
||||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||
import { NodeViewWrapper } from '@tiptap/react';
|
||||
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
||||
import { Resizeable } from 'components/resizeable';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
@ -58,53 +57,31 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
|||
}
|
||||
}, [src, hasTrigger]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className={cls(styles.wrap, 'render-wrapper')}>
|
||||
<Text>{error}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!src) {
|
||||
return (
|
||||
<div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
|
||||
<Spin spinning={loading}>
|
||||
<Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择图片'}</Text>
|
||||
<input ref={$upload} accept="image/*" type="file" hidden onChange={handleFile} />
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const img = <LazyLoadImage src={src} alt={alt} width={width} height={height} />;
|
||||
|
||||
if (isEditable) {
|
||||
return (
|
||||
<Resizeable
|
||||
className={cls('render-wrapper')}
|
||||
width={width || maxWidth}
|
||||
height={height}
|
||||
maxWidth={maxWidth}
|
||||
onChangeEnd={onResize}
|
||||
>
|
||||
{img}
|
||||
</Resizeable>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls('render-wrapper')} style={{ display: 'inline-block', width, height, maxWidth: '100%' }}>
|
||||
{img}
|
||||
</div>
|
||||
);
|
||||
}, [error, src, isEditable, width, height]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" style={{ textAlign, fontSize: 0, maxWidth: '100%' }}>
|
||||
{content}
|
||||
<NodeViewContent />
|
||||
<Resizeable
|
||||
className={'render-wrapper'}
|
||||
width={width || maxWidth}
|
||||
height={height}
|
||||
maxWidth={maxWidth}
|
||||
isEditable={isEditable}
|
||||
onChangeEnd={onResize}
|
||||
>
|
||||
{error ? (
|
||||
<div className={styles.wrap}>
|
||||
<Text>{error}</Text>
|
||||
</div>
|
||||
) : !src ? (
|
||||
<div className={styles.wrap} onClick={selectFile}>
|
||||
<Spin spinning={loading}>
|
||||
<Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择图片'}</Text>
|
||||
<input ref={$upload} accept="image/*" type="file" hidden onChange={handleFile} />
|
||||
</Spin>
|
||||
</div>
|
||||
) : (
|
||||
<LazyLoadImage src={src} alt={alt} width={width} height={height} />
|
||||
)}
|
||||
</Resizeable>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react';
|
||||
import { NodeViewWrapper } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Spin, Typography } from '@douyinfe/semi-ui';
|
||||
|
@ -15,6 +15,8 @@ import styles from './index.module.scss';
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%' };
|
||||
|
||||
export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const $container = useRef();
|
||||
const $mind = useRef<any>();
|
||||
|
@ -28,14 +30,14 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
const content = useMemo(() => {
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<div style={INHERIT_SIZE_STYLE}>
|
||||
<Text>{error.message || error}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Spin spinning={loading} style={{ width: '100%', height: '100%' }}></Spin>;
|
||||
return <Spin spinning={loading} style={INHERIT_SIZE_STYLE}></Spin>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -43,10 +45,10 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
ref={$container}
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
tabIndex={0}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
style={INHERIT_SIZE_STYLE}
|
||||
></div>
|
||||
);
|
||||
}, [loading, error]);
|
||||
}, [loading, error, width, height]);
|
||||
|
||||
const onResize = useCallback(
|
||||
(size) => {
|
||||
|
@ -203,15 +205,13 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
minder.execCommand('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
console.log(width, height);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
|
||||
{isEditable ? (
|
||||
<Resizeable width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
{content}
|
||||
</Resizeable>
|
||||
) : (
|
||||
<div style={{ display: 'inline-block', width, height, maxWidth: '100%' }}>{content}</div>
|
||||
)}
|
||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
{content}
|
||||
</Resizeable>
|
||||
<div className={styles.toolbarWrap}>
|
||||
<Toolbar
|
||||
isEditable={isEditable}
|
||||
|
@ -226,7 +226,6 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
setTheme={setTheme}
|
||||
/>
|
||||
</div>
|
||||
<NodeViewContent />
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue