285 lines
12 KiB
JavaScript
285 lines
12 KiB
JavaScript
|
import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js';
|
||
|
import { timeOut } from '@polymer/polymer/lib/utils/async.js';
|
||
|
import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
|
||
|
|
||
|
(function () {
|
||
|
const tryCatchWrapper = function (callback) {
|
||
|
return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Combo Box');
|
||
|
};
|
||
|
|
||
|
window.Vaadin.Flow.comboBoxConnector = {
|
||
|
initLazy: (comboBox) =>
|
||
|
tryCatchWrapper(function (comboBox) {
|
||
|
// Check whether the connector was already initialized for the ComboBox
|
||
|
if (comboBox.$connector) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
comboBox.$connector = {};
|
||
|
|
||
|
// holds pageIndex -> callback pairs of subsequent indexes (current active range)
|
||
|
const pageCallbacks = {};
|
||
|
let cache = {};
|
||
|
let lastFilter = '';
|
||
|
const placeHolder = new window.Vaadin.ComboBoxPlaceholder();
|
||
|
|
||
|
const serverFacade = (() => {
|
||
|
// Private variables
|
||
|
let lastFilterSentToServer = '';
|
||
|
let dataCommunicatorResetNeeded = false;
|
||
|
|
||
|
// Public methods
|
||
|
const needsDataCommunicatorReset = () => (dataCommunicatorResetNeeded = true);
|
||
|
const getLastFilterSentToServer = () => lastFilterSentToServer;
|
||
|
const requestData = (startIndex, endIndex, params) => {
|
||
|
const count = endIndex - startIndex;
|
||
|
const filter = params.filter;
|
||
|
|
||
|
comboBox.$server.setRequestedRange(startIndex, count, filter);
|
||
|
lastFilterSentToServer = filter;
|
||
|
if (dataCommunicatorResetNeeded) {
|
||
|
comboBox.$server.resetDataCommunicator();
|
||
|
dataCommunicatorResetNeeded = false;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return {
|
||
|
needsDataCommunicatorReset,
|
||
|
getLastFilterSentToServer,
|
||
|
requestData
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
const clearPageCallbacks = (pages = Object.keys(pageCallbacks)) => {
|
||
|
// Flush and empty the existing requests
|
||
|
pages.forEach((page) => {
|
||
|
pageCallbacks[page]([], comboBox.size);
|
||
|
delete pageCallbacks[page];
|
||
|
|
||
|
// Empty the comboBox's internal cache without invoking observers by filling
|
||
|
// the filteredItems array with placeholders (comboBox will request for data when it
|
||
|
// encounters a placeholder)
|
||
|
const pageStart = parseInt(page) * comboBox.pageSize;
|
||
|
const pageEnd = pageStart + comboBox.pageSize;
|
||
|
const end = Math.min(pageEnd, comboBox.filteredItems.length);
|
||
|
for (let i = pageStart; i < end; i++) {
|
||
|
comboBox.filteredItems[i] = placeHolder;
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
comboBox.dataProvider = function (params, callback) {
|
||
|
if (params.pageSize != comboBox.pageSize) {
|
||
|
throw 'Invalid pageSize';
|
||
|
}
|
||
|
|
||
|
if (comboBox._clientSideFilter) {
|
||
|
// For clientside filter we first make sure we have all data which we also
|
||
|
// filter based on comboBox.filter. While later we only filter clientside data.
|
||
|
|
||
|
if (cache[0]) {
|
||
|
performClientSideFilter(cache[0], params.filter, callback);
|
||
|
return;
|
||
|
} else {
|
||
|
// If client side filter is enabled then we need to first ask all data
|
||
|
// and filter it on client side, otherwise next time when user will
|
||
|
// input another filter, eg. continue to type, the local cache will be only
|
||
|
// what was received for the first filter, which may not be the whole
|
||
|
// data from server (keep in mind that client side filter is enabled only
|
||
|
// when the items count does not exceed one page).
|
||
|
params.filter = '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const filterChanged = params.filter !== lastFilter;
|
||
|
if (filterChanged) {
|
||
|
cache = {};
|
||
|
lastFilter = params.filter;
|
||
|
this._filterDebouncer = Debouncer.debounce(this._filterDebouncer, timeOut.after(500), () => {
|
||
|
if (serverFacade.getLastFilterSentToServer() === params.filter) {
|
||
|
// Fixes the case when the filter changes
|
||
|
// to something else and back to the original value
|
||
|
// within debounce timeout, and the
|
||
|
// DataCommunicator thinks it doesn't need to send data
|
||
|
serverFacade.needsDataCommunicatorReset();
|
||
|
}
|
||
|
if (params.filter !== lastFilter) {
|
||
|
throw new Error("Expected params.filter to be '" + lastFilter + "' but was '" + params.filter + "'");
|
||
|
}
|
||
|
// Remove the debouncer before clearing page callbacks.
|
||
|
// This makes sure that they are executed.
|
||
|
this._filterDebouncer = undefined;
|
||
|
// Call the method again after debounce.
|
||
|
clearPageCallbacks();
|
||
|
comboBox.dataProvider(params, callback);
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Postpone the execution of new callbacks if there is an active debouncer.
|
||
|
// They will be executed when the page callbacks are cleared within the debouncer.
|
||
|
if (this._filterDebouncer) {
|
||
|
pageCallbacks[params.page] = callback;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (cache[params.page]) {
|
||
|
// This may happen after skipping pages by scrolling fast
|
||
|
commitPage(params.page, callback);
|
||
|
} else {
|
||
|
pageCallbacks[params.page] = callback;
|
||
|
const maxRangeCount = Math.max(params.pageSize * 2, 500); // Max item count in active range
|
||
|
const activePages = Object.keys(pageCallbacks).map((page) => parseInt(page));
|
||
|
const rangeMin = Math.min(...activePages);
|
||
|
const rangeMax = Math.max(...activePages);
|
||
|
|
||
|
if (activePages.length * params.pageSize > maxRangeCount) {
|
||
|
if (params.page === rangeMin) {
|
||
|
clearPageCallbacks([String(rangeMax)]);
|
||
|
} else {
|
||
|
clearPageCallbacks([String(rangeMin)]);
|
||
|
}
|
||
|
comboBox.dataProvider(params, callback);
|
||
|
} else if (rangeMax - rangeMin + 1 !== activePages.length) {
|
||
|
// Wasn't a sequential page index, clear the cache so combo-box will request for new pages
|
||
|
clearPageCallbacks();
|
||
|
} else {
|
||
|
// The requested page was sequential, extend the requested range
|
||
|
const startIndex = params.pageSize * rangeMin;
|
||
|
const endIndex = params.pageSize * (rangeMax + 1);
|
||
|
|
||
|
serverFacade.requestData(startIndex, endIndex, params);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
comboBox.$connector.clear = tryCatchWrapper((start, length) => {
|
||
|
const firstPageToClear = Math.floor(start / comboBox.pageSize);
|
||
|
const numberOfPagesToClear = Math.ceil(length / comboBox.pageSize);
|
||
|
|
||
|
for (let i = firstPageToClear; i < firstPageToClear + numberOfPagesToClear; i++) {
|
||
|
delete cache[i];
|
||
|
}
|
||
|
});
|
||
|
|
||
|
comboBox.$connector.filter = tryCatchWrapper(function (item, filter) {
|
||
|
filter = filter ? filter.toString().toLowerCase() : '';
|
||
|
return comboBox._getItemLabel(item, comboBox.itemLabelPath).toString().toLowerCase().indexOf(filter) > -1;
|
||
|
});
|
||
|
|
||
|
comboBox.$connector.set = tryCatchWrapper(function (index, items, filter) {
|
||
|
if (filter != serverFacade.getLastFilterSentToServer()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (index % comboBox.pageSize != 0) {
|
||
|
throw 'Got new data to index ' + index + ' which is not aligned with the page size of ' + comboBox.pageSize;
|
||
|
}
|
||
|
|
||
|
if (index === 0 && items.length === 0 && pageCallbacks[0]) {
|
||
|
// Makes sure that the dataProvider callback is called even when server
|
||
|
// returns empty data set (no items match the filter).
|
||
|
cache[0] = [];
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const firstPageToSet = index / comboBox.pageSize;
|
||
|
const updatedPageCount = Math.ceil(items.length / comboBox.pageSize);
|
||
|
|
||
|
for (let i = 0; i < updatedPageCount; i++) {
|
||
|
let page = firstPageToSet + i;
|
||
|
let slice = items.slice(i * comboBox.pageSize, (i + 1) * comboBox.pageSize);
|
||
|
|
||
|
cache[page] = slice;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
comboBox.$connector.updateData = tryCatchWrapper(function (items) {
|
||
|
const itemsMap = new Map(items.map((item) => [item.key, item]));
|
||
|
|
||
|
comboBox.filteredItems = comboBox.filteredItems.map((item) => {
|
||
|
return itemsMap.get(item.key) || item;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
comboBox.$connector.updateSize = tryCatchWrapper(function (newSize) {
|
||
|
if (!comboBox._clientSideFilter) {
|
||
|
// FIXME: It may be that this size set is unnecessary, since when
|
||
|
// providing data to combobox via callback we may use data's size.
|
||
|
// However, if this size reflect the whole data size, including
|
||
|
// data not fetched yet into client side, and combobox expect it
|
||
|
// to be set as such, the at least, we don't need it in case the
|
||
|
// filter is clientSide only, since it'll increase the height of
|
||
|
// the popup at only at first user filter to this size, while the
|
||
|
// filtered items count are less.
|
||
|
comboBox.size = newSize;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
comboBox.$connector.reset = tryCatchWrapper(function () {
|
||
|
clearPageCallbacks();
|
||
|
cache = {};
|
||
|
comboBox.clearCache();
|
||
|
});
|
||
|
|
||
|
comboBox.$connector.confirm = tryCatchWrapper(function (id, filter) {
|
||
|
if (filter != serverFacade.getLastFilterSentToServer()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// We're done applying changes from this batch, resolve pending
|
||
|
// callbacks
|
||
|
let activePages = Object.getOwnPropertyNames(pageCallbacks);
|
||
|
for (let i = 0; i < activePages.length; i++) {
|
||
|
let page = activePages[i];
|
||
|
|
||
|
if (cache[page]) {
|
||
|
commitPage(page, pageCallbacks[page]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Let server know we're done
|
||
|
comboBox.$server.confirmUpdate(id);
|
||
|
});
|
||
|
|
||
|
const commitPage = tryCatchWrapper(function (page, callback) {
|
||
|
let data = cache[page];
|
||
|
|
||
|
if (comboBox._clientSideFilter) {
|
||
|
performClientSideFilter(data, comboBox.filter, callback);
|
||
|
} else {
|
||
|
// Remove the data if server-side filtering, but keep it for client-side
|
||
|
// filtering
|
||
|
delete cache[page];
|
||
|
|
||
|
// FIXME: It may be that we ought to provide data.length instead of
|
||
|
// comboBox.size and remove updateSize function.
|
||
|
callback(data, comboBox.size);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Perform filter on client side (here) using the items from specified page
|
||
|
// and submitting the filtered items to specified callback.
|
||
|
// The filter used is the one from combobox, not the lastFilter stored since
|
||
|
// that may not reflect user's input.
|
||
|
const performClientSideFilter = tryCatchWrapper(function (page, filter, callback) {
|
||
|
let filteredItems = page;
|
||
|
|
||
|
if (filter) {
|
||
|
filteredItems = page.filter((item) => comboBox.$connector.filter(item, filter));
|
||
|
}
|
||
|
|
||
|
callback(filteredItems, filteredItems.length);
|
||
|
});
|
||
|
|
||
|
// Prevent setting the custom value as the 'value'-prop automatically
|
||
|
comboBox.addEventListener(
|
||
|
'custom-value-set',
|
||
|
tryCatchWrapper((e) => e.preventDefault())
|
||
|
);
|
||
|
})(comboBox)
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
window.Vaadin.ComboBoxPlaceholder = ComboBoxPlaceholder;
|