From 65ac5b83cb8485bf10eee115dfe489f508ee0de9 Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Wed, 22 Jan 2020 18:53:44 -0500 Subject: [PATCH 01/11] Adds persistence and reloading of datatable pagination params --- js/extensions/bindings/datatableBinding.js | 139 ++++++++++++++++++++- js/utils/CommonUtils.js | 32 +++++ 2 files changed, 166 insertions(+), 5 deletions(-) diff --git a/js/extensions/bindings/datatableBinding.js b/js/extensions/bindings/datatableBinding.js index 84f088e4a..5e01ef600 100644 --- a/js/extensions/bindings/datatableBinding.js +++ b/js/extensions/bindings/datatableBinding.js @@ -6,6 +6,8 @@ define([ 'xss', 'moment', 'services/MomentAPI', + 'utils/CommonUtils', + 'urijs', 'datatables.net-buttons', 'colvis', 'datatables.net-buttons-html5', @@ -16,7 +18,9 @@ define([ config, filterXSS, moment, - momentApi + momentApi, + CommonUtils, + URI ) { function renderSelected(s, p, d) { @@ -59,6 +63,94 @@ 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); + } + + 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 + }); + } + ko.bindingHandlers.dataTable = { init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { @@ -130,7 +222,30 @@ define([ ko.applyBindings(bindingContext, $(element).find('thead')[0]); } - $(element).DataTable(binding.options); + const datatable = $(element).DataTable(binding.options); + + $(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(); + const columnIdx = newOrder[0][0]; + const orderDir = newOrder[0][1]; + if (!currentOrder || currentOrder.column !== columnIdx || currentOrder.direction !== orderDir) { + setOrderToUrl(datatable, columnIdx, orderDir); + } + }); if (binding.api != null) { @@ -185,9 +300,23 @@ 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); + const currentSearchStr = getSearchFromUrl(table); + if (currentSearchStr) { + table.search(currentSearchStr); + } + + const currentOrder = getOrderFromUrl(table); + if (currentOrder && currentOrder.column && currentOrder.direction) { + table.order([[currentOrder.column, currentOrder.direction]]); + } + + redrawTable(table); + + const currentPage = getPageNumFromUrl(table); + if (currentPage) { + table.page(currentPage); + redrawTable(table, 'page'); + } } diff --git a/js/utils/CommonUtils.js b/js/utils/CommonUtils.js index 046c455fb..5a1a6d224 100644 --- a/js/utils/CommonUtils.js +++ b/js/utils/CommonUtils.js @@ -208,6 +208,36 @@ define([ return tooltipText.replace(/'/g, "\\'").replace(/"/g, '"'); } + 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 Date: Wed, 22 Jan 2020 20:00:25 -0500 Subject: [PATCH 02/11] Fixes order setup --- js/extensions/bindings/datatableBinding.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/js/extensions/bindings/datatableBinding.js b/js/extensions/bindings/datatableBinding.js index 5e01ef600..6adaed7b5 100644 --- a/js/extensions/bindings/datatableBinding.js +++ b/js/extensions/bindings/datatableBinding.js @@ -238,11 +238,22 @@ define([ }); $(element).on('order.dt', function () { + const { defaultColumnIdx, defaultOrderDir } = binding.options && Array.isArray(binding.options.order[0]) + ? { defaultColumnIdx: binding.options.order[0][0], defaultOrderDir: binding.options.order[0][1] } + : {}; + const currentOrder = getOrderFromUrl(datatable); + const newOrder = datatable.order(); - const columnIdx = newOrder[0][0]; - const orderDir = newOrder[0][1]; - if (!currentOrder || currentOrder.column !== columnIdx || currentOrder.direction !== orderDir) { + if (!Array.isArray(newOrder) || !newOrder.length) { + return; + } + const newColumnIdx = newOrder[0][0]; + const newOrderDir = newOrder[0][1]; + + const isOrderChanged = !currentOrder || currentOrder.column !== newColumnIdx || currentOrder.direction !== newOrderDir; + const isOrderChangedFromDefault = !(!currentOrder && newColumnIdx === defaultColumnIdx && newOrderDir === defaultOrderDir); + if (isOrderChanged && isOrderChangedFromDefault) { setOrderToUrl(datatable, columnIdx, orderDir); } }); From cdf5b9c78e490535e26a300d7bc01f54a425001e Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Wed, 22 Jan 2020 20:26:06 -0500 Subject: [PATCH 03/11] Refactoring --- js/extensions/bindings/datatableBinding.js | 141 +------------------ js/utils/DatatablePaginationUtils.js | 153 +++++++++++++++++++++ 2 files changed, 159 insertions(+), 135 deletions(-) create mode 100644 js/utils/DatatablePaginationUtils.js diff --git a/js/extensions/bindings/datatableBinding.js b/js/extensions/bindings/datatableBinding.js index 6adaed7b5..c16dcf1e5 100644 --- a/js/extensions/bindings/datatableBinding.js +++ b/js/extensions/bindings/datatableBinding.js @@ -6,8 +6,7 @@ define([ 'xss', 'moment', 'services/MomentAPI', - 'utils/CommonUtils', - 'urijs', + 'utils/DatatablePaginationUtils', 'datatables.net-buttons', 'colvis', 'datatables.net-buttons-html5', @@ -19,8 +18,7 @@ define([ filterXSS, moment, momentApi, - CommonUtils, - URI + paginationUtils ) { function renderSelected(s, p, d) { @@ -70,87 +68,6 @@ define([ ko.ignoreDependencies(func); } - 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 - }); - } - ko.bindingHandlers.dataTable = { init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { @@ -223,40 +140,7 @@ define([ } const datatable = $(element).DataTable(binding.options); - - $(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 { defaultColumnIdx, defaultOrderDir } = binding.options && Array.isArray(binding.options.order[0]) - ? { defaultColumnIdx: binding.options.order[0][0], defaultOrderDir: binding.options.order[0][1] } - : {}; - - const currentOrder = getOrderFromUrl(datatable); - - const newOrder = datatable.order(); - if (!Array.isArray(newOrder) || !newOrder.length) { - return; - } - const newColumnIdx = newOrder[0][0]; - const newOrderDir = newOrder[0][1]; - - const isOrderChanged = !currentOrder || currentOrder.column !== newColumnIdx || currentOrder.direction !== newOrderDir; - const isOrderChangedFromDefault = !(!currentOrder && newColumnIdx === defaultColumnIdx && newOrderDir === defaultOrderDir); - if (isOrderChanged && isOrderChangedFromDefault) { - setOrderToUrl(datatable, columnIdx, orderDir); - } - }); + paginationUtils.applyPaginationListeners(element, datatable, binding); if (binding.api != null) { @@ -311,23 +195,10 @@ define([ if (data.length > 0) table.rows.add(data); - const currentSearchStr = getSearchFromUrl(table); - if (currentSearchStr) { - table.search(currentSearchStr); - } - - const currentOrder = getOrderFromUrl(table); - if (currentOrder && currentOrder.column && currentOrder.direction) { - table.order([[currentOrder.column, currentOrder.direction]]); - } - + paginationUtils.applyDtSearch(table); + paginationUtils.applyDtSorting(table); redrawTable(table); - - const currentPage = getPageNumFromUrl(table); - if (currentPage) { - table.page(currentPage); - redrawTable(table, 'page'); - } + paginationUtils.applyDtPage(table, redrawTable); } diff --git a/js/utils/DatatablePaginationUtils.js b/js/utils/DatatablePaginationUtils.js new file mode 100644 index 000000000..a4b5e4433 --- /dev/null +++ b/js/utils/DatatablePaginationUtils.js @@ -0,0 +1,153 @@ +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 applyPaginationListeners(element, datatable, binding) { + const {defaultColumnIdx, defaultOrderDir} = binding.options && Array.isArray(binding.options.order[0]) + ? {defaultColumnIdx: binding.options.order[0][0], defaultOrderDir: binding.options.order[0][1]} + : {}; + + $(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 = newOrder[0][0]; + const newOrderDir = newOrder[0][1]; + + 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, + } +}); \ No newline at end of file From 0d7d07ad9510c85ba57f78245e62d39dd0c9915c Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Thu, 23 Jan 2020 10:23:18 -0500 Subject: [PATCH 04/11] Reset datatables sequence before navigation to new page --- js/pages/Router.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/pages/Router.js b/js/pages/Router.js index 49d9e47b4..69a3a60a8 100644 --- a/js/pages/Router.js +++ b/js/pages/Router.js @@ -8,6 +8,7 @@ define( 'knockout', 'const', 'services/EventBus', + 'datatables.net', 'director', ], ( @@ -19,6 +20,7 @@ define( ko, constants, EventBus, + dataTables ) => { class AtlasRouter { constructor() { @@ -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); From fe5ebacdce60085ce1f1df677181b4a9738c148f Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Mon, 10 Feb 2020 14:28:39 -0500 Subject: [PATCH 05/11] Code review fixes --- js/utils/DatatablePaginationUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/utils/DatatablePaginationUtils.js b/js/utils/DatatablePaginationUtils.js index a4b5e4433..cd04feec4 100644 --- a/js/utils/DatatablePaginationUtils.js +++ b/js/utils/DatatablePaginationUtils.js @@ -87,9 +87,9 @@ define([ } function applyPaginationListeners(element, datatable, binding) { - const {defaultColumnIdx, defaultOrderDir} = binding.options && Array.isArray(binding.options.order[0]) + const {defaultColumnIdx, defaultOrderDir} = binding.options && binding.options.order && Array.isArray(binding.options.order[0]) ? {defaultColumnIdx: binding.options.order[0][0], defaultOrderDir: binding.options.order[0][1]} - : {}; + : {defaultColumnIdx: 0, defaultOrderDir: 'asc'}; $(element).on('page.dt', function () { const info = datatable.page.info(); From 9153505c28ca070beec7dfc3286ae966c419cf5d Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Wed, 12 Feb 2020 14:17:54 -0500 Subject: [PATCH 06/11] Code review fixes #2 --- .../cohort-definition-manager.html | 14 ++++++------ .../cohort-definition-manager.js | 4 ++++ js/utils/DatatablePaginationUtils.js | 22 +++++++++++++++---- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/js/pages/cohort-definitions/cohort-definition-manager.html b/js/pages/cohort-definitions/cohort-definition-manager.html index ecce4fe1d..886fc26eb 100644 --- a/js/pages/cohort-definitions/cohort-definition-manager.html +++ b/js/pages/cohort-definitions/cohort-definition-manager.html @@ -34,28 +34,28 @@ diff --git a/js/pages/cohort-definitions/cohort-definition-manager.js b/js/pages/cohort-definitions/cohort-definition-manager.js index d7ee2dd21..b33dffb69 100644 --- a/js/pages/cohort-definitions/cohort-definition-manager.js +++ b/js/pages/cohort-definitions/cohort-definition-manager.js @@ -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/utils/DatatablePaginationUtils.js b/js/utils/DatatablePaginationUtils.js index cd04feec4..eb3d7204f 100644 --- a/js/utils/DatatablePaginationUtils.js +++ b/js/utils/DatatablePaginationUtils.js @@ -86,9 +86,23 @@ define([ }); } + 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 && Array.isArray(binding.options.order[0]) - ? {defaultColumnIdx: binding.options.order[0][0], defaultOrderDir: binding.options.order[0][1]} + 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 () { @@ -111,8 +125,8 @@ define([ if (!Array.isArray(newOrder) || !newOrder.length) { return; } - const newColumnIdx = newOrder[0][0]; - const newOrderDir = newOrder[0][1]; + const newColumnIdx = getOrderCol(newOrder); + const newOrderDir = getOrderDir(newOrder); const isOrderChanged = !currentOrder || currentOrder.column !== newColumnIdx || currentOrder.direction !== newOrderDir; const isOrderChangedFromDefault = !(!currentOrder && newColumnIdx === defaultColumnIdx && newOrderDir === defaultOrderDir); From 666dd9319ba82209ba0c6db224fd25d11e17ffd5 Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Thu, 13 Feb 2020 17:27:33 -0500 Subject: [PATCH 07/11] Code review fixes #3 --- js/extensions/bindings/datatableBinding.js | 12 +- js/pages/Router.js | 7 +- js/utils/DatatablePaginationUtils.js | 127 +++++++++++++++++---- 3 files changed, 109 insertions(+), 37 deletions(-) diff --git a/js/extensions/bindings/datatableBinding.js b/js/extensions/bindings/datatableBinding.js index c16dcf1e5..79e5617de 100644 --- a/js/extensions/bindings/datatableBinding.js +++ b/js/extensions/bindings/datatableBinding.js @@ -61,13 +61,6 @@ 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) { @@ -195,10 +188,7 @@ define([ if (data.length > 0) table.rows.add(data); - paginationUtils.applyDtSearch(table); - paginationUtils.applyDtSorting(table); - redrawTable(table); - paginationUtils.applyDtPage(table, redrawTable); + paginationUtils.refreshTable(table); } diff --git a/js/pages/Router.js b/js/pages/Router.js index 69a3a60a8..3e05c298c 100644 --- a/js/pages/Router.js +++ b/js/pages/Router.js @@ -9,6 +9,7 @@ define( 'const', 'services/EventBus', 'datatables.net', + 'utils/DatatablePaginationUtils', 'director', ], ( @@ -20,7 +21,8 @@ define( ko, constants, EventBus, - dataTables + dataTables, + DatatablePaginationUtils ) => { class AtlasRouter { constructor() { @@ -130,6 +132,9 @@ define( this.routerParams(routerParams); } this.currentView(view); + $.fn.dataTable.tables().forEach(dt => { + DatatablePaginationUtils.refreshTable($(dt).dataTable().DataTable()); + }); } } return new AtlasRouter(); diff --git a/js/utils/DatatablePaginationUtils.js b/js/utils/DatatablePaginationUtils.js index eb3d7204f..544bf12c9 100644 --- a/js/utils/DatatablePaginationUtils.js +++ b/js/utils/DatatablePaginationUtils.js @@ -1,7 +1,9 @@ define([ + 'knockout', 'utils/CommonUtils', 'urijs', ], ( + ko, CommonUtils, URI ) => { @@ -10,10 +12,32 @@ define([ const ORDER_PARAM = 'dtOrder'; const PARAM_SEPARATOR = '_'; - function buildDtParamName(datatable, param) { + 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()); - const elId = CommonUtils.calculateStringHash(elPath); - return param + PARAM_SEPARATOR + elId; + return CommonUtils.calculateStringHash(elPath); + } + + function buildDtParamName(datatable, param) { + return param + PARAM_SEPARATOR + getDtId(datatable); } function getDtParamValue(param) { @@ -33,6 +57,14 @@ define([ 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); } @@ -100,25 +132,64 @@ define([ : undefined; } + 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 () { - const info = datatable.page.info(); - setPageNumToUrl(datatable, info.page); + if (shouldIgnoreEvent(datatable)) return; + const pageNum = getPageNumFromDt(datatable); + if (getPageNumFromUrl(datatable) !== pageNum) { + if (pageNum !== 0) { + setPageNumToUrl(datatable, pageNum); + } else { + removePageNumFromUrl(datatable); + } + } }); $(element).on('search.dt', function () { + if (shouldIgnoreEvent(datatable)) return; const currentSearchStr = getSearchFromUrl(datatable) || ''; const newSearchStr = datatable.search(); if (currentSearchStr !== newSearchStr) { - setSearchToUrl(datatable, newSearchStr); + if (newSearchStr !== '') { + setSearchToUrl(datatable, newSearchStr); + } else { + removeSearchFromUrl(datatable); + } } }); $(element).on('order.dt', function () { + if (shouldIgnoreEvent(datatable)) return; + const currentOrder = getOrderFromUrl(datatable); const newOrder = datatable.order(); @@ -129,39 +200,45 @@ define([ 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); + if (isOrderChanged) { + if (newColumnIdx !== defaultColumnIdx || newOrderDir !== defaultOrderDir) { + setOrderToUrl(datatable, newColumnIdx, newOrderDir); + } else { + removeOrderFromUrl(datatable); + } } }); } function applyDtSearch(table) { - const currentSearchStr = getSearchFromUrl(table); - if (currentSearchStr) { - table.search(currentSearchStr); + 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); - if (currentOrder && currentOrder.column && currentOrder.direction) { - table.order([[currentOrder.column, currentOrder.direction]]); - } + const currentOrder = getOrderFromUrl(table) || DEFAULT_DT_PARAMS[getDtId(table)]; + table.order([[currentOrder.column, currentOrder.direction]]); } - function applyDtPage(table, redrawTable) { - const currentPage = getPageNumFromUrl(table); - if (currentPage) { - table.page(currentPage); - redrawTable(table, 'page'); - } + 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, - applyDtSearch, - applyDtSorting, - applyDtPage, + refreshTable, } }); \ No newline at end of file From caa6e8719601403c7d8efa4da034ab036c568fb8 Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Sat, 15 Feb 2020 22:33:34 -0500 Subject: [PATCH 08/11] Adds persistence of facet values into URL --- js/components/faceted-datatable.html | 2 +- js/components/faceted-datatable.js | 63 +++++++++++++++++++++------- js/pages/Router.js | 1 + js/utils/DatatablePaginationUtils.js | 47 +++++++++++++++++++-- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/js/components/faceted-datatable.html b/js/components/faceted-datatable.html index 63d62ff11..edc482f78 100644 --- a/js/components/faceted-datatable.html +++ b/js/components/faceted-datatable.html @@ -1,7 +1,7 @@

Loading ...

-
+
diff --git a/js/components/faceted-datatable.js b/js/components/faceted-datatable.js index c8afcdff8..65f996dc6 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 && event && getDtElement($(event.target.closest('faceted-datatable'))).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); + } + }) + } } }); } @@ -144,6 +171,14 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'colvis', ], }) ); + $(self.element).on('refresh.faceted-dt', () => { + const savedSearch = getSavedSearch(self.element); + self.options.Facets.forEach(facetConfig => { + const savedFacetValues = savedSearch[facetConfig.caption] || []; + self.selectFacetItems(facetConfig.caption, savedFacetValues); + }); + }); + // init component if (ko.isComputed(self.reference)) { // valueHasMutated doesn't work for computed @@ -190,7 +225,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/pages/Router.js b/js/pages/Router.js index 3e05c298c..dc95bba66 100644 --- a/js/pages/Router.js +++ b/js/pages/Router.js @@ -135,6 +135,7 @@ define( $.fn.dataTable.tables().forEach(dt => { DatatablePaginationUtils.refreshTable($(dt).dataTable().DataTable()); }); + $('faceted-datatable').trigger('refresh.faceted-dt'); } } return new AtlasRouter(); diff --git a/js/utils/DatatablePaginationUtils.js b/js/utils/DatatablePaginationUtils.js index 544bf12c9..a8714b084 100644 --- a/js/utils/DatatablePaginationUtils.js +++ b/js/utils/DatatablePaginationUtils.js @@ -10,6 +10,7 @@ define([ const PAGE_PARAM = 'dtPage'; const SEARCH_PARAM = 'dtSearch'; const ORDER_PARAM = 'dtOrder'; + const FACET_PARAM = 'dtFacet'; const PARAM_SEPARATOR = '_'; const DEFAULT_DT_PARAMS = {}; @@ -37,13 +38,17 @@ define([ } function buildDtParamName(datatable, param) { - return param + PARAM_SEPARATOR + getDtId(datatable); + return getDtId(datatable) + PARAM_SEPARATOR + encodeURIComponent(param); } - function getDtParamValue(param) { + function getDtParams() { const currentUrl = URI(document.location.href); const fragment = URI(currentUrl.fragment()); - const params = fragment.search(true); + return fragment.search(true); + } + + function getDtParamValue(param) { + const params = getDtParams(); return params[param]; } @@ -132,6 +137,37 @@ define([ : undefined; } + function getFacetParamName(datatable, facetName) { + return buildDtParamName(datatable, FACET_PARAM + PARAM_SEPARATOR + facetName); + } + + function setFacetToUrl(datatable, facetName, facetValue, selected) { + let values = getFacets(datatable)[facetName] || []; + if (selected) { + values = [...new Set(values.concat([facetValue]))]; + } else { + values.splice(values.indexOf(facetValue), 1); + } + if (values.length > 0) { + 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)); } @@ -208,6 +244,10 @@ define([ } } }); + + $(element).on('facet.dt', function (event, data) { + setFacetToUrl(datatable, data.facet.caption, data.key, data.selected()); + }); } function applyDtSearch(table) { @@ -240,5 +280,6 @@ define([ return { applyPaginationListeners, refreshTable, + getFacets, } }); \ No newline at end of file From c0bdb3eab1d95915509addf311b6f10d0d788d50 Mon Sep 17 00:00:00 2001 From: Pavel Grafkin Date: Mon, 17 Feb 2020 10:35:24 -0500 Subject: [PATCH 09/11] Review fixes --- index.html | 34 +++++++++++++++------------- js/components/faceted-datatable.js | 12 ++++++---- js/pages/Router.js | 9 ++++---- js/utils/CommonUtils.js | 2 -- js/utils/DatatablePaginationUtils.js | 28 +++++++++++++++-------- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/index.html b/index.html index b87d5b2eb..614439b3c 100644 --- a/index.html +++ b/index.html @@ -84,24 +84,26 @@

- - -