mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Replace old search fields component with react component
This commit is contained in:
parent
4bf1acbf4e
commit
bd4626c682
6 changed files with 112 additions and 135 deletions
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -6912,6 +6912,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"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": {
|
"lodash._reinterpolate": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"jstree": "^3.3.11",
|
"jstree": "^3.3.11",
|
||||||
"libarchive.js": "^1.3.0",
|
"libarchive.js": "^1.3.0",
|
||||||
"libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv",
|
"libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"material-design-icons-iconfont": "^6.1.0",
|
"material-design-icons-iconfont": "^6.1.0",
|
||||||
"native-promise-only": "^0.8.0-a",
|
"native-promise-only": "^0.8.0-a",
|
||||||
"page": "^1.11.6",
|
"page": "^1.11.6",
|
||||||
|
|
40
src/components/alphaPicker/AlphaPickerComponent.js
Normal file
40
src/components/alphaPicker/AlphaPickerComponent.js
Normal file
|
@ -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 (
|
||||||
|
<div
|
||||||
|
ref={element}
|
||||||
|
className='alphaPicker align-items-center'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AlphaPickerComponent.propTypes = {
|
||||||
|
onAlphaPicked: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlphaPickerComponent;
|
|
@ -1,36 +1,85 @@
|
||||||
import { Events } from 'jellyfin-apiclient';
|
import debounce from 'lodash-es/debounce';
|
||||||
import PropTypes from 'prop-types';
|
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: `<input
|
||||||
|
is="emby-input"
|
||||||
|
class="searchfields-txtSearch"
|
||||||
|
type="text"
|
||||||
|
data-keyboard="true"
|
||||||
|
placeholder="${globalize.translate('Search')}"
|
||||||
|
autocomplete="off"
|
||||||
|
maxlength="40"
|
||||||
|
autofocus
|
||||||
|
/>`
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeInput = (value = '') => value.trim();
|
||||||
|
|
||||||
const SearchFieldsComponent = ({ onSearch = () => {} }) => {
|
const SearchFieldsComponent = ({ onSearch = () => {} }) => {
|
||||||
const [ searchFields, setSearchFields ] = useState(null);
|
const element = useRef(null);
|
||||||
const searchFieldsElement = useRef(null);
|
|
||||||
|
const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch');
|
||||||
|
|
||||||
|
const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchFields(
|
getSearchInput()?.addEventListener('input', e => {
|
||||||
new SearchFields({ element: searchFieldsElement.current })
|
debouncedOnSearch(normalizeInput(e.target?.value));
|
||||||
);
|
});
|
||||||
|
getSearchInput()?.focus();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
searchFields?.destroy();
|
debouncedOnSearch.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const onAlphaPicked = e => {
|
||||||
if (searchFields) {
|
const value = e.detail.value;
|
||||||
Events.on(searchFields, 'search', (e, value) => {
|
const searchInput = getSearchInput();
|
||||||
onSearch(value);
|
|
||||||
});
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className='padded-left padded-right searchFields'
|
className='padded-left padded-right searchFields'
|
||||||
ref={searchFieldsElement}
|
ref={element}
|
||||||
|
>
|
||||||
|
<div className='searchFieldsInner flex align-items-center justify-content-center'>
|
||||||
|
<span className='searchfields-icon material-icons search' />
|
||||||
|
<div
|
||||||
|
className='inputContainer flex-grow'
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
dangerouslySetInnerHTML={createInputElement()}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
{layoutManager.tv && !browser.tv &&
|
||||||
|
<AlphaPicker onAlphaPicked={onAlphaPicked} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -1,7 +0,0 @@
|
||||||
<div class="searchFieldsInner flex align-items-center justify-content-center">
|
|
||||||
<span class="searchfields-icon material-icons search"></span>
|
|
||||||
<div class="inputContainer flex-grow" style="margin-bottom: 0;">
|
|
||||||
<input is="emby-input" class="searchfields-txtSearch" type="text" data-keyboard="true" placeholder="${Search}" autocomplete="off" maxlength="40" autofocus />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="alphaPicker align-items-center hide"></div>
|
|
Loading…
Add table
Add a link
Reference in a new issue