Merge branch 'master' into feat/bread

Signed-off-by: John Molakvoæ <skjnldsv@users.noreply.github.com>
This commit is contained in:
John Molakvoæ 2023-03-29 10:40:53 +02:00 коммит произвёл GitHub
Родитель 33b28346dc 527eaf6c96
Коммит 50f3444878
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 476 добавлений и 147 удалений

Просмотреть файл

@ -2,6 +2,16 @@
All notable changes to this project will be documented in this file.
## [v7.8.5](https://github.com/nextcloud/nextcloud-vue/tree/v7.8.5) (2023-03-28)
[Full Changelog](https://github.com/nextcloud/nextcloud-vue/compare/v7.8.4...v7.8.5)
### :bug: Fixed bugs
- fix\(NcRichContenteditable\): Completely stop event propagation for keyup events [\#3937](https://github.com/nextcloud/nextcloud-vue/pull/3937) ([nickvergessen](https://github.com/nickvergessen))
- fix\(NcRichText\): Match IP addresses as links [\#3935](https://github.com/nextcloud/nextcloud-vue/pull/3935) ([nickvergessen](https://github.com/nickvergessen))
- fix\(NcRichText\): Fix NcRichText style [\#3932](https://github.com/nextcloud/nextcloud-vue/pull/3932) ([julien-nc](https://github.com/julien-nc))
## [v7.8.4](https://github.com/nextcloud/nextcloud-vue/tree/v7.8.4) (2023-03-24)
[Full Changelog](https://github.com/nextcloud/nextcloud-vue/compare/v7.8.3...v7.8.4)

108
package-lock.json сгенерированный
Просмотреть файл

@ -1,19 +1,19 @@
{
"name": "@nextcloud/vue",
"version": "7.8.4",
"version": "7.8.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@nextcloud/vue",
"version": "7.8.4",
"version": "7.8.5",
"license": "AGPL-3.0",
"dependencies": {
"@floating-ui/dom": "^1.1.0",
"@nextcloud/auth": "^2.0.0",
"@nextcloud/axios": "^2.0.0",
"@nextcloud/browser-storage": "^0.2.0",
"@nextcloud/calendar-js": "^5.0.3",
"@nextcloud/calendar-js": "^6.0.0",
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^4.0.0",
"@nextcloud/event-bus": "^3.0.0",
@ -1887,6 +1887,15 @@
"node": ">= 0.12"
}
},
"node_modules/@cypress/request/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@cypress/vue2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cypress/vue2/-/vue2-2.0.1.tgz",
@ -2949,16 +2958,16 @@
}
},
"node_modules/@nextcloud/calendar-js": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-js/-/calendar-js-5.0.4.tgz",
"integrity": "sha512-/vQlQZL59wSEI8YRhae1vumSq4iY7dxueytH7HsPzdE8C1iX3qJdMANQflSQfFpd8u9gUfuyRD+/RsguKjNeKw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-js/-/calendar-js-6.0.0.tgz",
"integrity": "sha512-kZBRFIG8J3TNU6K92iEpNzBa3r9JbpCr1MZFJHqVy/5+xTtQG9FqsHhqUWptPwLEBhUNMwN+oCCa7QJAnBKKyg==",
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"peerDependencies": {
"ical.js": "^1.5.0",
"uuid": "^8.3.2"
"uuid": "^9.0.0"
}
},
"node_modules/@nextcloud/capabilities": {
@ -3168,7 +3177,7 @@
},
"node_modules/@nextcloud/webpack-vue-config": {
"version": "5.5.0",
"resolved": "git+ssh://git@github.com/nextcloud/webpack-vue-config.git#62f0c1a419579ed950ba39ecbd7ae7119fb565a1",
"resolved": "git+ssh://git@github.com/nextcloud/webpack-vue-config.git#841abf3c3505d17d478b2149004448127f0bcc29",
"integrity": "sha512-bKlGYEqblSiSHNmpaGM9fz/f9v6JNwHp63V63yaI26gE0Zs+DpZSzJWC6HWWbJ1BgmoKT7wLN1GJc4W/NxvnxQ==",
"dev": true,
"license": "AGPL-3.0-or-later",
@ -8126,9 +8135,9 @@
"dev": true
},
"node_modules/cypress": {
"version": "12.8.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.8.1.tgz",
"integrity": "sha512-lIFbKdaSYAOarNLHNFa2aPZu6YSF+8UY4VRXMxJrFUnk6RvfG0AWsZ7/qle/aIz30TNUD4aOihz2ZgS4vuQVSA==",
"version": "12.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.9.0.tgz",
"integrity": "sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@ -9655,9 +9664,9 @@
}
},
"node_modules/eslint-plugin-cypress": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz",
"integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.1.tgz",
"integrity": "sha512-THjc7IT3S9H4KwmRhzAhMGQaEqy78/7W75He/gBhJEH0vIuAY16vOI4YSliDo/ZY+Wm6DtvMHR+8uVvICcI3Lw==",
"dev": true,
"dependencies": {
"globals": "^11.12.0"
@ -15899,9 +15908,9 @@
"dev": true
},
"node_modules/linkify-string": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/linkify-string/-/linkify-string-4.1.0.tgz",
"integrity": "sha512-mw4KyPoE/vP0lamGbFFtDsutxOw0b+3g2/lH5bwS7X4tRHQyLBoJ60avPVGUoHfU8G1bLS329u13hhpxBIqFiA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/linkify-string/-/linkify-string-4.1.1.tgz",
"integrity": "sha512-9+kj8xr7GLiyNyO9ri7lIxq2ixVYjjqvtomPQpeYNNT56/PxQq6utzXFLm8HxOaGTiMpimj1UAQWwYYPV88L1g==",
"peerDependencies": {
"linkifyjs": "^4.0.0"
}
@ -21504,6 +21513,15 @@
"ms": "^2.1.1"
}
},
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@ -23958,9 +23976,10 @@
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"peer": true,
"bin": {
"uuid": "dist/bin/uuid"
}
@ -28432,6 +28451,12 @@
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true
}
}
},
@ -29257,9 +29282,9 @@
"dev": true
},
"@nextcloud/calendar-js": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-js/-/calendar-js-5.0.4.tgz",
"integrity": "sha512-/vQlQZL59wSEI8YRhae1vumSq4iY7dxueytH7HsPzdE8C1iX3qJdMANQflSQfFpd8u9gUfuyRD+/RsguKjNeKw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-js/-/calendar-js-6.0.0.tgz",
"integrity": "sha512-kZBRFIG8J3TNU6K92iEpNzBa3r9JbpCr1MZFJHqVy/5+xTtQG9FqsHhqUWptPwLEBhUNMwN+oCCa7QJAnBKKyg==",
"requires": {}
},
"@nextcloud/capabilities": {
@ -29406,10 +29431,10 @@
"requires": {}
},
"@nextcloud/webpack-vue-config": {
"version": "git+ssh://git@github.com/nextcloud/webpack-vue-config.git#62f0c1a419579ed950ba39ecbd7ae7119fb565a1",
"version": "git+ssh://git@github.com/nextcloud/webpack-vue-config.git#841abf3c3505d17d478b2149004448127f0bcc29",
"integrity": "sha512-bKlGYEqblSiSHNmpaGM9fz/f9v6JNwHp63V63yaI26gE0Zs+DpZSzJWC6HWWbJ1BgmoKT7wLN1GJc4W/NxvnxQ==",
"dev": true,
"from": "@nextcloud/webpack-vue-config@github:nextcloud/webpack-vue-config#62f0c1a419579ed950ba39ecbd7ae7119fb565a1",
"from": "@nextcloud/webpack-vue-config@github:nextcloud/webpack-vue-config#841abf3c3505d17d478b2149004448127f0bcc29",
"requires": {}
},
"@nicolo-ribaudo/eslint-scope-5-internals": {
@ -33291,9 +33316,9 @@
"dev": true
},
"cypress": {
"version": "12.8.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.8.1.tgz",
"integrity": "sha512-lIFbKdaSYAOarNLHNFa2aPZu6YSF+8UY4VRXMxJrFUnk6RvfG0AWsZ7/qle/aIz30TNUD4aOihz2ZgS4vuQVSA==",
"version": "12.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.9.0.tgz",
"integrity": "sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg==",
"dev": true,
"requires": {
"@cypress/request": "^2.88.10",
@ -34657,9 +34682,9 @@
}
},
"eslint-plugin-cypress": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz",
"integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.1.tgz",
"integrity": "sha512-THjc7IT3S9H4KwmRhzAhMGQaEqy78/7W75He/gBhJEH0vIuAY16vOI4YSliDo/ZY+Wm6DtvMHR+8uVvICcI3Lw==",
"dev": true,
"requires": {
"globals": "^11.12.0"
@ -39222,9 +39247,9 @@
"dev": true
},
"linkify-string": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/linkify-string/-/linkify-string-4.1.0.tgz",
"integrity": "sha512-mw4KyPoE/vP0lamGbFFtDsutxOw0b+3g2/lH5bwS7X4tRHQyLBoJ60avPVGUoHfU8G1bLS329u13hhpxBIqFiA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/linkify-string/-/linkify-string-4.1.1.tgz",
"integrity": "sha512-9+kj8xr7GLiyNyO9ri7lIxq2ixVYjjqvtomPQpeYNNT56/PxQq6utzXFLm8HxOaGTiMpimj1UAQWwYYPV88L1g==",
"requires": {}
},
"linkifyjs": {
@ -43423,6 +43448,14 @@
"faye-websocket": "^0.11.3",
"uuid": "^8.3.2",
"websocket-driver": "^0.7.4"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true
}
}
},
"sockjs-client": {
@ -45340,9 +45373,10 @@
"dev": true
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"peer": true
},
"uvu": {
"version": "0.5.6",

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "@nextcloud/vue",
"version": "7.8.4",
"version": "7.8.5",
"description": "Nextcloud vue components",
"keywords": [
"vuejs",
@ -44,7 +44,7 @@
"@nextcloud/auth": "^2.0.0",
"@nextcloud/axios": "^2.0.0",
"@nextcloud/browser-storage": "^0.2.0",
"@nextcloud/calendar-js": "^5.0.3",
"@nextcloud/calendar-js": "^6.0.0",
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^4.0.0",
"@nextcloud/event-bus": "^3.0.0",

Просмотреть файл

@ -191,6 +191,7 @@ export default {
<slot name="icon">
<span :class="[isIconUrl ? 'action-button__icon--url' : icon]"
:style="{ backgroundImage: isIconUrl ? `url(${icon})` : null }"
:aria-hidden="ariaHidden"
class="action-button__icon" />
</slot>
@ -240,6 +241,13 @@ export default {
type: Boolean,
default: false,
},
/**
* aria-hidden attribute for the icon slot
*/
ariaHidden: {
type: Boolean,
default: null,
},
},
computed: {
/**

Просмотреть файл

@ -132,6 +132,7 @@ For the multiselect component, all events will be passed through. Please see the
<slot name="icon">
<span :class="[isIconUrl ? 'action-input__icon--url' : icon]"
:style="{ backgroundImage: isIconUrl ? `url(${icon})` : null }"
:aria-hidden="ariaHidden"
class="action-input__icon" />
</slot>
</span>
@ -341,6 +342,13 @@ export default {
type: String,
default: '',
},
/**
* aria-hidden attribute for the icon slot
*/
ariaHidden: {
type: Boolean,
default: null,
},
},
emits: [

Просмотреть файл

@ -62,6 +62,7 @@ export default {
<slot name="icon">
<span :class="[isIconUrl ? 'action-link__icon--url' : icon]"
:style="{ backgroundImage: isIconUrl ? `url(${icon})` : null }"
:aria-hidden="ariaHidden"
class="action-link__icon" />
</slot>
@ -134,6 +135,20 @@ export default {
return value && (!value.startsWith('_') || ['_blank', '_self', '_parent', '_top'].indexOf(value) > -1)
},
},
/**
* Declares a native tooltip when not null
*/
title: {
type: String,
default: null,
},
/**
* aria-hidden attribute for the icon slot
*/
ariaHidden: {
type: Boolean,
default: null,
},
},
}
</script>

Просмотреть файл

@ -29,6 +29,7 @@
<slot name="icon">
<span v-if="icon !== ''"
:class="[isIconUrl ? 'action-text__icon--url' : icon]"
:aria-hidden="ariaHidden"
:style="{ backgroundImage: isIconUrl ? `url(${icon})` : null }"
class="action-text__icon" />
</slot>

Просмотреть файл

@ -671,6 +671,14 @@ export default {
default: t('Actions'),
},
/**
* aria-hidden attribute for the icon slot
*/
ariaHidden: {
type: Boolean,
default: null,
},
/**
* Wanted direction of the menu
*/
@ -1008,6 +1016,7 @@ export default {
// If it has a menuTitle, we use a secondary button
type: this.type || (buttonText ? 'secondary' : 'tertiary'),
disabled: this.disabled || action?.componentOptions?.propsData?.disabled,
ariaHidden: this.ariaHidden,
...action?.componentOptions?.propsData,
},
on: {
@ -1084,6 +1093,7 @@ export default {
props: {
type: this.triggerBtnType,
disabled: this.disabled,
ariaHidden: this.ariaHidden,
},
slot: 'trigger',
ref: 'menuButton',

Просмотреть файл

@ -34,6 +34,12 @@ include a standard-header like it's used by the files app.
<NcAppSidebar
title="cat-picture.jpg"
subtitle="last edited 3 weeks ago">
<NcAppSidebarTab name="Search" id="search-tab">
<template #icon>
<Magnify :size="20" />
</template>
Search tab content
</NcAppSidebarTab>
<NcAppSidebarTab name="Settings" id="settings-tab">
<template #icon>
<Cog :size="20" />
@ -49,11 +55,13 @@ include a standard-header like it's used by the files app.
</NcAppSidebar>
</template>
<script>
import Magnify from 'vue-material-design-icons/Magnify'
import Cog from 'vue-material-design-icons/Cog'
import ShareVariant from 'vue-material-design-icons/ShareVariant'
export default {
components: {
Magnify,
Cog,
ShareVariant,
},
@ -61,6 +69,181 @@ include a standard-header like it's used by the files app.
</script>
```
### One tab
Single tab is rendered without navigation.
```vue
<template>
<div>
<NcAppSidebar
title="cat-picture.jpg"
subtitle="last edited 3 weeks ago">
<NcAppSidebarTab name="Settings" id="settings-tab">
<template #icon>
<Cog :size="20" />
</template>
Single tab content
</NcAppSidebarTab>
</NcAppSidebar>
</div>
</template>
<script>
import Cog from 'vue-material-design-icons/Cog'
export default {
components: {
Cog,
},
}
</script>
```
### Dynamic tabs
```vue
<template>
<div>
<NcCheckboxRadioSwitch :checked.sync="showTabs[0]">Show search tab</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="showTabs[1]">Show settings tab</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="showTabs[2]">Show sharing tab</NcCheckboxRadioSwitch>
<NcAppSidebar
title="cat-picture.jpg"
subtitle="last edited 3 weeks ago">
<NcAppSidebarTab v-if="showTabs[0]" name="Search" id="search-tab">
<template #icon>
<Magnify :size="20" />
</template>
Search tab content
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showTabs[1]" name="Settings" id="settings-tab">
<template #icon>
<Cog :size="20" />
</template>
Settings
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showTabs[2]" name="Sharing" id="share-tab">
<template #icon>
<ShareVariant :size="20" />
</template>
Sharing tab content
</NcAppSidebarTab>
</NcAppSidebar>
</div>
</template>
<script>
import Magnify from 'vue-material-design-icons/Magnify'
import Cog from 'vue-material-design-icons/Cog'
import ShareVariant from 'vue-material-design-icons/ShareVariant'
export default {
components: {
Magnify,
Cog,
ShareVariant,
},
data() {
return {
showTabs: [true, true, false],
}
},
}
</script>
```
### Custom order
```vue
<template>
<NcAppSidebar
title="cat-picture.jpg"
subtitle="last edited 3 weeks ago">
<NcAppSidebarTab name="Search" id="search-tab" order="3">
<template #icon>
<Magnify :size="20" />
</template>
Search tab content
</NcAppSidebarTab>
<NcAppSidebarTab name="Settings" id="settings-tab" order="2">
<template #icon>
<Cog :size="20" />
</template>
Settings tab content
</NcAppSidebarTab>
<NcAppSidebarTab name="Sharing" id="share-tab" order="1">
<template #icon>
<ShareVariant :size="20" />
</template>
Sharing tab content
</NcAppSidebarTab>
</NcAppSidebar>
</template>
<script>
import Magnify from 'vue-material-design-icons/Magnify'
import Cog from 'vue-material-design-icons/Cog'
import ShareVariant from 'vue-material-design-icons/ShareVariant'
export default {
components: {
Magnify,
Cog,
ShareVariant,
},
}
</script>
```
### Activating tab programmatically
```vue
<template>
<div>
<NcMultiselect v-model="active" :options="['search-tab', 'settings-tab', 'share-tab']" />
<NcAppSidebar
title="cat-picture.jpg"
subtitle="last edited 3 weeks ago"
:active.sync="active">
<NcAppSidebarTab name="Search" id="search-tab">
<template #icon>
<Magnify :size="20" />
</template>
Search tab content
</NcAppSidebarTab>
<NcAppSidebarTab name="Settings" id="settings-tab">
<template #icon>
<Cog :size="20" />
</template>
Settings
</NcAppSidebarTab>
<NcAppSidebarTab name="Sharing" id="share-tab">
<template #icon>
<ShareVariant :size="20" />
</template>
Sharing tab content
</NcAppSidebarTab>
</NcAppSidebar>
</div>
</template>
<script>
import Magnify from 'vue-material-design-icons/Magnify'
import Cog from 'vue-material-design-icons/Cog'
import ShareVariant from 'vue-material-design-icons/ShareVariant'
export default {
components: {
Magnify,
Cog,
ShareVariant,
},
data() {
return {
active: 'search-tab',
}
},
}
</script>
```
### Editable title
```vue

Просмотреть файл

@ -33,6 +33,8 @@
@keydown.left.exact.prevent="focusPreviousTab"
@keydown.right.exact.prevent="focusNextTab"
@keydown.tab.exact.prevent="focusActiveTabContent"
@keydown.home.exact.prevent="focusFirstTab"
@keydown.end.exact.prevent="focusLastTab"
@keydown.33.exact.prevent="focusFirstTab"
@keydown.34.exact.prevent="focusLastTab">
<ul>
@ -43,12 +45,11 @@
:class="{ active: activeTab === tab.id }"
:data-id="tab.id"
:href="`#tab-${tab.id}`"
:tabindex="activeTab === tab.id ? undefined : -1"
:tabindex="activeTab === tab.id ? 0 : -1"
role="tab"
@click.prevent="setActive(tab.id)">
<span class="app-sidebar-tabs__tab-icon">
<NcVNodes v-if="hasMdIcon(tab)" :vnodes="tab.$slots.icon[0]" />
<span v-else :class="tab.icon" />
<NcVNodes :vnodes="tab.renderIcon()" />
</span>
{{ tab.name }}
</a>
@ -59,6 +60,7 @@
<!-- tabs content -->
<div :class="{'app-sidebar-tabs__content--multiple': hasMultipleTabs}"
class="app-sidebar-tabs__content">
<!-- @slot Tabs content - NcAppSidebarTab components or any content if there is no tabs -->
<slot />
</div>
</div>
@ -67,24 +69,22 @@
<script>
import NcVNodes from '../NcVNodes/index.js'
import Vue from 'vue'
const IsValidString = function(value) {
return value && typeof value === 'string' && value.trim() !== ''
}
const IsValidStringWithoutSpaces = function(value) {
return IsValidString(value) && value.indexOf(' ') === -1
}
export default {
name: 'NcAppSidebarTabs',
components: {
// Component to render the material design icon (vnodes)
NcVNodes,
},
provide() {
return {
registerTab: this.registerTab,
unregisterTab: this.unregisterTab,
// Getter as an alternative to Vue 2.7 computed(() => this.activeTab)
getActiveTab: () => this.activeTab,
}
},
props: {
/**
* Id of the tab to activate
@ -100,26 +100,28 @@ export default {
data() {
return {
/**
* The tab component instances to build the tab navbar from.
* Tab descriptions from the passed NcSidebarTab components' props to build the tab navbar from.
*/
tabs: [],
/**
* The id of the currently active tab.
* Local active (open) tab's ID. It allows to use component without active.sync
*/
activeTab: '',
/**
* Dummy array to react on slot changes.
*/
children: [],
}
},
computed: {
/**
* Has multiple tabs. If only one tab - its content is shown without navigation
*
* @return {boolean}
*/
hasMultipleTabs() {
return this.tabs.length > 1
},
currentTabIndex() {
return this.tabs.findIndex(tab => tab.id === this.activeTab)
return this.tabs.findIndex((tab) => tab.id === this.activeTab)
},
},
@ -130,18 +132,6 @@ export default {
this.updateActive()
}
},
children() {
this.updateTabs()
},
},
mounted() {
// Init the tabs list
this.updateTabs()
// Let's make the children list reactive
this.children = this.$children
},
methods: {
@ -153,6 +143,9 @@ export default {
*/
setActive(id) {
this.activeTab = id
/**
* @property {string} active - active tab's id
*/
this.$emit('update:active', this.activeTab)
},
@ -164,7 +157,7 @@ export default {
if (this.currentTabIndex > 0) {
this.setActive(this.tabs[this.currentTabIndex - 1].id)
}
this.focusActiveTab() // focus nav item
this.focusActiveTab()
},
/**
@ -175,7 +168,7 @@ export default {
if (this.currentTabIndex < this.tabs.length - 1) {
this.setActive(this.tabs[this.currentTabIndex + 1].id)
}
this.focusActiveTab() // focus nav item
this.focusActiveTab()
},
/**
@ -184,7 +177,7 @@ export default {
*/
focusFirstTab() {
this.setActive(this.tabs[0].id)
this.focusActiveTab() // focus nav item
this.focusActiveTab()
},
/**
@ -193,7 +186,7 @@ export default {
*/
focusLastTab() {
this.setActive(this.tabs[this.tabs.length - 1].id)
this.focusActiveTab() // focus nav item
this.focusActiveTab()
},
/**
@ -216,62 +209,42 @@ export default {
*/
updateActive() {
this.activeTab = this.active
&& this.tabs.findIndex(tab => tab.id === this.active) !== -1
&& this.tabs.some(tab => tab.id === this.active)
? this.active
: this.tabs.length > 0
? this.tabs[0].id
: ''
},
hasMdIcon(tab) {
return tab?.$slots?.icon
/**
* Register child tab in the tabs
*
* @param {object} tab child tab passed to slot
*/
registerTab(tab) {
this.tabs.push(tab)
this.tabs.sort((a, b) => {
if (a.order === b.order) {
return OC.Util.naturalSortCompare(a.name, b.name)
}
return a.order - b.order
})
if (!this.activeTab) {
this.updateActive()
}
},
/**
* Manually update the sidebar tabs according to $slots.default
* Unregister child tab from the tabs
*
* @param {string} id tab's id
*/
updateTabs() {
if (!this.$slots.default) {
this.tabs = []
return
unregisterTab(id) {
const tabIndex = this.tabs.findIndex((tab) => tab.id === id)
if (tabIndex !== -1) {
this.tabs.splice(tabIndex, 1)
}
// Find all valid children (AppSidebarTab, other components, text nodes, etc.)
const children = this.$slots.default.filter(elem => elem.tag || elem.text.trim())
// Find all valid instances of AppSidebarTab
const invalidTabs = []
const tabs = children.reduce((tabs, tabNode) => {
const tab = tabNode.componentInstance
// Make sure all required props are provided and valid
if (IsValidString(tab?.name)
&& IsValidStringWithoutSpaces(tab?.id)
&& (IsValidStringWithoutSpaces(tab?.icon) || tab?.$slots?.icon)) {
tabs.push(tab)
} else {
invalidTabs.push(tabNode)
}
return tabs
}, [])
// Tabs are optional, but you can use either tabs or non-tab-content only
if (tabs.length !== 0 && tabs.length !== children.length) {
Vue.util.warn('Mixing tabs and non-tab-content is not possible.')
invalidTabs.map(invalid => console.debug('Ignoring invalid tab', invalid))
}
// We sort the tabs by their order or by their name
this.tabs = tabs.sort((a, b) => {
const orderA = a.order || 0
const orderB = b.order || 0
if (orderA === orderB) {
return OC.Util.naturalSortCompare(a.name, b.name)
}
return orderA - orderB
})
// Init active tab if exists
if (this.tabs.length > 0) {
if (this.activeTab === id) {
this.updateActive()
}
},

Просмотреть файл

@ -35,27 +35,44 @@
<h3 class="hidden-visually">
{{ name }}
</h3>
<!-- @slot Tab panel content -->
<slot />
</section>
</template>
<script>
import { h } from 'vue'
export default {
name: 'NcAppSidebarTab',
inject: ['registerTab', 'unregisterTab', 'getActiveTab'],
props: {
id: {
type: String,
required: true,
},
/**
* Tab title in navigation
*/
name: {
type: String,
required: true,
},
/**
* Tab icon's html class in navigation. Used if #icon slot is not provided
*/
icon: {
type: String,
default: '',
},
/**
* Tab order in navigation. If not provided, name is used.
*/
order: {
type: Number,
default: 0,
@ -67,24 +84,55 @@ export default {
'scroll',
],
expose: ['id', 'name', 'icon', 'order', 'renderIcon'],
computed: {
// TODO: implement a better way to force pass a prop fromm Sidebar
/**
* Is the current tab an active tab, that should be shown?
*
* @return {boolean}
*/
isActive() {
return this.$parent.activeTab === this.id
return this.getActiveTab() === this.id
},
},
created() {
// As the tab is created - register it in the tabs component
// It's better to provide computed tab object, not component instance as it easy
this.registerTab(this)
},
beforeDestroy() {
// Unregister the tab from tabs
this.unregisterTab(this.id)
},
methods: {
onScroll(event) {
// Are we scrolled to the very bottom ?
if (this.$el.scrollHeight - this.$el.scrollTop === this.$el.clientHeight) {
/**
* Bottom scroll is reached
*
* @property {Event} event Native scroll event
*/
this.$emit('bottom-reached', event)
}
/**
* @property {Event} event Native scroll event
*/
this.$emit('scroll', event)
},
/**
* Render tab's icon from slot or icon prop
*
* @return {import('vue').VNode|import('vue').VNode[]}
*/
renderIcon() {
return this.$slots.icon || this.$scopedSlots.icon?.() || h('span', { staticClass: this.icon })
},
},
}
</script>

Просмотреть файл

@ -287,6 +287,13 @@ export default {
type: Boolean,
default: false,
},
/**
* aria-hidden attribute for the icon slot
*/
ariaHidden: {
type: Boolean,
default: null,
},
},
/**
@ -345,7 +352,16 @@ export default {
},
[
h('span', { class: 'button-vue__wrapper' }, [
hasIcon ? h('span', { class: 'button-vue__icon' }, [this.$slots.icon]) : null,
hasIcon
? h('span', {
class: 'button-vue__icon',
attrs: {
'aria-hidden': this.ariaHidden,
},
},
[this.$slots.icon],
)
: null,
hasText ? h('span', { class: 'button-vue__text' }, [text]) : null,
]),
]
@ -429,7 +445,8 @@ export default {
&__wrapper {
display: inline-flex;
align-items: center;
justify-content: space-around;
justify-content: center;
width: 100%;
}
&__icon {
@ -446,6 +463,9 @@ export default {
font-weight: bold;
margin-bottom: 1px;
padding: 2px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
// Icon-only button

Просмотреть файл

@ -660,6 +660,7 @@ export default {
onKeyUp(event) {
// prevent tribute from opening on keyup
event.stopImmediatePropagation()
},
},
}

Просмотреть файл

@ -39,6 +39,8 @@ export default {
return {
text: `Hello {username}. The file {file} was added by {username}. Go visit https://nextcloud.com
Local IP: http://127.0.0.1/status.php should be clickable
Some examples for markdown syntax: **bold text** *italic text* ~~strikethrough~~`,
autolink: true,
useMarkdown: true,
@ -238,6 +240,9 @@ export default {
}
</script>
<style scoped>
/* stylelint-disable-next-line scss/at-import-partial-extension */
@import './richtext.scss';
a:not(.rich-text--component) {
text-decoration: underline;
}

Просмотреть файл

@ -1,4 +1,16 @@
/**
* Regex pattern to match links to be resolved by the link-reference provider
*
* @type {RegExp}
*/
export const URL_PATTERN = /(\s|^)(https?:\/\/)((?:[-A-Z0-9+_]+\.)+[-A-Z]+(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*)(\s|$)/ig
// FIXME See if we can merge those two
export const URL_PATTERN_AUTOLINK = /(\s|\(|^)((https?:\/\/)((?:[-A-Z0-9+_]+\.)+[-A-Z]+(?::[0-9]+)?(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*))(?=\s|\)|$)/ig
/**
* Regex pattern to identify strings as links and then making them clickable
* Opposed to the above regex this one also matches IP addresses, which we would like to be clickable,
* but in general resolving references for them might mostly not work,
* as the link provider checks for local addresses and does not resolve them.
*
* @type {RegExp}
*/
export const URL_PATTERN_AUTOLINK = /(\s|\(|^)((https?:\/\/)((?:[-A-Z0-9+_]+\.)+[-A-Z0-9]+(?::[0-9]+)?(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*))(?=\s|\)|$)/ig

Просмотреть файл

@ -44,9 +44,11 @@ export const prepareTextNode = ({ h, context }, text) => {
return entry
}
const { component, props } = entry
// do not override class of NcLink
const componentClass = component.name === 'NcLink' ? undefined : 'rich-text--component'
return h(component, {
props,
class: 'rich-text--component',
class: componentClass,
})
})
}

Просмотреть файл

@ -67,6 +67,13 @@ export default {
type: String,
default: '',
},
/**
* aria-hidden attribute for the icon slot
*/
ariaHidden: {
type: Boolean,
default: null,
},
},
emits: [

Просмотреть файл

@ -189,23 +189,4 @@ describe('NcAppSidebarTabs.vue', () => {
})
})
})
describe('when tabs and other elements are mixed', () => {
it('Issues a warning and logs to console .', () => {
mount(NcAppSidebarTabs, {
slots: {
default: [
'<nc-app-sidebar-tab id="1" icon="icon-details" name="Tab1">Tab1</nc-app-sidebar-tab>',
'<NcAppSidebarTab id="2" icon="icon-details" name="Tab2">Tab2</NcAppSidebarTab>',
'<div>Non-tab-content</div>',
'Test',
],
},
stubs: {
NcAppSidebarTab,
},
})
expect(onWarning).toHaveBeenCalledTimes(1)
expect(consoleDebug).toHaveBeenCalledTimes(2)
})
})
})

Просмотреть файл

@ -152,6 +152,17 @@ describe('Foo', () => {
expect(wrapper.find('a').attributes('href')).toEqual('https://example.com:444')
})
it('properly recognizes an url with an IP address and inserts a link', async() => {
const wrapper = mount(NcRichText, {
propsData: {
text: 'Testwith a link to https://127.0.0.1/status.php - go visit it',
autolink: true
}
})
expect(wrapper.text()).toEqual('Testwith a link to https://127.0.0.1/status.php - go visit it')
expect(wrapper.find('a').attributes('href')).toEqual('https://127.0.0.1/status.php')
})
it('properly formats markdown', async() => {
const wrapper = mount(NcRichText, {
propsData: {