Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>tags/v20.0.0beta3
@@ -5,6 +5,9 @@ $color-main-background: #181818; | |||
$color-background-dark: lighten($color-main-background, 4%); | |||
$color-background-darker: lighten($color-main-background, 8%); | |||
$color-placeholder-light: lighten($color-main-background, 10%); | |||
$color-placeholder-dark: lighten($color-main-background, 20%); | |||
$color-text-maxcontrast: darken($color-main-text, 30%); | |||
$color-text-light: darken($color-main-text, 10%); | |||
$color-text-lighter: darken($color-main-text, 20%); |
@@ -5,6 +5,9 @@ $color-main-background: #fff; | |||
$color-background-dark: darken($color-main-background, 30%); | |||
$color-background-darker: darken($color-main-background, 30%); | |||
$color-placeholder-light: darken($color-main-background, 30%); | |||
$color-placeholder-dark: darken($color-main-background, 45%); | |||
$color-text-maxcontrast: $color-main-text; | |||
$color-text-light: $color-main-text; | |||
$color-text-lighter: $color-main-text; |
@@ -11,6 +11,9 @@ | |||
--color-background-dark: $color-background-dark; | |||
--color-background-darker: $color-background-darker; | |||
--color-placeholder-light: $color-placeholder-light; | |||
--color-placeholder-dark: $color-placeholder-dark; | |||
--color-primary: $color-primary; | |||
--color-primary-light: $color-primary-light; | |||
--color-primary-text: $color-primary-text; |
@@ -307,6 +307,7 @@ audio, canvas, embed, iframe, img, input, object, video { | |||
@include icon-black-white('upload', 'actions', 1, true); | |||
@include icon-black-white('user', 'actions', 1, true); | |||
@include icon-black-white('group', 'actions', 1, true); | |||
@include icon-black-white('filter', 'actions', 1, true); | |||
@include icon-black-white('video', 'actions', 2, true); | |||
.icon-video-white { |
@@ -40,6 +40,9 @@ $color-background-hover: nc-darken($color-main-background, 4%) !default; | |||
$color-background-dark: nc-darken($color-main-background, 7%) !default; | |||
$color-background-darker: nc-darken($color-main-background, 14%) !default; | |||
$color-placeholder-light: nc-darken($color-main-background, 10%) !default; | |||
$color-placeholder-dark: nc-darken($color-main-background, 20%) !default; | |||
$color-primary: #0082c9 !default; | |||
$color-primary-light: mix($color-primary, $color-main-background, 10%) !default; | |||
$color-primary-text: #ffffff !default; |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1.19 2.4l5.05 6.48v5.24c0 .49.4.88.88.88h1.76c.49 0 .88-.4.88-.88V8.88l5.05-6.47a.87.87 0 00-.7-1.41H1.89a.87.87 0 00-.7 1.4z"/></svg> |
@@ -20,7 +20,7 @@ | |||
- | |||
--> | |||
<template> | |||
<div v-click-outside="closeMenu" :class="{ 'header-menu--opened': opened }" class="header-menu"> | |||
<div v-click-outside="clickOutsideConfig" :class="{ 'header-menu--opened': opened }" class="header-menu"> | |||
<a class="header-menu__trigger" | |||
href="#" | |||
:aria-controls="`header-menu-${id}`" | |||
@@ -44,6 +44,7 @@ | |||
<script> | |||
import { directive as ClickOutside } from 'v-click-outside' | |||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' | |||
import excludeClickOutsideClasses from '@nextcloud/vue/dist/Mixins/excludeClickOutsideClasses' | |||
export default { | |||
name: 'HeaderMenu', | |||
@@ -52,6 +53,10 @@ export default { | |||
ClickOutside, | |||
}, | |||
mixins: [ | |||
excludeClickOutsideClasses, | |||
], | |||
props: { | |||
id: { | |||
type: String, | |||
@@ -66,6 +71,10 @@ export default { | |||
data() { | |||
return { | |||
opened: this.open, | |||
clickOutsideConfig: { | |||
handler: this.closeMenu, | |||
middleware: this.clickOutsideMiddleware, | |||
}, | |||
} | |||
}, | |||
@@ -1,77 +0,0 @@ | |||
<!-- | |||
- @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> | |||
<li> | |||
<a :title="t('core', 'Search for {name} only', { name })" | |||
class="unified-search__filter" | |||
href="#" | |||
@click.prevent="onClick"> | |||
{{ filter }} | |||
</a> | |||
</li> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'SearchFilter', | |||
props: { | |||
type: { | |||
type: String, | |||
required: true, | |||
}, | |||
name: { | |||
type: String, | |||
required: true, | |||
}, | |||
}, | |||
computed: { | |||
filter() { | |||
return `in:${this.type}` | |||
}, | |||
}, | |||
methods: { | |||
onClick() { | |||
this.$emit('click', this.filter) | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.unified-search__filter { | |||
height: 1em; | |||
margin-right: 5px; | |||
padding: 3px 8px; | |||
border-radius: 1em; | |||
background-color: var(--color-background-darker); | |||
&:active, | |||
&:focus, | |||
&:hover { | |||
background-color: var(--color-background-hover); | |||
} | |||
} | |||
</style> |
@@ -1,68 +0,0 @@ | |||
<template> | |||
<svg | |||
class="unified-search__result-placeholder" | |||
xmlns="http://www.w3.org/2000/svg" | |||
fill="url(#unified-search__result-placeholder-gradient)"> | |||
<defs> | |||
<linearGradient id="unified-search__result-placeholder-gradient"> | |||
<stop offset="0%" stop-color="#ededed"><animate attributeName="stop-color" | |||
values="#ededed; #ededed; #cccccc; #cccccc; #ededed" | |||
dur="2s" | |||
repeatCount="indefinite" /></stop> | |||
<stop offset="100%" stop-color="#cccccc"><animate attributeName="stop-color" | |||
values="#cccccc; #ededed; #ededed; #cccccc; #cccccc" | |||
dur="2s" | |||
repeatCount="indefinite" /></stop> | |||
</linearGradient> | |||
</defs> | |||
<rect class="unified-search__result-placeholder-icon" /> | |||
<rect class="unified-search__result-placeholder-line-one" /> | |||
<rect class="unified-search__result-placeholder-line-two" :style="{width: `calc(${randWidth}%)`}" /> | |||
</svg> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'SearchResultPlaceholder', | |||
data() { | |||
return { | |||
randWidth: Math.floor(Math.random() * 20) + 30, | |||
} | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
$clickable-area: 44px; | |||
$margin: 10px; | |||
.unified-search__result-placeholder { | |||
width: calc(100% - 2 * #{$margin}); | |||
height: $clickable-area; | |||
margin: $margin; | |||
&-icon { | |||
width: $clickable-area; | |||
height: $clickable-area; | |||
rx: var(--border-radius); | |||
ry: var(--border-radius); | |||
} | |||
&-line-one, | |||
&-line-two { | |||
width: calc(100% - #{$margin + $clickable-area}); | |||
height: 1em; | |||
x: $margin + $clickable-area; | |||
} | |||
&-line-one { | |||
y: 5px; | |||
} | |||
&-line-two { | |||
y: 25px; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,100 @@ | |||
<template> | |||
<ul> | |||
<!-- Placeholder animation --> | |||
<svg class="unified-search__result-placeholder-gradient"> | |||
<defs> | |||
<linearGradient id="unified-search__result-placeholder-gradient"> | |||
<stop offset="0%" :stop-color="light"> | |||
<animate attributeName="stop-color" | |||
:values="`${light}; ${light}; ${dark}; ${dark}; ${light}`" | |||
dur="2s" | |||
repeatCount="indefinite" /> | |||
</stop> | |||
<stop offset="100%" :stop-color="dark"> | |||
<animate attributeName="stop-color" | |||
:values="`${dark}; ${light}; ${light}; ${dark}; ${dark}`" | |||
dur="2s" | |||
repeatCount="indefinite" /> | |||
</stop> | |||
</linearGradient> | |||
</defs> | |||
</svg> | |||
<!-- Placeholders --> | |||
<li v-for="placeholder in [1, 2, 3]" :key="placeholder"> | |||
<svg | |||
class="unified-search__result-placeholder" | |||
xmlns="http://www.w3.org/2000/svg" | |||
fill="url(#unified-search__result-placeholder-gradient)"> | |||
<rect class="unified-search__result-placeholder-icon" /> | |||
<rect class="unified-search__result-placeholder-line-one" /> | |||
<rect class="unified-search__result-placeholder-line-two" :style="{width: `calc(${randWidth()}%)`}" /> | |||
</svg> | |||
</li> | |||
</ul> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'SearchResultPlaceholders', | |||
data() { | |||
return { | |||
light: null, | |||
dark: null, | |||
} | |||
}, | |||
mounted() { | |||
const styles = getComputedStyle(document.documentElement) | |||
this.dark = styles.getPropertyValue('--color-placeholder-dark') | |||
this.light = styles.getPropertyValue('--color-placeholder-light') | |||
}, | |||
methods: { | |||
randWidth() { | |||
return Math.floor(Math.random() * 20) + 30 | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
$clickable-area: 44px; | |||
$margin: 10px; | |||
.unified-search__result-placeholder-gradient { | |||
position: fixed; | |||
height: 0; | |||
width: 0; | |||
z-index: -1; | |||
} | |||
.unified-search__result-placeholder { | |||
width: calc(100% - 2 * #{$margin}); | |||
height: $clickable-area; | |||
margin: $margin; | |||
&-icon { | |||
width: $clickable-area; | |||
height: $clickable-area; | |||
rx: var(--border-radius); | |||
ry: var(--border-radius); | |||
} | |||
&-line-one, | |||
&-line-two { | |||
width: calc(100% - #{$margin + $clickable-area}); | |||
height: 1em; | |||
x: $margin + $clickable-area; | |||
} | |||
&-line-one { | |||
y: 5px; | |||
} | |||
&-line-two { | |||
y: 25px; | |||
} | |||
} | |||
</style> |
@@ -22,6 +22,7 @@ | |||
<template> | |||
<HeaderMenu id="unified-search" | |||
class="unified-search" | |||
exclude-click-outside-classes="popover" | |||
:open.sync="open" | |||
@open="onOpen" | |||
@close="onClose"> | |||
@@ -39,26 +40,21 @@ | |||
:placeholder="t('core', 'Search {types} …', { types: typesNames.join(', ').toLowerCase() })" | |||
@input="onInputDebounced" | |||
@keypress.enter.prevent.stop="onInputEnter"> | |||
</div> | |||
<!-- Search filters --> | |||
<div v-if="availableFilters.length > 1" class="unified-search__filters"> | |||
<ul> | |||
<SearchFilter v-for="type in availableFilters" | |||
<!-- Search filters --> | |||
<Actions v-if="availableFilters.length > 1" class="unified-search__filters" placement="bottom"> | |||
<ActionButton v-for="type in availableFilters" | |||
:key="type" | |||
:type="type" | |||
:name="typesMap[type]" | |||
@click="onClickFilter" /> | |||
</ul> | |||
icon="icon-filter" | |||
:title="t('core', 'Search for {name} only', { name: typesMap[type] })" | |||
@click="onClickFilter(`in:${type}`)"> | |||
{{ `in:${type}` }} | |||
</ActionButton> | |||
</Actions> | |||
</div> | |||
<template v-if="!hasResults"> | |||
<!-- Loading placeholders --> | |||
<ul v-if="isLoading"> | |||
<li v-for="placeholder in [1, 2, 3]" :key="placeholder"> | |||
<SearchResultPlaceholder /> | |||
</li> | |||
</ul> | |||
<SearchResultPlaceholders v-if="isLoading" /> | |||
<EmptyContent v-else-if="isValidQuery && isDoneSearching" icon="icon-search"> | |||
{{ t('core', 'No results for {query}', {query}) }} | |||
@@ -109,6 +105,9 @@ | |||
<script> | |||
import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot } from '../services/UnifiedSearchService' | |||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' | |||
import Actions from '@nextcloud/vue/dist/Components/Actions' | |||
import debounce from 'debounce' | |||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' | |||
import Magnify from 'vue-material-design-icons/Magnify' | |||
import debounce from 'debounce' | |||
@@ -117,18 +116,20 @@ import { emit } from '@nextcloud/event-bus' | |||
import HeaderMenu from '../components/HeaderMenu' | |||
import SearchFilter from '../components/UnifiedSearch/SearchFilter' | |||
import SearchResult from '../components/UnifiedSearch/SearchResult' | |||
import SearchResultPlaceholder from '../components/UnifiedSearch/SearchResultPlaceholder' | |||
import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders' | |||
export default { | |||
name: 'UnifiedSearch', | |||
components: { | |||
ActionButton, | |||
Actions, | |||
EmptyContent, | |||
HeaderMenu, | |||
Magnify, | |||
SearchFilter, | |||
SearchResult, | |||
SearchResultPlaceholder, | |||
SearchResultPlaceholders, | |||
}, | |||
data() { | |||
@@ -352,12 +353,12 @@ export default { | |||
types = this.typesIDs.filter(type => this.usedFiltersIn.indexOf(type) > -1) | |||
} | |||
// remove any filters from the query | |||
// Remove any filters from the query | |||
query = query.replace(regexFilterIn, '').replace(regexFilterNot, '') | |||
console.debug('Searching', query, 'in', types) | |||
// reset search if the query changed | |||
// Reset search if the query changed | |||
this.resetState() | |||
types.forEach(async type => { | |||
@@ -566,10 +567,13 @@ $input-padding: 6px; | |||
} | |||
&__input-wrapper { | |||
width: 100%; | |||
position: sticky; | |||
// above search results | |||
z-index: 2; | |||
top: 0; | |||
display: inline-flex; | |||
align-items: center; | |||
background-color: var(--color-main-background); | |||
} | |||
@@ -582,8 +586,7 @@ $input-padding: 6px; | |||
} | |||
&__input { | |||
// Minus margins | |||
width: calc(100% - 2 * #{$margin}); | |||
width: 100%; | |||
height: 34px; | |||
margin: $margin; | |||
padding: $input-padding; | |||
@@ -596,6 +599,10 @@ $input-padding: 6px; | |||
} | |||
} | |||
&__filters { | |||
margin-right: $margin / 2; | |||
} | |||
&__results { | |||
&::before { | |||
display: block; |