diff --git a/package-lock.json b/package-lock.json index 70e88ee5e..c280d080d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6912,6 +6912,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", diff --git a/package.json b/package.json index 6cac8d4d5..218e2b090 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "jstree": "^3.3.11", "libarchive.js": "^1.3.0", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", + "lodash-es": "^4.17.21", "material-design-icons-iconfont": "^6.1.0", "native-promise-only": "^0.8.0-a", "page": "^1.11.6", diff --git a/src/components/alphaPicker/AlphaPickerComponent.js b/src/components/alphaPicker/AlphaPickerComponent.js new file mode 100644 index 000000000..717b9f969 --- /dev/null +++ b/src/components/alphaPicker/AlphaPickerComponent.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useState } from 'react'; + +import AlphaPicker from './alphaPicker'; + +// React compatibility wrapper component for alphaPicker.js +const AlphaPickerComponent = ({ onAlphaPicked = () => {} }) => { + const [ alphaPicker, setAlphaPicker ] = useState(null); + const element = useRef(null); + + useEffect(() => { + setAlphaPicker(new AlphaPicker({ + element: element.current, + mode: 'keyboard' + })); + + element.current?.addEventListener('alphavalueclicked', onAlphaPicked); + + return () => { + alphaPicker?.destroy(); + }; + }, []); + + useEffect(() => { + + }, [ alphaPicker ]); + + return ( +
+ ); +}; + +AlphaPickerComponent.propTypes = { + onAlphaPicked: PropTypes.func +}; + +export default AlphaPickerComponent; diff --git a/src/components/search/SearchFieldsComponent.js b/src/components/search/SearchFieldsComponent.js index 10526fa16..652e2b718 100644 --- a/src/components/search/SearchFieldsComponent.js +++ b/src/components/search/SearchFieldsComponent.js @@ -1,36 +1,85 @@ -import { Events } from 'jellyfin-apiclient'; +import debounce from 'lodash-es/debounce'; import PropTypes from 'prop-types'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; -import SearchFields from './searchfields'; +import AlphaPicker from '../alphaPicker/AlphaPickerComponent'; +import globalize from '../../scripts/globalize'; + +import 'material-design-icons-iconfont'; + +import '../../elements/emby-input/emby-input'; +import '../../assets/css/flexstyles.scss'; +import './searchfields.scss'; +import layoutManager from '../layoutManager'; +import browser from '../../scripts/browser'; + +// There seems to be some compatibility issues here between +// React and our legacy web components, so we need to inject +// them as an html string for now =/ +const createInputElement = () => ({ + __html: `` +}); + +const normalizeInput = (value = '') => value.trim(); const SearchFieldsComponent = ({ onSearch = () => {} }) => { - const [ searchFields, setSearchFields ] = useState(null); - const searchFieldsElement = useRef(null); + const element = useRef(null); + + const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch'); + + const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), []); useEffect(() => { - setSearchFields( - new SearchFields({ element: searchFieldsElement.current }) - ); + getSearchInput()?.addEventListener('input', e => { + debouncedOnSearch(normalizeInput(e.target?.value)); + }); + getSearchInput()?.focus(); return () => { - searchFields?.destroy(); + debouncedOnSearch.cancel(); }; }, []); - useEffect(() => { - if (searchFields) { - Events.on(searchFields, 'search', (e, value) => { - onSearch(value); - }); + const onAlphaPicked = e => { + const value = e.detail.value; + const searchInput = getSearchInput(); + + if (value === 'backspace') { + const currentValue = searchInput.value; + searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : ''; + } else { + searchInput.value += value; } - }, [ searchFields ]); + + searchInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); + }; return (
+ ref={element} + > +
+ +
+
+ {layoutManager.tv && !browser.tv && + + } +
); }; diff --git a/src/components/search/searchfields.js b/src/components/search/searchfields.js deleted file mode 100644 index 1844ad811..000000000 --- a/src/components/search/searchfields.js +++ /dev/null @@ -1,111 +0,0 @@ -import layoutManager from '../layoutManager'; -import globalize from '../../scripts/globalize'; -import { Events } from 'jellyfin-apiclient'; -import browser from '../../scripts/browser'; -import AlphaPicker from '../alphaPicker/alphaPicker'; -import '../../elements/emby-input/emby-input'; -import '../../assets/css/flexstyles.scss'; -import 'material-design-icons-iconfont'; -import './searchfields.scss'; -import template from './searchfields.template.html'; - -function onSearchTimeout() { - const instance = this; - let value = instance.nextSearchValue; - - value = (value || '').trim(); - Events.trigger(instance, 'search', [value]); -} - -function triggerSearch(instance, value) { - if (instance.searchTimeout) { - clearTimeout(instance.searchTimeout); - } - - instance.nextSearchValue = value; - instance.searchTimeout = setTimeout(onSearchTimeout.bind(instance), 400); -} - -function onAlphaValueClicked(e) { - const value = e.detail.value; - const searchFieldsInstance = this; - - const txtSearch = searchFieldsInstance.options.element.querySelector('.searchfields-txtSearch'); - - if (value === 'backspace') { - const val = txtSearch.value; - txtSearch.value = val.length ? val.substring(0, val.length - 1) : ''; - } else { - txtSearch.value += value; - } - - txtSearch.dispatchEvent(new CustomEvent('input', { - bubbles: true - })); -} - -function initAlphaPicker(alphaPickerElement, instance) { - instance.alphaPicker = new AlphaPicker({ - element: alphaPickerElement, - mode: 'keyboard' - }); - - alphaPickerElement.addEventListener('alphavalueclicked', onAlphaValueClicked.bind(instance)); -} - -function onSearchInput(e) { - const value = e.target.value; - const searchFieldsInstance = this; - triggerSearch(searchFieldsInstance, value); -} - -function embed(elem, instance) { - elem.innerHTML = globalize.translateHtml(template, 'core'); - - elem.classList.add('searchFields'); - - const txtSearch = elem.querySelector('.searchfields-txtSearch'); - - if (layoutManager.tv && !browser.tv) { - const alphaPickerElement = elem.querySelector('.alphaPicker'); - - elem.querySelector('.alphaPicker').classList.remove('hide'); - initAlphaPicker(alphaPickerElement, instance); - } - - txtSearch.addEventListener('input', onSearchInput.bind(instance)); - - instance.focus(); -} - -class SearchFields { - constructor(options) { - this.options = options; - embed(options.element, this); - } - focus() { - this.options.element.querySelector('.searchfields-txtSearch').focus(); - } - destroy() { - const options = this.options; - if (options) { - options.element.classList.remove('searchFields'); - } - this.options = null; - - const alphaPicker = this.alphaPicker; - if (alphaPicker) { - alphaPicker.destroy(); - } - this.alphaPicker = null; - - const searchTimeout = this.searchTimeout; - if (searchTimeout) { - clearTimeout(searchTimeout); - } - this.searchTimeout = null; - this.nextSearchValue = null; - } -} - -export default SearchFields; diff --git a/src/components/search/searchfields.template.html b/src/components/search/searchfields.template.html deleted file mode 100644 index 2ba21492b..000000000 --- a/src/components/search/searchfields.template.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -
- -
-
-