Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds persistence and reloading of datatable pagination params #2112

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
21 changes: 16 additions & 5 deletions js/extensions/bindings/datatableBinding.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ define([
'xss',
'moment',
'services/MomentAPI',
'utils/DatatablePaginationUtils',
'datatables.net-buttons',
'colvis',
'datatables.net-buttons-html5',
Expand All @@ -16,7 +17,8 @@ define([
config,
filterXSS,
moment,
momentApi
momentApi,
paginationUtils
) {

function renderSelected(s, p, d) {
Expand Down Expand Up @@ -59,6 +61,13 @@ define([
return abxX < absY ? -1 : abxX>absY ? 1 : 0;
}

function redrawTable(table, mode) {
// 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);
}

ko.bindingHandlers.dataTable = {

init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
Expand Down Expand Up @@ -130,7 +139,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)
{
Expand Down Expand Up @@ -185,9 +195,10 @@ 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.applyDtSearch(table);
paginationUtils.applyDtSorting(table);
redrawTable(table);
paginationUtils.applyDtPage(table, redrawTable);
}


Expand Down
3 changes: 3 additions & 0 deletions js/pages/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ define(
'knockout',
'const',
'services/EventBus',
'datatables.net',
'director',
],
(
Expand All @@ -19,6 +20,7 @@ define(
ko,
constants,
EventBus,
dataTables
) => {
class AtlasRouter {
constructor() {
Expand Down Expand Up @@ -122,6 +124,7 @@ define(
setCurrentView(view, routerParams = false) {
if (view !== this.currentView()) {
this.currentView('loading');
dataTables.ext._unique = 0;
}
if (routerParams !== false) {
this.routerParams(routerParams);
Expand Down
14 changes: 7 additions & 7 deletions js/pages/cohort-definitions/cohort-definition-manager.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,28 @@
</div>

<ul class="nav nav-tabs">
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'definition' }, click: function() { $component.tabMode('definition'); }">
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'definition' }, click: function() { $component.selectTab('definition'); }">
<a>Definition <i data-bind="click: function () { $component.cohortDefinitionOpened(true) }" class="fa fa-question-circle-o"></i></a>
</li>

<li role="presentation" data-bind="css: { active: $component.tabMode() == 'conceptsets' }, click: function() { $component.tabMode('conceptsets'); }">
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'conceptsets' }, click: function() { $component.selectTab('conceptsets'); }">
<a>Concept Sets</a>
</li>

<li role="presentation" data-bind="css: { active: $component.tabMode() == 'generation' }, click: function() { $component.tabMode('generation'); }">
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'generation' }, click: function() { $component.selectTab('generation'); }">
<a>Generation</a>
</li>

<li role="presentation" data-bind="css: { active: $component.tabMode() == 'reporting' }, click: function() { $component.tabMode('reporting'); }">
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'reporting' }, click: function() { $component.selectTab('reporting'); }">
<a>Reporting</a>
</li>
<!--
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'explore' }, click: function() { $component.tabMode('explore'); }"><a>Explore</a></li>
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'explore' }, click: function() { $component.selectTab('explore'); }"><a>Explore</a></li>
-->
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'export' }, click: () => $component.tabMode('export')">
<li role="presentation" data-bind="css: { active: $component.tabMode() == 'export' }, click: () => $component.selectTab('export')">
<a>Export</a>
</li>
<li role="presentation" data-bind="css: { active: $component.tabMode() === 'warnings' }, click: function(){ $component.tabMode('warnings'); } ">
<li role="presentation" data-bind="css: { active: $component.tabMode() === 'warnings' }, click: function(){ $component.selectTab('warnings'); } ">
<a data-bind="attr: { class: warningClass }">Messages <span class="badge" data-bind="text: warningsTotals, visible: warningsTotals() > 0"></span></a>
</li>
</ul>
Expand Down
4 changes: 4 additions & 0 deletions js/pages/cohort-definitions/cohort-definition-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down
32 changes: 32 additions & 0 deletions js/utils/CommonUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,36 @@ define([
return tooltipText.replace(/'/g, "\\'").replace(/"/g, '&quot;');
}

const getPathTo = function(element) {
if (element.id!=='')
return 'id("'+element.id+'")';
if (element===document.body)
return element.tagName;

let ix = 0;
const siblings = element.parentNode.childNodes;
for (let i= 0; i<siblings.length; i++) {
const sibling = siblings[i];
if (sibling===element)
return getPathTo(element.parentNode)+'/'+element.tagName+'['+(ix+1)+']';
if (sibling.nodeType===1 && sibling.tagName===element.tagName)
ix++;
}
};

const calculateStringHash = function(string) {
let hash = 0;
if (string.length == 0) {
return hash;
}
for (var i = 0; i < string.length; i++) {
var char = string.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
};

return {
build,
confirmAndDelete,
Expand All @@ -229,5 +259,7 @@ define([
normalizeUrl,
toggleConceptSetCheckbox,
escapeTooltip,
getPathTo,
calculateStringHash,
};
});
167 changes: 167 additions & 0 deletions js/utils/DatatablePaginationUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
define([
'utils/CommonUtils',
'urijs',
], (
CommonUtils,
URI
) => {
const PAGE_PARAM = 'dtPage';
const SEARCH_PARAM = 'dtSearch';
const ORDER_PARAM = 'dtOrder';
const PARAM_SEPARATOR = '_';

function buildDtParamName(datatable, param) {
const elPath = CommonUtils.getPathTo(datatable.table().container());
const elId = CommonUtils.calculateStringHash(elPath);
return param + PARAM_SEPARATOR + elId;
}

function getDtParamValue(param) {
const currentUrl = URI(document.location.href);
const fragment = URI(currentUrl.fragment());
const params = fragment.search(true);
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 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 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'};

$(element).on('page.dt', function () {
const info = datatable.page.info();
setPageNumToUrl(datatable, info.page);
});

$(element).on('search.dt', function () {
const currentSearchStr = getSearchFromUrl(datatable) || '';
const newSearchStr = datatable.search();
if (currentSearchStr !== newSearchStr) {
setSearchToUrl(datatable, newSearchStr);
}
});

$(element).on('order.dt', function () {
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;
const isOrderChangedFromDefault = !(!currentOrder && newColumnIdx === defaultColumnIdx && newOrderDir === defaultOrderDir);
if (isOrderChanged && isOrderChangedFromDefault) {
setOrderToUrl(datatable, newColumnIdx, newOrderDir);
}
});
}

function applyDtSearch(table) {
const currentSearchStr = getSearchFromUrl(table);
if (currentSearchStr) {
table.search(currentSearchStr);
}
}

function applyDtSorting(table) {
const currentOrder = getOrderFromUrl(table);
if (currentOrder && currentOrder.column && currentOrder.direction) {
table.order([[currentOrder.column, currentOrder.direction]]);
}
}

function applyDtPage(table, redrawTable) {
const currentPage = getPageNumFromUrl(table);
if (currentPage) {
table.page(currentPage);
redrawTable(table, 'page');
}
}

return {
applyPaginationListeners,
applyDtSearch,
applyDtSorting,
applyDtPage,
}
});