mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-13 00:02:30 +10:00
Editor drag handle
* add darkmode border to navbar
This commit is contained in:
@ -6,7 +6,7 @@
|
|||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/*border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));*/
|
border-right: rem(1px) solid light-dark('', var(--mantine-color-dark-4));
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
|
|||||||
@ -1,223 +0,0 @@
|
|||||||
import { Extension } from '@tiptap/core';
|
|
||||||
import { NodeSelection, Plugin } from '@tiptap/pm/state';
|
|
||||||
// @ts-ignore
|
|
||||||
import { __serializeForClipboard as serializeForClipboard, EditorView } from '@tiptap/pm/view';
|
|
||||||
|
|
||||||
export interface DragHandleOptions {
|
|
||||||
dragHandleWidth: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNode(node) {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
function absoluteRect(node) {
|
|
||||||
const data = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
top: data.top,
|
|
||||||
left: data.left,
|
|
||||||
width: data.width,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
|
||||||
return document
|
|
||||||
.elementsFromPoint(coords.x, coords.y)
|
|
||||||
.find(
|
|
||||||
(elem: HTMLElement) =>
|
|
||||||
elem.parentElement?.matches?.('.ProseMirror') ||
|
|
||||||
elem.matches(
|
|
||||||
[
|
|
||||||
'li',
|
|
||||||
'p:not(:first-child)',
|
|
||||||
'pre',
|
|
||||||
'blockquote',
|
|
||||||
'h1, h2, h3',
|
|
||||||
'[data-type=horizontalRule]',
|
|
||||||
].join(', '),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nodePosAtDOM(node: Element, view: EditorView) {
|
|
||||||
const boundingRect = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
return view.posAtCoords({
|
|
||||||
left: boundingRect.left + 1,
|
|
||||||
top: boundingRect.top + 1,
|
|
||||||
})?.inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DragHandle(options: DragHandleOptions) {
|
|
||||||
function handleDragStart(event: DragEvent, view: EditorView) {
|
|
||||||
view.focus();
|
|
||||||
|
|
||||||
if (!event.dataTransfer) return;
|
|
||||||
|
|
||||||
const node = nodeDOMAtCoords({
|
|
||||||
x: event.clientX + 50 + options.dragHandleWidth,
|
|
||||||
y: event.clientY,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!(node instanceof Element)) return;
|
|
||||||
|
|
||||||
const nodePos = nodePosAtDOM(node, view);
|
|
||||||
if (!nodePos) return;
|
|
||||||
|
|
||||||
view.dispatch(
|
|
||||||
view.state.tr.setSelection(
|
|
||||||
NodeSelection.create(view.state.doc, nodePos),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const slice = view.state.selection.content();
|
|
||||||
const { dom, text } = serializeForClipboard(view, slice);
|
|
||||||
|
|
||||||
event.dataTransfer.clearData();
|
|
||||||
event.dataTransfer.setData('text/html', dom.innerHTML);
|
|
||||||
event.dataTransfer.setData('text/plain', text);
|
|
||||||
event.dataTransfer.effectAllowed = 'copyMove';
|
|
||||||
|
|
||||||
event.dataTransfer.setDragImage(node, 0, 0);
|
|
||||||
|
|
||||||
view.dragging = { slice, move: event.ctrlKey };
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClick(event: MouseEvent, view: EditorView) {
|
|
||||||
view.focus();
|
|
||||||
view.dom.classList.remove('dragging');
|
|
||||||
|
|
||||||
const node = nodeDOMAtCoords({
|
|
||||||
x: event.clientX + 50 + options.dragHandleWidth,
|
|
||||||
y: event.clientY,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!(node instanceof Element)) return;
|
|
||||||
|
|
||||||
const nodePos = nodePosAtDOM(node, view);
|
|
||||||
if (!nodePos) return;
|
|
||||||
|
|
||||||
view.dispatch(
|
|
||||||
view.state.tr.setSelection(
|
|
||||||
NodeSelection.create(view.state.doc, nodePos),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dragHandleElement: HTMLElement | null = null;
|
|
||||||
|
|
||||||
function hideDragHandle() {
|
|
||||||
if (dragHandleElement) {
|
|
||||||
dragHandleElement.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDragHandle() {
|
|
||||||
if (dragHandleElement) {
|
|
||||||
dragHandleElement.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return new Plugin({
|
|
||||||
view: (view) => {
|
|
||||||
dragHandleElement = document.createElement('div');
|
|
||||||
dragHandleElement.draggable = true;
|
|
||||||
dragHandleElement.dataset.dragHandle = '';
|
|
||||||
dragHandleElement.classList.add('drag-handle');
|
|
||||||
dragHandleElement.addEventListener('dragstart', (e) => {
|
|
||||||
handleDragStart(e, view);
|
|
||||||
});
|
|
||||||
dragHandleElement.addEventListener('click', (e) => {
|
|
||||||
handleClick(e, view);
|
|
||||||
});
|
|
||||||
|
|
||||||
hideDragHandle();
|
|
||||||
|
|
||||||
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy: () => {
|
|
||||||
dragHandleElement?.remove?.();
|
|
||||||
dragHandleElement = null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
handleDOMEvents: {
|
|
||||||
mousemove: (view, event) => {
|
|
||||||
if (!view.editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = nodeDOMAtCoords({
|
|
||||||
x: event.clientX + 50 + options.dragHandleWidth,
|
|
||||||
y: event.clientY,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!(node instanceof Element)) {
|
|
||||||
hideDragHandle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const compStyle = window.getComputedStyle(node);
|
|
||||||
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
|
||||||
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
|
||||||
|
|
||||||
const rect = absoluteRect(node);
|
|
||||||
|
|
||||||
rect.top += (lineHeight - 24) / 2;
|
|
||||||
rect.top += paddingTop;
|
|
||||||
// Li markers
|
|
||||||
if (
|
|
||||||
node.matches('ul:not([data-type=taskList]) li, ol li')
|
|
||||||
) {
|
|
||||||
rect.left -= options.dragHandleWidth;
|
|
||||||
}
|
|
||||||
rect.width = options.dragHandleWidth;
|
|
||||||
|
|
||||||
if (!dragHandleElement) return;
|
|
||||||
|
|
||||||
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
|
||||||
dragHandleElement.style.top = `${rect.top}px`;
|
|
||||||
showDragHandle();
|
|
||||||
},
|
|
||||||
keydown: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
mousewheel: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
// dragging class is used for CSS
|
|
||||||
dragstart: (view) => {
|
|
||||||
view.dom.classList.add('dragging');
|
|
||||||
},
|
|
||||||
drop: (view) => {
|
|
||||||
view.dom.classList.remove('dragging');
|
|
||||||
},
|
|
||||||
dragend: (view) => {
|
|
||||||
view.dom.classList.remove('dragging');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DragAndDropOptions {
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const DragAndDrop = Extension.create<DragAndDropOptions>({
|
|
||||||
name: 'dragAndDrop',
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [
|
|
||||||
DragHandle({
|
|
||||||
dragHandleWidth: 24,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default DragAndDrop;
|
|
||||||
@ -9,7 +9,6 @@ import { Superscript } from "@tiptap/extension-superscript";
|
|||||||
import SubScript from "@tiptap/extension-subscript";
|
import SubScript from "@tiptap/extension-subscript";
|
||||||
import { Highlight } from "@tiptap/extension-highlight";
|
import { Highlight } from "@tiptap/extension-highlight";
|
||||||
import { Typography } from "@tiptap/extension-typography";
|
import { Typography } from "@tiptap/extension-typography";
|
||||||
import DragAndDrop from "@/features/editor/extensions/drag-handle";
|
|
||||||
import { TextStyle } from "@tiptap/extension-text-style";
|
import { TextStyle } from "@tiptap/extension-text-style";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
import SlashCommand from "@/features/editor/extensions/slash-command";
|
import SlashCommand from "@/features/editor/extensions/slash-command";
|
||||||
@ -17,6 +16,7 @@ import { Collaboration } from "@tiptap/extension-collaboration";
|
|||||||
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
||||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
import { Comment, TrailingNode } from "@docmost/editor-ext";
|
import { Comment, TrailingNode } from "@docmost/editor-ext";
|
||||||
|
import GlobalDragHandle from "tiptap-extension-global-drag-handle";
|
||||||
|
|
||||||
export const mainExtensions = [
|
export const mainExtensions = [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
@ -43,7 +43,7 @@ export const mainExtensions = [
|
|||||||
}),
|
}),
|
||||||
Typography,
|
Typography,
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
DragAndDrop,
|
GlobalDragHandle,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
Color,
|
Color,
|
||||||
SlashCommand,
|
SlashCommand,
|
||||||
|
|||||||
@ -37,6 +37,7 @@
|
|||||||
"@tiptap/starter-kit": "^2.2.4",
|
"@tiptap/starter-kit": "^2.2.4",
|
||||||
"@tiptap/suggestion": "^2.2.4",
|
"@tiptap/suggestion": "^2.2.4",
|
||||||
"fractional-indexing-jittered": "^0.9.1",
|
"fractional-indexing-jittered": "^0.9.1",
|
||||||
|
"tiptap-extension-global-drag-handle": "^0.1.6",
|
||||||
"y-indexeddb": "^9.0.12",
|
"y-indexeddb": "^9.0.12",
|
||||||
"yjs": "^13.6.14"
|
"yjs": "^13.6.14"
|
||||||
},
|
},
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -101,6 +101,9 @@ importers:
|
|||||||
fractional-indexing-jittered:
|
fractional-indexing-jittered:
|
||||||
specifier: ^0.9.1
|
specifier: ^0.9.1
|
||||||
version: 0.9.1
|
version: 0.9.1
|
||||||
|
tiptap-extension-global-drag-handle:
|
||||||
|
specifier: ^0.1.6
|
||||||
|
version: 0.1.6
|
||||||
y-indexeddb:
|
y-indexeddb:
|
||||||
specifier: ^9.0.12
|
specifier: ^9.0.12
|
||||||
version: 9.0.12(yjs@13.6.14)
|
version: 9.0.12(yjs@13.6.14)
|
||||||
@ -11035,6 +11038,10 @@ packages:
|
|||||||
'@popperjs/core': 2.11.8
|
'@popperjs/core': 2.11.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tiptap-extension-global-drag-handle@0.1.6:
|
||||||
|
resolution: {integrity: sha512-kjRFd/glJGTHEglHPNWtML1iINuEBG4eWvWbNIuuXE2LbylFXYHKVgLvZgxilqXSBH/o+lKDEp7/iL3lKvnxGw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tmp@0.0.33:
|
/tmp@0.0.33:
|
||||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||||
engines: {node: '>=0.6.0'}
|
engines: {node: '>=0.6.0'}
|
||||||
|
|||||||
Reference in New Issue
Block a user