improve dragable
This commit is contained in:
fantasticit 2023-01-08 14:04:13 +08:00
parent 1d2c9402f2
commit 5d13733d86
3 changed files with 248 additions and 56 deletions

View File

@ -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;
hideDragHandleDOM();
if (!isMenuVisible) {
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;
}, },
}, },
}), }),

View File

@ -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;
} }

View File

@ -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 };
};