WebUI: Add support for running concurrent searches

This PR adds support for running multiple concurrent searches in the Web UI. This is already supported in the GUI as well as by the Web API. Behavior mimics the GUI as closely as possible.

All filters and sorting are preserved per-tab, allowing you to apply unique filters and sorts to each of your searches. Row selection is also preserved across tab navigation.

Closes #12840.
PR #20593.
This commit is contained in:
Thomas Piccirello 2024-03-29 00:05:43 -07:00 committed by GitHub
parent f5cac13979
commit eb9e98a4b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 387 additions and 99 deletions

View file

@ -67,7 +67,7 @@ repos:
hooks:
- id: codespell
name: Check spelling (codespell)
args: ["--ignore-words-list", "additionals,curren,fo,ist,ket,superseeding,te,ths"]
args: ["--ignore-words-list", "additionals,curren,fo,ist,ket,searchin,superseeding,te,ths"]
exclude: |
(?x)^(
.*\.desktop |

View file

@ -667,9 +667,9 @@ td.statusBarSeparator {
}
#searchResultsTableContainer {
-moz-height: calc(100% - 140px);
-webkit-height: calc(100% - 140px);
height: calc(100% - 140px);
-moz-height: calc(100% - 177px);
-webkit-height: calc(100% - 177px);
height: calc(100% - 177px);
overflow: auto;
}

View file

@ -516,16 +516,20 @@ window.qBittorrent.DynamicTable = (function() {
return LocalPreferences.get('sorted_column_' + this.dynamicTableDivId);
},
setSortedColumn: function(column) {
/**
* @param {string} column name to sort by
* @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
*/
setSortedColumn: function(column, reverse = null) {
if (column != this.sortedColumn) {
const oldColumn = this.sortedColumn;
this.sortedColumn = column;
this.reverseSort = '0';
this.reverseSort = reverse ?? '0';
this.setSortedColumnIcon(column, oldColumn, false);
}
else {
// Toggle sort order
this.reverseSort = this.reverseSort === '0' ? '1' : '0';
this.reverseSort = reverse ?? (this.reverseSort === '0' ? '1' : '0');
this.setSortedColumnIcon(column, null, (this.reverseSort === '1'));
}
LocalPreferences.set('sorted_column_' + this.dynamicTableDivId, column);

View file

@ -18,15 +18,16 @@
width: 150px;
}
#searchResultsNoPlugins {
#searchResultsNoPlugins,
#searchResultsNoSearches {
height: calc(100% - 110px);
}
#searchResultsNoPlugins table {
table {
height: 100%;
width: 100%;
text-align: center;
}
}
#searchResultsFilters {
height: 30px;
@ -75,9 +76,9 @@
</style>
<div id="searchResults">
<div style="overflow: hidden; height: 70px;">
<div style="margin: 20px 0; height: 30px;">
<input type="text" id="searchPattern" class="searchInputField" placeholder="QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocomplete="off" autocapitalize="none" />
<div style="overflow: hidden; height: 60px;">
<div style="margin: 20px 0 10px 0; height: 30px;">
<input type="text" id="searchPattern" class="searchInputField" placeholder="QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocomplete="off" autocapitalize="none" oninput="qBittorrent.Search.onSearchPatternChanged()" />
<select id="categorySelect" class="searchInputField" onchange="qBittorrent.Search.categorySelected()"></select>
<select id="pluginsSelect" class="searchInputField" onchange="qBittorrent.Search.pluginSelected()"></select>
<button type="button" id="startSearchButton" class="searchInputField" onclick="qBittorrent.Search.startStopSearch()">QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]</button>
@ -99,7 +100,25 @@
<span></span>
</div>
<div id="searchResultsFilters">
<div id="searchResultsNoSearches" style="display: none">
<table>
<tbody>
<tr>
<td>
QBT_TR(Start a search above.)QBT_TR[CONTEXT=SearchEngineWidget]
</td>
</tr>
</tbody>
</table>
<span></span>
</div>
<div id="searchTabsToolbar" class="toolbarTabs" style="border-bottom: 1px solid var(--color-border-default); display: none">
<ul id="searchTabs" class="tab-menu"></ul>
<div class="clear"></div>
</div>
<div id="searchResultsFilters" style="padding-top: 10px; display: none">
<input type="text" id="searchInNameFilter" placeholder="QBT_TR(Filter)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocapitalize="none" />
<span>QBT_TR(Results)QBT_TR[CONTEXT=SearchEngineWidget] (QBT_TR(showing)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsVisible" class="numSearchResults">0</span> QBT_TR(out of)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsTotal" class="numSearchResults">0</span>):</span>
@ -135,8 +154,8 @@
<select id="searchMaxSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()">
<option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option>
<option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option>
<option value="2" selected>QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option>
<option value="3">QBT_TR(GiB)QBT_TR[CONTEXT=misc]</option>
<option value="2">QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option>
<option value="3" selected>QBT_TR(GiB)QBT_TR[CONTEXT=misc]</option>
<option value="4">QBT_TR(TiB)QBT_TR[CONTEXT=misc]</option>
<option value="5">QBT_TR(PiB)QBT_TR[CONTEXT=misc]</option>
<option value="6">QBT_TR(EiB)QBT_TR[CONTEXT=misc]</option>
@ -145,7 +164,7 @@
</div>
</div>
<div id="searchResultsTableContainer">
<div id="searchResultsTableContainer" style="display: none">
<div id="searchResultsTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable" style="position:relative;">
<thead>
@ -200,22 +219,41 @@
init: init,
getPlugin: getPlugin,
searchInTorrentName: searchInTorrentName,
onSearchPatternChanged: onSearchPatternChanged,
categorySelected: categorySelected,
pluginSelected: pluginSelected,
searchSeedsFilterChanged: searchSeedsFilterChanged,
searchSizeFilterChanged: searchSizeFilterChanged,
searchSizeFilterPrefixChanged: searchSizeFilterPrefixChanged
searchSizeFilterPrefixChanged: searchSizeFilterPrefixChanged,
closeSearchTab: closeSearchTab,
};
};
let searchResultsTable;
let loadSearchResultsTimer;
const searchTabIdPrefix = "Search-";
let loadSearchPluginsTimer;
let searchResultsRowId = 0;
let searchRunning = false;
let requestCount = 0;
const searchPlugins = [];
let prevSearchPluginsResponse;
let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
let selectedPlugin = "all";
let prevSelectedPlugin;
// whether the current search pattern differs from the pattern that the active search was performed with
let searchPatternChanged = false;
let searchResultsTable;
/** @type Map<number, {
* searchPattern: string,
* filterPattern: string,
* seedsFilter: {min: number, max: number},
* sizeFilter: {min: number, minUnit: number, max: number, maxUnit: number},
* searchIn: string,
* rows: [],
* rowId: number,
* selectedRowIds: number[],
* running: boolean,
* loadResultsTimer: Timer,
* sort: {column: string, reverse: string},
* }> **/
const searchState = new Map();
const searchText = {
pattern: "",
filterPattern: ""
@ -230,10 +268,6 @@
max: 0.00,
maxUnit: 3
};
let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
let selectedPlugin = "all";
let prevSelectedPlugin;
let activeSearchId = null;
const init = function() {
// load "Search in" preference from local storage
@ -290,13 +324,227 @@
}).activate();
};
const startSearch = function(pattern, category, plugins) {
clearTimeout(loadSearchResultsTimer);
const numSearchTabs = function() {
return $('searchTabs').getElements('li').length;
};
const getSearchIdFromTab = function(tab) {
return Number(tab.id.substring(searchTabIdPrefix.length));
};
const createSearchTab = function(searchId, pattern) {
const newTabId = `${searchTabIdPrefix}${searchId}`;
const tabElem = new Element('a', {
text: pattern,
});
const closeTabElem = new Element('img', {
alt: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]',
title: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]',
src: 'images/application-exit.svg',
width: '8',
height: '8',
style: 'padding-right: 7px; margin-bottom: -1px; margin-left: -5px',
onclick: 'qBittorrent.Search.closeSearchTab(this)',
});
closeTabElem.inject(tabElem, 'top');
tabElem.appendChild(getStatusIconElement('QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/queued.svg'));
$('searchTabs').appendChild(new Element('li', {
id: newTabId,
class: 'selected',
html: tabElem.outerHTML,
}));
// unhide the results elements
if (numSearchTabs() >= 1) {
$('searchResultsNoSearches').style.display = "none";
$('searchResultsFilters').style.display = "block";
$('searchResultsTableContainer').style.display = "block";
$('searchTabsToolbar').style.display = "block";
}
// reinitialize tabs
$('searchTabs').getElements('li').removeEvents('click');
$('searchTabs').getElements('li').addEvent('click', function(e) {
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
setActiveTab(this);
});
// select new tab
setActiveTab($(newTabId));
searchResultsTable.clear();
resetFilters();
searchState.set(searchId, {
searchPattern: pattern,
filterPattern: searchText.filterPattern,
seedsFilter: { min: searchSeedsFilter.min, max: searchSeedsFilter.max },
sizeFilter: { min: searchSizeFilter.min, minUnit: searchSizeFilter.minUnit, max: searchSizeFilter.max, maxUnit: searchSizeFilter.maxUnit },
searchIn: getSearchInTorrentName(),
rows: [],
rowId: 0,
selectedRowIds: [],
running: true,
loadResultsTimer: null,
sort: { column: searchResultsTable.sortedColumn, reverse: searchResultsTable.reverseSort },
});
updateSearchResultsData(searchId);
};
const closeSearchTab = function(el) {
const tab = el.parentElement.parentElement;
const searchId = getSearchIdFromTab(tab);
const isTabSelected = tab.hasClass('selected');
const newTabToSelect = isTabSelected ? tab.nextSibling || tab.previousSibling : null;
const currentSearchId = getSelectedSearchId();
const state = searchState.get(currentSearchId);
// don't bother sending a stop request if already stopped
if (state && state.running) {
stopSearch(searchId);
}
tab.destroy();
if (numSearchTabs() === 0) {
resetSearchState();
resetFilters();
$('numSearchResultsVisible').set('html', 0);
$('numSearchResultsTotal').set('html', 0);
$('searchResultsNoSearches').style.display = "block";
$('searchResultsFilters').style.display = "none";
$('searchResultsTableContainer').style.display = "none";
$('searchTabsToolbar').style.display = "none";
}
else if (isTabSelected && newTabToSelect) {
setActiveTab(newTabToSelect);
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
}
};
const saveCurrentTabState = function() {
const currentSearchId = getSelectedSearchId();
if (!currentSearchId)
return;
const state = searchState.get(currentSearchId);
if (!state)
return;
state.filterPattern = searchText.filterPattern;
state.seedsFilter = {
min: searchSeedsFilter.min,
max: searchSeedsFilter.max,
};
state.sizeFilter = {
min: searchSizeFilter.min,
minUnit: searchSizeFilter.minUnit,
max: searchSizeFilter.max,
maxUnit: searchSizeFilter.maxUnit,
};
state.searchIn = getSearchInTorrentName();
state.sort = {
column: searchResultsTable.sortedColumn,
reverse: searchResultsTable.reverseSort,
};
// we must copy the array to avoid taking a reference to it
state.selectedRowIds = [...searchResultsTable.selectedRows];
};
const setActiveTab = function(tab) {
const searchId = getSearchIdFromTab(tab);
if (searchId === getSelectedSearchId())
return;
saveCurrentTabState();
MochaUI.selected(tab, 'searchTabs');
const state = searchState.get(searchId);
let rowsToSelect = [];
// restore table rows
searchResultsTable.clear();
if (state) {
for (const row of state.rows) {
searchResultsTable.updateRowData(row);
}
rowsToSelect = state.selectedRowIds;
// restore filters
searchText.pattern = state.searchPattern;
searchText.filterPattern = state.filterPattern;
$('searchInNameFilter').set("value", state.filterPattern);
searchSeedsFilter.min = state.seedsFilter.min;
searchSeedsFilter.max = state.seedsFilter.max;
$('searchMinSeedsFilter').set('value', state.seedsFilter.min);
$('searchMaxSeedsFilter').set('value', state.seedsFilter.max);
searchSizeFilter.min = state.sizeFilter.min;
searchSizeFilter.minUnit = state.sizeFilter.minUnit;
searchSizeFilter.max = state.sizeFilter.max;
searchSizeFilter.maxUnit = state.sizeFilter.maxUnit;
$('searchMinSizeFilter').set('value', state.sizeFilter.min);
$('searchMinSizePrefix').set('value', state.sizeFilter.minUnit);
$('searchMaxSizeFilter').set('value', state.sizeFilter.max);
$('searchMaxSizePrefix').set('value', state.sizeFilter.maxUnit);
const currentSearchPattern = $('searchPattern').getProperty('value').trim();
if (state.running && state.searchPattern === currentSearchPattern) {
// allow search to be stopped
$('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
searchPatternChanged = false;
}
searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse);
$('searchInTorrentName').set('value', state.searchIn);
}
// must restore all filters before calling updateTable
searchResultsTable.updateTable();
searchResultsTable.altRow();
// must reselect rows after calling updateTable
if (rowsToSelect.length > 0) {
searchResultsTable.reselectRows(rowsToSelect);
}
$('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length);
$('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length);
searchResultsRowId = 0;
requestCount = 0;
setupSearchTableEvents(true);
};
const getStatusIconElement = function(text, image) {
return new Element('img', {
alt: text,
title: text,
src: image,
class: 'statusIcon',
width: '10',
height: '10',
style: 'margin-bottom: -2px; margin-left: 7px',
});
};
const updateStatusIconElement = function(searchId, text, image) {
const searchTab = $(`${searchTabIdPrefix}${searchId}`);
if (searchTab) {
const statusIcon = searchTab.getElement('.statusIcon');
statusIcon.set('alt', text);
statusIcon.set('title', text);
statusIcon.set('src', image);
}
};
const startSearch = function(pattern, category, plugins) {
searchPatternChanged = false;
const url = new URI('api/v2/search/start');
new Request.JSON({
@ -309,29 +557,38 @@
},
onSuccess: function(response) {
$('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
searchRunning = true;
activeSearchId = response.id;
updateSearchResultsData();
const searchId = response.id;
createSearchTab(searchId, pattern);
}
}).send();
};
const stopSearch = function() {
const stopSearch = function(searchId) {
const url = new URI('api/v2/search/stop');
new Request({
url: url,
method: 'post',
data: {
id: activeSearchId
id: searchId
},
onSuccess: function(response) {
resetSearchState();
resetSearchState(searchId);
// not strictly necessary to do this when the tab is being closed, but there's no harm in it
updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg');
}
}).send();
};
const getSelectedSearchId = function() {
const selectedTab = $('searchTabs').getElement('li.selected');
return selectedTab ? getSearchIdFromTab(selectedTab) : null;
};
const startStopSearch = function() {
if (!searchRunning || !activeSearchId) {
const currentSearchId = getSelectedSearchId();
const state = searchState.get(currentSearchId);
const isSearchRunning = state && state.running;
if (!isSearchRunning || searchPatternChanged) {
const pattern = $('searchPattern').getProperty('value').trim();
let category = $('categorySelect').getProperty('value');
const plugins = $('pluginsSelect').getProperty('value');
@ -339,13 +596,11 @@
if (!pattern || !category || !plugins)
return;
resetFilters();
searchText.pattern = pattern;
startSearch(pattern, category, plugins);
}
else {
stopSearch();
stopSearch(currentSearchId);
}
};
@ -423,6 +678,21 @@
loadSearchPluginsTimer = loadSearchPlugins.delay(2000);
};
const onSearchPatternChanged = function() {
const currentSearchId = getSelectedSearchId();
const state = searchState.get(currentSearchId);
const currentSearchPattern = $('searchPattern').getProperty('value').trim();
// start a new search if pattern has changed, otherwise allow the search to be stopped
if (state && state.searchPattern === currentSearchPattern) {
searchPatternChanged = false;
$('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
}
else {
searchPatternChanged = true;
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
}
};
const categorySelected = function() {
selectedCategory = $("categorySelect").get("value");
};
@ -452,12 +722,13 @@
pluginSelected();
};
const resetSearchState = function() {
clearTimeout(loadSearchResultsTimer);
const resetSearchState = function(searchId) {
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
searchResultsRowId = 0;
searchRunning = false;
activeSearchId = null;
const state = searchState.get(searchId);
if (state) {
state.running = false;
clearTimeout(state.loadResultsTimer);
}
};
const getSearchCategories = function() {
@ -526,15 +797,9 @@
pluginsHtml.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
const searchPluginsEmpty = (searchPlugins.length === 0);
if (searchPluginsEmpty) {
$('searchResultsNoPlugins').style.display = "block";
$('searchResultsFilters').style.display = "none";
$('searchResultsTableContainer').style.display = "none";
}
else {
if (!searchPluginsEmpty) {
$('searchResultsNoPlugins').style.display = "none";
$('searchResultsFilters').style.display = "block";
$('searchResultsTableContainer').style.display = "block";
$('searchResultsNoSearches').style.display = "block";
// sort plugins alphabetically
const allPlugins = searchPlugins.sort((left, right) => {
@ -576,23 +841,32 @@
return null;
};
const searchInTorrentName = function() {
if ($('searchInTorrentName').get('value') === "names")
LocalPreferences.set('search_in_filter', "names");
else
LocalPreferences.set('search_in_filter', "everywhere");
const resetFilters = function() {
searchText.filterPattern = '';
$('searchInNameFilter').set('value', '');
searchFilterChanged();
searchSeedsFilter.min = 0;
searchSeedsFilter.max = 0;
$('searchMinSeedsFilter').set('value', searchSeedsFilter.min);
$('searchMaxSeedsFilter').set('value', searchSeedsFilter.max);
searchSizeFilter.min = 0.00;
searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
searchSizeFilter.max = 0.00;
searchSizeFilter.maxUnit = 3;
$('searchMinSizeFilter').set('value', searchSizeFilter.min);
$('searchMinSizePrefix').set('value', searchSizeFilter.minUnit);
$('searchMaxSizeFilter').set('value', searchSizeFilter.max);
$('searchMaxSizePrefix').set('value', searchSizeFilter.maxUnit);
};
const resetFilters = function() {
// reset filters
$('searchMinSeedsFilter').set('value', '0');
$('searchMaxSeedsFilter').set('value', '0');
$('searchMinSizeFilter').set('value', '0.00');
$('searchMinSizePrefix').set('value', '2'); // MiB
$('searchMaxSizeFilter').set('value', '0.00');
$('searchMaxSizePrefix').set('value', '3'); // GiB
const getSearchInTorrentName = function() {
return $('searchInTorrentName').get('value') === "names" ? "names" : "everywhere";
};
const searchInTorrentName = function() {
LocalPreferences.set('search_in_filter', getSearchInTorrentName());
searchFilterChanged();
};
const searchSeedsFilterChanged = function() {
@ -632,7 +906,9 @@
});
};
const loadSearchResultsData = function() {
const loadSearchResultsData = function(searchId) {
const state = searchState.get(searchId);
const maxResults = 500;
const url = new URI('api/v2/search/results');
new Request.JSON({
@ -640,39 +916,44 @@
method: 'get',
noCache: true,
data: {
id: activeSearchId,
id: searchId,
limit: maxResults,
offset: searchResultsRowId
offset: state.rowId
},
onFailure: function(response) {
if (response.status === 400) {
// bad params. search id is invalid
resetSearchState();
resetSearchState(searchId);
updateStatusIconElement(searchId, 'QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/error.svg');
}
else {
clearTimeout(loadSearchResultsTimer);
loadSearchResultsTimer = loadSearchResultsData.delay(3000);
clearTimeout(state.loadResultsTimer);
state.loadResultsTimer = loadSearchResultsData.delay(3000, this, searchId);
}
},
onSuccess: function(response) {
$('error_div').set('html', '');
const state = searchState.get(searchId);
// check if user stopped the search prior to receiving the response
if (!searchRunning) {
clearTimeout(loadSearchResultsTimer);
searchResultsRowId = 0;
if (!state.running) {
clearTimeout(state.loadResultsTimer);
updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg');
return;
}
if (response) {
setupSearchTableEvents(false);
const state = searchState.get(searchId);
const newRows = [];
if (response.results) {
const results = response.results;
for (let i = 0; i < results.length; ++i) {
const result = results[i];
const row = {
rowId: searchResultsRowId,
rowId: state.rowId,
descrLink: result.descrLink,
fileName: result.fileName,
fileSize: result.fileSize,
@ -682,41 +963,44 @@
siteUrl: result.siteUrl,
};
newRows.push(row);
state.rows.push(row);
state.rowId += 1;
}
}
// only update table if this search is currently being displayed
if (searchId === getSelectedSearchId()) {
for (const row of newRows) {
searchResultsTable.updateRowData(row);
++searchResultsRowId;
}
$('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length);
$('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length);
}
searchResultsTable.updateTable();
searchResultsTable.altRow();
}
if ((response.status === "Stopped") && (searchResultsRowId >= response.total)) {
resetSearchState();
if ((response.status === "Stopped") && (state.rowId >= response.total)) {
resetSearchState(searchId);
updateStatusIconElement(searchId, 'QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-complete.svg');
return;
}
setupSearchTableEvents(true);
}
let timeout = 1000;
if (requestCount > 30)
timeout = 3000;
else if (requestCount > 10)
timeout = 2000;
clearTimeout(loadSearchResultsTimer);
loadSearchResultsTimer = loadSearchResultsData.delay(timeout);
++requestCount;
clearTimeout(state.loadResultsTimer);
state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId);
}
}).send();
};
const updateSearchResultsData = function() {
clearTimeout(loadSearchResultsTimer);
loadSearchResultsTimer = loadSearchResultsData.delay(500);
const updateSearchResultsData = function(searchId) {
const state = searchState.get(searchId);
clearTimeout(state.loadResultsTimer);
state.loadResultsTimer = loadSearchResultsData.delay(500, this, searchId);
};
new ClipboardJS('.copySearchDataToClipboard', {