Skip to content

Commit

Permalink
feat(editor): Use custom implementation of a bubble plugin for the li…
Browse files Browse the repository at this point in the history
…nk bubble

Signed-off-by: Jonas <[email protected]>
  • Loading branch information
mejo- committed Jan 15, 2024
1 parent 67bcae5 commit 42256a8
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 121 deletions.
3 changes: 0 additions & 3 deletions src/components/Editor/ContentContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
role="document"
class="editor__content text-editor__content"
:editor="$editor" />
<LinkViewBubble />
<div class="text-editor__content-wrapper__right" />
</div>
</template>
Expand All @@ -44,14 +43,12 @@ import { EditorContent } from '@tiptap/vue-2'
import { useEditorMixin } from '../Editor.provider.js'
import { useOutlineStateMixin } from './Wrapper.provider.js'
import EditorOutline from './EditorOutline.vue'
import LinkViewBubble from '../Link/LinkViewBubble.vue'
export default {
name: 'ContentContainer',
components: {
EditorContent,
EditorOutline,
LinkViewBubble,
},
mixins: [useEditorMixin, useOutlineStateMixin],
computed: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,103 +1,108 @@
<template>
<BubbleMenu v-if="$editor"
ref="bubbleContainer"
:editor="$editor"
:should-show="shouldShow"
:tippy-options="tippyOptions()"
class="link-view-bubble">
<div :key="key">
<div class="link-view-bubble__header">
<!-- copy link -->
<NcActions>
<NcActionButton :title="copyLinkTooltip"
:aria-label="copyLinkTooltip"
@click="copyLink">
<div :key="key" class="link-view-bubble">
<!-- link header with buttons -->
<div class="link-view-bubble__header">
<!-- copy link -->
<NcActions>
<NcActionButton :title="copyLinkTooltip"
:aria-label="copyLinkTooltip"
@click="copyLink">
<template #icon>
<CheckIcon v-if="copySuccess" :size="20" />
<NcLoadingIcon v-else-if="copyLoading" :size="20" />
<ContentCopyIcon v-else :size="20" />
</template>
</NcActionButton>
</NcActions>

<!-- edit/save -->
<template v-if="isEditable">
<NcActions v-if="!edit">
<NcActionButton :title="t('text', 'Edit link')"
:aria-label="t('text', 'Edit link')"
@click="startEdit">
<template #icon>
<CheckIcon v-if="copySuccess" :size="20" />
<NcLoadingIcon v-else-if="copyLoading" :size="20" />
<ContentCopyIcon v-else :size="20" />
<PencilIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
<NcActions v-else>
<NcActionButton :title="t('text', 'Save changes')"
:aria-label="t('text', 'Save changes')"
@click="updateLink">
<template #icon>
<CheckIcon :size="20" />
</template>
</NcActionButton>
</NcActions>

<!-- edit/save -->
<template v-if="isEditable">
<NcActions v-if="!edit">
<NcActionButton :title="t('text', 'Edit link')"
:aria-label="t('text', 'Edit link')"
@click="startEdit">
<template #icon>
<PencilIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
<NcActions v-else>
<NcActionButton :title="t('text', 'Save changes')"
:aria-label="t('text', 'Save changes')"
@click="updateLink">
<template #icon>
<CheckIcon :size="20" />
</template>
</NcActionButton>
</NcActions>

<!-- remove link / dismiss changes -->
<NcActions v-if="!edit">
<NcActionButton :title="t('text', 'Remove link')"
:aria-label="t('text', 'Remove link')"
@click="removeLink">
<template #icon>
<LinkOffIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
<NcActions v-else>
<NcActionButton :title="t('text', 'Cancel')"
:aria-label="t('text', 'Cancel')"
@click="stopEdit">
<template #icon>
<CloseIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
</template>
</div>
<div v-if="isEditable && edit" class="link-view-bubble__edit">
<NcTextField name="text"
:label="t('text', 'Text')"
:value.sync="newText"
@keypress.enter.prevent="updateLink" />
<NcTextField name="newHref"
:label="t('text', 'URL')"
:value.sync="newHref"
@keypress.enter.prevent="updateLink" />
</div>
<NcReferenceList v-else
:text="href"
:limit="1"
class="link-view-bubble__reference-list" />
<!-- remove link / dismiss changes -->
<NcActions v-if="!edit">
<NcActionButton :title="t('text', 'Remove link')"
:aria-label="t('text', 'Remove link')"
@click="removeLink">
<template #icon>
<LinkOffIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
<NcActions v-else>
<NcActionButton :title="t('text', 'Cancel')"
:aria-label="t('text', 'Cancel')"
@click="stopEdit">
<template #icon>
<CloseIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
</template>
</div>
</BubbleMenu>

<!-- link edit form -->
<div v-if="isEditable && edit" class="link-view-bubble__edit">
<NcTextField name="text"
:label="t('text', 'Text')"
:value.sync="newText"
@keypress.enter.prevent="updateLink" />
<NcTextField name="newHref"
:label="t('text', 'URL')"
:value.sync="newHref"
@keypress.enter.prevent="updateLink" />
</div>

<!-- link preview (if authenticated) -->
<NcReferenceList v-else-if="isLoggedIn"
:text="href"
:limit="1"
class="link-view-bubble__reference-list" />

<!-- link with URL (is unauthenticated) -->
<a v-else :href="href" rel="noopener noreferrer" target="_blank" class="href-widget">
<div class="href-widget--details">
<p class="href-widget--name">{{ href }}</p>
<p class="href-widget--link">{{ href }}</p>
</div>
</a>
</div>
</template>

<script>
import { BubbleMenu } from '@tiptap/vue-2'
import { NcActionButton, NcActions, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
import { NcReferenceList } from '@nextcloud/vue/dist/Components/NcRichText.js'
import { getCurrentUser } from '@nextcloud/auth'
import { translate as t } from '@nextcloud/l10n'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
import ContentCopyIcon from 'vue-material-design-icons/ContentCopy.vue'
import LinkOffIcon from 'vue-material-design-icons/LinkOff.vue'
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin.js'
import { useEditorMixin } from '../Editor.provider.js'
export default {
name: 'LinkViewBubble',
name: 'LinkBubbleView',
components: {
BubbleMenu,
CheckIcon,
CloseIcon,
ContentCopyIcon,
Expand All @@ -112,17 +117,30 @@ export default {
mixins: [
CopyToClipboardMixin,
useEditorMixin,
],
props: {
editor: {
type: Object,
required: true,
},
href: {
type: String,
default: null,
},
text: {
type: String,
default: null,
},
},
data() {
return {
isEditable: false,
edit: false,
href: null,
newHref: null,
text: '',
newText: '',
isLoggedIn: !!getCurrentUser(),
}
},
Expand All @@ -144,25 +162,28 @@ export default {
watch: {
href() {
console.debug('LinkBubbleView href changes', this.href, this.text)
this.edit = false
this.newHref = null
this.newText = ''
},
},
beforeMount() {
this.isEditable = this.$editor.isEditable
this.$editor.on('update', ({ editor }) => {
this.isEditable = this.editor.isEditable
this.editor.on('update', ({ editor }) => {
this.isEditable = editor.isEditable
})
},
methods: {
t,
/**
* Get text node with link mark from selection
*/
textLinkNodeFromSelection() {
const { selection } = this.$editor.state
const { selection } = this.editor.state
const { $from, $to } = selection
const node = $from.parent.child($from.index())
Expand All @@ -180,42 +201,13 @@ export default {
return node
},
shouldShow({ view, state, from, to }) {
let node
try {
node = this.textLinkNodeFromSelection()
} catch (error) {
this.resetBubble()
return false
}
const hasBubbleFocus = this.$refs.bubbleContainer.$el.contains(document.activeElement)
const hasEditorFocus = view.hasFocus() || hasBubbleFocus
if (!hasEditorFocus) {
this.resetBubble()
return false
}
this.text = node.textContent
this.href = node.marks.find(m => m.type.name === 'link').attrs.href
return true
},
resetBubble() {
console.debug('reset link view bubble')
this.edit = false
this.href = null
this.newHref = null
this.text = ''
this.newText = ''
},
tippyOptions() {
return {
placement: 'bottom',
duration: 100,
}
},
async copyLink() {
await this.copyToClipboard(this.href)
},
Expand All @@ -237,14 +229,13 @@ export default {
this.replaceLinkNode(this.newText, this.newHref)
} else if (this.href !== this.newHref) {
this.setLinkUrl(this.newHref)
this.href = this.newHref
}
this.stopEdit()
},
replaceLinkNode(text, href) {
// Copy original node and replace text and href
const { selection } = this.$editor.state
const { selection } = this.editor.state
const { $from } = selection
let textNodeJSON
Expand All @@ -267,19 +258,19 @@ export default {
const pos = $from.pos - $from.textOffset + 1
// Replace node
this.$editor.chain()
this.editor.chain()
.extendMarkRange('link')
.insertContent(textNodeJSON)
.focus(pos)
.run()
},
setLinkUrl(href) {
this.$editor.chain().extendMarkRange('link').setLink({ href }).focus().run()
this.editor.chain().extendMarkRange('link').setLink({ href }).focus().run()
},
removeLink() {
this.$editor.chain().unsetLink().focus().run()
this.editor.chain().unsetLink().focus().run()
this.stopEdit()
},
},
Expand All @@ -293,6 +284,7 @@ export default {
background-color: var(--color-main-background);
border-radius: var(--border-radius-large);
filter: drop-shadow(0 1px 10px var(--color-box-shadow));
box-sizing: initial !important;
&__header {
display: flex;
Expand All @@ -312,5 +304,28 @@ export default {
margin-bottom: 12px;
}
}
.href-widget {
width: 100%;
display: flex;
&--details {
padding: calc(var(--default-grid-baseline, 4px) * 2);
width: 60%;
}
&--name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
&--link {
color: var(--color-text-maxcontrast);
overflow: hidden;
text-overflow: ellipsis;
}
}
}
</style>
Loading

0 comments on commit 42256a8

Please sign in to comment.