Skip to content

Commit

Permalink
Merge pull request #3708 from nextcloud/enh/noid/link-picker-in-richc…
Browse files Browse the repository at this point in the history
…ontenteditable-with-tribute

Add link picker in RichContentEditable with tribute
  • Loading branch information
julien-nc authored Feb 22, 2023
2 parents 7ac495d + 0c58c3c commit cf3c71d
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 5 deletions.
3 changes: 3 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ msgstr ""
msgid "No emoji found"
msgstr ""

msgid "No link provider found"
msgstr ""

msgid "No results"
msgstr ""

Expand Down
139 changes: 138 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@nextcloud/l10n": "^2.0.1",
"@nextcloud/logger": "^2.2.1",
"@nextcloud/router": "^2.0.0",
"@nextcloud/vue-richtext": "^2.1.0-beta.5",
"@nextcloud/vue-select": "^3.21.2",
"@skjnldsv/sanitize-svg": "^1.0.2",
"debounce": "1.2.1",
Expand Down
94 changes: 90 additions & 4 deletions src/components/NcRichContenteditable/NcRichContenteditable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ import NcAutoCompleteResult from './NcAutoCompleteResult.vue'
import richEditor from '../../mixins/richEditor/index.js'
import Tooltip from '../../directives/Tooltip/index.js'
import { emojiSearch, emojiAddRecent } from '../../functions/emoji/index.js'
import { linkProviderSearch, getLink } from '../../functions/linkPicker/index.js'
import Tribute from 'tributejs/dist/tribute.esm.js'
import debounce from 'debounce'
Expand Down Expand Up @@ -227,6 +228,14 @@ export default {
type: Boolean,
default: true,
},
/**
* Enable or disable link autocompletion
*/
linkAutocomplete: {
type: Boolean,
default: true,
},
},
emits: [
Expand Down Expand Up @@ -278,6 +287,25 @@ export default {
// Class added to each list item
itemClass: 'tribute-container-emoji__item',
},
linkOptions: {
trigger: '/',
// Don't use the tribute search function at all
// We pass search results as values (see below)
lookup: (result, query) => query,
// Where to inject the menu popup
menuContainer: this.menuContainer,
// Popup mention autocompletion templates
menuItemTemplate: item => `<img class="tribute-container-link__item__icon" src="${item.original.icon_url}"> <span class="tribute-container-link__item__label">${item.original.title}</span>`,
// Hide if no results
noMatchTemplate: () => t('No link provider found'),
selectTemplate: this.getLink,
// Pass the search results as values
values: (text, cb) => cb(linkProviderSearch(text)),
// Class added to the menu container
containerClass: 'tribute-container-link',
// Class added to each list item
itemClass: 'tribute-container-link__item',
},
// Represent the raw untrimmed text of the contenteditable
// serves no other purpose than to check whether the
Expand Down Expand Up @@ -367,6 +395,11 @@ export default {
this.emojiTribute.attach(this.$el)
}
if (this.linkAutocomplete) {
this.linkTribute = new Tribute(this.linkOptions)
this.linkTribute.attach(this.$el)
}
// Update default value
this.updateContent(this.value)
Expand All @@ -381,9 +414,40 @@ export default {
if (this.emojiTribute) {
this.emojiTribute.detach(this.$el)
}
if (this.linkTribute) {
this.linkTribute.detach(this.$el)
}
},
methods: {
getLink(item) {
// there is no way to get a tribute result asynchronously
// so we immediately insert a node and replace it when the result comes
getLink(item.original.id)
.then(link => {
// replace dummy temp element by a text node which contains the link
const tmpElem = document.getElementById('tmp-link-result-node')
const newElem = document.createTextNode(link)
tmpElem.replaceWith(newElem)
this.setCursorAfter(newElem)
this.updateValue(this.$refs.contenteditable.innerHTML)
})
.catch((error) => {
console.debug('Link picker promise rejected:', error)
const tmpElem = document.getElementById('tmp-link-result-node')
this.setCursorAfter(tmpElem)
tmpElem.remove()
})
return '<span id="tmp-link-result-node"></span>'
},
setCursorAfter(element) {
const range = document.createRange()
range.setEndAfter(element)
range.collapse()
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
},
/**
* Re-emit the input event to the parent
*
Expand Down Expand Up @@ -527,7 +591,7 @@ export default {
// Prevent submitting if autocompletion menu
// is opened or length is over maxlength
if (this.multiline || this.isOverMaxlength
|| this.autocompleteTribute.isActive || this.emojiTribute.isActive) {
|| this.autocompleteTribute.isActive || this.emojiTribute.isActive || this.linkTribute.isActive) {
return
}
Expand Down Expand Up @@ -611,7 +675,7 @@ export default {
</style>
<style lang="scss">
.tribute-container, .tribute-container-emoji {
.tribute-container, .tribute-container-emoji, .tribute-container-link {
z-index: 9000;
overflow: auto;
min-width: 250px;
Expand All @@ -627,12 +691,12 @@ export default {
box-shadow: 0 1px 5px var(--color-box-shadow);
}
.tribute-container-emoji {
.tribute-container-emoji, .tribute-container-link {
min-width: 200px;
max-width: 200px;
padding: 4px;
// Show maximum 5 entries and a half to show scroll
max-height: 34.5px * 5 + math.div(29.5px, 2);
max-height: 35px * 5 + math.div(35px, 2) !important;
&__item {
border-radius: 8px;
Expand Down Expand Up @@ -665,4 +729,26 @@ export default {
}
}
.tribute-container-link {
min-width: 200px;
max-width: 300px;
&__item {
display: flex;
align-items: center;
&__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__icon {
margin: auto 0;
width: 20px;
height: 20px;
object-fit: contain;
padding-right: 8px;
filter: var(--background-invert-if-dark);
}
}
}
</style>
27 changes: 27 additions & 0 deletions src/functions/linkPicker/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @copyright Copyright (c) 2023 Julien Veyssier <[email protected]>
*
* @author Julien Veyssier <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { searchProvider, getLinkWithPicker } from '@nextcloud/vue-richtext'

export const linkProviderSearch = searchProvider

export const getLink = getLinkWithPicker

0 comments on commit cf3c71d

Please sign in to comment.