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 <jonas@freesources.org>
  • Loading branch information
mejo- committed Jan 16, 2024
1 parent 6a8debd commit 3aae00f
Show file tree
Hide file tree
Showing 6 changed files with 573 additions and 319 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
308 changes: 308 additions & 0 deletions src/components/Link/LinkBubbleView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
<template>
<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>
<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>

<!-- 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 (if unauthenticated or no link preview) -->
<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 { 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'
export default {
name: 'LinkBubbleView',
components: {
CheckIcon,
CloseIcon,
ContentCopyIcon,
NcActionButton,
NcActions,
NcLoadingIcon,
NcReferenceList,
NcTextField,
LinkOffIcon,
PencilIcon,
},
mixins: [
CopyToClipboardMixin,
],
props: {
editor: {
type: Object,
required: true,
},
href: {
type: String,
default: null,
},
text: {
type: String,
default: null,
},
},
data() {
return {
isEditable: false,
edit: false,
newHref: null,
newText: '',
isLoggedIn: !!getCurrentUser(),
}
},
computed: {
key() {
return this.href || 'no-href'
},
copyLinkTooltip() {
if (this.copied) {
if (this.copySuccess) {
return ''
}
return t('text', 'Cannot copy, please copy the link manually')
}
return t('text', 'Copy link to clipboard')
},
},
watch: {
href() {
this.edit = false
this.newHref = null
this.newText = ''
},
},
beforeMount() {
this.isEditable = this.editor.isEditable
this.editor.on('update', ({ editor }) => {
this.isEditable = editor.isEditable
})
},
methods: {
t,
resetBubble() {
this.edit = false
this.newHref = null
this.newText = ''
},
async copyLink() {
await this.copyToClipboard(this.href)
},
startEdit() {
this.edit = true
this.newHref = this.href
this.newText = this.text
},
stopEdit() {
this.edit = false
this.newHref = null
this.newText = ''
},
updateLink() {
if (this.text !== this.newText) {
this.replaceLinkNode(this.newText, this.newHref)
} else if (this.href !== this.newHref) {
this.setLinkUrl(this.newHref)
}
this.stopEdit()
},
replaceLinkNode(text, href) {
// Copy original node and replace text and href
const { selection } = this.editor.state
const { $from } = selection
const linkNode = this.editor.commands.linkNodeFromSelection()
if (!linkNode) {
return
}
const linkNodeJSON = linkNode.toJSON()
// Update href of link mark
const linkIndex = linkNodeJSON.marks.findIndex(m => m.type === 'link')
if (linkIndex !== -1) {
linkNodeJSON.marks[linkIndex].attrs.href = href
}
// Update text of text node
linkNodeJSON.text = text
// Position inside new node. Used to place cursor inside link after replace
const pos = $from.pos - $from.textOffset + 1
// Replace node
this.editor.chain()
.extendMarkRange('link')
.insertContent(linkNodeJSON)
.focus(pos)
.run()
},
setLinkUrl(href) {
this.editor.chain().extendMarkRange('link').setLink({ href }).focus().run()
},
removeLink() {
this.editor.chain().unsetLink().focus().run()
this.stopEdit()
},
},
}
</script>

<style lang="scss" scoped>
.link-view-bubble {
padding: 4px;
width: 342px;
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;
justify-content: flex-end;
}
&__reference-list {
:deep(a.widget-default) {
margin: 0;
border: 0;
border-radius: unset;
}
}
&__edit {
.input-field {
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 3aae00f

Please sign in to comment.