Fix search placeholder animation & dark theme compatibility
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
f359529555
commit
f04b182b94
|
@ -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>
|
After Width: | Height: | Size: 207 B |
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue