mirror of https://github.com/fantasticit/think.git
Merge pull request #145 from fantasticit/fix/drag
commit
14beaff5c8
|
@ -0,0 +1,173 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey, Selection } from 'prosemirror-state';
|
||||
import { NodeSelection } from 'prosemirror-state';
|
||||
import { __serializeForClipboard, EditorView } from 'prosemirror-view';
|
||||
import { ActiveNode, removePossibleTable, selectRootNodeByDom } from 'tiptap/prose-utils';
|
||||
|
||||
export const DragablePluginKey = new PluginKey('dragable');
|
||||
|
||||
export const Dragable = Extension.create({
|
||||
name: 'dragable',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
let editorView: EditorView;
|
||||
let dragHandleDOM: HTMLElement;
|
||||
let activeNode: ActiveNode;
|
||||
let activeSelection: Selection;
|
||||
let dragging = false;
|
||||
|
||||
const createDragHandleDOM = () => {
|
||||
const dom = document.createElement('div');
|
||||
dom.draggable = true;
|
||||
dom.setAttribute('data-drag-handle', 'true');
|
||||
return dom;
|
||||
};
|
||||
|
||||
const showDragHandleDOM = () => {
|
||||
dragHandleDOM.classList.add('show');
|
||||
dragHandleDOM.classList.remove('hide');
|
||||
};
|
||||
|
||||
const hideDragHandleDOM = () => {
|
||||
dragHandleDOM.classList.remove('show');
|
||||
dragHandleDOM.classList.add('hide');
|
||||
};
|
||||
|
||||
const renderDragHandleDOM = (view: EditorView, el: HTMLElement) => {
|
||||
const root = view.dom.parentElement;
|
||||
|
||||
if (!root) return;
|
||||
|
||||
const targetNodeRect = (<HTMLElement>el).getBoundingClientRect();
|
||||
const rootRect = root.getBoundingClientRect();
|
||||
const handleRect = dragHandleDOM.getBoundingClientRect();
|
||||
|
||||
const left = targetNodeRect.left - rootRect.left - handleRect.width - handleRect.width / 2;
|
||||
const top = targetNodeRect.top - rootRect.top + handleRect.height / 2 + root.scrollTop;
|
||||
|
||||
dragHandleDOM.style.left = `${left}px`;
|
||||
dragHandleDOM.style.top = `${top}px`;
|
||||
|
||||
showDragHandleDOM();
|
||||
};
|
||||
|
||||
const handleMouseDown = () => {
|
||||
if (!activeNode) return null;
|
||||
|
||||
if (NodeSelection.isSelectable(activeNode.node)) {
|
||||
const nodeSelection = NodeSelection.create(editorView.state.doc, activeNode.$pos.pos - activeNode.offset);
|
||||
editorView.dispatch(editorView.state.tr.setSelection(nodeSelection));
|
||||
editorView.focus();
|
||||
activeSelection = nodeSelection;
|
||||
return nodeSelection;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!dragging) return;
|
||||
|
||||
dragging = false;
|
||||
activeSelection = null;
|
||||
};
|
||||
|
||||
const handleDragStart = (event) => {
|
||||
dragging = true;
|
||||
if (event.dataTransfer && activeSelection) {
|
||||
const brokenClipboardAPI = false;
|
||||
const slice = activeSelection.content();
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
const { dom, text } = __serializeForClipboard(editorView, slice);
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData(brokenClipboardAPI ? 'Text' : 'text/html', dom.innerHTML);
|
||||
if (!brokenClipboardAPI) event.dataTransfer.setData('text/plain', text);
|
||||
editorView.dragging = {
|
||||
slice,
|
||||
move: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: DragablePluginKey,
|
||||
view: (view) => {
|
||||
if (view.editable) {
|
||||
dragHandleDOM = createDragHandleDOM();
|
||||
dragHandleDOM.addEventListener('mousedown', handleMouseDown);
|
||||
dragHandleDOM.addEventListener('mouseup', handleMouseUp);
|
||||
dragHandleDOM.addEventListener('dragstart', handleDragStart);
|
||||
view.dom.parentNode?.appendChild(dragHandleDOM);
|
||||
}
|
||||
|
||||
return {
|
||||
update(view) {
|
||||
editorView = view;
|
||||
},
|
||||
destroy: () => {
|
||||
if (!dragHandleDOM) return;
|
||||
|
||||
dragHandleDOM.remove();
|
||||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop: (view, event: DragEvent) => {
|
||||
if (!view.editable || !dragHandleDOM) return false;
|
||||
|
||||
if (dragging) {
|
||||
const tr = removePossibleTable(view, event);
|
||||
dragging = false;
|
||||
|
||||
if (tr) {
|
||||
view.dispatch(tr);
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
mousemove: (view, event) => {
|
||||
if (!view.editable || !dragHandleDOM) return false;
|
||||
|
||||
const dom = event.target;
|
||||
|
||||
if (!(dom instanceof Element)) {
|
||||
if (dragging) return false;
|
||||
hideDragHandleDOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = selectRootNodeByDom(dom, view);
|
||||
activeNode = result;
|
||||
|
||||
if (!result) {
|
||||
if (dragging) return false;
|
||||
hideDragHandleDOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.node.type.name === 'title') {
|
||||
if (dragging) return false;
|
||||
hideDragHandleDOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
renderDragHandleDOM(view, result.el);
|
||||
return false;
|
||||
},
|
||||
keydown: () => {
|
||||
if (!editorView.editable || !dragHandleDOM) return false;
|
||||
dragHandleDOM.classList.remove('show');
|
||||
hideDragHandleDOM();
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -3,20 +3,5 @@ import TitapParagraph from '@tiptap/extension-paragraph';
|
|||
|
||||
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],
|
||||
];
|
||||
},
|
||||
selectable: true,
|
||||
});
|
||||
|
|
|
@ -1,91 +1,26 @@
|
|||
/* stylelint-disable */
|
||||
.ProseMirror {
|
||||
&.is-editable {
|
||||
[data-drag-handle] {
|
||||
position: relative;
|
||||
display: inline;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
z-index: 100;
|
||||
[data-drag-handle] {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
display: inline;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: move;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
&.show {
|
||||
opacity: 0.3;
|
||||
|
||||
&::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%;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
/* stylelint-disable */
|
||||
.ProseMirror {
|
||||
p.selected-node {
|
||||
outline: 1px solid var(--node-selected-border-color);
|
||||
}
|
||||
|
||||
hr.selected-node {
|
||||
&::after {
|
||||
background-color: var(--node-selected-border-color);
|
||||
|
|
|
@ -154,10 +154,5 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
|
|||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={'drag-container'}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>{content}</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
return <NodeViewWrapper>{content}</NodeViewWrapper>;
|
||||
};
|
||||
|
|
|
@ -25,29 +25,26 @@ export const CalloutWrapper = ({ editor, node, updateAttributes }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper id="js-callout-container" className={cls('drag-container', styles.wrap)}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>
|
||||
<div
|
||||
className={cls(styles.innerWrap, 'render-wrapper')}
|
||||
<NodeViewWrapper id="js-callout-container" className={cls(styles.wrap)}>
|
||||
<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={{
|
||||
borderColor,
|
||||
backgroundColor: backgroundColorOpacity,
|
||||
color: textColor,
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
|
|
@ -4,8 +4,6 @@ import { NodeViewContent, NodeViewWrapper } from '@tiptap/react';
|
|||
import cls from 'classnames';
|
||||
import { copy } from 'helpers/copy';
|
||||
import React, { useRef } from 'react';
|
||||
import { CodeBlock } from 'tiptap/core/extensions/code-block';
|
||||
import { DragableWrapper } from 'tiptap/core/wrappers/dragable';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -16,39 +14,36 @@ export const CodeBlockWrapper = ({ editor, node: { attrs }, updateAttributes, ex
|
|||
const $container = useRef<HTMLPreElement>();
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={cls('drag-container', styles.wrap, !isPrint && styles.maxHeight, 'render-wrapper')}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>
|
||||
<div className={styles.handleWrap}>
|
||||
<Select
|
||||
<NodeViewWrapper className={cls(styles.wrap, !isPrint && styles.maxHeight, 'render-wrapper')}>
|
||||
<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"
|
||||
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"
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
icon={<IconCopy />}
|
||||
onClick={() => copy($container.current.innerText)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<pre ref={$container}>
|
||||
<NodeViewContent as="code" />
|
||||
</pre>
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
icon={<IconCopy />}
|
||||
onClick={() => copy($container.current.innerText)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<pre ref={$container}>
|
||||
<NodeViewContent as="code" />
|
||||
</pre>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,13 +32,10 @@ export const CountdownWrapper = ({ editor, node }) => {
|
|||
const { title, date } = node.attrs;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={'drag-container'}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>
|
||||
<div className={cls(styles.wrap, 'render-wrapper')}>
|
||||
<Text>{title}</Text>
|
||||
<ReactCountdown date={date} renderer={renderer}></ReactCountdown>
|
||||
</div>
|
||||
<NodeViewWrapper>
|
||||
<div className={cls(styles.wrap, 'render-wrapper')}>
|
||||
<Text>{title}</Text>
|
||||
<ReactCountdown date={date} renderer={renderer}></ReactCountdown>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
|
|
@ -35,48 +35,48 @@ export const DocumentChildrenWrapper = ({ editor, node, updateAttributes }) => {
|
|||
}, [node.attrs, wikiId, documentId, updateAttributes]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className={cls('drag-container', 'render-wrapper')}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={cls('drag-content', styles.wrap, isEditable && styles.isEditable, 'documentChildren')}>
|
||||
<div>
|
||||
<Text type="tertiary">子文档</Text>
|
||||
</div>
|
||||
{wikiId || documentId ? (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
if (!documents || !documents.length) {
|
||||
return <Empty message="暂无子文档" />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{documents.map((doc) => {
|
||||
return (
|
||||
<Link
|
||||
key={doc.id}
|
||||
href={{
|
||||
pathname: isShare
|
||||
? `/share/wiki/[wikiId]/document/[documentId]`
|
||||
: `/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]`,
|
||||
query: { organizationId: doc.organizationId, wikiId: doc.wikiId, documentId: doc.id },
|
||||
}}
|
||||
>
|
||||
<a className={styles.itemWrap} target="_blank">
|
||||
<IconDocument />
|
||||
<span>{doc.title}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text type="tertiary">当前页面无法使用子文档</Text>
|
||||
)}
|
||||
<NodeViewWrapper
|
||||
as="div"
|
||||
className={cls('render-wrapper', styles.wrap, isEditable && styles.isEditable, 'documentChildren')}
|
||||
>
|
||||
<div>
|
||||
<Text type="tertiary">子文档</Text>
|
||||
</div>
|
||||
{wikiId || documentId ? (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
if (!documents || !documents.length) {
|
||||
return <Empty message="暂无子文档" />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{documents.map((doc) => {
|
||||
return (
|
||||
<Link
|
||||
key={doc.id}
|
||||
href={{
|
||||
pathname: isShare
|
||||
? `/share/wiki/[wikiId]/document/[documentId]`
|
||||
: `/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]`,
|
||||
query: { organizationId: doc.organizationId, wikiId: doc.wikiId, documentId: doc.id },
|
||||
}}
|
||||
>
|
||||
<a className={styles.itemWrap} target="_blank">
|
||||
<IconDocument />
|
||||
<span>{doc.title}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text type="tertiary">当前页面无法使用子文档</Text>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -50,9 +50,8 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
|
|||
}, [organizationId, wikiId, documentId, isEditable, isShare, title]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className={cls('drag-container', styles.wrap, isEditable && 'render-wrapper')}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>{content}</div>
|
||||
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && 'render-wrapper')}>
|
||||
{content}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -95,48 +95,45 @@ export const FlowWrapper = ({ editor, node, updateAttributes }) => {
|
|||
}, [toggleLoading, data]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={cls('drag-container', isActive && styles.isActive)}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={cls('drag-content', styles.wrap)}>
|
||||
<VisibilitySensor onChange={onViewportChange}>
|
||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
<div
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden', backgroundColor: bgColor }}
|
||||
>
|
||||
{loading && (
|
||||
<div>
|
||||
<Spin spinning>
|
||||
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||
<div></div>
|
||||
</Spin>
|
||||
</div>
|
||||
)}
|
||||
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
|
||||
<VisibilitySensor onChange={onViewportChange}>
|
||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
<div
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden', backgroundColor: bgColor }}
|
||||
>
|
||||
{loading && (
|
||||
<div>
|
||||
<Spin spinning>
|
||||
{/* FIXME: semi-design 的问题,不加 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>}
|
||||
</div>
|
||||
{!loading && !error && visible && <div style={{ maxHeight: '100%' }} ref={setMxgraph}></div>}
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
<Space>
|
||||
<span className={styles.icon}>
|
||||
<IconFlow />
|
||||
</span>
|
||||
流程图
|
||||
</Space>
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<Space>
|
||||
<span className={styles.icon}>
|
||||
<IconFlow />
|
||||
</span>
|
||||
流程图
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className={styles.toolbarWrap}>
|
||||
<Space spacing={2}>
|
||||
<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={zoomIn} icon={<IconZoomIn />} />
|
||||
</Space>
|
||||
</div>
|
||||
</Resizeable>
|
||||
</VisibilitySensor>
|
||||
</div>
|
||||
<div className={styles.toolbarWrap}>
|
||||
<Space spacing={2}>
|
||||
<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={zoomIn} icon={<IconZoomIn />} />
|
||||
</Space>
|
||||
</div>
|
||||
</Resizeable>
|
||||
</VisibilitySensor>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,23 +22,20 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={'drag-container'}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>
|
||||
<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>
|
||||
</div>
|
||||
<NodeViewWrapper>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -69,33 +69,30 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
|||
}, [src, hasTrigger, selectFile, updateAttributes]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={'drag-container'} style={{ textAlign, fontSize: 0, maxWidth: '100%' }}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>
|
||||
<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>
|
||||
</div>
|
||||
<NodeViewWrapper style={{ textAlign, fontSize: 0, maxWidth: '100%' }}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -35,15 +35,12 @@ export const KatexWrapper = ({ node, editor }) => {
|
|||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
className={'drag-container render-wrapper'}
|
||||
className={'render-wrapper'}
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>
|
||||
<div className={styles.wrap}>{content}</div>
|
||||
</div>
|
||||
<div className={styles.wrap}>{content}</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -108,69 +108,60 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
}, [width, height, setCenter]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={cls('drag-container', styles.wrap, isActive && styles.isActive)}>
|
||||
<div className={'drag-handle'} contentEditable="false" draggable="true" data-drag-handle />
|
||||
<div className={'drag-content'}>
|
||||
<VisibilitySensor onChange={onViewportChange}>
|
||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
<div
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }}
|
||||
>
|
||||
{error && (
|
||||
<div style={INHERIT_SIZE_STYLE}>
|
||||
<Text>{error.message || error}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
|
||||
<VisibilitySensor onChange={onViewportChange}>
|
||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
<div
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }}
|
||||
>
|
||||
{error && (
|
||||
<div style={INHERIT_SIZE_STYLE}>
|
||||
<Text>{error.message || error}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.mindHandlerWrap}>
|
||||
<Tooltip content="居中">
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconMindCenter />}
|
||||
onClick={setCenter}
|
||||
/>
|
||||
</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>
|
||||
{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>
|
||||
</Resizeable>
|
||||
</VisibilitySensor>
|
||||
</div>
|
||||
|
||||
<div className={styles.mindHandlerWrap}>
|
||||
<Tooltip content="居中">
|
||||
<Button size="small" theme="borderless" type="tertiary" icon={<IconMindCenter />} onClick={setCenter} />
|
||||
</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>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import { Countdown } from 'tiptap/core/extensions/countdown';
|
|||
import { Document } from 'tiptap/core/extensions/document';
|
||||
import { DocumentChildren } from 'tiptap/core/extensions/document-children';
|
||||
import { DocumentReference } from 'tiptap/core/extensions/document-reference';
|
||||
import { Dragable } from 'tiptap/core/extensions/dragable';
|
||||
import { Dropcursor } from 'tiptap/core/extensions/dropcursor';
|
||||
import { Emoji } from 'tiptap/core/extensions/emoji';
|
||||
import { EventEmitter } from 'tiptap/core/extensions/event-emitter';
|
||||
|
@ -156,4 +157,5 @@ export const CollaborationKit = [
|
|||
}),
|
||||
Title,
|
||||
DocumentWithTitle,
|
||||
Dragable,
|
||||
];
|
||||
|
|
|
@ -18,6 +18,7 @@ export * from './markdown-source-map';
|
|||
export * from './mention';
|
||||
export * from './node';
|
||||
export * from './position';
|
||||
export * from './select-node-by-dom';
|
||||
export * from './table';
|
||||
export * from './text';
|
||||
export * from './type';
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { Node, ResolvedPos } from 'prosemirror-model';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
|
||||
export type ActiveNode = Readonly<{
|
||||
$pos: ResolvedPos;
|
||||
node: Node;
|
||||
el: HTMLElement;
|
||||
offset: number;
|
||||
}>;
|
||||
|
||||
const nodeIsNotBlock = (node: Node) => !node.type.isBlock;
|
||||
|
||||
const nodeIsFirstChild = (pos: ResolvedPos) => {
|
||||
let parent = pos.parent;
|
||||
const node = pos.node();
|
||||
|
||||
if (parent === node) {
|
||||
parent = pos.node(pos.depth - 1);
|
||||
}
|
||||
if (!parent || parent.type.name === 'doc') return false;
|
||||
|
||||
return parent.firstChild === node;
|
||||
};
|
||||
|
||||
const getDOMByPos = (view: EditorView, root: HTMLElement, $pos: ResolvedPos) => {
|
||||
const { node } = view.domAtPos($pos.pos);
|
||||
|
||||
let el: HTMLElement = node as HTMLElement;
|
||||
let parent = el.parentElement;
|
||||
while (parent && parent !== root && $pos.pos === view.posAtDOM(parent, 0)) {
|
||||
el = parent;
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
export const selectRootNodeByDom = (dom: Element, view: EditorView): ActiveNode | null => {
|
||||
const root = view.dom.parentElement;
|
||||
|
||||
if (!root) return null;
|
||||
|
||||
let pos = view.posAtDOM(dom, 0);
|
||||
|
||||
/**
|
||||
* img 节点修正
|
||||
*/
|
||||
if (dom.tagName === 'IMG') {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if (pos === 0) return null;
|
||||
|
||||
let $pos = view.state.doc.resolve(pos);
|
||||
let node = $pos.node();
|
||||
|
||||
/**
|
||||
* 自定义节点修正
|
||||
*/
|
||||
if (node.type.name === 'doc') {
|
||||
const nodeAtPos = view.state.doc.nodeAt(pos);
|
||||
|
||||
if (nodeAtPos && nodeAtPos.type.name !== 'doc' && nodeAtPos.type.name !== 'text') {
|
||||
node = nodeAtPos;
|
||||
$pos = view.state.doc.resolve(pos);
|
||||
const el = view.nodeDOM(pos);
|
||||
return { node, $pos, el, offset: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
while (node && (nodeIsNotBlock(node) || nodeIsFirstChild($pos))) {
|
||||
$pos = view.state.doc.resolve($pos.before());
|
||||
node = $pos.node();
|
||||
}
|
||||
|
||||
if (node.type.name.includes('table')) {
|
||||
while (node.type.name !== 'table') {
|
||||
$pos = view.state.doc.resolve($pos.before());
|
||||
node = $pos.node();
|
||||
}
|
||||
}
|
||||
|
||||
$pos = view.state.doc.resolve($pos.pos - $pos.parentOffset);
|
||||
const el = getDOMByPos(view, root, $pos);
|
||||
|
||||
return { node, $pos, el, offset: 1 };
|
||||
};
|
|
@ -2,6 +2,7 @@ import { findParentNode } from '@tiptap/core';
|
|||
import { Node, ResolvedPos } from 'prosemirror-model';
|
||||
import { Selection, Transaction } from 'prosemirror-state';
|
||||
import { CellSelection, TableMap } from 'prosemirror-tables';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
|
||||
export const isRectSelected = (rect: any) => (selection: CellSelection) => {
|
||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||
|
@ -220,3 +221,62 @@ export const selectTable = (tr: Transaction) => {
|
|||
}
|
||||
return tr;
|
||||
};
|
||||
|
||||
function dropPoint(doc, pos, slice) {
|
||||
const $pos = doc.resolve(pos);
|
||||
if (!slice.content.size) {
|
||||
return pos;
|
||||
}
|
||||
let content = slice.content;
|
||||
for (let i = 0; i < slice.openStart; i++) {
|
||||
content = content.firstChild.content;
|
||||
}
|
||||
for (let pass = 1; pass <= (slice.openStart == 0 && slice.size ? 2 : 1); pass++) {
|
||||
for (let d = $pos.depth; d >= 0; d--) {
|
||||
const bias = d == $pos.depth ? 0 : $pos.pos <= ($pos.start(d + 1) + $pos.end(d + 1)) / 2 ? -1 : 1;
|
||||
const insertPos = $pos.index(d) + (bias > 0 ? 1 : 0);
|
||||
const parent = $pos.node(d);
|
||||
let fits = false;
|
||||
if (pass == 1) {
|
||||
fits = parent.canReplace(insertPos, insertPos, content);
|
||||
} else {
|
||||
const wrapping = parent.contentMatchAt(insertPos).findWrapping(content.firstChild.type);
|
||||
fits = wrapping && parent.canReplaceWith(insertPos, insertPos, wrapping[0]);
|
||||
}
|
||||
if (fits) {
|
||||
return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const removePossibleTable = (view: EditorView, event: DragEvent): Transaction | null => {
|
||||
const { state } = view;
|
||||
|
||||
const $pos = state.selection.$anchor;
|
||||
for (let d = $pos.depth; d > 0; d--) {
|
||||
const node = $pos.node(d);
|
||||
if (node.type.spec['tableRole'] == 'table') {
|
||||
const eventPos = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
if (!eventPos) return null;
|
||||
const slice = view.dragging?.slice;
|
||||
if (!slice) return null;
|
||||
|
||||
const $mouse = view.state.doc.resolve(eventPos.pos);
|
||||
const insertPos = dropPoint(view.state.doc, $mouse.pos, slice);
|
||||
if (!insertPos) return null;
|
||||
|
||||
let tr = state.tr;
|
||||
tr = tr.delete($pos.before(d), $pos.after(d));
|
||||
|
||||
const pos = tr.mapping.map(insertPos);
|
||||
|
||||
tr = tr.replaceRange(pos, pos, slice).scrollIntoView();
|
||||
|
||||
return tr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue