-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(editor): Use custom implementation of a bubble plugin for the li…
…nk bubble Signed-off-by: Jonas <jonas@freesources.org>
- Loading branch information
Showing
6 changed files
with
573 additions
and
319 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.