diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-08-03 12:54:37 +0200 |
---|---|---|
committer | npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com> | 2020-08-03 11:26:03 +0000 |
commit | 1a1b3e20e470a945dd9f5fab1d99174b10cbb141 (patch) | |
tree | aacff8872bcfd47685e9a9fb3e5e3a423e498f59 /core/src/components | |
parent | 4987fe9a51f0b889d2b99428c967014d95bb13ae (diff) | |
download | nextcloud-server-1a1b3e20e470a945dd9f5fab1d99174b10cbb141.tar.gz nextcloud-server-1a1b3e20e470a945dd9f5fab1d99174b10cbb141.zip |
Fix unified search
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Signed-off-by: npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>
Diffstat (limited to 'core/src/components')
-rw-r--r-- | core/src/components/HeaderMenu.vue | 206 | ||||
-rw-r--r-- | core/src/components/UnifiedSearch/SearchResult.vue | 211 | ||||
-rw-r--r-- | core/src/components/login/PasswordLessLoginForm.vue | 2 |
3 files changed, 418 insertions, 1 deletions
diff --git a/core/src/components/HeaderMenu.vue b/core/src/components/HeaderMenu.vue new file mode 100644 index 00000000000..2cc5b79d6dd --- /dev/null +++ b/core/src/components/HeaderMenu.vue @@ -0,0 +1,206 @@ + <!-- + - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.com> + - + - @license GNU AGPL version 3 or any later version + - + - 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/>. + - + --> +<template> + <div v-click-outside="closeMenu" :class="{ 'header-menu--opened': opened }" class="header-menu"> + <a class="header-menu__trigger" + href="#" + :aria-controls="`header-menu-${id}`" + :aria-expanded="opened" + aria-haspopup="true" + @click.prevent="toggleMenu"> + <slot name="trigger" /> + </a> + <div v-if="opened" + :id="`header-menu-${id}`" + class="header-menu__wrapper" + role="menu"> + <div class="header-menu__carret" /> + <div class="header-menu__content"> + <slot /> + </div> + </div> + </div> +</template> + +<script> +import { directive as ClickOutside } from 'v-click-outside' +import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' + +export default { + name: 'HeaderMenu', + + directives: { + ClickOutside, + }, + + props: { + id: { + type: String, + required: true, + }, + open: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + opened: this.open, + } + }, + + watch: { + open(newVal) { + this.opened = newVal + this.$nextTick(() => { + if (this.opened) { + this.openMenu() + } else { + this.closeMenu() + } + }) + }, + }, + + mounted() { + document.addEventListener('keydown', this.onKeyDown) + }, + + beforeMount() { + subscribe(`header-menu-${this.id}-close`, this.closeMenu) + subscribe(`header-menu-${this.id}-open`, this.openMenu) + }, + + beforeDestroy() { + unsubscribe(`header-menu-${this.id}-close`, this.closeMenu) + unsubscribe(`header-menu-${this.id}-open`, this.openMenu) + }, + + methods: { + /** + * Toggle the current menu open state + */ + toggleMenu() { + // Toggling current state + if (!this.opened) { + this.openMenu() + } else { + this.closeMenu() + } + }, + + /** + * Close the current menu + */ + closeMenu() { + if (!this.opened) { + return + } + + this.opened = false + this.$emit('close') + this.$emit('update:open', false) + emit(`header-menu-${this.id}-close`) + }, + + /** + * Open the current menu + */ + openMenu() { + if (this.opened) { + return + } + + this.opened = true + this.$emit('open') + this.$emit('update:open', true) + emit(`header-menu-${this.id}-open`) + }, + + onKeyDown(event) { + // If opened and escape pressed, close + if (event.key === 'Escape' && this.opened) { + event.preventDefault() + this.closeMenu() + } + }, + }, +} +</script> + +<style lang="scss" scoped> +.header-menu { + &__trigger { + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 100%; + margin: 0; + padding: 0; + cursor: pointer; + opacity: .6; + } + + &--opened &__trigger, + &__trigger:hover, + &__trigger:focus, + &__trigger:active { + opacity: 1; + } + + &__wrapper { + position: absolute; + z-index: 2000; + top: 50px; + right: 5px; + box-sizing: border-box; + margin: 0; + border-radius: 0 0 var(--border-radius) var(--border-radius); + background-color: var(--color-main-background); + + filter: drop-shadow(0 1px 5px var(--color-box-shadow)); + } + + &__carret { + position: absolute; + right: 10px; + bottom: 100%; + width: 0; + height: 0; + content: ' '; + pointer-events: none; + border: 10px solid transparent; + border-bottom-color: var(--color-main-background); + } + + &__content { + overflow: auto; + width: 350px; + max-width: 350px; + min-height: calc(44px * 1.5); + max-height: calc(100vh - 50px * 2); + } +} + +</style> diff --git a/core/src/components/UnifiedSearch/SearchResult.vue b/core/src/components/UnifiedSearch/SearchResult.vue new file mode 100644 index 00000000000..832770c9abe --- /dev/null +++ b/core/src/components/UnifiedSearch/SearchResult.vue @@ -0,0 +1,211 @@ +<template> + <a :href="resourceUrl || '#'" + class="unified-search__result" + :class="{ + 'unified-search__result--focused': focused + }" + @click="reEmitEvent" + @focus="reEmitEvent"> + <!-- Icon describing the result --> + <div class="unified-search__result-icon" + :class="{ + 'unified-search__result-icon--rounded': rounded, + 'unified-search__result-icon--no-preview': !hasValidThumbnail && !loaded, + 'unified-search__result-icon--with-thumbnail': hasValidThumbnail && loaded, + [iconClass]: true + }" + role="img"> + <img v-if="hasValidThumbnail" + :src="thumbnailUrl" + :alt="t('core', 'Thumbnail for {result}', {result: title})" + @error="onError" + @load="onLoad"> + </div> + + <!-- Title and sub-title --> + <span class="unified-search__result-content"> + <h3 class="unified-search__result-line-one"> + <Highlight :text="title" :search="query" /> + </h3> + <h4 v-if="subline" class="unified-search__result-line-two">{{ subline }}</h4> + </span> + </a> +</template> + +<script> +import Highlight from '@nextcloud/vue/dist/Components/Highlight' + +export default { + name: 'SearchResult', + + components: { + Highlight, + }, + + props: { + thumbnailUrl: { + type: String, + default: null, + }, + title: { + type: String, + required: true, + }, + subline: { + type: String, + default: null, + }, + resourceUrl: { + type: String, + default: null, + }, + iconClass: { + type: String, + default: '', + }, + rounded: { + type: Boolean, + default: false, + }, + query: { + type: String, + default: '', + }, + + /** + * Only used for the first result as a visual feedback + * so we can keep the search input focused but pressing + * enter still opens the first result + */ + focused: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + hasValidThumbnail: this.thumbnailUrl && this.thumbnailUrl.trim() !== '', + loaded: false, + } + }, + + watch: { + // Make sure to reset state on change even when vue recycle the component + thumbnailUrl() { + this.hasValidThumbnail = this.thumbnailUrl && this.thumbnailUrl.trim() !== '' + this.loaded = false + }, + }, + + methods: { + reEmitEvent(e) { + this.$emit(e.type, e) + }, + + /** + * If the image fails to load, fallback to iconClass + */ + onError() { + this.hasValidThumbnail = false + }, + + onLoad() { + this.loaded = true + }, + }, +} +</script> + +<style lang="scss" scoped> +$clickable-area: 44px; +$margin: 10px; + +.unified-search__result { + display: flex; + height: $clickable-area; + padding: $margin; + border-bottom: 1px solid var(--color-border); + + // Load more entry, + &:last-child { + border-bottom: none; + } + + &--focused, + &:active, + &:hover, + &:focus { + background-color: var(--color-background-hover); + } + + * { + cursor: pointer; + } + + &-icon { + overflow: hidden; + width: $clickable-area; + height: $clickable-area; + border-radius: var(--border-radius); + background-position: center center; + background-size: 32px; + &--rounded { + border-radius: $clickable-area / 2; + } + &--no-preview { + background-size: 32px; + } + &--with-thumbnail { + background-size: cover; + } + &--with-thumbnail:not(&--rounded) { + // compensate for border + max-width: $clickable-area - 2px; + max-height: $clickable-area - 2px; + border: 1px solid var(--color-border); + } + + img { + // Make sure to keep ratio + width: 100%; + height: 100%; + + object-fit: cover; + object-position: center; + } + } + + &-icon, + &-actions { + flex: 0 0 $clickable-area; + } + + &-content { + display: flex; + align-items: center; + flex: 1 1 100%; + flex-wrap: wrap; + // Set to minimum and gro from it + min-width: 0; + padding-left: $margin; + } + + &-line-one, + &-line-two { + overflow: hidden; + flex: 1 1 100%; + margin: 0; + white-space: nowrap; + text-overflow: ellipsis; + // Use the same color as the `a` + color: inherit; + font-size: inherit; + } + &-line-two { + opacity: .7; + font-size: 14px; + } +} + +</style> diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue index 0cd7cb81cfe..df774599f92 100644 --- a/core/src/components/login/PasswordLessLoginForm.vue +++ b/core/src/components/login/PasswordLessLoginForm.vue @@ -41,7 +41,7 @@ import { startAuthentication, finishAuthentication, -} from '../../service/WebAuthnAuthenticationService' +} from '../../services/WebAuthnAuthenticationService' import LoginButton from './LoginButton' class NoValidCredentials extends Error { |