Properly use form role=search and unify reset button
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
6120496e54
commit
283e66b300
|
@ -28,6 +28,12 @@ export const minSearchLength = 2
|
||||||
export const regexFilterIn = /[^-]in:([a-z_-]+)/ig
|
export const regexFilterIn = /[^-]in:([a-z_-]+)/ig
|
||||||
export const regexFilterNot = /-in:([a-z_-]+)/ig
|
export const regexFilterNot = /-in:([a-z_-]+)/ig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cancel token
|
||||||
|
* @returns {CancelTokenSource}
|
||||||
|
*/
|
||||||
|
const createCancelToken = () => axios.CancelToken.source()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of available search providers
|
* Get the list of available search providers
|
||||||
*
|
*
|
||||||
|
@ -54,13 +60,20 @@ export async function getTypes() {
|
||||||
/**
|
/**
|
||||||
* Get the list of available search providers
|
* Get the list of available search providers
|
||||||
*
|
*
|
||||||
* @param {string} type the type to search
|
* @param {Object} options destructuring object
|
||||||
* @param {string} query the search
|
* @param {string} options.type the type to search
|
||||||
* @param {int|string|undefined} cursor the offset for paginated searches
|
* @param {string} options.query the search
|
||||||
* @returns {Promise}
|
* @param {int|string|undefined} options.cursor the offset for paginated searches
|
||||||
|
* @returns {Object} {request: Promise, cancel: Promise}
|
||||||
*/
|
*/
|
||||||
export function search(type, query, cursor) {
|
export function search({ type, query, cursor }) {
|
||||||
return axios.get(generateOcsUrl('search', 2) + `providers/${type}/search`, {
|
/**
|
||||||
|
* Generate an axios cancel token
|
||||||
|
*/
|
||||||
|
const cancelToken = createCancelToken()
|
||||||
|
|
||||||
|
const request = async() => axios.get(generateOcsUrl('search', 2) + `providers/${type}/search`, {
|
||||||
|
cancelToken: cancelToken.token,
|
||||||
params: {
|
params: {
|
||||||
term: query,
|
term: query,
|
||||||
cursor,
|
cursor,
|
||||||
|
@ -68,4 +81,9 @@ export function search(type, query, cursor) {
|
||||||
from: window.location.pathname.replace('/index.php', '') + window.location.search,
|
from: window.location.pathname.replace('/index.php', '') + window.location.search,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
request,
|
||||||
|
cancel: cancelToken.cancel,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,16 +31,30 @@
|
||||||
<Magnify class="unified-search__trigger" :size="20" fill-color="var(--color-primary-text)" />
|
<Magnify class="unified-search__trigger" :size="20" fill-color="var(--color-primary-text)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Search input -->
|
<!-- Search form & filters wrapper -->
|
||||||
<div class="unified-search__input-wrapper">
|
<div class="unified-search__input-wrapper">
|
||||||
<input ref="input"
|
<form class="unified-search__form"
|
||||||
v-model="query"
|
role="search"
|
||||||
class="unified-search__input"
|
@submit.prevent.stop="onInputEnter"
|
||||||
type="search"
|
@reset.prevent.stop="onReset">
|
||||||
:placeholder="t('core', 'Search {types} …', { types: typesNames.join(', ').toLowerCase() })"
|
<!-- Search input -->
|
||||||
@input="onInputDebounced"
|
<input ref="input"
|
||||||
@keypress.enter.prevent.stop="onInputEnter"
|
v-model="query"
|
||||||
@search="onSearch">
|
class="unified-search__form-input"
|
||||||
|
type="search"
|
||||||
|
:class="{'unified-search__form-input--with-reset': !!query}"
|
||||||
|
:placeholder="t('core', 'Search {types} …', { types: typesNames.join(', ').toLowerCase() })"
|
||||||
|
@input="onInputDebounced"
|
||||||
|
@keypress.enter.prevent.stop="onInputEnter">
|
||||||
|
|
||||||
|
<!-- Reset search button -->
|
||||||
|
<input v-if="!!query"
|
||||||
|
type="reset"
|
||||||
|
class="unified-search__form-reset icon-close"
|
||||||
|
:aria-label="t('core','Reset search')"
|
||||||
|
value="">
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Search filters -->
|
<!-- Search filters -->
|
||||||
<Actions v-if="availableFilters.length > 1" class="unified-search__filters" placement="bottom">
|
<Actions v-if="availableFilters.length > 1" class="unified-search__filters" placement="bottom">
|
||||||
<ActionButton v-for="type in availableFilters"
|
<ActionButton v-for="type in availableFilters"
|
||||||
|
@ -107,6 +121,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { emit } from '@nextcloud/event-bus'
|
import { emit } from '@nextcloud/event-bus'
|
||||||
import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot } from '../services/UnifiedSearchService'
|
import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot } from '../services/UnifiedSearchService'
|
||||||
|
import { showError } from '@nextcloud/dialogs'
|
||||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||||
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
|
@ -116,7 +131,6 @@ import Magnify from 'vue-material-design-icons/Magnify'
|
||||||
import HeaderMenu from '../components/HeaderMenu'
|
import HeaderMenu from '../components/HeaderMenu'
|
||||||
import SearchResult from '../components/UnifiedSearch/SearchResult'
|
import SearchResult from '../components/UnifiedSearch/SearchResult'
|
||||||
import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders'
|
import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders'
|
||||||
import { showError } from '@nextcloud/dialogs'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UnifiedSearch',
|
name: 'UnifiedSearch',
|
||||||
|
@ -135,10 +149,17 @@ export default {
|
||||||
return {
|
return {
|
||||||
types: [],
|
types: [],
|
||||||
|
|
||||||
|
// Cursors per types
|
||||||
cursors: {},
|
cursors: {},
|
||||||
|
// Various search limits per types
|
||||||
limits: {},
|
limits: {},
|
||||||
|
// Loading types
|
||||||
loading: {},
|
loading: {},
|
||||||
|
// Reached search types
|
||||||
reached: {},
|
reached: {},
|
||||||
|
// Pending cancellable requests
|
||||||
|
requests: [],
|
||||||
|
// List of all results
|
||||||
results: {},
|
results: {},
|
||||||
|
|
||||||
query: '',
|
query: '',
|
||||||
|
@ -296,10 +317,12 @@ export default {
|
||||||
/**
|
/**
|
||||||
* Reset the search state
|
* Reset the search state
|
||||||
*/
|
*/
|
||||||
resetSearch() {
|
onReset() {
|
||||||
emit('nextcloud:unified-search:reset')
|
emit('nextcloud:unified-search:reset')
|
||||||
|
this.logger.debug('Search reset')
|
||||||
this.query = ''
|
this.query = ''
|
||||||
this.resetState()
|
this.resetState()
|
||||||
|
this.focusInput()
|
||||||
},
|
},
|
||||||
resetState() {
|
resetState() {
|
||||||
this.cursors = {}
|
this.cursors = {}
|
||||||
|
@ -308,6 +331,15 @@ export default {
|
||||||
this.reached = {}
|
this.reached = {}
|
||||||
this.results = {}
|
this.results = {}
|
||||||
this.focused = null
|
this.focused = null
|
||||||
|
this.cancelPendingRequests()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel any ongoing searches
|
||||||
|
*/
|
||||||
|
cancelPendingRequests() {
|
||||||
|
// Cancel all pending requests
|
||||||
|
this.requests.forEach(cancel => cancel())
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -320,18 +352,6 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Watch the search event on the input
|
|
||||||
* Used to detect the reset button press
|
|
||||||
* @param {Event} event the search event
|
|
||||||
*/
|
|
||||||
onSearch(event) {
|
|
||||||
// If value is empty, the reset button has been pressed
|
|
||||||
if (event.target.value === '') {
|
|
||||||
this.resetSearch()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If we have results already, open first one
|
* If we have results already, open first one
|
||||||
* If not, trigger the search again
|
* If not, trigger the search again
|
||||||
|
@ -378,29 +398,34 @@ export default {
|
||||||
// Reset search if the query changed
|
// Reset search if the query changed
|
||||||
this.resetState()
|
this.resetState()
|
||||||
|
|
||||||
types.forEach(async type => {
|
Promise.all(types.map(async type => {
|
||||||
try {
|
try {
|
||||||
|
// Init cancellable request
|
||||||
|
const { request, cancel } = search({ type, query })
|
||||||
|
this.requests.push(cancel)
|
||||||
|
|
||||||
|
// Mark this type as loading and fetch results
|
||||||
this.$set(this.loading, type, true)
|
this.$set(this.loading, type, true)
|
||||||
const request = await search(type, query)
|
const { data } = await request()
|
||||||
|
|
||||||
// Process results
|
// Process results
|
||||||
if (request.data.ocs.data.entries.length > 0) {
|
if (data.ocs.data.entries.length > 0) {
|
||||||
this.$set(this.results, type, request.data.ocs.data.entries)
|
this.$set(this.results, type, data.ocs.data.entries)
|
||||||
} else {
|
} else {
|
||||||
this.$delete(this.results, type)
|
this.$delete(this.results, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save cursor if any
|
// Save cursor if any
|
||||||
if (request.data.ocs.data.cursor) {
|
if (data.ocs.data.cursor) {
|
||||||
this.$set(this.cursors, type, request.data.ocs.data.cursor)
|
this.$set(this.cursors, type, data.ocs.data.cursor)
|
||||||
} else if (!request.data.ocs.data.isPaginated) {
|
} else if (!data.ocs.data.isPaginated) {
|
||||||
// If no cursor and no pagination, we save the default amount
|
// If no cursor and no pagination, we save the default amount
|
||||||
// provided by server's initial state `defaultLimit`
|
// provided by server's initial state `defaultLimit`
|
||||||
this.$set(this.limits, type, this.defaultLimit)
|
this.$set(this.limits, type, this.defaultLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we reached end of pagination
|
// Check if we reached end of pagination
|
||||||
if (request.data.ocs.data.entries.length < this.defaultLimit) {
|
if (data.ocs.data.entries.length < this.defaultLimit) {
|
||||||
this.$set(this.reached, type, true)
|
this.$set(this.reached, type, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,13 +434,19 @@ export default {
|
||||||
this.focused = 0
|
this.focused = 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error searching for ${this.typesMap[type]}`, error)
|
// If this is not a cancelled throw
|
||||||
showError(this.t('core', 'An error occurred while looking for {type}', { type: this.typesMap[type] }))
|
if (error.response && error.response.status) {
|
||||||
|
this.logger.error(`Error searching for ${this.typesMap[type]}`, error)
|
||||||
|
showError(this.t('core', 'An error occurred while searching for {type}', { type: this.typesMap[type] }))
|
||||||
|
}
|
||||||
|
|
||||||
this.$delete(this.results, type)
|
this.$delete(this.results, type)
|
||||||
} finally {
|
} finally {
|
||||||
this.$set(this.loading, type, false)
|
this.$set(this.loading, type, false)
|
||||||
}
|
}
|
||||||
|
})).then(() => {
|
||||||
|
// We finished all searches
|
||||||
|
this.loading = {}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onInputDebounced: debounce(function(e) {
|
onInputDebounced: debounce(function(e) {
|
||||||
|
@ -434,7 +465,7 @@ export default {
|
||||||
this.$set(this.loading, type, true)
|
this.$set(this.loading, type, true)
|
||||||
|
|
||||||
if (this.cursors[type]) {
|
if (this.cursors[type]) {
|
||||||
const request = await search(type, this.query, this.cursors[type])
|
const request = await search({ type, query: this.query, cursor: this.cursors[type] })
|
||||||
|
|
||||||
// Save cursor if any
|
// Save cursor if any
|
||||||
if (request.data.ocs.data.cursor) {
|
if (request.data.ocs.data.cursor) {
|
||||||
|
@ -582,6 +613,7 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$margin: 10px;
|
$margin: 10px;
|
||||||
|
$input-height: 34px;
|
||||||
$input-padding: 6px;
|
$input-padding: 6px;
|
||||||
|
|
||||||
.unified-search {
|
.unified-search {
|
||||||
|
@ -591,13 +623,13 @@ $input-padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input-wrapper {
|
&__input-wrapper {
|
||||||
width: 100%;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
// above search results
|
// above search results
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
top: 0;
|
top: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
background-color: var(--color-main-background);
|
background-color: var(--color-main-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -609,17 +641,60 @@ $input-padding: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__form {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 34px;
|
|
||||||
margin: $margin;
|
margin: $margin;
|
||||||
padding: $input-padding;
|
|
||||||
&,
|
&-input,
|
||||||
&[placeholder],
|
&-reset {
|
||||||
&::placeholder {
|
margin: $input-padding / 2;
|
||||||
overflow: hidden;
|
}
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
&-input {
|
||||||
|
width: 100%;
|
||||||
|
height: $input-height;
|
||||||
|
padding: $input-padding;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&[placeholder],
|
||||||
|
&::placeholder {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide webkit clear search
|
||||||
|
&::-webkit-search-decoration,
|
||||||
|
&::-webkit-search-cancel-button,
|
||||||
|
&::-webkit-search-results-button,
|
||||||
|
&::-webkit-search-results-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ellipsis earlier if reset button is here
|
||||||
|
&--with-reset {
|
||||||
|
padding-right: $input-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-reset {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: $input-height - $input-padding;
|
||||||
|
height: $input-height - $input-padding;
|
||||||
|
padding: 0;
|
||||||
|
opacity: .5;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue