Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enh(ui): smart picker button at the start of line #6855

Merged
merged 11 commits into from
Jan 22, 2025
Merged
1 change: 1 addition & 0 deletions src/components/Assistant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<template>
<div v-if="showAssistant" class="text-assistant">
<FloatingMenu v-if="$editor"
plugin-key="assistantMenu"
:editor="$editor"
:tippy-options="floatingOptions()"
:should-show="floatingShow"
Expand Down
29 changes: 4 additions & 25 deletions src/components/Editor/PreviewOptions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{{ t('text', 'Show link preview') }}
</NcActionRadio>
<NcActionSeparator />
<NcActionButton close-after-click="true" @click="deleteNode">
<NcActionButton close-after-click @click="deleteNode">
<template #icon>
<DeleteIcon :size="20" />
</template>
Expand Down Expand Up @@ -60,18 +60,6 @@
type: String,
required: true,
},
offset: {
type: Number,
required: true,
},
nodeSize: {
type: Number,
required: true,
},
$editor: {
type: Object,
required: true,
},
},

data() {
Expand All @@ -82,23 +70,14 @@

methods: {
onOpen() {
this.$editor.commands.hideLinkBubble()
this.$emit('open')

Check warning on line 73 in src/components/Editor/PreviewOptions.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/PreviewOptions.vue#L73

Added line #L73 was not covered by tests
},
toggle(type) {
this.open = false
const chain = this.$editor.chain().focus()
.setTextSelection(this.offset + 1)
if (type === 'text-only') {
chain.unsetPreview().run()
return
}
chain.setPreview().run()
this.$emit('toggle', type)

Check warning on line 77 in src/components/Editor/PreviewOptions.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/PreviewOptions.vue#L77

Added line #L77 was not covered by tests
},
deleteNode() {
this.$editor.commands.deleteRange({
from: this.offset,
to: this.offset + this.nodeSize,
})
this.$emit('delete')

Check warning on line 80 in src/components/Editor/PreviewOptions.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/PreviewOptions.vue#L80

Added line #L80 was not covered by tests
},
},
}
Expand Down
43 changes: 43 additions & 0 deletions src/components/Editor/SmartPickerMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
- SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<div contenteditable="false" class="smart-picker-menu-container">
<NcButton :aria-label="t('text', 'Open the Smart Picker')" :type="'tertiary'" @click="$emit('open-smart-picker')">
<template #icon>
<PlusIcon />
</template>
</NcButton>
</div>
</template>

<script>
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import { NcButton } from '@nextcloud/vue'

export default {
name: 'SmartPickerMenu',
components: {
PlusIcon,
NcButton,
},
}

</script>
<style lang="scss" scoped>

div[contenteditable=false] {
padding: 0;
margin: 0;
}

.smart-picker-menu-container {
position: absolute;
width: 0 !important;
left: -84px;
top: 50%;
transform: translate(0, -50%);
}
</style>
7 changes: 0 additions & 7 deletions src/extensions/RichText.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import Mention from './../extensions/Mention.js'
import Search from './../extensions/Search.js'
import OrderedList from './../nodes/OrderedList.js'
import Paragraph from './../nodes/Paragraph.js'
import Placeholder from '@tiptap/extension-placeholder'
import Preview from './../nodes/Preview.js'
import Table from './../nodes/Table.js'
import TaskItem from './../nodes/TaskItem.js'
Expand All @@ -45,7 +44,6 @@ import TrailingNode from './../nodes/TrailingNode.js'
/* eslint-enable import/no-named-as-default */

import { Strong, Italic, Strike, Link, Underline } from './../marks/index.js'
import { translate as t } from '@nextcloud/l10n'

const lowlight = createLowlight(common)
lowlight.registerAlias('plaintext', 'mermaid')
Expand Down Expand Up @@ -112,11 +110,6 @@ export default Extension.create({
relativePath: this.options.relativePath,
}),
LinkBubble,
this.options.editing
? Placeholder.configure({
placeholder: t('text', 'Start writing, or try \'/\' to add, \'@\' to mention…'),
})
: null,
TrailingNode,
]
const additionalExtensionNames = this.options.extensions.map(e => e.name)
Expand Down
7 changes: 3 additions & 4 deletions src/nodes/CodeBlockView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<NcActionButton v-if="hasCode"
data-cy="copy-code"
:aria-label="t('text', 'Copy code')"
:close-after-click="false"
@click="copyCode">
<template #icon>
<Check v-if="copySuccess" :size="20" />
Expand All @@ -36,19 +35,19 @@

<NcActionSeparator v-if="supportPreview" />

<NcActionButton v-if="supportPreview" :close-after-click="true" @click="viewMode = 'code'">
<NcActionButton v-if="supportPreview" close-after-click @click="viewMode = 'code'">
<template #icon>
<CodeBraces :size="20" />
</template>
{{ t('text', 'Source code') }}
</NcActionButton>
<NcActionButton v-if="supportPreview" :close-after-click="true" @click="viewMode = 'preview'">
<NcActionButton v-if="supportPreview" close-after-click @click="viewMode = 'preview'">
<template #icon>
<Eye :size="20" />
</template>
{{ t('text', 'Diagram') }}
</NcActionButton>
<NcActionButton v-if="supportPreview" :close-after-click="true" @click="viewMode = 'side-by-side'">
<NcActionButton v-if="supportPreview" close-after-click @click="viewMode = 'side-by-side'">
<template #icon>
<ViewSplitVertical :size="20" />
</template>
Expand Down
6 changes: 3 additions & 3 deletions src/nodes/Paragraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import TiptapParagraph from '@tiptap/extension-paragraph'
import previewOptions from '../plugins/previewOptions.js'
import currentLineMenu from '../plugins/currentLineMenu.js'

const Paragraph = TiptapParagraph.extend({

Expand Down Expand Up @@ -38,9 +39,8 @@ const Paragraph = TiptapParagraph.extend({

addProseMirrorPlugins() {
return [
previewOptions({
editor: this.editor,
}),
currentLineMenu({ editor: this.editor }),
previewOptions({ editor: this.editor }),
]
},
})
Expand Down
10 changes: 6 additions & 4 deletions src/plugins/LinkBubblePluginView.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,17 @@
if (Object.prototype.toString.call(referenceEl) === '[object Text]') {
referenceEl = referenceEl.parentElement
}
const clientRect = referenceEl?.getBoundingClientRect()

this.#component?.updateProps({
href: domHref(mark),
})

this.tippy?.setProps({
getReferenceClientRect: () => clientRect,
})
const clientRect = referenceEl?.getBoundingClientRect()
if (clientRect) {
this.tippy?.setProps({
getReferenceClientRect: () => clientRect,
})
}

Check warning on line 126 in src/plugins/LinkBubblePluginView.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/LinkBubblePluginView.js#L121-L126

Added lines #L121 - L126 were not covered by tests

this.tippy?.show()
this.addEventListeners()
Expand Down
168 changes: 168 additions & 0 deletions src/plugins/currentLineMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { EditorState, Plugin, PluginKey, Transaction } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import { Editor } from '@tiptap/core'
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<DecorationSet>}
*/
export default function currentLineMenu({ editor }) {
return new Plugin({
key: currentLineMenuKey,

state: {
init(_, state) {
if (!editor.options.editable) {
return { decorations: DecorationSet.empty }
}

Check warning on line 31 in src/plugins/currentLineMenu.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/currentLineMenu.js#L30-L31

Added lines #L30 - L31 were not covered by tests
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 }
}

Check warning on line 41 in src/plugins/currentLineMenu.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/currentLineMenu.js#L40-L41

Added lines #L40 - L41 were not covered by tests
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 contentToInsert = textContent.match(/(^| )$/) ? '/' : ' /'
editor.chain()
.focus()
.setTextSelection(eol)
.insertContent(contentToInsert)
.run()

Check warning on line 165 in src/plugins/currentLineMenu.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/currentLineMenu.js#L157-L165

Added lines #L157 - L165 were not covered by tests
})
return menu.$el
}
6 changes: 3 additions & 3 deletions src/plugins/extractLinkParagraphs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import { isLinkToSelfWithHash } from './../helpers/links.js'
export default function extractLinkParagraphs(doc) {
const paragraphs = []

doc.descendants((node, offset) => {
doc.descendants((node, pos) => {
if (previewPossible(node)) {
paragraphs.push(Object.freeze({
offset,
pos,
nodeSize: node.nodeSize,
type: 'text-only',
}))
} else if (node.type.name === 'preview') {
paragraphs.push(Object.freeze({
offset,
pos,
nodeSize: node.nodeSize,
type: 'link-preview',
}))
Expand Down
Loading
Loading