Skip to content

Commit

Permalink
Update ft-input for top navbar search input to behave more like Youtu…
Browse files Browse the repository at this point in the history
…be one (#3793)

* * Update ft-input for top navbar search input to behave more like Youtube one

* * Implement mouseleave = deselect

* ! Fix clicking option/enter causes incorrect displayed input

* * Update search input to update input data based on KB selected suggesion value on keydown

* * Allow suggesion deselection via arrow up/down

* $ Fix naming, import code style

* - Remove unused import
  • Loading branch information
PikachuEXE authored Aug 2, 2023
1 parent b2d95eb commit d8ed6b9
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 40 deletions.
82 changes: 56 additions & 26 deletions src/renderer/components/ft-input/ft-input.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue'
import FtTooltip from '../ft-tooltip/ft-tooltip.vue'
import { mapActions } from 'vuex'
import { isKeyboardEventKeyPrintableChar, isNullOrEmpty } from '../../helpers/strings'

export default defineComponent({
name: 'FtInput',
Expand Down Expand Up @@ -73,7 +74,8 @@ export default defineComponent({
searchState: {
showOptions: false,
selectedOption: -1,
isPointerInList: false
isPointerInList: false,
keyboardSelectedOptionIndex: -1,
},
visibleDataList: this.dataList,
// This button should be invisible on app start
Expand All @@ -98,7 +100,23 @@ export default defineComponent({

inputDataPresent: function () {
return this.inputData.length > 0
}
},
inputDataDisplayed() {
if (!this.isSearch) { return this.inputData }

const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue
if (selectedOptionValue != null && selectedOptionValue !== '') {
return selectedOptionValue
}

return this.inputData
},

searchStateKeyboardSelectedOptionValue() {
if (this.searchState.keyboardSelectedOptionIndex === -1) { return null }

return this.visibleDataList[this.searchState.keyboardSelectedOptionIndex]
},
},
watch: {
dataList(val, oldVal) {
Expand Down Expand Up @@ -128,11 +146,15 @@ export default defineComponent({
if (!this.inputDataPresent) { return }

this.searchState.showOptions = false
this.searchState.selectedOption = -1
this.searchState.keyboardSelectedOptionIndex = -1
this.$emit('input', this.inputData)
this.$emit('click', this.inputData, { event: e })
},

handleInput: function (val) {
this.inputData = val

if (this.isSearch &&
this.searchState.selectedOption !== -1 &&
this.inputData === this.visibleDataList[this.searchState.selectedOption]) { return }
Expand Down Expand Up @@ -212,6 +234,9 @@ export default defineComponent({
this.handleClick()
},

/**
* @param {KeyboardEvent} event
*/
handleKeyDown: function (event) {
if (event.key === 'Enter') {
// Update Input box value if enter key was pressed and option selected
Expand All @@ -229,25 +254,32 @@ export default defineComponent({

this.searchState.showOptions = true
const isArrow = event.key === 'ArrowDown' || event.key === 'ArrowUp'
if (isArrow) {
event.preventDefault()
if (event.key === 'ArrowDown') {
this.searchState.selectedOption = (this.searchState.selectedOption + 1) % this.visibleDataList.length
} else if (event.key === 'ArrowUp') {
if (this.searchState.selectedOption < 1) {
this.searchState.selectedOption = this.visibleDataList.length - 1
} else {
this.searchState.selectedOption--
}
}
if (this.searchState.selectedOption < 0) {
this.searchState.selectedOption = this.visibleDataList.length
} else if (this.searchState.selectedOption > this.visibleDataList.length - 1) {
this.searchState.selectedOption = 0
if (!isArrow) {
const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue
// Keyboard selected & is char
if (!isNullOrEmpty(selectedOptionValue) && isKeyboardEventKeyPrintableChar(event.key)) {
// Update input based on KB selected suggestion value instead of current input value
event.preventDefault()
this.handleInput(`${selectedOptionValue}${event.key}`)
return
}
} else {
return
}

event.preventDefault()
if (event.key === 'ArrowDown') {
this.searchState.selectedOption++
} else if (event.key === 'ArrowUp') {
this.searchState.selectedOption--
}
// 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
},

handleInputBlur: function () {
Expand All @@ -260,21 +292,19 @@ export default defineComponent({

updateVisibleDataList: function () {
if (this.dataList.length === 0) { return }
// Reset selected option before it's updated
this.searchState.selectedOption = -1
this.searchState.keyboardSelectedOptionIndex = -1
if (this.inputData === '') {
this.visibleDataList = this.dataList
return
}
// get list of items that match input
const lowerCaseInputData = this.inputData.toLowerCase()
const visList = this.dataList.filter(x => {
if (x.toLowerCase().indexOf(lowerCaseInputData) !== -1) {
return true
} else {
return false
}
})

this.visibleDataList = visList
this.visibleDataList = this.dataList.filter(x => {
return x.toLowerCase().indexOf(lowerCaseInputData) !== -1
})
},

updateInputData: function(text) {
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/components/ft-input/ft-input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<input
:id="id"
ref="input"
v-model="inputData"
:value="inputDataDisplayed"
:list="idDataList"
class="ft-input"
:type="inputType"
Expand Down Expand Up @@ -77,9 +77,10 @@
<li
v-for="(list, index) in visibleDataList"
:key="index"
:class="searchState.selectedOption == index ? 'hover': ''"
:class="searchState.selectedOption === index ? 'hover': ''"
@click="handleOptionClick(index)"
@mouseenter="searchState.selectedOption = index"
@mouseleave="searchState.selectedOption = -1"
>
{{ list }}
</li>
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/helpers/api/invidious.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import store from '../../store/index'
import { isNullOrEmpty, stripHTML, toLocalePublicationString } from '../utils'
import { stripHTML, toLocalePublicationString } from '../utils'
import { isNullOrEmpty } from '../strings'
import autolinker from 'autolinker'

function getCurrentInstance() {
Expand Down
25 changes: 25 additions & 0 deletions src/renderer/helpers/strings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* This will return true if a string is null, undefined or empty.
* @param {string|null|undefined} _string the string to process
* @returns {boolean} whether the string is empty or not
*/
export function isNullOrEmpty(_string) {
return _string == null || _string === ''
}

/**
* Is KeyboardEvent.key a printable char
* @param {string} eventKey the string from KeyboardEvent.key to process
* @returns {boolean} whether the string from KeyboardEvent.key is a printable char or not
*/
export function isKeyboardEventKeyPrintableChar(eventKey) {
// Most printable chars are all strings with length 1 (except Unicode)
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
// https://www.w3.org/TR/DOM-Level-3-Events-key/
if (eventKey.length === 1) { return true }
// Emoji
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape
if (/\p{Emoji_Presentation}/u.test(eventKey)) { return true }

return false
}
9 changes: 0 additions & 9 deletions src/renderer/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -627,15 +627,6 @@ export function formatNumber(number, options = undefined) {
return Intl.NumberFormat([i18n.locale.replace('_', '-'), 'en'], options).format(number)
}

/**
* This will return true if a string is null, undefined or empty.
* @param {string} _string the string to process
* @returns {bool} whether the string is empty or not
*/
export function isNullOrEmpty(_string) {
return _string == null || _string === ''
}

export function getTodayDateStrLocalTimezone() {
const timeNow = new Date()
// `Date#getTimezoneOffset` returns the difference, in minutes
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/views/Channel/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import FtSubscribeButton from '../../components/ft-subscribe-button/ft-subscribe
import ChannelAbout from '../../components/channel-about/channel-about.vue'

import autolinker from 'autolinker'
import { copyToClipboard, extractNumberFromString, formatNumber, isNullOrEmpty, showToast } from '../../helpers/utils'
import { copyToClipboard, extractNumberFromString, formatNumber, showToast } from '../../helpers/utils'
import { isNullOrEmpty } from '../../helpers/strings'
import packageDetails from '../../../../package.json'
import {
invidiousAPICall,
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/views/Hashtag/Hashtag.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import packageDetails from '../../../../package.json'
import { getHashtagLocal, parseLocalListVideo } from '../../helpers/api/local'
import { copyToClipboard, isNullOrEmpty, showToast } from '../../helpers/utils'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { isNullOrEmpty } from '../../helpers/strings'
import { getHashtagInvidious } from '../../helpers/api/invidious'

export default defineComponent({
Expand Down

0 comments on commit d8ed6b9

Please sign in to comment.