summaryrefslogtreecommitdiffstats
path: root/core/src/components
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-08-03 12:54:37 +0200
committernpmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>2020-08-03 11:26:03 +0000
commit1a1b3e20e470a945dd9f5fab1d99174b10cbb141 (patch)
treeaacff8872bcfd47685e9a9fb3e5e3a423e498f59 /core/src/components
parent4987fe9a51f0b889d2b99428c967014d95bb13ae (diff)
downloadnextcloud-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.vue206
-rw-r--r--core/src/components/UnifiedSearch/SearchResult.vue211
-rw-r--r--core/src/components/login/PasswordLessLoginForm.vue2
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 {