diff --git a/js/components/faceted-datatable.js b/js/components/faceted-datatable.js
index c8afcdff8..8f71011e9 100644
--- a/js/components/faceted-datatable.js
+++ b/js/components/faceted-datatable.js
@@ -1,9 +1,21 @@
-define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'colvis', ], function (ko, view, crossfilter) {
+define(
+ ['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'utils/DatatablePaginationUtils', 'colvis', ],
+ function (ko, view, crossfilter, DatatablePaginationUtils)
+{
- function facetedDatatable(params) {
+ function getDtElement(facetedDtEl) {
+ return $(facetedDtEl).find('table');
+ }
+
+ function getSavedSearch(element) {
+ return DatatablePaginationUtils.getFacets(getDtElement(element).dataTable().DataTable());
+ }
+
+ function facetedDatatable(params, componentInfo) {
var self = this;
var subscriptions = [];
+ self.element = componentInfo.element;
self.selectedData = params.selectedData || null;
self.headersTemplateId = params.headersTemplateId;
self.reference = params.reference;
@@ -66,9 +78,16 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'colvis', ],
self.scrollY = params.scrollY || null;
self.scrollCollapse = params.scrollCollapse || false;
- self.updateFilters = function (data, event) {
+ self.selectFacetItems = function(targetCaption, selectedFacetValues) {
+ const facetItems = self.facets().find(f => f.caption === targetCaption).facetItems;
+ if (facetItems) {
+ facetItems.forEach(fi => self.updateFilters(fi, null, selectedFacetValues.includes(fi.key), false));
+ }
+ };
+
+ self.updateFilters = function (data, event, selected, emitEvent = true) {
var facet = data.facet;
- data.selected(!data.selected());
+ data.selected(typeof selected !== 'undefined' ? selected : !data.selected());
if (data.selected()) {
if (!facet.selectedItems.hasOwnProperty(data.key)) {
facet.selectedItems[data.key] = data;
@@ -88,6 +107,8 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'colvis', ],
});
}
self.data.valueHasMutated();
+
+ emitEvent && getDtElement(self.element).trigger('facet.dt', data);
}
// additional helper function to help with crossfilter-ing dimensions that contain nulls
@@ -126,16 +147,22 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'colvis', ],
self.facets.push(facet);
});
// Iterate over the facets and set any defaults
+ const savedSearch = getSavedSearch(self.element);
$.each(self.options.Facets, function (i, facetConfig) {
- if (facetConfig.defaultFacets && facetConfig.defaultFacets.length > 0) {
- $.each(facetConfig.defaultFacets, function (d, defaultFacet) {
- var facetItem = $.grep(self.facets()[i].facetItems, function (f) {
- return f.key == defaultFacet;
- });
- if (facetItem.length > 0) {
- self.updateFilters(facetItem[0], null);
- }
- })
+ const savedFacetValues = savedSearch[facetConfig.caption];
+ if (savedFacetValues) {
+ self.selectFacetItems(facetConfig.caption, savedFacetValues);
+ } else {
+ if (facetConfig.defaultFacets && facetConfig.defaultFacets.length > 0) {
+ $.each(facetConfig.defaultFacets, function (d, defaultFacet) {
+ var facetItem = $.grep(self.facets()[i].facetItems, function (f) {
+ return f.key == defaultFacet;
+ });
+ if (facetItem.length > 0) {
+ self.updateFilters(facetItem[0], null, true, false);
+ }
+ })
+ }
}
});
}
@@ -144,6 +171,16 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'colvis', ],
})
);
+ $(self.element).on('refresh.faceted-dt', () => {
+ if (Array.isArray(self.options.Facets)) {
+ const savedSearch = getSavedSearch(self.element);
+ self.options.Facets.forEach(facetConfig => {
+ const savedFacetValues = savedSearch[facetConfig.caption] || facetConfig.defaultFacets || [];
+ self.selectFacetItems(facetConfig.caption, savedFacetValues);
+ });
+ }
+ });
+
// init component
if (ko.isComputed(self.reference)) {
// valueHasMutated doesn't work for computed
@@ -190,7 +227,7 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'colvis', ],
};
var component = {
- viewModel: facetedDatatable,
+ viewModel: {createViewModel: (params, info) => new facetedDatatable(params, info)},
template: view
};
diff --git a/js/extensions/bindings/datatableBinding.js b/js/extensions/bindings/datatableBinding.js
index 84f088e4a..79e5617de 100644
--- a/js/extensions/bindings/datatableBinding.js
+++ b/js/extensions/bindings/datatableBinding.js
@@ -6,6 +6,7 @@ define([
'xss',
'moment',
'services/MomentAPI',
+ 'utils/DatatablePaginationUtils',
'datatables.net-buttons',
'colvis',
'datatables.net-buttons-html5',
@@ -16,7 +17,8 @@ define([
config,
filterXSS,
moment,
- momentApi
+ momentApi,
+ paginationUtils
) {
function renderSelected(s, p, d) {
@@ -130,7 +132,8 @@ define([
ko.applyBindings(bindingContext, $(element).find('thead')[0]);
}
- $(element).DataTable(binding.options);
+ const datatable = $(element).DataTable(binding.options);
+ paginationUtils.applyPaginationListeners(element, datatable, binding);
if (binding.api != null)
{
@@ -185,9 +188,7 @@ define([
if (data.length > 0)
table.rows.add(data);
- // drawing may access observables, which updating we do not want to trigger a redraw to the table
- // see: https://knockoutjs.com/documentation/computed-dependency-tracking.html#IgnoringDependencies
- ko.ignoreDependencies(table.draw);
+ paginationUtils.refreshTable(table);
}
diff --git a/js/pages/Router.js b/js/pages/Router.js
index 49d9e47b4..14d8f699c 100644
--- a/js/pages/Router.js
+++ b/js/pages/Router.js
@@ -8,6 +8,8 @@ define(
'knockout',
'const',
'services/EventBus',
+ 'datatables.net',
+ 'utils/DatatablePaginationUtils',
'director',
],
(
@@ -19,6 +21,8 @@ define(
ko,
constants,
EventBus,
+ dataTables,
+ DatatablePaginationUtils
) => {
class AtlasRouter {
constructor() {
@@ -127,6 +131,12 @@ define(
this.routerParams(routerParams);
}
this.currentView(view);
+ setTimeout(() => {
+ $.fn.dataTable.tables().forEach(dt => {
+ DatatablePaginationUtils.refreshTable($(dt).dataTable().DataTable());
+ });
+ $('faceted-datatable').trigger('refresh.faceted-dt');
+ });
}
}
return new AtlasRouter();
diff --git a/js/pages/cohort-definitions/cohort-definition-manager.html b/js/pages/cohort-definitions/cohort-definition-manager.html
index ecce4fe1d..d99cdd5e9 100644
--- a/js/pages/cohort-definitions/cohort-definition-manager.html
+++ b/js/pages/cohort-definitions/cohort-definition-manager.html
@@ -1,9 +1,11 @@
-
-
-
+
+
+
+
+
@@ -34,28 +36,28 @@
diff --git a/js/pages/cohort-definitions/cohort-definition-manager.js b/js/pages/cohort-definitions/cohort-definition-manager.js
index d7ee2dd21..6da09eb5f 100644
--- a/js/pages/cohort-definitions/cohort-definition-manager.js
+++ b/js/pages/cohort-definitions/cohort-definition-manager.js
@@ -1054,8 +1054,8 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
}
}
- onRouterParamsChanged(params) {
- const { cohortDefinitionId, conceptSetId, mode = 'definition', sourceKey } = params;
+ onRouterParamsChanged(changedParamsMap, newParams) {
+ const { cohortDefinitionId, conceptSetId, mode = 'definition', sourceKey } = newParams || changedParamsMap;
this.clearConceptSet();
this.tabMode(mode);
if (!this.checkifDataLoaded(cohortDefinitionId, conceptSetId, sourceKey)) {
@@ -1063,6 +1063,10 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
}
}
+ selectTab(key) {
+ commonUtils.routeTo(`/cohortdefinition/${this.currentCohortDefinition().id()}/${key}`);
+ }
+
getSourceInfo(source) {
const info = this.currentCohortDefinitionInfo();
for (var i = 0; i < info.length; i++) {
diff --git a/js/pages/cohort-definitions/routes.js b/js/pages/cohort-definitions/routes.js
index ceab63bfc..97fc4dd88 100644
--- a/js/pages/cohort-definitions/routes.js
+++ b/js/pages/cohort-definitions/routes.js
@@ -56,7 +56,7 @@ define(
var sourceKey = qs.sourceKey || null;
router.setCurrentView('cohort-definition-manager', {
cohortDefinitionId,
- mode: 'definition',
+ mode: view,
sourceKey,
});
sharedState.ConceptSet.source('cohort');
diff --git a/js/utils/CommonUtils.js b/js/utils/CommonUtils.js
index 046c455fb..57b5bf221 100644
--- a/js/utils/CommonUtils.js
+++ b/js/utils/CommonUtils.js
@@ -208,6 +208,34 @@ define([
return tooltipText.replace(/'/g, "\\'").replace(/"/g, '"');
}
+ const getPathTo = function(element) {
+ if (element===document.body)
+ return element.tagName;
+
+ let ix = 0;
+ const siblings = element.parentNode.childNodes;
+ for (let i= 0; i
{
+ const PAGE_PARAM = 'dtPage';
+ const SEARCH_PARAM = 'dtSearch';
+ const ORDER_PARAM = 'dtOrder';
+ const FACET_PARAM = 'dtFacet';
+ const PARAM_SEPARATOR = '_';
+
+ const DEFAULT_DT_PARAMS = {};
+ const IGNORE_LISTENERS_FOR_DT = [];
+
+ function getPageNumFromDt(datatable) {
+ const info = datatable.page.info();
+ return info.page;
+ }
+
+ function redrawTable(table, mode) {
+ IGNORE_LISTENERS_FOR_DT.push(getDtId(table));
+
+ // drawing may access observables, which updating we do not want to trigger a redraw to the table
+ // see: https://knockoutjs.com/documentation/computed-dependency-tracking.html#IgnoringDependencies
+ const func = mode ? table.draw.bind(null, mode) : table.draw;
+ ko.ignoreDependencies(func);
+
+ IGNORE_LISTENERS_FOR_DT.splice(IGNORE_LISTENERS_FOR_DT.indexOf(getDtId(table)), 1);
+ }
+
+ function getDtId(datatable) {
+ const elPath = CommonUtils.getPathTo(datatable.table().container());
+ return CommonUtils.calculateStringHash(elPath);
+ }
+
+ function buildDtParamName(datatable, param) {
+ return getDtId(datatable) + PARAM_SEPARATOR + encodeURIComponent(param);
+ }
+
+ function getDtParams() {
+ const currentUrl = URI(document.location.href);
+ const fragment = URI(currentUrl.fragment());
+ return fragment.search(true);
+ }
+
+ function getDtParamValue(param) {
+ const params = getDtParams();
+ return params[param];
+ }
+
+ function setUrlParams(obj) {
+ const currentUrl = URI(document.location.href);
+ const fragment = URI(currentUrl.fragment());
+ Object.keys(obj).forEach(k => {
+ fragment.removeSearch(k).addSearch(k, obj[k]);
+ });
+ const updatedUrl = currentUrl.fragment(fragment.toString()).toString();
+ document.location = updatedUrl;
+ }
+
+ function removeUrlParams(paramKeys) {
+ const currentUrl = URI(document.location.href);
+ const fragment = URI(currentUrl.fragment());
+ paramKeys.forEach(k => fragment.removeSearch(k));
+ const updatedUrl = currentUrl.fragment(fragment.toString()).toString();
+ document.location = updatedUrl;
+ }
+
+ function getPageParamName(datatable) {
+ return buildDtParamName(datatable, PAGE_PARAM);
+ }
+
+ function getPageNumFromUrl(datatable) {
+ return +getDtParamValue(getPageParamName(datatable));
+ }
+
+ function setPageNumToUrl(datatable, num) {
+ setUrlParams({
+ [getPageParamName(datatable)]: num
+ });
+ }
+
+ function getSearchParamName(datatable) {
+ return buildDtParamName(datatable, SEARCH_PARAM);
+ }
+
+ function getSearchFromUrl(datatable) {
+ return getDtParamValue(getSearchParamName(datatable));
+ }
+
+ function setSearchToUrl(datatable, searchStr) {
+ setUrlParams({
+ [getSearchParamName(datatable)]: searchStr,
+ [getPageParamName(datatable)]: 0
+ });
+ }
+
+ function getOrderParamName(datatable) {
+ return buildDtParamName(datatable, ORDER_PARAM);
+ }
+
+ function getOrderFromUrl(datatable) {
+ const rawValue = getDtParamValue(getOrderParamName(datatable));
+ if (!rawValue) {
+ return null;
+ }
+ const parts = rawValue.split(',');
+ return {
+ column: +parts[0],
+ direction: parts[1]
+ };
+ }
+
+ function setOrderToUrl(datatable, column, direction) {
+
+ setUrlParams({
+ [getOrderParamName(datatable)]: column + ',' + direction,
+ [getPageParamName(datatable)]: 0
+ });
+ }
+
+ function getOrderCol(order) {
+
+ return !!order[0]
+ ? Array.isArray(order[0]) ? order[0][0] : order[0]
+ : undefined;
+ }
+
+ function getOrderDir(order) {
+
+ return !!order[0]
+ ? Array.isArray(order[0]) ? order[0][1] : order[1]
+ : undefined;
+ }
+
+ function getFacetParamName(datatable, facetName) {
+ return buildDtParamName(datatable, FACET_PARAM + PARAM_SEPARATOR + facetName);
+ }
+
+ function setFacetToUrl(datatable, facetOptions, facetName, facetValue, selected) {
+ const defaultValues = facetOptions.find(o => o.caption === facetName).defaultFacets;
+ let values = [].concat(getFacets(datatable)[facetName] || defaultValues || []);
+ if (selected) {
+ values = [...new Set(values.concat([facetValue]))];
+ } else {
+ if (values.indexOf(facetValue) !== -1) {
+ values.splice(values.indexOf(facetValue), 1);
+ }
+ }
+
+ const defaultValuesDefined = Array.isArray(defaultValues) && defaultValues.length > 0;
+ const selectedSameAsDefault = defaultValuesDefined && defaultValues.length === values.length && defaultValues.length === new Set([...defaultValues, ...values]).size;
+ if ((!defaultValuesDefined && values.length > 0) || (defaultValuesDefined && !selectedSameAsDefault)) {
+ setUrlParams({
+ [getFacetParamName(datatable, facetName)]: values.join(','),
+ });
+ } else {
+ removeUrlParams([ getFacetParamName(datatable, facetName) ]);
+ }
+ }
+
+ function getFacets(datatable) {
+ const params = getDtParams();
+ const prefix = getDtId(datatable) + PARAM_SEPARATOR + FACET_PARAM + PARAM_SEPARATOR;
+ const facetKeys = Object.keys(params)
+ .filter(k => k.startsWith(prefix))
+ .map(f => f.substring(prefix.length));
+ const facets = {};
+ facetKeys.forEach(k => facets[decodeURIComponent(k)] = params[prefix + k] ? params[prefix + k].split(',') : []);
+ return facets;
+ }
+
+ function shouldIgnoreEvent(datatable) {
+ return IGNORE_LISTENERS_FOR_DT.includes(getDtId(datatable));
+ }
+
+ function removeOrderFromUrl(datatable) {
+ const paramsToRemove = [ getOrderParamName(datatable) ];
+ if (getPageNumFromDt(datatable) === 0) {
+ paramsToRemove.push(getPageParamName(datatable));
+ }
+ removeUrlParams(paramsToRemove);
+ }
+
+ function removeSearchFromUrl(datatable) {
+ removeUrlParams([ getSearchParamName(datatable) ]);
+ }
+
+ function removePageNumFromUrl(datatable) {
+ removeUrlParams([ getPageParamName(datatable) ]);
+ }
+
+ function applyPaginationListeners(element, datatable, binding) {
+ const {defaultColumnIdx, defaultOrderDir} = binding.options && binding.options.order && (getOrderCol(binding.options.order) || getOrderDir(binding.options.order))
+ ? {defaultColumnIdx: getOrderCol(binding.options.order), defaultOrderDir: getOrderDir(binding.options.order)}
+ : {defaultColumnIdx: 0, defaultOrderDir: 'asc'};
+
+ DEFAULT_DT_PARAMS[getDtId(datatable)] = {
+ column: defaultColumnIdx,
+ direction: defaultOrderDir,
+ };
+
+ $(element).on('page.dt', function () {
+ if (shouldIgnoreEvent(datatable)) return;
+ const pageNum = getPageNumFromDt(datatable);
+ if (getPageNumFromUrl(datatable) !== pageNum) {
+ if (pageNum !== 0) {
+ setPageNumToUrl(datatable, pageNum);
+ } else {
+ removePageNumFromUrl(datatable);
+ }
+ }
+ });
+
+ const onSearchEvent = function() {
+ const currentSearchStr = getSearchFromUrl(datatable) || '';
+ const newSearchStr = datatable.search();
+ if (currentSearchStr !== newSearchStr) {
+ if (newSearchStr !== '') {
+ setSearchToUrl(datatable, newSearchStr);
+ } else {
+ removeSearchFromUrl(datatable);
+ }
+ }
+ };
+
+ $(element).on('search.dt', function () {
+ if (shouldIgnoreEvent(datatable)) return;
+ onSearchEvent();
+ });
+
+ $(element).on('order.dt', function () {
+ if (shouldIgnoreEvent(datatable)) return;
+
+ const currentOrder = getOrderFromUrl(datatable);
+
+ const newOrder = datatable.order();
+ if (!Array.isArray(newOrder) || !newOrder.length) {
+ return;
+ }
+ const newColumnIdx = getOrderCol(newOrder);
+ const newOrderDir = getOrderDir(newOrder);
+
+ const isOrderChanged = !currentOrder || currentOrder.column !== newColumnIdx || currentOrder.direction !== newOrderDir;
+ if (isOrderChanged) {
+ if (newColumnIdx !== defaultColumnIdx || newOrderDir !== defaultOrderDir) {
+ setOrderToUrl(datatable, newColumnIdx, newOrderDir);
+ } else {
+ removeOrderFromUrl(datatable);
+ }
+ }
+ });
+
+ $(element).on('facet.dt', function (event, data) {
+ setFacetToUrl(datatable, binding.options.Facets, data.facet.caption, data.key, data.selected());
+ });
+ }
+
+ function applyDtSearch(table) {
+ const searchStrFromUrl = getSearchFromUrl(table) || '';
+ const searchStrFromDom = table.search();
+ if (searchStrFromDom !== searchStrFromUrl) {
+ document.activeElement.blur(); // Otherwise text in search input is not updated
+ table.search(searchStrFromUrl);
+ }
+ }
+
+ function applyDtSorting(table) {
+ const currentOrder = getOrderFromUrl(table) || DEFAULT_DT_PARAMS[getDtId(table)];
+ table.order([[currentOrder.column, currentOrder.direction]]);
+ }
+
+ function applyDtPage(table) {
+ const currentPage = getPageNumFromUrl(table) || 0;
+ table.page(currentPage);
+ redrawTable(table, 'page');
+ }
+
+ function refreshTable(table) {
+ applyDtSearch(table);
+ applyDtSorting(table);
+ redrawTable(table);
+ applyDtPage(table);
+ }
+
+ return {
+ applyPaginationListeners,
+ refreshTable,
+ getFacets,
+ }
+});
\ No newline at end of file