diff --git a/src/renderer/components/ft-input/ft-input.css b/src/renderer/components/ft-input/ft-input.css index 799dab09e0153..9587079ad6665 100644 --- a/src/renderer/components/ft-input/ft-input.css +++ b/src/renderer/components/ft-input/ft-input.css @@ -219,6 +219,17 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp block-size: 16px; } +.removeButton { + text-decoration: none; + float: var(--float-right-ltr-rtl-value); + font-size: 13px; +} + +.removeButton:hover, +.removeButton.removeButtonSelected { + text-decoration: underline; +} + .hover { background-color: var(--scrollbar-color-hover); color: var(--scrollbar-text-color-hover); diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 1a7cd3281d5d2..0dea2b751db6d 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -67,6 +67,10 @@ export default defineComponent({ type: Array, default: null }, + canRemoveResults: { + type: Boolean, + default: false + }, showDataWhenEmpty: { type: Boolean, default: false @@ -76,7 +80,7 @@ export default defineComponent({ default: '' } }, - emits: ['clear', 'click', 'input'], + emits: ['clear', 'click', 'input', 'remove'], data: function () { let actionIcon = ['fas', 'search'] if (this.forceActionButtonIconName !== null) { @@ -96,6 +100,8 @@ export default defineComponent({ // As the text input box should be empty clearTextButtonExisting: false, clearTextButtonVisible: false, + removeButtonSelectedIndex: -1, + removalMade: false, actionButtonIconName: actionIcon } }, @@ -166,6 +172,7 @@ export default defineComponent({ this.searchState.showOptions = false this.searchState.selectedOption = -1 this.searchState.keyboardSelectedOptionIndex = -1 + this.removeButtonSelectedIndex = -1 this.$emit('input', this.inputData) this.$emit('click', this.inputData, { event: e }) }, @@ -247,19 +254,34 @@ export default defineComponent({ }, handleOptionClick: function (index) { + if (this.removeButtonSelectedIndex !== -1) { + this.handleRemoveClick(index) + return + } this.searchState.showOptions = false this.inputData = this.visibleDataList[index] this.$emit('input', this.inputData) this.handleClick() }, + handleRemoveClick: function (index) { + if (!this.canRemoveResults) { return } + + // keep focus in input even if removed "Remove" button was clicked + this.$refs.input.focus() + this.removalMade = true + this.$emit('remove', this.visibleDataList[index]) + }, + /** * @param {KeyboardEvent} event */ handleKeyDown: function (event) { if (event.key === 'Enter') { // Update Input box value if enter key was pressed and option selected - if (this.searchState.selectedOption !== -1) { + if (this.removeButtonSelectedIndex !== -1) { + this.handleRemoveClick(this.removeButtonSelectedIndex) + } else if (this.searchState.selectedOption !== -1) { this.searchState.showOptions = false event.preventDefault() this.inputData = this.getTextForArrayAtIndex(this.visibleDataList, this.searchState.selectedOption) @@ -274,7 +296,7 @@ export default defineComponent({ if (this.visibleDataList.length === 0) { return } this.searchState.showOptions = true - const isArrow = event.key === 'ArrowDown' || event.key === 'ArrowUp' + const isArrow = event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' if (!isArrow) { const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue // Keyboard selected & is char @@ -288,17 +310,33 @@ export default defineComponent({ } event.preventDefault() - if (event.key === 'ArrowDown') { - this.searchState.selectedOption++ - } else if (event.key === 'ArrowUp') { - this.searchState.selectedOption-- + + if (event.key === 'ArrowRight') { + this.removeButtonSelectedIndex = this.searchState.selectedOption + } + + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + const newIndex = this.searchState.selectedOption + (event.key === 'ArrowDown' ? 1 : -1) + this.updateSelectedOptionIndex(newIndex) + + // reset removal + this.removeButtonSelectedIndex = -1 + } + + if (this.searchState.selectedOption !== -1 && event.key === 'ArrowRight') { + this.removeButtonSelectedIndex = this.searchState.selectedOption } + }, + + updateSelectedOptionIndex: function (index) { + this.searchState.selectedOption = index // Allow deselecting suggestion if (this.searchState.selectedOption < -1) { this.searchState.selectedOption = this.visibleDataList.length - 1 } else if (this.searchState.selectedOption > this.visibleDataList.length - 1) { this.searchState.selectedOption = -1 } + // Update displayed value this.searchState.keyboardSelectedOptionIndex = this.searchState.selectedOption }, @@ -313,8 +351,14 @@ export default defineComponent({ updateVisibleDataList: function () { // Reset selected option before it's updated - this.searchState.selectedOption = -1 - this.searchState.keyboardSelectedOptionIndex = -1 + if (!this.removalMade || this.searchState.selectedOption >= this.dataList.length) { + this.searchState.selectedOption = -1 + this.searchState.keyboardSelectedOptionIndex = -1 + this.removeButtonSelectedIndex = -1 + } + + this.removalMade = false + if (this.inputData.trim() === '') { this.visibleDataList = this.dataList return diff --git a/src/renderer/components/ft-input/ft-input.vue b/src/renderer/components/ft-input/ft-input.vue index 8a8429bda9861..a0046ada87c23 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -82,14 +82,25 @@ :class="{ hover: searchState.selectedOption === index }" @click="handleOptionClick(index)" @mouseenter="searchState.selectedOption = index" - @mouseleave="searchState.selectedOption = -1" + @mouseleave="searchState.selectedOption = -1; removeButtonSelectedIndex = -1" > - {{ entry }} + {{ entry }} + + {{ $t('Search Bar.Remove') }} + diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 57f8dd774aca5..a46c9eddf7c6f 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -1,5 +1,5 @@ import { defineComponent } from 'vue' -import { mapActions } from 'vuex' +import { mapActions, mapMutations } from 'vuex' import FtInput from '../ft-input/ft-input.vue' import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue' import FtIconButton from '../ft-icon-button/ft-icon-button.vue' @@ -121,17 +121,21 @@ export default defineComponent({ ) }, + usingSearchHistoryResults: function () { + return this.lastSuggestionQuery === '' + }, + // show latest search history when the search bar is empty activeDataList: function () { if (!this.enableSearchSuggestions) { return } - return this.lastSuggestionQuery === '' ? this.$store.getters.getLatestUniqueSearchHistoryNames : this.searchSuggestionsDataList + return this.usingSearchHistoryResults ? this.$store.getters.getLatestUniqueSearchHistoryNames : this.searchSuggestionsDataList }, searchResultIcon: function () { - return this.lastSuggestionQuery === '' ? ['fas', 'clock-rotate-left'] : ['fas', 'magnifying-glass'] - } + return this.usingSearchHistoryResults ? ['fas', 'clock-rotate-left'] : ['fas', 'magnifying-glass'] + }, }, watch: { $route: function () { @@ -424,10 +428,19 @@ export default defineComponent({ this.navigationHistoryDropdownActiveEntry.label = value } }, + removeSearchHistoryEntryInDbAndCache(query) { + this.removeSearchHistoryEntry(query) + this.removeFromSessionSearchHistory(query) + }, ...mapActions([ 'getYoutubeUrlInfo', + 'removeSearchHistoryEntry', 'showSearchFilters' + ]), + + ...mapMutations([ + 'removeFromSessionSearchHistory' ]) } }) diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue index b5c6861f06f25..e44a23b1beda5 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -97,9 +97,11 @@ :spellcheck="false" :show-clear-text-button="true" :show-data-when-empty="true" + :can-remove-results="usingSearchHistoryResults" @input="getSearchSuggestionsDebounce" @click="goToSearch" @clear="() => lastSuggestionQuery = ''" + @remove="removeSearchHistoryEntryInDbAndCache" /> { - return searchHistoryEntry._id === _id - }) - - state.searchHistoryEntries.splice(i, 1) + state.searchHistoryEntries = state.searchHistoryEntries.filter((searchHistoryEntry) => searchHistoryEntry._id !== _id) }, removeSearchHistoryEntriesFromList(state, ids) { diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js index b76cb04811fc1..c5b9075487899 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -830,6 +830,10 @@ const mutations = { vueSet(state.deArrowCache, payload.videoId, payload) }, + removeFromSessionSearchHistory (state, query) { + state.sessionSearchHistory = state.sessionSearchHistory.filter((search) => search.query !== query) + }, + addToSessionSearchHistory (state, payload) { const sameSearch = state.sessionSearchHistory.findIndex((search) => { return search.query === payload.query && searchFiltersMatch(payload.searchSettings, search.searchSettings) diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 4e0a172cd9803..38b0aed05d7e3 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -63,6 +63,7 @@ Global: Search / Go to URL: Search / Go to URL Search Bar: Clear Input: Clear Input + Remove: Remove Search character limit: Search query is over the {searchCharacterLimit} character limit Search Listing: Label: