From 808c940521cf328750b8ebecb15dfe70580a5b23 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 22 Oct 2025 15:06:27 +0200 Subject: [PATCH] fix(FloatingButtons): group smartpicker button and drag handle together - The button group follows the cursor now - The button group is top-aligned to hovered paragraph node - The drag handle button has cursor type `grab` - Not displayed on mobile and full width view Also removes block margin from link previews. We already have margin between paragraphs and it makes the floating buttons look off otherwise. Fixes: #7272 Fixes: #7604 Signed-off-by: Jonas --- src/components/Editor/ContentContainer.vue | 36 +---- src/components/Editor/FloatingButtons.vue | 112 +++++++++++++ src/components/Editor/SmartPickerMenu.vue | 48 ------ src/nodes/Paragraph.js | 5 - src/nodes/Preview.vue | 1 + src/plugins/currentLineMenu.js | 177 --------------------- 6 files changed, 121 insertions(+), 258 deletions(-) create mode 100644 src/components/Editor/FloatingButtons.vue delete mode 100644 src/components/Editor/SmartPickerMenu.vue delete mode 100644 src/plugins/currentLineMenu.js diff --git a/src/components/Editor/ContentContainer.vue b/src/components/Editor/ContentContainer.vue index f7740c4fd1c..7d820304581 100644 --- a/src/components/Editor/ContentContainer.vue +++ b/src/components/Editor/ContentContainer.vue @@ -14,13 +14,7 @@ - - - - - + + + diff --git a/src/components/Editor/SmartPickerMenu.vue b/src/components/Editor/SmartPickerMenu.vue deleted file mode 100644 index 7b2313bc773..00000000000 --- a/src/components/Editor/SmartPickerMenu.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - diff --git a/src/nodes/Paragraph.js b/src/nodes/Paragraph.js index 665ed5c8621..468da5ec9c8 100644 --- a/src/nodes/Paragraph.js +++ b/src/nodes/Paragraph.js @@ -4,7 +4,6 @@ */ import TiptapParagraph from '@tiptap/extension-paragraph' -import currentLineMenu from '../plugins/currentLineMenu.js' const Paragraph = TiptapParagraph.extend({ parseHTML() { @@ -42,10 +41,6 @@ const Paragraph = TiptapParagraph.extend({ }, } }, - - addProseMirrorPlugins() { - return [currentLineMenu({ editor: this.editor })] - }, }) export default Paragraph diff --git a/src/nodes/Preview.vue b/src/nodes/Preview.vue index 328719d48b8..deff784391d 100644 --- a/src/nodes/Preview.vue +++ b/src/nodes/Preview.vue @@ -79,6 +79,7 @@ export default { :deep(div.widgets--list a.widget-default) { color: var(--color-main-text); + margin: 0; padding: 0; text-decoration: none; max-width: calc(100vw - 156px); diff --git a/src/plugins/currentLineMenu.js b/src/plugins/currentLineMenu.js deleted file mode 100644 index 731bc4aa53b..00000000000 --- a/src/plugins/currentLineMenu.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { Editor } from '@tiptap/core' -import { EditorState, Plugin, PluginKey, Transaction } from '@tiptap/pm/state' -import { Decoration, DecorationSet } from '@tiptap/pm/view' -import Vue from 'vue' -import SmartPickerMenu from '../components/Editor/SmartPickerMenu.vue' - -export const currentLineMenuKey = new PluginKey('currentLineMenu') - -/** - * Menu for the current line the cursor is on. - * ProseMirror plugin providing a single decoration for the current line. - * - * @param {object} options - options for the plugin - * @param {Editor} options.editor - the tiptap editor - * - * @return {Plugin} - */ -export default function currentLineMenu({ editor }) { - return new Plugin({ - key: currentLineMenuKey, - - state: { - init(_, state) { - if (!editor.options.editable) { - return { decorations: DecorationSet.empty } - } - const currentParagraph = getCurrentParagraph(state) - return { - currentParagraph, - decorations: currentParagraphDecorations( - state.doc, - currentParagraph, - editor, - ), - } - }, - apply(tr, value, _oldState, newState) { - if (!editor.options.editable) { - return { decorations: DecorationSet.empty } - } - const currentParagraph = getCurrentParagraph(newState) - if (!currentParagraph) { - return { decorations: DecorationSet.empty } - } - const decorations = - mapDecorations(value, tr, currentParagraph) - || currentParagraphDecorations( - newState.doc, - currentParagraph, - editor, - ) - return { currentParagraph, decorations } - }, - }, - - props: { - decorations(state) { - return this.getState(state).decorations - }, - }, - }) -} - -/** - * Map the previous deocrations to current document state - * - * Return false if previewParagraphs changes or decorations would get removed. The latter prevents - * lost decorations in case of replacements. - * - * @param {object} value - previous plugin state - * @param {Transaction} tr - current transaction - * @param {object|undefined} currentParagraph - attributes of the current paragraph - * - * @return {false|DecorationSet} - */ -function mapDecorations(value, tr, currentParagraph) { - if (value.currentParagraph?.pos !== currentParagraph.pos) { - return false - } - let removedDecorations = false - const decorations = value.decorations.map(tr.mapping, tr.doc, { - onRemove: () => { - removedDecorations = true - }, - }) - return removedDecorations ? false : decorations -} - -/** - * Get the paragraph node the cursor is on. - * - * @param {EditorState} state - the prosemirror state - * @return {object|undefined} - the current paragraph if the cursor is in one - */ -function getCurrentParagraph({ selection }) { - const { parent, depth } = selection.$anchor - // handle invalid cursor position - if (depth > 1) { - return false - } - const pos = depth === 0 ? 0 : selection.$anchor.before() - const isRootDepth = depth === 1 - const noLinkPickerYet = !parent.textContent.match(/(^| )\/$/) - if ( - isRootDepth - && noLinkPickerYet - && selection.empty - && parent.isTextblock - && !parent.type.spec.code - ) { - return { pos } - } -} - -/** - * Create a menu decorations for the given paragraph - * - * @param {Document} doc - prosemirror doc - * @param {object} currentParagraph - paragraph to decorate - * @param {Editor} editor - tiptap editor - * - * @return {DecorationSet} - */ -function currentParagraphDecorations(doc, currentParagraph, editor) { - if (!currentParagraph) { - return DecorationSet.empty - } - const decorations = [decorationForCurrentParagraph(currentParagraph, editor)] - return DecorationSet.create(doc, decorations) -} - -/** - * Create a decoration for the currentParagraph - * - * @param {object} currentParagraph to decorate - * @param {Editor} editor - tiptap editor - * - * @return {Decoration} - */ -function decorationForCurrentParagraph(currentParagraph, editor) { - return Decoration.widget( - currentParagraph.pos + 1, - menuForCurrentParagraph(editor), - { side: -1 }, - ) -} - -/** - * Create a menu element for the given currentParagraph - * - * @param {Editor} editor - tiptap editor - * - * @return {Element} - */ -function menuForCurrentParagraph(editor) { - const el = document.createElement('div') - const Component = Vue.extend(SmartPickerMenu) - const menu = new Component() - menu.$mount(el) - menu.$on('open-smart-picker', () => { - const { selection } = editor.state - const { textContent } = selection.$anchor.parent - const eol = selection.$anchor.end() - const chain = editor.chain().focus().setTextSelection(eol) - if (textContent.trim() === '') { - chain.insertContent('/').run() - } else { - chain.splitBlock().insertContent('/').run() - } - }) - return menu.$el -}