mirror of https://github.com/fantasticit/think.git
parent
1d2c9402f2
commit
5d13733d86
|
@ -1,28 +1,42 @@
|
||||||
import { Extension } from '@tiptap/core';
|
import { Extension } from '@tiptap/core';
|
||||||
import { Plugin, PluginKey, Selection } from 'prosemirror-state';
|
import {
|
||||||
import { NodeSelection, TextSelection } from 'prosemirror-state';
|
NodeSelection,
|
||||||
import { __serializeForClipboard, EditorView } from 'prosemirror-view';
|
Plugin as PMPlugin,
|
||||||
import { ActiveNode, getNodeAtPos, removePossibleTable, safePos, selectRootNodeByDom } from 'tiptap/prose-utils';
|
PluginKey as PMPluginKey,
|
||||||
|
Selection,
|
||||||
|
TextSelection,
|
||||||
|
} from 'prosemirror-state';
|
||||||
|
import { findParentNodeClosestToPos } from 'prosemirror-utils';
|
||||||
|
import { __serializeForClipboard, Decoration, DecorationSet, EditorView } from 'prosemirror-view';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { safePos } from 'tiptap/prose-utils';
|
||||||
|
import { ActiveNode, removePossibleTable, selectAncestorNodeByDom } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
export const DragablePluginKey = new PluginKey('dragable');
|
const DragablePluginKey = new PMPluginKey('dragable');
|
||||||
|
|
||||||
export const Dragable = Extension.create({
|
export const Dragable = Extension.create({
|
||||||
name: 'dragable',
|
name: 'dragable',
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
if (!this.editor.isEditable) return [];
|
|
||||||
|
|
||||||
let editorView: EditorView;
|
let editorView: EditorView;
|
||||||
let dragHandleDOM: HTMLElement;
|
let dragHandleDOM: HTMLElement;
|
||||||
let activeNode: ActiveNode;
|
let activeNode: ActiveNode | null;
|
||||||
let activeSelection: Selection;
|
let activeSelection: Selection | null;
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
|
const isMenuVisible = false;
|
||||||
let mouseleaveTimer = null;
|
let mouseleaveTimer = null;
|
||||||
|
const menuActions = { setVisible: (arg: boolean) => {}, update: () => {} };
|
||||||
|
|
||||||
|
const getEditorView = () => editorView;
|
||||||
|
const getActiveNode = () => activeNode;
|
||||||
|
|
||||||
const createDragHandleDOM = () => {
|
const createDragHandleDOM = () => {
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
|
dom.className = 'dragable';
|
||||||
dom.draggable = true;
|
dom.draggable = true;
|
||||||
dom.setAttribute('data-drag-handle', 'true');
|
dom.setAttribute('data-drag-handle', 'true');
|
||||||
|
|
||||||
return dom;
|
return dom;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,25 +45,41 @@ export const Dragable = Extension.create({
|
||||||
dragHandleDOM?.classList?.remove('hide');
|
dragHandleDOM?.classList?.remove('hide');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const activeDragHandleDOM = () => {
|
||||||
|
dragHandleDOM?.classList?.add('active');
|
||||||
|
dragHandleDOM?.classList?.remove('hide');
|
||||||
|
};
|
||||||
|
|
||||||
const hideDragHandleDOM = () => {
|
const hideDragHandleDOM = () => {
|
||||||
dragHandleDOM?.classList?.remove('show');
|
dragHandleDOM?.classList?.remove('show');
|
||||||
|
dragHandleDOM?.classList?.remove('active');
|
||||||
dragHandleDOM?.classList?.add('hide');
|
dragHandleDOM?.classList?.add('hide');
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDragHandleDOM = (view: EditorView, el: HTMLElement) => {
|
const renderDragHandleDOM = (view: EditorView, referenceRectDOM: HTMLElement) => {
|
||||||
const root = view.dom.parentElement;
|
const root = view.dom.parentElement;
|
||||||
|
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
|
||||||
const targetNodeRect = (<HTMLElement>el).getBoundingClientRect();
|
const targetNodeRect = referenceRectDOM.getBoundingClientRect();
|
||||||
const rootRect = root.getBoundingClientRect();
|
const rootRect = root.getBoundingClientRect();
|
||||||
const handleRect = dragHandleDOM.getBoundingClientRect();
|
const handleRect = dragHandleDOM.getBoundingClientRect();
|
||||||
|
|
||||||
const left = targetNodeRect.left - rootRect.left - handleRect.width - handleRect.width / 2;
|
let offsetX = -5;
|
||||||
|
|
||||||
|
if (referenceRectDOM.tagName === 'LI') {
|
||||||
|
offsetX = referenceRectDOM.getAttribute('data-checked') ? -3 : -16;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = targetNodeRect.left - rootRect.left - handleRect.width + offsetX;
|
||||||
const top = targetNodeRect.top - rootRect.top + handleRect.height / 2 + root.scrollTop;
|
const top = targetNodeRect.top - rootRect.top + handleRect.height / 2 + root.scrollTop;
|
||||||
|
|
||||||
dragHandleDOM.style.left = `${left}px`;
|
const offsetLeft = 0;
|
||||||
dragHandleDOM.style.top = `${top}px`;
|
|
||||||
|
dragHandleDOM.style.left = `${left + offsetLeft}px`;
|
||||||
|
dragHandleDOM.style.top = `${top - 2}px`;
|
||||||
|
|
||||||
|
menuActions?.update?.();
|
||||||
|
|
||||||
showDragHandleDOM();
|
showDragHandleDOM();
|
||||||
};
|
};
|
||||||
|
@ -63,7 +93,10 @@ export const Dragable = Extension.create({
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
if (!activeNode) return null;
|
if (!activeNode) return null;
|
||||||
|
|
||||||
|
if (!isMenuVisible) {
|
||||||
hideDragHandleDOM();
|
hideDragHandleDOM();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = () => {
|
||||||
|
@ -85,6 +118,7 @@ export const Dragable = Extension.create({
|
||||||
|
|
||||||
dragging = false;
|
dragging = false;
|
||||||
activeSelection = null;
|
activeSelection = null;
|
||||||
|
activeNode = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = (event) => {
|
const handleDragStart = (event) => {
|
||||||
|
@ -102,11 +136,13 @@ export const Dragable = Extension.create({
|
||||||
slice,
|
slice,
|
||||||
move: true,
|
move: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
menuActions?.setVisible?.(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new PMPlugin({
|
||||||
key: DragablePluginKey,
|
key: DragablePluginKey,
|
||||||
view: (view) => {
|
view: (view) => {
|
||||||
if (view.editable) {
|
if (view.editable) {
|
||||||
|
@ -117,6 +153,7 @@ export const Dragable = Extension.create({
|
||||||
dragHandleDOM.addEventListener('mouseup', handleMouseUp);
|
dragHandleDOM.addEventListener('mouseup', handleMouseUp);
|
||||||
dragHandleDOM.addEventListener('dragstart', handleDragStart);
|
dragHandleDOM.addEventListener('dragstart', handleDragStart);
|
||||||
view.dom.parentNode?.appendChild(dragHandleDOM);
|
view.dom.parentNode?.appendChild(dragHandleDOM);
|
||||||
|
view.dom.parentNode.style = 'position: relative;';
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -126,6 +163,8 @@ export const Dragable = Extension.create({
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
if (!dragHandleDOM) return;
|
if (!dragHandleDOM) return;
|
||||||
|
|
||||||
|
clearTimeout(mouseleaveTimer);
|
||||||
|
ReactDOM.unmountComponentAtNode(dragHandleDOM);
|
||||||
dragHandleDOM.removeEventListener('mouseenter', handleMouseEnter);
|
dragHandleDOM.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
dragHandleDOM.removeEventListener('mouseleave', handleMouseLeave);
|
dragHandleDOM.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
dragHandleDOM.removeEventListener('mousedown', handleMouseDown);
|
dragHandleDOM.removeEventListener('mousedown', handleMouseDown);
|
||||||
|
@ -136,14 +175,16 @@ export const Dragable = Extension.create({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
// @ts-ignore
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
drop: (view, event: DragEvent) => {
|
drop: (view, event: DragEvent) => {
|
||||||
if (!view.editable || !dragHandleDOM) return false;
|
if (!view.editable || !dragHandleDOM) return false;
|
||||||
|
if (!activeSelection) return false;
|
||||||
|
|
||||||
const eventPos = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
const eventPos = view.posAtCoords({
|
||||||
if (!eventPos) {
|
left: event.clientX,
|
||||||
return true;
|
top: event.clientY,
|
||||||
}
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (activeSelection) {
|
if (activeSelection) {
|
||||||
|
@ -165,18 +206,25 @@ export const Dragable = Extension.create({
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
const $mouse = view.state.doc.resolve(eventPos.pos);
|
if (!eventPos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
const maybeTitle = findParentNodeClosestToPos(
|
||||||
* 不允许在 title 处放置
|
view.state.doc.resolve(safePos(this.editor.state, eventPos.pos)),
|
||||||
*/
|
(node) => node.type.name === 'title'
|
||||||
if ($mouse?.parent?.type?.name === 'title') {
|
);
|
||||||
|
|
||||||
|
// 不允许在 title 处放置
|
||||||
|
if (eventPos.pos === 0 || maybeTitle) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
const tr = removePossibleTable(view, event);
|
const tr = removePossibleTable(view, event);
|
||||||
|
|
||||||
dragging = false;
|
dragging = false;
|
||||||
|
|
||||||
if (tr) {
|
if (tr) {
|
||||||
view.dispatch(tr);
|
view.dispatch(tr);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -187,9 +235,61 @@ export const Dragable = Extension.create({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
mousemove: (view, event) => {
|
mousemove: (view, event) => {
|
||||||
|
if (isMenuVisible) return false;
|
||||||
if (!view.editable || !dragHandleDOM) return false;
|
if (!view.editable || !dragHandleDOM) return false;
|
||||||
|
|
||||||
const dom = event.target;
|
const coords = { left: event.clientX, top: event.clientY };
|
||||||
|
const pos = view.posAtCoords(coords);
|
||||||
|
|
||||||
|
if (!pos || !pos.pos) return false;
|
||||||
|
|
||||||
|
let dom = view.nodeDOM(pos.pos) || view.domAtPos(pos.pos)?.node || event.target;
|
||||||
|
|
||||||
|
const maybeTaskItemOrListItem = findParentNodeClosestToPos(view.state.doc.resolve(pos.pos), (node) =>
|
||||||
|
['taskItem', 'listItem'].some((name) => name === node.type.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dom) {
|
||||||
|
if (dragging) return false;
|
||||||
|
hideDragHandleDOM();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (dom && dom.nodeType === 3) {
|
||||||
|
dom = dom.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中列表项
|
||||||
|
if (maybeTaskItemOrListItem) {
|
||||||
|
while (dom && dom.tagName !== 'LI') {
|
||||||
|
dom = dom.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dom.tagName === 'LI') {
|
||||||
|
if (dom?.parentElement?.childElementCount === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不允许选中整个列表
|
||||||
|
if (dom.tagName === 'UL' || dom.tagName === 'OL') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let maybeReactRenderer: HTMLElement | null = dom;
|
||||||
|
|
||||||
|
while (maybeReactRenderer && !maybeReactRenderer.classList?.contains('react-renderer')) {
|
||||||
|
maybeReactRenderer = maybeReactRenderer.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybeReactRenderer && !maybeReactRenderer?.classList?.contains('node-columns')) {
|
||||||
|
dom = maybeReactRenderer;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
if (!(dom instanceof Element)) {
|
if (!(dom instanceof Element)) {
|
||||||
if (dragging) return false;
|
if (dragging) return false;
|
||||||
|
@ -197,57 +297,66 @@ export const Dragable = Extension.create({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = selectRootNodeByDom(dom, view);
|
const result = selectAncestorNodeByDom(dom, view);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!result ||
|
!result ||
|
||||||
result.node.type.name === 'doc' ||
|
result.node.type.name === 'doc' ||
|
||||||
result.node.type.name === 'title' ||
|
result.node.type.name === 'title' ||
|
||||||
result.node.type.name === 'tableOfContents' ||
|
result.node.type.name === 'tableOfContents'
|
||||||
result.node.type.name === 'column' ||
|
|
||||||
// empty paragraph
|
|
||||||
(result.node.type.name === 'paragraph' && result.node.nodeSize === 2)
|
|
||||||
) {
|
) {
|
||||||
if (dragging) return false;
|
if (dragging) return false;
|
||||||
hideDragHandleDOM();
|
hideDragHandleDOM();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// if (result.el.parentElement?.classList.contains('ProseMirror')) {
|
||||||
* 嵌套在其他节点的 paragraph
|
// if (dragging) return false;
|
||||||
*/
|
// hideDragHandleDOM();
|
||||||
if (result.node.type.name === 'paragraph') {
|
// return false;
|
||||||
const { $from, to } = view.state.selection;
|
// }
|
||||||
const same = $from.sharedDepth(to);
|
|
||||||
if (same != 0) {
|
|
||||||
const pos = $from.before(same);
|
|
||||||
const parent = getNodeAtPos(view.state, pos);
|
|
||||||
|
|
||||||
if (parent && parent.type.name !== 'paragraph') {
|
|
||||||
if (dragging) return false;
|
|
||||||
hideDragHandleDOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeNode = result;
|
activeNode = result;
|
||||||
|
|
||||||
renderDragHandleDOM(view, result.el);
|
renderDragHandleDOM(view, result.el);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
mouseleave: () => {
|
|
||||||
clearTimeout(mouseleaveTimer);
|
|
||||||
mouseleaveTimer = setTimeout(() => {
|
|
||||||
hideDragHandleDOM();
|
|
||||||
}, 400);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
keydown: () => {
|
keydown: () => {
|
||||||
if (!editorView.editable || !dragHandleDOM) return false;
|
if (!editorView.editable || !dragHandleDOM) return false;
|
||||||
hideDragHandleDOM();
|
hideDragHandleDOM();
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
mouseleave: () => {
|
||||||
|
clearTimeout(mouseleaveTimer);
|
||||||
|
mouseleaveTimer = setTimeout(() => {
|
||||||
|
if (!isMenuVisible) {
|
||||||
|
hideDragHandleDOM();
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new PMPlugin({
|
||||||
|
key: new PMPluginKey('AncestorDragablePluginFocusKey'),
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
const usingActiveSelection = !!activeSelection;
|
||||||
|
const selection = state.selection;
|
||||||
|
|
||||||
|
if (selection instanceof NodeSelection) {
|
||||||
|
const { from, to } = selection;
|
||||||
|
|
||||||
|
return DecorationSet.create(state.doc, [
|
||||||
|
Decoration.node(safePos(state, from), safePos(state, to), {
|
||||||
|
class: usingActiveSelection
|
||||||
|
? 'ProseMirror-selectedblocknode-dragable'
|
||||||
|
: 'ProseMirror-selectedblocknode-normal',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecorationSet.empty;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|
||||||
|
&.is-editable {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,3 +85,82 @@ export const selectRootNodeByDom = (dom: Element, view: EditorView): ActiveNode
|
||||||
|
|
||||||
return { node, $pos, el, offset: 1 };
|
return { node, $pos, el, offset: 1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectAncestorNodeByDom = (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 (dom && dom.classList.contains('react-renderer')) {
|
||||||
|
const el = view.nodeDOM(pos) as HTMLElement;
|
||||||
|
|
||||||
|
if (el === dom) {
|
||||||
|
const $pos = view.state.doc.resolve(pos);
|
||||||
|
let node = $pos.node();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodeName =
|
||||||
|
dom?.className
|
||||||
|
?.match(/node-(.+)(\S)?/)?.[1]
|
||||||
|
?.split(' ')
|
||||||
|
?.shift() ?? '';
|
||||||
|
|
||||||
|
if (nodeName) {
|
||||||
|
if (node.type.name !== nodeName) {
|
||||||
|
if ($pos?.nodeAfter?.type?.name === nodeName) {
|
||||||
|
node = $pos.nodeAfter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
$pos,
|
||||||
|
el,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) as HTMLElement;
|
||||||
|
return { node, $pos, el, offset: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue