Merge pull request #144 from fantasticit/fix/drag

This commit is contained in:
fantasticit 2022-08-03 18:55:10 +08:00 committed by GitHub
commit a4ea4bf965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 384 additions and 505 deletions

View File

@ -158,6 +158,8 @@ export interface CodeBlockLowlightOptions extends CodeBlockOptions {
} }
export const CodeBlock = BuiltInCodeBlock.extend<CodeBlockLowlightOptions>({ export const CodeBlock = BuiltInCodeBlock.extend<CodeBlockLowlightOptions>({
draggable: true,
addOptions() { addOptions() {
return { return {
...this.parent?.(), ...this.parent?.(),

View File

@ -64,15 +64,12 @@ export const Countdown = Node.create({
} }
const { selection } = editor.state; const { selection } = editor.state;
const pos = selection.$head;
return chain() return chain()
.insertContentAt(pos.after(), [ .insertContent({
{ type: this.name,
type: this.name, attrs: options,
attrs: options, })
},
])
.run(); .run();
}, },
}; };

View File

@ -1,158 +0,0 @@
import { Extension } from '@tiptap/core';
import { Plugin } from 'prosemirror-state';
import { NodeSelection } from 'prosemirror-state';
import { __serializeForClipboard } from 'prosemirror-view';
function createRect(rect) {
if (rect == null) {
return null;
}
const newRect = {
left: rect.left + document.body.scrollLeft,
top: rect.top + document.body.scrollTop,
width: rect.width,
height: rect.height,
bottom: 0,
right: 0,
};
newRect.bottom = newRect.top + newRect.height;
newRect.right = newRect.left + newRect.width;
return newRect;
}
function absoluteRect(element) {
return createRect(element.getBoundingClientRect());
}
export const Dragable = Extension.create({
name: 'dragable',
addProseMirrorPlugins() {
let scrollContainer;
let dropElement;
let currentNode;
let editorView;
const WIDTH = 24;
function drag(e) {
if (!currentNode || currentNode.nodeType !== 1) return;
let pos = null;
const desc = editorView.docView.nearestDesc(currentNode, true);
if (!(!desc || desc === editorView.docView)) {
pos = desc.posBefore;
}
if (!pos) return;
editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pos)));
const slice = editorView.state.selection.content();
const { dom, text } = __serializeForClipboard(editorView, slice);
e.dataTransfer.clearData();
e.dataTransfer.setData('text/html', dom.innerHTML);
e.dataTransfer.setData('text/plain', text);
editorView.dragging = { slice, move: true };
}
function onScroll() {
if (!dropElement) return;
dropElement.style.opacity = 0;
}
return [
new Plugin({
view(view) {
if (view.editable) {
editorView = view;
dropElement = document.createElement('div');
dropElement.setAttribute('draggable', 'true');
dropElement.className = 'drag-handler';
dropElement.addEventListener('dragstart', drag);
view.dom.parentElement.appendChild(dropElement);
scrollContainer = view.dom.parentElement.parentElement?.parentElement?.parentElement;
if (scrollContainer) {
scrollContainer.addEventListener('scroll', onScroll);
}
}
return {
update(view) {
editorView = view;
},
destroy() {
if (dropElement && dropElement.parentNode) {
dropElement.removeEventListener('dragstart', drag);
dropElement.parentNode.removeChild(dropElement);
}
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', onScroll);
}
},
};
},
props: {
handleDOMEvents: {
drop() {
if (!dropElement) return;
dropElement.style.opacity = 0;
setTimeout(() => {
const node = document.querySelector('.ProseMirror-hideselection');
if (node) {
node.classList.remove('ProseMirror-hideselection');
}
}, 50);
},
mousedown(view, event) {
if (!dropElement) return;
const coords = { left: event.clientX, top: event.clientY };
const pos = view.posAtCoords(coords);
if (!pos) {
dropElement.style.opacity = 0;
return;
}
let node = view.domAtPos(pos.pos);
node = node.node;
while (node && node.parentNode) {
if (node.parentNode?.classList?.contains?.('ProseMirror')) {
break;
}
node = node.parentNode;
}
if (!node || !node.getBoundingClientRect) {
dropElement.style.opacity = 0;
return;
}
if (node?.classList?.contains('node-title') || node?.classList?.contains('node-table')) {
dropElement.style.opacity = 0;
return;
}
currentNode = node;
const rect = absoluteRect(node);
const win = node.ownerDocument.defaultView;
rect.top += win.pageYOffset;
rect.left += win.pageXOffset;
rect.width = WIDTH + 'px';
dropElement.style.left = rect.left - WIDTH + 'px';
dropElement.style.top = rect.top + 6 + 'px';
dropElement.style.opacity = 1;
},
},
},
}),
];
},
});

View File

@ -84,15 +84,12 @@ export const Flow = Node.create({
} }
const { selection } = editor.state; const { selection } = editor.state;
const pos = selection.$head;
return chain() return chain()
.insertContentAt(pos.after(), [ .insertContent({
{ type: this.name,
type: this.name, attrs: options,
attrs: options, })
},
])
.run(); .run();
}, },
}; };

View File

@ -84,15 +84,12 @@ export const Iframe = Node.create({
const attrs = options || { url: '' }; const attrs = options || { url: '' };
const { selection } = editor.state; const { selection } = editor.state;
const pos = selection.$head;
return chain() return chain()
.insertContentAt(pos.after(), [ .insertContent({
{ type: this.name,
type: this.name, attrs,
attrs, })
},
])
.run(); .run();
}, },
}; };

View File

@ -95,15 +95,12 @@ export const Mind = Node.create({
} }
const { selection } = editor.state; const { selection } = editor.state;
const pos = selection.$head;
return chain() return chain()
.insertContentAt(pos.after(), [ .insertContent({
{ type: this.name,
type: this.name, attrs: options,
attrs: options, })
},
])
.run(); .run();
}, },
}; };

View File

@ -1,3 +1,22 @@
import { mergeAttributes } from '@tiptap/core';
import TitapParagraph from '@tiptap/extension-paragraph'; import TitapParagraph from '@tiptap/extension-paragraph';
export const Paragraph = TitapParagraph.extend({}); export const Paragraph = TitapParagraph.extend({
draggable: true,
renderHTML({ HTMLAttributes }) {
return [
'p',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
[
'div',
{
'contentEditable': 'false',
'draggable': 'true',
'data-drag-handle': 'true',
},
],
['div', 0],
];
},
});

View File

@ -22,6 +22,16 @@ export const useEditor = (options: Partial<EditorOptions> = {}, deps: Dependency
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
useEffect(() => { useEffect(() => {
options.editorProps = options.editorProps || {};
if (options.editable) {
options.editorProps.attributes = options.editorProps.attributes || {};
// @ts-ignore
options.editorProps.attributes.class = options.editorProps.attributes.class || '';
// @ts-ignore
options.editorProps.attributes.class += ' is-editable';
}
const instance = new Editor(options); const instance = new Editor(options);
setEditor(instance); setEditor(instance);

View File

@ -1,11 +1,91 @@
.drag-handler { /* stylelint-disable */
position: fixed; .ProseMirror {
width: 16px; &.is-editable {
height: 16px; [data-drag-handle] {
cursor: grab; position: relative;
opacity: 1; display: inline;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 16"><path fill-opacity="0.2" d="M4 14c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM2 6C.9 6 0 6.9 0 8s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6C.9 0 0 .9 0 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" /></svg>'); opacity: 0;
background-repeat: no-repeat; transition: opacity 0.3s ease-out;
background-size: contain; z-index: 100;
background-position: center;
&:hover {
opacity: 1 !important;
}
&::before {
content: '';
position: absolute;
left: -24px;
top: 2px;
width: 16px;
height: 16px;
text-align: center;
margin-left: auto;
cursor: move;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='3' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3C/svg%3E");
background-size: contain;
background-position: center 0;
background-repeat: no-repeat;
filter: var(--invert-filter);
}
}
p {
[data-drag-handle] {
&::before {
top: 6px;
}
}
&:hover {
[data-drag-handle] {
opacity: 0.3;
}
}
}
ul {
li {
[data-drag-handle] {
&::before {
left: -36px;
}
}
}
&[data-type='taskList'] {
li {
[data-drag-handle] {
&::before {
left: -46px;
}
}
}
}
}
ol {
li {
[data-drag-handle] {
&::before {
left: -36px;
}
}
}
}
.drag-container {
position: relative;
&:hover {
[data-drag-handle] {
opacity: 0.3;
}
}
.drag-content {
width: 100%;
}
}
}
} }

View File

@ -21,7 +21,8 @@
} }
.node-codeBlock, .node-codeBlock,
.node-katex { .node-katex,
.node-documentChildren {
.render-wrapper { .render-wrapper {
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }

View File

@ -1,13 +1,12 @@
import { IconClose, IconDownload, IconPlayCircle } from '@douyinfe/semi-icons'; import { IconClose, IconDownload, IconPlayCircle } from '@douyinfe/semi-icons';
import { Button, Collapsible, Progress, Space, Spin, Toast, Typography } from '@douyinfe/semi-ui'; import { Button, Collapsible, Progress, Space, Spin, Toast, Typography } from '@douyinfe/semi-ui';
import { FILE_CHUNK_SIZE } from '@think/domains'; import { FILE_CHUNK_SIZE } from '@think/domains';
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { uploadFile } from 'services/file'; import { uploadFile } from 'services/file';
import { Attachment } from 'tiptap/core/extensions/attachment';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import { download, extractFileExtension, extractFilename, normalizeFileSize } from 'tiptap/prose-utils'; import { download, extractFileExtension, extractFilename, normalizeFileSize } from 'tiptap/prose-utils';
import { getFileTypeIcon } from './file-icon'; import { getFileTypeIcon } from './file-icon';
@ -156,8 +155,9 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
})(); })();
return ( return (
<DragableWrapper editor={editor} extensionName={Attachment.name}> <NodeViewWrapper className={'drag-container'}>
{content} <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
</DragableWrapper> <div className={'drag-content'}>{content}</div>
</NodeViewWrapper>
); );
}; };

View File

@ -44,4 +44,10 @@
display: flex; display: flex;
padding: 10px; padding: 10px;
} }
[data-node-view-content] {
[data-drag-handle] {
opacity: 0 !important;
}
}
} }

View File

@ -1,11 +1,9 @@
import { NodeViewContent } from '@tiptap/react'; import { NodeViewContent, NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { EmojiPicker } from 'components/emoji-picker'; import { EmojiPicker } from 'components/emoji-picker';
import { convertColorToRGBA } from 'helpers/color'; import { convertColorToRGBA } from 'helpers/color';
import { Theme, ThemeEnum } from 'hooks/use-theme'; import { Theme, ThemeEnum } from 'hooks/use-theme';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { Callout } from 'tiptap/core/extensions/callout';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -27,32 +25,30 @@ export const CalloutWrapper = ({ editor, node, updateAttributes }) => {
); );
return ( return (
<DragableWrapper <NodeViewWrapper id="js-callout-container" className={cls('drag-container', styles.wrap)}>
editor={editor} <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
extensionName={Callout.name} <div className={'drag-content'}>
id="js-callout-container" <div
className={cls(styles.wrap)} className={cls(styles.innerWrap, 'render-wrapper')}
>
<div
className={cls(styles.innerWrap, 'render-wrapper')}
style={{
borderColor,
backgroundColor: backgroundColorOpacity,
}}
>
{isEditable ? (
<EmojiPicker onSelectEmoji={onSelectEmoji}>
<span className={styles.icon}>{emoji || 'Icon'}</span>
</EmojiPicker>
) : (
emoji && <span className={styles.icon}>{emoji}</span>
)}
<NodeViewContent
style={{ style={{
color: textColor, borderColor,
backgroundColor: backgroundColorOpacity,
}} }}
/> >
{isEditable ? (
<EmojiPicker onSelectEmoji={onSelectEmoji}>
<span className={styles.icon}>{emoji || 'Icon'}</span>
</EmojiPicker>
) : (
emoji && <span className={styles.icon}>{emoji}</span>
)}
<NodeViewContent
style={{
color: textColor,
}}
/>
</div>
</div> </div>
</DragableWrapper> </NodeViewWrapper>
); );
}; };

View File

@ -1,6 +1,6 @@
import { IconCopy } from '@douyinfe/semi-icons'; import { IconCopy } from '@douyinfe/semi-icons';
import { Button, Select, Tooltip } from '@douyinfe/semi-ui'; import { Button, Select, Tooltip } from '@douyinfe/semi-ui';
import { NodeViewContent } from '@tiptap/react'; import { NodeViewContent, NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { copy } from 'helpers/copy'; import { copy } from 'helpers/copy';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
@ -16,40 +16,39 @@ export const CodeBlockWrapper = ({ editor, node: { attrs }, updateAttributes, ex
const $container = useRef<HTMLPreElement>(); const $container = useRef<HTMLPreElement>();
return ( return (
<DragableWrapper <NodeViewWrapper className={cls('drag-container', styles.wrap, !isPrint && styles.maxHeight, 'render-wrapper')}>
editor={editor} <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
extensionName={CodeBlock.name} <div className={'drag-content'}>
className={cls(styles.wrap, !isPrint && styles.maxHeight, 'render-wrapper')} <div className={styles.handleWrap}>
> <Select
<div className={styles.handleWrap}>
<Select
size="small"
defaultValue={defaultLanguage || 'null'}
onChange={(value) => updateAttributes({ language: value })}
className={styles.selectorWrap}
disabled={!isEditable}
filter
>
<Select.Option value="null">auto</Select.Option>
{extension.options.lowlight.listLanguages().map((lang, index) => (
<Select.Option key={index} value={lang}>
{lang}
</Select.Option>
))}
</Select>
<Tooltip content="复制" spacing={6}>
<Button
size="small" size="small"
type="tertiary" defaultValue={defaultLanguage || 'null'}
theme="borderless" onChange={(value) => updateAttributes({ language: value })}
icon={<IconCopy />} className={styles.selectorWrap}
onClick={() => copy($container.current.innerText)} disabled={!isEditable}
/> filter
</Tooltip> >
<Select.Option value="null">auto</Select.Option>
{extension.options.lowlight.listLanguages().map((lang, index) => (
<Select.Option key={index} value={lang}>
{lang}
</Select.Option>
))}
</Select>
<Tooltip content="复制" spacing={6}>
<Button
size="small"
type="tertiary"
theme="borderless"
icon={<IconCopy />}
onClick={() => copy($container.current.innerText)}
/>
</Tooltip>
</div>
<pre ref={$container}>
<NodeViewContent as="code" />
</pre>
</div> </div>
<pre ref={$container}> </NodeViewWrapper>
<NodeViewContent as="code" />
</pre>
</DragableWrapper>
); );
}; };

View File

@ -1,8 +1,7 @@
import { Space, Typography } from '@douyinfe/semi-ui'; import { Space, Typography } from '@douyinfe/semi-ui';
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import ReactCountdown from 'react-countdown'; import ReactCountdown from 'react-countdown';
import { Countdown } from 'tiptap/core/extensions/countdown';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -33,11 +32,14 @@ export const CountdownWrapper = ({ editor, node }) => {
const { title, date } = node.attrs; const { title, date } = node.attrs;
return ( return (
<DragableWrapper editor={editor} extensionName={Countdown.name}> <NodeViewWrapper className={'drag-container'}>
<div className={cls(styles.wrap, 'render-wrapper')}> <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
<Text>{title}</Text> <div className={'drag-content'}>
<ReactCountdown date={date} renderer={renderer}></ReactCountdown> <div className={cls(styles.wrap, 'render-wrapper')}>
<Text>{title}</Text>
<ReactCountdown date={date} renderer={renderer}></ReactCountdown>
</div>
</div> </div>
</DragableWrapper> </NodeViewWrapper>
); );
}; };

View File

@ -1,6 +1,5 @@
.wrap { .wrap {
padding: 12px; padding: 12px;
border: 1px solid var(--node-border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
user-select: none; user-select: none;

View File

@ -1,4 +1,5 @@
import { Typography } from '@douyinfe/semi-ui'; import { Typography } from '@douyinfe/semi-ui';
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { Empty } from 'components/empty'; import { Empty } from 'components/empty';
@ -7,8 +8,6 @@ import { useChildrenDocument } from 'data/document';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { DocumentChildren } from 'tiptap/core/extensions/document-children';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -36,13 +35,9 @@ export const DocumentChildrenWrapper = ({ editor, node, updateAttributes }) => {
}, [node.attrs, wikiId, documentId, updateAttributes]); }, [node.attrs, wikiId, documentId, updateAttributes]);
return ( return (
<DragableWrapper <NodeViewWrapper as="div" className={cls('drag-container', 'render-wrapper')}>
editor={editor} <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
extensionName={DocumentChildren.name} <div className={cls('drag-content', styles.wrap, isEditable && styles.isEditable, 'documentChildren')}>
as="div"
className={cls('render-wrapper', styles.wrap, isEditable && styles.isEditable, 'documentChildren')}
>
<div>
<div> <div>
<Text type="tertiary"></Text> <Text type="tertiary"></Text>
</div> </div>
@ -82,6 +77,6 @@ export const DocumentChildrenWrapper = ({ editor, node, updateAttributes }) => {
<Text type="tertiary">使</Text> <Text type="tertiary">使</Text>
)} )}
</div> </div>
</DragableWrapper> </NodeViewWrapper>
); );
}; };

View File

@ -1,10 +1,9 @@
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { IconDocument } from 'components/icons'; import { IconDocument } from 'components/icons';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { DocumentReference } from 'tiptap/core/extensions/document-reference';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -51,13 +50,9 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
}, [organizationId, wikiId, documentId, isEditable, isShare, title]); }, [organizationId, wikiId, documentId, isEditable, isShare, title]);
return ( return (
<DragableWrapper <NodeViewWrapper as="div" className={cls('drag-container', styles.wrap, isEditable && 'render-wrapper')}>
editor={editor} <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
extensionName={DocumentReference.name} <div className={'drag-content'}>{content}</div>
as="div" </NodeViewWrapper>
className={cls(styles.wrap, isEditable && 'render-wrapper')}
>
{content}
</DragableWrapper>
); );
}; };

View File

@ -1,30 +0,0 @@
.draggableItem {
position: relative;
display: flex;
.dragHandle {
position: absolute;
top: 0.3rem;
left: -24px;
width: 16px;
height: 16px;
cursor: grab;
opacity: 0;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 16"><path fill-opacity="0.2" d="M4 14c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM2 6C.9 6 0 6.9 0 8s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6C.9 0 0 .9 0 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" /></svg>');
background-repeat: no-repeat;
background-size: contain;
background-position: center;
}
.content {
width: 100%;
}
&.isEditable {
&.isActive {
.dragHandle {
opacity: 1;
}
}
}
}

View File

@ -1,31 +0,0 @@
import { Editor } from '@tiptap/core';
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames';
import React, { ElementType } from 'react';
import { useActive } from 'tiptap/core/hooks/use-active';
import styles from './index.module.scss';
export const DragableWrapper: React.FC<{
editor: Editor;
extensionName: string;
as?: ElementType;
id?: string;
className?: string;
style?: React.CSSProperties;
}> = ({ editor, extensionName, as = 'div', id, className, style = {}, children }) => {
const isEditable = editor.isEditable;
const isActive = useActive(editor, extensionName);
return (
<NodeViewWrapper
as={as}
id={id}
className={cls(styles.draggableItem, isEditable && styles.isEditable, isActive && styles.isActive, className)}
style={style}
>
<div className={styles.dragHandle} contentEditable="false" draggable="true" data-drag-handle />
<div className={styles.content}>{children}</div>
</NodeViewWrapper>
);
};

View File

@ -1,4 +1,5 @@
import { Button, Space, Spin, Typography } from '@douyinfe/semi-ui'; import { Button, Space, Spin, Typography } from '@douyinfe/semi-ui';
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { IconFlow, IconMindCenter, IconZoomIn, IconZoomOut } from 'components/icons'; import { IconFlow, IconMindCenter, IconZoomIn, IconZoomOut } from 'components/icons';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
@ -7,7 +8,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import VisibilitySensor from 'react-visibility-sensor'; import VisibilitySensor from 'react-visibility-sensor';
import { load, renderXml } from 'thirtypart/diagram'; import { load, renderXml } from 'thirtypart/diagram';
import { Flow } from 'tiptap/core/extensions/flow'; import { Flow } from 'tiptap/core/extensions/flow';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import { getEditorContainerDOMSize } from 'tiptap/prose-utils'; import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -95,49 +95,48 @@ export const FlowWrapper = ({ editor, node, updateAttributes }) => {
}, [toggleLoading, data]); }, [toggleLoading, data]);
return ( return (
<DragableWrapper <NodeViewWrapper className={cls('drag-container', isActive && styles.isActive)}>
editor={editor} <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
className={cls(styles.wrap, isActive && styles.isActive)} <div className={cls('drag-content', styles.wrap)}>
extensionName={Flow.name} <VisibilitySensor onChange={onViewportChange}>
> <Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
<VisibilitySensor onChange={onViewportChange}> <div
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}> className={cls(styles.renderWrap, 'render-wrapper')}
<div style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden', backgroundColor: bgColor }}
className={cls(styles.renderWrap, 'render-wrapper')} >
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden', backgroundColor: bgColor }} {loading && (
> <div>
{loading && ( <Spin spinning>
<div> {/* FIXME: semi-design 的问题,不加 div文字会换行! */}
<Spin spinning> <div></div>
{/* FIXME: semi-design 的问题,不加 div文字会换行! */} </Spin>
<div></div> </div>
</Spin> )}
</div>
)}
{error && <Text>{(error && error.message) || '未知错误'}</Text>} {error && <Text>{(error && error.message) || '未知错误'}</Text>}
{!loading && !error && visible && <div style={{ maxHeight: '100%' }} ref={setMxgraph}></div>} {!loading && !error && visible && <div style={{ maxHeight: '100%' }} ref={setMxgraph}></div>}
</div> </div>
<div className={styles.title}> <div className={styles.title}>
<Space> <Space>
<span className={styles.icon}> <span className={styles.icon}>
<IconFlow /> <IconFlow />
</span> </span>
</Space> </Space>
</div> </div>
<div className={styles.toolbarWrap}> <div className={styles.toolbarWrap}>
<Space spacing={2}> <Space spacing={2}>
<Button type="tertiary" theme="borderless" size="small" onClick={center} icon={<IconMindCenter />} /> <Button type="tertiary" theme="borderless" size="small" onClick={center} icon={<IconMindCenter />} />
<Button type="tertiary" theme="borderless" size="small" onClick={zoomOut} icon={<IconZoomOut />} /> <Button type="tertiary" theme="borderless" size="small" onClick={zoomOut} icon={<IconZoomOut />} />
<Button type="tertiary" theme="borderless" size="small" onClick={zoomIn} icon={<IconZoomIn />} /> <Button type="tertiary" theme="borderless" size="small" onClick={zoomIn} icon={<IconZoomIn />} />
</Space> </Space>
</div> </div>
</Resizeable> </Resizeable>
</VisibilitySensor> </VisibilitySensor>
</DragableWrapper> </div>
</NodeViewWrapper>
); );
}; };

View File

@ -1,9 +1,8 @@
import { Typography } from '@douyinfe/semi-ui'; import { Typography } from '@douyinfe/semi-ui';
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Iframe } from 'tiptap/core/extensions/iframe';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import { getEditorContainerDOMSize } from 'tiptap/prose-utils'; import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -23,20 +22,23 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
); );
return ( return (
<DragableWrapper editor={editor} extensionName={Iframe.name}> <NodeViewWrapper className={'drag-container'}>
<Resizeable width={width} maxWidth={maxWidth} height={height} isEditable={isEditable} onChangeEnd={onResize}> <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
<div className={cls(styles.wrap, 'render-wrapper')}> <div className={'drag-content'}>
{url ? ( <Resizeable width={width} maxWidth={maxWidth} height={height} isEditable={isEditable} onChangeEnd={onResize}>
<div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}> <div className={cls(styles.wrap, 'render-wrapper')}>
<iframe src={url}></iframe> {url ? (
</div> <div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}>
) : ( <iframe src={url}></iframe>
<div className={styles.emptyWrap}> </div>
<Text></Text> ) : (
</div> <div className={styles.emptyWrap}>
)} <Text></Text>
</div> </div>
</Resizeable> )}
</DragableWrapper> </div>
</Resizeable>
</div>
</NodeViewWrapper>
); );
}; };

View File

@ -1,11 +1,10 @@
import { Spin, Typography } from '@douyinfe/semi-ui'; import { Spin, Typography } from '@douyinfe/semi-ui';
import { NodeViewWrapper } from '@tiptap/react';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component'; import { LazyLoadImage } from 'react-lazy-load-image-component';
import { uploadFile } from 'services/file'; import { uploadFile } from 'services/file';
import { Image } from 'tiptap/core/extensions/image';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import { import {
extractFileExtension, extractFileExtension,
extractFilename, extractFilename,
@ -70,30 +69,33 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
}, [src, hasTrigger, selectFile, updateAttributes]); }, [src, hasTrigger, selectFile, updateAttributes]);
return ( return (
<DragableWrapper editor={editor} extensionName={Image.name} style={{ textAlign, fontSize: 0, maxWidth: '100%' }}> <NodeViewWrapper className={'drag-container'} style={{ textAlign, fontSize: 0, maxWidth: '100%' }}>
<Resizeable <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
className={'render-wrapper'} <div className={'drag-content'}>
width={width || maxWidth} <Resizeable
height={height} className={'render-wrapper'}
maxWidth={maxWidth} width={width || maxWidth}
isEditable={isEditable} height={height}
onChangeEnd={onResize} maxWidth={maxWidth}
> isEditable={isEditable}
{error ? ( onChangeEnd={onResize}
<div className={styles.wrap}> >
<Text>{error}</Text> {error ? (
</div> <div className={styles.wrap}>
) : !src ? ( <Text>{error}</Text>
<div className={styles.wrap} onClick={selectFile}> </div>
<Spin spinning={loading}> ) : !src ? (
<Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择图片'}</Text> <div className={styles.wrap} onClick={selectFile}>
<input ref={$upload} accept="image/*" type="file" hidden onChange={handleFile} /> <Spin spinning={loading}>
</Spin> <Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择图片'}</Text>
</div> <input ref={$upload} accept="image/*" type="file" hidden onChange={handleFile} />
) : ( </Spin>
<LazyLoadImage src={src} alt={alt} width={width} height={height} /> </div>
)} ) : (
</Resizeable> <LazyLoadImage src={src} alt={alt} width={width} height={height} />
</DragableWrapper> )}
</Resizeable>
</div>
</NodeViewWrapper>
); );
}; };

View File

@ -1,9 +1,8 @@
import { NodeViewWrapper } from '@tiptap/react';
import { convertColorToRGBA } from 'helpers/color'; import { convertColorToRGBA } from 'helpers/color';
import { Theme, ThemeEnum } from 'hooks/use-theme'; import { Theme, ThemeEnum } from 'hooks/use-theme';
import katex from 'katex'; import katex from 'katex';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Katex } from 'tiptap/core/extensions/katex';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -35,15 +34,16 @@ export const KatexWrapper = ({ node, editor }) => {
); );
return ( return (
<DragableWrapper <NodeViewWrapper
editor={editor} className={'drag-container render-wrapper'}
extensionName={Katex.name}
className={'render-wrapper'}
style={{ style={{
backgroundColor, backgroundColor,
}} }}
> >
<div className={styles.wrap}>{content}</div> <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
</DragableWrapper> <div className={'drag-content'}>
<div className={styles.wrap}>{content}</div>
</div>
</NodeViewWrapper>
); );
}; };

View File

@ -1,4 +1,5 @@
import { Button, Space, Spin, Typography } from '@douyinfe/semi-ui'; import { Button, Space, Spin, Typography } from '@douyinfe/semi-ui';
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { IconMind, IconMindCenter, IconZoomIn, IconZoomOut } from 'components/icons'; import { IconMind, IconMindCenter, IconZoomIn, IconZoomOut } from 'components/icons';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
@ -10,7 +11,6 @@ import VisibilitySensor from 'react-visibility-sensor';
import { load, renderMind } from 'thirtypart/kityminder'; import { load, renderMind } from 'thirtypart/kityminder';
import { Mind } from 'tiptap/core/extensions/mind'; import { Mind } from 'tiptap/core/extensions/mind';
import { MAX_ZOOM, MIN_ZOOM, ZOOM_STEP } from 'tiptap/core/menus/mind/constant'; import { MAX_ZOOM, MIN_ZOOM, ZOOM_STEP } from 'tiptap/core/menus/mind/constant';
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
import { clamp, getEditorContainerDOMSize } from 'tiptap/prose-utils'; import { clamp, getEditorContainerDOMSize } from 'tiptap/prose-utils';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -108,64 +108,69 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
}, [width, height, setCenter]); }, [width, height, setCenter]);
return ( return (
<DragableWrapper <NodeViewWrapper className={cls('drag-container', styles.wrap, isActive && styles.isActive)}>
editor={editor} <div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
extensionName={Mind.name} <div className={'drag-content'}>
className={cls(styles.wrap, isActive && styles.isActive)} <VisibilitySensor onChange={onViewportChange}>
> <Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
<VisibilitySensor onChange={onViewportChange}> <div
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}> className={cls(styles.renderWrap, 'render-wrapper')}
<div style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }}
className={cls(styles.renderWrap, 'render-wrapper')} >
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }} {error && (
> <div style={INHERIT_SIZE_STYLE}>
{error && ( <Text>{error.message || error}</Text>
<div style={INHERIT_SIZE_STYLE}> </div>
<Text>{error.message || error}</Text> )}
{loading && <Spin spinning style={INHERIT_SIZE_STYLE}></Spin>}
{!loading && !error && visible && (
<div style={{ height: '100%', maxHeight: '100%', overflow: 'hidden' }} ref={setMind}></div>
)}
<div className={styles.title}>
<Space>
<span className={styles.icon}>
<IconMind />
</span>
</Space>
</div> </div>
)}
{loading && <Spin spinning style={INHERIT_SIZE_STYLE}></Spin>} <div className={styles.mindHandlerWrap}>
<Tooltip content="居中">
{!loading && !error && visible && ( <Button
<div style={{ height: '100%', maxHeight: '100%', overflow: 'hidden' }} ref={setMind}></div> size="small"
)} theme="borderless"
type="tertiary"
<div className={styles.title}> icon={<IconMindCenter />}
<Space> onClick={setCenter}
<span className={styles.icon}> />
<IconMind /> </Tooltip>
</span> <Tooltip content="缩小">
<Button
</Space> size="small"
theme="borderless"
type="tertiary"
icon={<IconZoomOut />}
onClick={setZoom('minus')}
/>
</Tooltip>
<Tooltip content="放大">
<Button
size="small"
theme="borderless"
type="tertiary"
icon={<IconZoomIn />}
onClick={setZoom('plus')}
/>
</Tooltip>
</div>
</div> </div>
</Resizeable>
<div className={styles.mindHandlerWrap}> </VisibilitySensor>
<Tooltip content="居中"> </div>
<Button size="small" theme="borderless" type="tertiary" icon={<IconMindCenter />} onClick={setCenter} /> </NodeViewWrapper>
</Tooltip>
<Tooltip content="缩小">
<Button
size="small"
theme="borderless"
type="tertiary"
icon={<IconZoomOut />}
onClick={setZoom('minus')}
/>
</Tooltip>
<Tooltip content="放大">
<Button
size="small"
theme="borderless"
type="tertiary"
icon={<IconZoomIn />}
onClick={setZoom('plus')}
/>
</Tooltip>
</div>
</div>
</Resizeable>
</VisibilitySensor>
</DragableWrapper>
); );
}; };

View File

@ -16,7 +16,6 @@ import { Countdown } from 'tiptap/core/extensions/countdown';
import { Document } from 'tiptap/core/extensions/document'; import { Document } from 'tiptap/core/extensions/document';
import { DocumentChildren } from 'tiptap/core/extensions/document-children'; import { DocumentChildren } from 'tiptap/core/extensions/document-children';
import { DocumentReference } from 'tiptap/core/extensions/document-reference'; import { DocumentReference } from 'tiptap/core/extensions/document-reference';
import { Dragable } from 'tiptap/core/extensions/dragable';
import { Dropcursor } from 'tiptap/core/extensions/dropcursor'; import { Dropcursor } from 'tiptap/core/extensions/dropcursor';
import { Emoji } from 'tiptap/core/extensions/emoji'; import { Emoji } from 'tiptap/core/extensions/emoji';
import { EventEmitter } from 'tiptap/core/extensions/event-emitter'; import { EventEmitter } from 'tiptap/core/extensions/event-emitter';
@ -99,7 +98,6 @@ export const CollaborationKit = [
CodeBlock, CodeBlock,
Color, Color,
ColorHighlighter, ColorHighlighter,
Dragable,
Dropcursor, Dropcursor,
EventEmitter, EventEmitter,
Focus, Focus,