Skip to content

Commit

Permalink
Merge pull request #3841 from nextcloud/fix/3812/vue-richtext
Browse files Browse the repository at this point in the history
Merge `@nextcloud/vue-richtext` into `@nextcloud/vue`
  • Loading branch information
julien-nc committed Mar 2, 2023
2 parents fd829de + e5e3a5e commit 0390767
Show file tree
Hide file tree
Showing 28 changed files with 4,409 additions and 341 deletions.
2,112 changes: 1,803 additions & 309 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions package.json
Expand Up @@ -52,9 +52,9 @@
"@nextcloud/l10n": "^2.0.1",
"@nextcloud/logger": "^2.2.1",
"@nextcloud/router": "^2.0.0",
"@nextcloud/vue-richtext": "^2.1.0-beta.6",
"@nextcloud/vue-select": "^3.21.2",
"@skjnldsv/sanitize-svg": "^1.0.2",
"clone": "^2.1.2",
"debounce": "1.2.1",
"emoji-mart-vue-fast": "^12.0.1",
"escape-html": "^1.0.3",
Expand All @@ -64,10 +64,18 @@
"linkify-string": "^4.0.0",
"md5": "^2.3.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"rehype-react": "^7.1.2",
"remark-breaks": "^3.0.2",
"remark-external-links": "^9.0.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"splitpanes": "^2.4.1",
"string-length": "^5.0.1",
"striptags": "^3.2.0",
"tributejs": "^5.1.3",
"unified": "^10.1.2",
"unist-builder": "^3.0.1",
"unist-util-visit": "^4.1.2",
"v-click-outside": "^3.2.0",
"vue": "^2.7.14",
"vue-color": "^2.8.1",
Expand Down Expand Up @@ -124,7 +132,7 @@
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
},
"transformIgnorePatterns": [
"/node_modules/(?!vue-material-design-icons)"
"/node_modules/(?!(.*)/)"
],
"snapshotSerializers": [
"<rootDir>/node_modules/jest-serializer-vue"
Expand Down
Expand Up @@ -148,7 +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 { searchProvider, getLinkWithPicker } from '../NcRichText/index.js'
import Tribute from 'tributejs/dist/tribute.esm.js'
import debounce from 'debounce'
Expand Down Expand Up @@ -300,7 +300,7 @@ export default {
noMatchTemplate: () => t('No link provider found'),
selectTemplate: this.getLink,
// Pass the search results as values
values: (text, cb) => cb(linkProviderSearch(text)),
values: (text, cb) => cb(searchProvider(text)),
// Class added to the menu container
containerClass: 'tribute-container-link',
// Class added to each list item
Expand Down Expand Up @@ -423,7 +423,7 @@ export default {
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)
getLinkWithPicker(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')
Expand Down
99 changes: 99 additions & 0 deletions src/components/NcRichText/NcReferenceList.vue
@@ -0,0 +1,99 @@
<template>
<div class="widgets--list" :class="{'icon-loading': loading }">
<div v-for="reference in displayedReferences" :key="reference.openGraphObject.id">
<NcReferenceWidget :reference="reference" />
</div>
</div>
</template>
<script>
import NcReferenceWidget from './NcReferenceWidget.vue'
import { URL_PATTERN } from './helpers.js'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
export default {
name: 'NcReferenceList',
components: {
NcReferenceWidget,
},
props: {
text: {
type: String,
default: '',
},
referenceData: {
type: Object,
default: null,
},
limit: {
type: Number,
default: 1,
},
},
data() {
return {
references: null,
loading: true,
}
},
computed: {
values() {
return this.referenceData
? this.referenceData
: (this.references ? Object.values(this.references) : [])
},
firstReference() {
return this.values[0] ?? null
},
displayedReferences() {
return this.values.slice(0, this.limit)
},
},
watch: {
text: 'fetch',
},
mounted() {
this.fetch()
},
methods: {
fetch() {
this.loading = true
if (this.referenceData) {
this.loading = false
return
}
if (!(new RegExp(URL_PATTERN).exec(this.text))) {
this.loading = false
return
}
this.resolve().then((response) => {
this.references = response.data.ocs.data.references
this.loading = false
}).catch((error) => {
console.error('Failed to extract references', error)
this.loading = false
})
},
resolve() {
const match = (new RegExp(URL_PATTERN).exec(this.text.trim()))
if (this.limit === 1 && match) {
return axios.get(generateOcsUrl('references/resolve', 2) + `?reference=${encodeURIComponent(match[0])}`)
}
return axios.post(generateOcsUrl('references/extract', 2), {
text: this.text,
resolve: true,
limit: this.limit,
})
},
},
}
</script>
<style lang="scss" scoped>
.widgets--list.icon-loading {
min-height: 44px;
}
</style>
@@ -0,0 +1,67 @@
<template>
<div ref="domElement" />
</template>

<script>
import { renderCustomPickerElement, isCustomPickerElementRegistered, destroyCustomPickerElement } from './customPickerElements.js'
export default {
name: 'NcCustomPickerElement',
props: {
/**
* The reference provider
*/
provider: {
type: Object,
required: true,
},
},
data() {
return {
isRegistered: isCustomPickerElementRegistered(this.provider.id),
renderResult: null,
}
},
mounted() {
if (this.isRegistered) {
this.renderElement()
}
},
beforeDestroy() {
if (this.isRegistered) {
destroyCustomPickerElement(this.provider.id, this.$el, this.renderResult)
}
},
methods: {
renderElement() {
if (this.$refs.domElement) {
this.$refs.domElement.innerHTML = ''
}
const renderFunctionResult = renderCustomPickerElement(this.$refs.domElement, { providerId: this.provider.id, accessible: false })
// this works whether renderCustomPickerElement returns a promise or a value
Promise.resolve(renderFunctionResult).then(result => {
this.renderResult = result
if (this.renderResult.object?._isVue && this.renderResult.object?.$on) {
this.renderResult.object.$on('submit', this.onSubmit)
this.renderResult.object.$on('cancel', this.onCancel)
}
this.renderResult.element.addEventListener('submit', (e) => {
this.onSubmit(e.detail)
})
this.renderResult.element.addEventListener('cancel', this.onCancel)
})
},
onSubmit(value) {
this.$emit('submit', value)
},
onCancel() {
this.$emit('cancel')
},
},
}
</script>
<style lang="scss" scoped>
// nothing yet
</style>
142 changes: 142 additions & 0 deletions src/components/NcRichText/NcReferencePicker/NcProviderList.vue
@@ -0,0 +1,142 @@
<template>
<div class="provider-list">
<NcMultiselect ref="provider-select"
v-model="selectedProvider"
class="provider-list--select"
track-by="id"
label="title"
:placeholder="multiselectPlaceholder"
:options="options"
:internal-search="false"
:clear-on-select="true"
:preserve-search="true"
:option-height="44"
@search-change="query = $event"
@input="onProviderSelected">
<template #option="{option}">
<div v-if="option.isLink" class="provider">
<LinkVariantIcon class="link-icon" :size="20" />
<span>{{ option.title }}</span>
</div>
<div v-else class="provider">
<img class="provider-icon"
:src="option.icon_url">
<NcHighlight class="option-text"
:search="query"
:text="option.title" />
</div>
</template>
</NcMultiselect>
<NcEmptyContent class="provider-list--empty-content">
<template #icon>
<LinkVariantIcon />
</template>
</NcEmptyContent>
</div>
</template>

<script>
import { searchProvider } from './providerHelper.js'
import { isUrl } from './utils.js'
import NcEmptyContent from '../../NcEmptyContent/index.js'
import NcHighlight from '../../NcHighlight/index.js'
import NcMultiselect from '../../NcMultiselect/index.js'
import LinkVariantIcon from 'vue-material-design-icons/LinkVariant.vue'
export default {
name: 'NcProviderList',
components: {
NcMultiselect,
NcHighlight,
NcEmptyContent,
LinkVariantIcon,
},
data() {
return {
selectedProvider: null,
query: '',
// TODO translate?
multiselectPlaceholder: 'Select a link provider',
}
},
computed: {
options() {
const result = []
if (this.query !== '' && isUrl(this.query)) {
result.push({
id: this.query,
title: this.query,
isLink: true,
})
}
result.push(...searchProvider(this.query))
return result
},
},
methods: {
focus() {
this.$nextTick(() => {
this.$refs['provider-select']?.$el?.focus()
})
},
onProviderSelected(p) {
if (p !== null) {
if (p.isLink) {
this.$emit('submit', p.title)
} else {
this.$emit('select-provider', p)
}
this.selectedProvider = null
}
},
},
}
</script>
<style lang="scss" scoped>
.provider-list {
width: 100%;
min-height: 350px;
// multiselect dropdown is wider than the select input
// this avoids overflow
padding-right: 2px;
display: flex;
flex-direction: column;
&--empty-content {
margin-top: auto !important;
margin-bottom: auto !important;
}
&--select {
width: 100%;
.provider {
display: flex;
align-items: center;
height: 28px;
overflow: hidden;
.link-icon {
margin-right: 8px;
}
.provider-icon {
width: 20px;
height: 20px;
object-fit: contain;
margin-right: 8px;
filter: var(--background-invert-if-dark);
}
.option-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
</style>

0 comments on commit 0390767

Please sign in to comment.