From bd10af8dca588fe02ce585f79a504fe3f99dd96a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 6 Nov 2018 23:46:06 -0400 Subject: [PATCH 01/67] fix(table): Only emit filtered event if filtered rows has changed. Fixes #1989 --- src/components/table/table.js | 55 ++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index a0dbd1d179f..023b8cb72c4 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -13,13 +13,29 @@ import listenOnRootMixin from '../../mixins/listen-on-root' // Import styles import './table.css' -// Object of item keys that should be ignored for headers and stringification +// Object of item keys that should be ignored for headers and stringification and filter events const IGNORED_FIELD_KEYS = { _rowVariant: true, _cellVariants: true, _showDetails: true } +// Return a copy of a row after all reserved fields have been filtered out +function sanitizeRow (row) { + return keys(row).reduce((obj, key) => { + // Ignore special fields that start with _ + if (!IGNORED_FIELD_KEYS[key]) { + obj[k] = row[k] + } + return obj + }, {}) +} + +// Return a new array of records/rows that have been sanitized +function sanitizeRows (rows) { + return rows.map(row => sanitizeRow(row)) +} + // Stringifies the values of an object // { b: 3, c: { z: 'zzz', d: null, e: 2 }, d: [10, 12, 11], a: 'one' } // becomes @@ -39,19 +55,11 @@ function toString (v) { } // Stringifies the values of a record, ignoring any special top level keys -function recToString (obj) { +function recToString (row) { if (!(obj instanceof Object)) { return '' } - return toString( - keys(obj).reduce((o, k) => { - // Ignore special fields that start with _ - if (!IGNORED_FIELD_KEYS[k]) { - o[k] = obj[k] - } - return o - }, {}) - ) + return toString(sanitizeRow(row)) } function defaultSortCompare (a, b, sortBy) { @@ -413,8 +421,8 @@ export default { localSortBy: this.sortBy || '', localSortDesc: this.sortDesc || false, localItems: [], - // Note: filteredItems only used to determine if # of items changed - filteredItems: [], + // Note: filteredItems only used to determine if items changed (set to null initially) + filteredItems: null, localBusy: false } }, @@ -617,10 +625,23 @@ export default { this.$emit('context-changed', newVal) } }, - filteredItems (newVal, oldVal) { - if (this.localFiltering && newVal.length !== oldVal.length) { - // Emit a filtered notification event, as number of filtered items has changed - this.$emit('filtered', newVal) + filteredItems (newRows, oldRows) { + if (!this.localFiltering || newRows === oldRows) { + // If not local Filtering or nothing changed, don't emit a filtered event + return + } + if (oldRows === null || newRows === null) { + // If first run of updating items, don't emit a filtered event. + return + } + // We compare lengths first for performance reasons, even though looseEqual + // does this test, as we must sanitize the row data before passing it to + // looseEqual, of which both could take time to run on long record sets! + if (newRows.length !== oldRows.length || + !looseEqual(sanitizeRows(newRows), sanitizeRows(oldRows))) { + // Emit a filtered notification event, as filtered items has changed + // Note this may fire before the v-model has updated + this.$emit('filtered', newRows) } }, sortDesc (newVal, oldVal) { From 2fd01fd8eb0779bb6c75646555805a3ecd868f2c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 6 Nov 2018 23:52:52 -0400 Subject: [PATCH 02/67] lint --- src/components/table/table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 023b8cb72c4..8092a8f3013 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -25,7 +25,7 @@ function sanitizeRow (row) { return keys(row).reduce((obj, key) => { // Ignore special fields that start with _ if (!IGNORED_FIELD_KEYS[key]) { - obj[k] = row[k] + obj[key] = row[key] } return obj }, {}) @@ -56,7 +56,7 @@ function toString (v) { // Stringifies the values of a record, ignoring any special top level keys function recToString (row) { - if (!(obj instanceof Object)) { + if (!(row instanceof Object)) { return '' } return toString(sanitizeRow(row)) From be38c7a7232b2ee3653b03d53dacde1d56453e0c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 01:01:39 -0400 Subject: [PATCH 03/67] Move filtered check and emit into the filterItems() method filtered event check moved to the filteredItems() method. Filtered event is now emitted after the table has updated (via $nextTick). Improves run-time memory footprint by not maintaining a separate copy of the filtered data. --- src/components/table/table.js | 61 ++++++++++++++--------------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 8092a8f3013..17c5f87ce5e 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -421,8 +421,6 @@ export default { localSortBy: this.sortBy || '', localSortDesc: this.sortDesc || false, localItems: [], - // Note: filteredItems only used to determine if items changed (set to null initially) - filteredItems: null, localBusy: false } }, @@ -625,25 +623,6 @@ export default { this.$emit('context-changed', newVal) } }, - filteredItems (newRows, oldRows) { - if (!this.localFiltering || newRows === oldRows) { - // If not local Filtering or nothing changed, don't emit a filtered event - return - } - if (oldRows === null || newRows === null) { - // If first run of updating items, don't emit a filtered event. - return - } - // We compare lengths first for performance reasons, even though looseEqual - // does this test, as we must sanitize the row data before passing it to - // looseEqual, of which both could take time to run on long record sets! - if (newRows.length !== oldRows.length || - !looseEqual(sanitizeRows(newRows), sanitizeRows(oldRows))) { - // Emit a filtered notification event, as filtered items has changed - // Note this may fire before the v-model has updated - this.$emit('filtered', newRows) - } - }, sortDesc (newVal, oldVal) { if (newVal === this.localSortDesc) { return @@ -841,7 +820,7 @@ export default { // Array copy for sorting, filtering, etc. items = items.slice() // Apply local filter - this.filteredItems = items = this.filterItems(items) + items = this.filterItems(items) // Apply local sort items = this.sortItems(items) // Apply local pagination @@ -857,25 +836,35 @@ export default { methods: { keys, filterItems (items) { + let filtered = items if (this.localFiltering && this.filter) { if (this.filter instanceof Function) { - return items.filter(this.filter) - } - - let regex - if (this.filter instanceof RegExp) { - regex = this.filter + filtered = items.filter(this.filter) } else { - regex = new RegExp('.*' + this.filter + '.*', 'ig') + let regex + if (this.filter instanceof RegExp) { + regex = this.filter + } else { + regex = new RegExp('.*' + this.filter + '.*', 'ig') + } + filtered = items.filter(item => { + const test = regex.test(recToString(item)) + regex.lastIndex = 0 + return test + }) + } + // If the filtered items are not the same as the unfiltered, emit a filtered event. + // We check the legnths first for performance reasons. + if (filtered.length !== items.length || + !looseEqual(sanitizeRows(filtered), sanitizeRows(items))) { + this.$nextTick(() => { + // Wait for table to render updates before emitting + this.$emit('filtered', filtered) + }) } - - return items.filter(item => { - const test = regex.test(recToString(item)) - regex.lastIndex = 0 - return test - }) } - return items + + return filtered }, sortItems (items) { const sortBy = this.localSortBy From f4af8d049cae746cc123d4a20bf922330b9ffb12 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 01:30:20 -0400 Subject: [PATCH 04/67] Add fix for issue #1517 Handles case where filter is a function and `empty` message instead of `emptyFiltered` message was showing when filtered items was 0 length array. --- src/components/table/table.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 17c5f87ce5e..c1829d5d977 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -341,7 +341,7 @@ export default { // Empty Items / Empty Filtered Row slot if (this.showEmpty && (!items || items.length === 0)) { - let empty = this.filter ? $slots['emptyfiltered'] : $slots['empty'] + let empty = this.isFiltered ? $slots['emptyfiltered'] : $slots['empty'] if (!empty) { empty = h('div', { class: ['text-center', 'my-2'], @@ -421,7 +421,8 @@ export default { localSortBy: this.sortBy || '', localSortDesc: this.sortDesc || false, localItems: [], - localBusy: false + localBusy: false, + isFiltered: false } }, props: { @@ -857,13 +858,20 @@ export default { // We check the legnths first for performance reasons. if (filtered.length !== items.length || !looseEqual(sanitizeRows(filtered), sanitizeRows(items))) { + // Flag that the table is showing filtered items + this.isFiltered = true + // Wait for table to render updates before emitting filtered event this.$nextTick(() => { - // Wait for table to render updates before emitting this.$emit('filtered', filtered) }) + } else { + // Flag that the table is not showing filtered items + this.isFiltered = false } + } else { + // Flag that the table is not showing filtered items + this.isFiltered = false } - return filtered }, sortItems (items) { From 3776b216ff268c18c207b566c5bda8bcab7f62eb Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 03:37:40 -0400 Subject: [PATCH 05/67] Update table.js --- src/components/table/table.js | 45 +++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index c1829d5d977..79771390e1b 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -837,9 +837,18 @@ export default { methods: { keys, filterItems (items) { + if (!this.localFiltering) { + // If the provider is filtering, we just set the state of this.isFiltered based on the + // truthy-ness of the fitler prop, and then return the original items as-is. + // We dont emit filtered events for non-local filtering. + this.isFiltered = Boolean(this.filter) + return items + } let filtered = items - if (this.localFiltering && this.filter) { + const oldIsFiltered = this.isFiltered + if (this.filter) { if (this.filter instanceof Function) { + // filter the items array using the function set in the filter prop filtered = items.filter(this.filter) } else { let regex @@ -854,24 +863,36 @@ export default { return test }) } - // If the filtered items are not the same as the unfiltered, emit a filtered event. - // We check the legnths first for performance reasons. - if (filtered.length !== items.length || - !looseEqual(sanitizeRows(filtered), sanitizeRows(items))) { - // Flag that the table is showing filtered items + // Determine if the filtered items are indeed filtered/different from items + if (filtered === items) { + // If the filtered array is the same reference as the items array + // then the table is flagged as not filtered + this.isFiltered = false + } else if (filtered.length !== items.length) { + // If the length of the filterd items is not the same as teh original items, + // then we flag the table as filtered. + this.isFiltered = true + } else if (!(filtered.every((f, idx) => looseEqual(sanitizeRow(f), sanitizeRow(items[idx]))))) { + // We compare row-by-row until the first non loosely equal row is found. + // A differing row at some index was found, so we flag the table as filtered. this.isFiltered = true - // Wait for table to render updates before emitting filtered event - this.$nextTick(() => { - this.$emit('filtered', filtered) - }) } else { - // Flag that the table is not showing filtered items + // Filtered items are "loosely" equal to the original items, so we + // flag that the table is not showing filtered items. this.isFiltered = false } } else { - // Flag that the table is not showing filtered items + // No filter prop specified, so we flag that the table as not filtered this.isFiltered = false } + // We emit a filtered event if filtering is active, or if filtering state has changed. + if (this.isFiltered || this.isFiltered !== oldIsFiltered) { + // Wait for table to render updates before emitting filtered event + this.$nextTick(() => { + this.$emit('filtered', filtered) + }) + } + // Return the possibly filtered items return filtered }, sortItems (items) { From c787688f44bf00b5fbfb76cc88d8ec06c5664ed2 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 04:29:38 -0400 Subject: [PATCH 06/67] Update table.js --- src/components/table/table.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 79771390e1b..7a53a2be67b 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -31,11 +31,6 @@ function sanitizeRow (row) { }, {}) } -// Return a new array of records/rows that have been sanitized -function sanitizeRows (rows) { - return rows.map(row => sanitizeRow(row)) -} - // Stringifies the values of an object // { b: 3, c: { z: 'zzz', d: null, e: 2 }, d: [10, 12, 11], a: 'one' } // becomes @@ -869,7 +864,7 @@ export default { // then the table is flagged as not filtered this.isFiltered = false } else if (filtered.length !== items.length) { - // If the length of the filterd items is not the same as teh original items, + // If the length of the filterd items is not the same as the original items, // then we flag the table as filtered. this.isFiltered = true } else if (!(filtered.every((f, idx) => looseEqual(sanitizeRow(f), sanitizeRow(items[idx]))))) { From 042796fdb963337ab10360cfe5161fd7fd6792d3 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 05:05:36 -0400 Subject: [PATCH 07/67] Update table.js --- src/components/table/table.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 7a53a2be67b..0c14f31f87b 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -859,11 +859,7 @@ export default { }) } // Determine if the filtered items are indeed filtered/different from items - if (filtered === items) { - // If the filtered array is the same reference as the items array - // then the table is flagged as not filtered - this.isFiltered = false - } else if (filtered.length !== items.length) { + if (filtered.length !== items.length) { // If the length of the filterd items is not the same as the original items, // then we flag the table as filtered. this.isFiltered = true From 54f82d02638d04e2c1c5bfb18615fcf19e5e3afe Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 05:15:28 -0400 Subject: [PATCH 08/67] Update table.js --- src/components/table/table.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 0c14f31f87b..a58ea1e5ccd 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -839,12 +839,12 @@ export default { this.isFiltered = Boolean(this.filter) return items } - let filtered = items + let filtered = items || [] const oldIsFiltered = this.isFiltered - if (this.filter) { + if (this.filter && items && items.length) { if (this.filter instanceof Function) { // filter the items array using the function set in the filter prop - filtered = items.filter(this.filter) + filtered = items.filter(this.filter) || [] } else { let regex if (this.filter instanceof RegExp) { @@ -873,7 +873,7 @@ export default { this.isFiltered = false } } else { - // No filter prop specified, so we flag that the table as not filtered + // No filter prop specified or no items to filter, so we flag that the table as not filtered this.isFiltered = false } // We emit a filtered event if filtering is active, or if filtering state has changed. @@ -884,7 +884,7 @@ export default { }) } // Return the possibly filtered items - return filtered + return filtered || [] }, sortItems (items) { const sortBy = this.localSortBy From 0012baaf7ce37f8a09a8a3dca59cbaa45e411918 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 10:29:41 -0400 Subject: [PATCH 09/67] Update table.spec.js --- src/components/table/table.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/table/table.spec.js b/src/components/table/table.spec.js index 148319e473d..0899ac6d62f 100755 --- a/src/components/table/table.spec.js +++ b/src/components/table/table.spec.js @@ -645,6 +645,7 @@ describe('table', async () => { // Empty filter alert await setData(app, 'filter', 'ZZZZZZZZZZZZZZZZZzzzzzzzzzzzzzzzzz........') await nextTick() + await nextTick() expect(vm.value.length).toBe(0) expect(tbody.children.length).toBe(1) expect(tbody.children[0].children[0].textContent).toContain( From c427ab1749aac782017974273261c8714042304b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 10:35:49 -0400 Subject: [PATCH 10/67] Update table.js --- src/components/table/table.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index a58ea1e5ccd..679ac9f3671 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -878,10 +878,7 @@ export default { } // We emit a filtered event if filtering is active, or if filtering state has changed. if (this.isFiltered || this.isFiltered !== oldIsFiltered) { - // Wait for table to render updates before emitting filtered event - this.$nextTick(() => { - this.$emit('filtered', filtered) - }) + this.$emit('filtered', filtered) } // Return the possibly filtered items return filtered || [] From 4eaf9fd5fc23845207606db63f4df550c2d02aa5 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 10:45:38 -0400 Subject: [PATCH 11/67] Update table.spec.js --- src/components/table/table.spec.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.spec.js b/src/components/table/table.spec.js index 0899ac6d62f..27c3443266c 100755 --- a/src/components/table/table.spec.js +++ b/src/components/table/table.spec.js @@ -616,7 +616,8 @@ describe('table', async () => { const { app: { $refs } } = window const vm = $refs.table_paginated const app = window.app - const spy = jest.fn() + const spyInput = jest.fn() + const spyFiltered = jest.fn() expect(vm.showEmpty).toBe(true) expect(app.items.length > 10).toBe(true) @@ -627,7 +628,7 @@ describe('table', async () => { if (tbody) { expect(app.items.length > 1).toBe(true) - vm.$on('input', spy) + vm.$on('input', spyInput) // Set page size to max number of items await setData(app, 'currentPage', 1) @@ -643,16 +644,19 @@ describe('table', async () => { expect(tbody.children.length < app.items.length).toBe(true) // Empty filter alert + vm.$on('filtered', spyFiltered) await setData(app, 'filter', 'ZZZZZZZZZZZZZZZZZzzzzzzzzzzzzzzzzz........') await nextTick() - await nextTick() + + expect(spyFiltered).toHaveBeenCalled() + expect(vm.value.length).toBe(0) expect(tbody.children.length).toBe(1) expect(tbody.children[0].children[0].textContent).toContain( vm.emptyFilteredText ) - expect(spy).toHaveBeenCalled() + expect(spyInput).toHaveBeenCalled() } }) From bb8c7244ca4a09001fe03fc2c2e75747a0646d5a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 12:00:53 -0400 Subject: [PATCH 12/67] Update table.js --- src/components/table/table.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 679ac9f3671..3a23abaec5b 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -841,6 +841,7 @@ export default { } let filtered = items || [] const oldIsFiltered = this.isFiltered + let newIsFiltered = false if (this.filter && items && items.length) { if (this.filter instanceof Function) { // filter the items array using the function set in the filter prop @@ -862,23 +863,19 @@ export default { if (filtered.length !== items.length) { // If the length of the filterd items is not the same as the original items, // then we flag the table as filtered. - this.isFiltered = true + newIsFiltered = true } else if (!(filtered.every((f, idx) => looseEqual(sanitizeRow(f), sanitizeRow(items[idx]))))) { // We compare row-by-row until the first non loosely equal row is found. // A differing row at some index was found, so we flag the table as filtered. - this.isFiltered = true - } else { - // Filtered items are "loosely" equal to the original items, so we - // flag that the table is not showing filtered items. - this.isFiltered = false + newIsFiltered = true } - } else { - // No filter prop specified or no items to filter, so we flag that the table as not filtered - this.isFiltered = false } // We emit a filtered event if filtering is active, or if filtering state has changed. - if (this.isFiltered || this.isFiltered !== oldIsFiltered) { - this.$emit('filtered', filtered) + if (newIsFiltered || newIsFiltered !== oldIsFiltered) { + this.$nextTick(() => { + this.isFiltered = newIsFiltered + this.$emit('filtered', filtered) + }) } // Return the possibly filtered items return filtered || [] @@ -1076,11 +1073,15 @@ export default { if (data && data.then && typeof data.then === 'function') { // Provider returned Promise data.then(items => { - this._providerSetLocal(items) + // Provider resolved with items + this._providerSetLocal(Array.isArray(items) ? items : []) + }, () => { + // Provider rejected with no items + this._providerSetLocal([]) }) } else { // Provider returned Array data - this._providerSetLocal(data) + this._providerSetLocal(Array.isArray(data) ? data : []) } }) }, From 65a2aa9c26659b01b45f46a6516f09c77a90087f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 12:35:40 -0400 Subject: [PATCH 13/67] create computedFilter to generate filter function --- src/components/table/table.js | 47 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 3a23abaec5b..a01cd5cbf5f 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -827,7 +827,29 @@ export default { }, computedBusy () { return this.busy || this.localBusy - } + }, + computedFilter () { + if (this.filter instanceof Function) { + // filter the items array using the function set in the filter prop + return this.filter + } else if (this.filter) { + let regex == null + if (this.filter instanceof RegExp) { + regex = this.filter + } else { + // Escape special RegExp characters in the string + const string = this.filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + regex = new RegExp(`.*${string}.*`, 'ig') + } + // return a filter function + return (item) => { + const test = regex.test(recToString(item)) + regex.lastIndex = 0 + return test + } + } else { + return null + } }, methods: { keys, @@ -842,30 +864,15 @@ export default { let filtered = items || [] const oldIsFiltered = this.isFiltered let newIsFiltered = false - if (this.filter && items && items.length) { - if (this.filter instanceof Function) { - // filter the items array using the function set in the filter prop - filtered = items.filter(this.filter) || [] - } else { - let regex - if (this.filter instanceof RegExp) { - regex = this.filter - } else { - regex = new RegExp('.*' + this.filter + '.*', 'ig') - } - filtered = items.filter(item => { - const test = regex.test(recToString(item)) - regex.lastIndex = 0 - return test - }) - } + if (!!this.computedFilter && items && items.length) { + filtered = items.filter(this.computedFilter) || [] // Determine if the filtered items are indeed filtered/different from items if (filtered.length !== items.length) { // If the length of the filterd items is not the same as the original items, - // then we flag the table as filtered. + // then we flag the table as filtered. We do this test first for performance reasons newIsFiltered = true } else if (!(filtered.every((f, idx) => looseEqual(sanitizeRow(f), sanitizeRow(items[idx]))))) { - // We compare row-by-row until the first non loosely equal row is found. + // We compare row-by-row until the first loosely non-equal row is found. // A differing row at some index was found, so we flag the table as filtered. newIsFiltered = true } From e5d5842a042ddbc9a40746ff2f6a4c53646579aa Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 15:46:39 -0400 Subject: [PATCH 14/67] lint --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index a01cd5cbf5f..59e4401e239 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -833,7 +833,7 @@ export default { // filter the items array using the function set in the filter prop return this.filter } else if (this.filter) { - let regex == null + let regex = null if (this.filter instanceof RegExp) { regex = this.filter } else { From fd96541144d2c91927005b4a7b74d9cb11510b96 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 16:39:56 -0400 Subject: [PATCH 15/67] lint --- src/components/table/table.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/table/table.js b/src/components/table/table.js index 59e4401e239..c0f2dfcab92 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -850,6 +850,7 @@ export default { } else { return null } + } }, methods: { keys, From ef15a87274a0617fd779ec80e78534bd0cd62d6a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 16:49:26 -0400 Subject: [PATCH 16/67] lint --- src/components/table/table.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index c0f2dfcab92..f374f5da7d2 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -838,7 +838,8 @@ export default { regex = this.filter } else { // Escape special RegExp characters in the string - const string = this.filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + / + const string = this.filter.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') regex = new RegExp(`.*${string}.*`, 'ig') } // return a filter function From 8e53cffe9fd333df4d683bade2bd470a388c8a24 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 16:56:34 -0400 Subject: [PATCH 17/67] lint --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index f374f5da7d2..316a811c587 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -839,7 +839,7 @@ export default { } else { // Escape special RegExp characters in the string / - const string = this.filter.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + const string = this.filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') regex = new RegExp(`.*${string}.*`, 'ig') } // return a filter function From e0dd09eeba0144bf60ee1ed53f87de135ef42d92 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 17:01:16 -0400 Subject: [PATCH 18/67] Update table.js --- src/components/table/table.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 316a811c587..c0f2dfcab92 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -838,7 +838,6 @@ export default { regex = this.filter } else { // Escape special RegExp characters in the string - / const string = this.filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') regex = new RegExp(`.*${string}.*`, 'ig') } From 62376ab1dc934772eb74b84721d20d7560cdfb1e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 17:08:50 -0400 Subject: [PATCH 19/67] Update table.js --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index c0f2dfcab92..2fcb6178a6a 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -838,7 +838,7 @@ export default { regex = this.filter } else { // Escape special RegExp characters in the string - const string = this.filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + const string = this.filter.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') regex = new RegExp(`.*${string}.*`, 'ig') } // return a filter function From a444cc4bf86c4089bb731020147c8902f0cac934 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 19:01:34 -0400 Subject: [PATCH 20/67] make currentPage prop syncable Trying to fix a race condition with pagination when filtered --- src/components/table/table.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 2fcb6178a6a..37d167ab88f 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -831,7 +831,10 @@ export default { computedFilter () { if (this.filter instanceof Function) { // filter the items array using the function set in the filter prop - return this.filter + // Wrap it in a function so we can prevent the filter function from mutating the top level data + return (item) => { + return this.filter(assign({}, item)) + } } else if (this.filter) { let regex = null if (this.filter instanceof RegExp) { @@ -872,7 +875,7 @@ export default { // If the length of the filterd items is not the same as the original items, // then we flag the table as filtered. We do this test first for performance reasons newIsFiltered = true - } else if (!(filtered.every((f, idx) => looseEqual(sanitizeRow(f), sanitizeRow(items[idx]))))) { + } else if (!(filtered.every((item, idx) => looseEqual(item, items[idx])))) { // We compare row-by-row until the first loosely non-equal row is found. // A differing row at some index was found, so we flag the table as filtered. newIsFiltered = true @@ -911,11 +914,18 @@ export default { return items }, paginateItems (items) { - const currentPage = this.currentPage + let currentPage = this.currentPage const perPage = this.perPage const localPaging = this.localPaging // Apply local pagination if (!!perPage && localPaging) { + // Does the requested page exist? + const maxPages = Math.ceil(items.length / perPage) || 1 + currentPage Math.max(Math.min(currentPage, maxPages), 1) + if (currentPage !== this.currentPage) { + // send out a .sync update to the currentPage prop + this.$emit('update:currentPage', currentPage) + } // Grab the current page of data (which may be past filtered items) return items.slice((currentPage - 1) * perPage, currentPage * perPage) } From fe3222972e9aee17f705330bf7c5bb93a66abf3a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 19:23:53 -0400 Subject: [PATCH 21/67] Update table.js --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 37d167ab88f..7228b1ef5be 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -921,7 +921,7 @@ export default { if (!!perPage && localPaging) { // Does the requested page exist? const maxPages = Math.ceil(items.length / perPage) || 1 - currentPage Math.max(Math.min(currentPage, maxPages), 1) + currentPage = Math.max(Math.min(currentPage, maxPages), 1) if (currentPage !== this.currentPage) { // send out a .sync update to the currentPage prop this.$emit('update:currentPage', currentPage) From 85630ee9ce491bc9a0d44fafbfd19e53a446baf5 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 22:06:49 -0400 Subject: [PATCH 22/67] some code re-writes to prevent race conditions and make computedItems more reactive --- src/components/table/table.js | 87 +++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 7228b1ef5be..efb1f9a7106 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -415,9 +415,11 @@ export default { return { localSortBy: this.sortBy || '', localSortDesc: this.sortDesc || false, - localItems: [], localBusy: false, - isFiltered: false + localItems: Array.isArray(this.items) ? this.items.slice() : [], + filteredItems: [], + isFiltered: false, + filteredTrigger: false } }, props: { @@ -611,7 +613,17 @@ export default { watch: { items (newVal, oldVal) { if (oldVal !== newVal) { - this._providerUpdate() + if (this.hasProvider) { + this._providerUpdate() + } else { + this.localItems = this.items.slice() + } + } + }, + filteredTrigger (newVal, oldVal) { + // Whenever this value changes state (true=>false, false=>true), we emit a filtered event. + if (newVal !== oldVal) { + this.$emit('filtered', this.filteredItems.slice()) } }, context (newVal, oldVal) { @@ -808,21 +820,34 @@ export default { }) }, computedItems () { - let items = this.hasProvider ? this.localItems : this.items - if (!items) { - this.$nextTick(this._providerUpdate) - return [] + let items = this.localItems || [] + // Grab some values to help trigger reactivity to prop changes, since + // all of the grunt work is done via methods (which arent reactive) + const sortBy = this.localSortBy + const sortDesc = this.localSortDesc + const sortCompare = this.sortCompare + const filter = this.computedFilter + const currPage = this.currentPage + const perPage = this.perPage + // If table is busy, just return the current localItems + if (this.computedBusy) { + return items } - // Array copy for sorting, filtering, etc. - items = items.slice() - // Apply local filter - items = this.filterItems(items) - // Apply local sort - items = this.sortItems(items) - // Apply local pagination - items = this.paginateItems(items) - // Update the value model with the filtered/sorted/paginated data set + // Apply local filter (returns a new array if filtering) + if (filter) { + items = this.filterItems(items) + } + // Apply local sort (returns a new array if sorting) + if (sortBy || typeof sortDesc === 'boolean' || sortCompare) { + items = this.sortItems(items) + } + // Apply local pagination (returns new array if paginating) + if (currPage || perPage) { + items = this.paginateItems(items) + } + // Update the value model with the filtered/sorted/paginated data set. this.$emit('input', items) + // send the (possibly) updated items to the table. return items }, computedBusy () { @@ -881,22 +906,26 @@ export default { newIsFiltered = true } } - // We emit a filtered event if filtering is active, or if filtering state has changed. + // Store a reference of the filtered items in data for later use by the emitter + this.filteredItems = filtered + // We set some flags if filtering is active, or if filtering state has changed. if (newIsFiltered || newIsFiltered !== oldIsFiltered) { - this.$nextTick(() => { - this.isFiltered = newIsFiltered - this.$emit('filtered', filtered) - }) + // We toggle this flag to trigger the emit of 'filtered' event + this.filteredTrigger = !this.filteredTrigger + // Set a flag for showing that filtering is active + this.isFiltered = newIsFiltered } // Return the possibly filtered items - return filtered || [] + return filtered }, sortItems (items) { + // Sorts the items and returns a new array of the sorted items const sortBy = this.localSortBy const sortDesc = this.localSortDesc const sortCompare = this.sortCompare const localSorting = this.localSorting if (sortBy && localSorting) { + // stableSort returns a new arary, and leaves the original array intact return stableSort(items, (a, b) => { let result = null if (typeof sortCompare === 'function') { @@ -919,14 +948,7 @@ export default { const localPaging = this.localPaging // Apply local pagination if (!!perPage && localPaging) { - // Does the requested page exist? - const maxPages = Math.ceil(items.length / perPage) || 1 - currentPage = Math.max(Math.min(currentPage, maxPages), 1) - if (currentPage !== this.currentPage) { - // send out a .sync update to the currentPage prop - this.$emit('update:currentPage', currentPage) - } - // Grab the current page of data (which may be past filtered items) + // Grab the current page of data (which may be past filtered items limit) return items.slice((currentPage - 1) * perPage, currentPage * perPage) } return items @@ -1069,8 +1091,6 @@ export default { this.localItems = items && items.length > 0 ? items.slice() : [] this.localBusy = false this.$emit('refreshed') - // Deprecated root emit - this.emitOnRoot('table::refreshed', this.id) // New root emit if (this.id) { this.emitOnRoot('bv::table::refreshed', this.id) @@ -1093,9 +1113,6 @@ export default { data.then(items => { // Provider resolved with items this._providerSetLocal(Array.isArray(items) ? items : []) - }, () => { - // Provider rejected with no items - this._providerSetLocal([]) }) } else { // Provider returned Array data From 05ac9f7cf09dc439304c7156eed4b5ba53c5082f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Wed, 7 Nov 2018 22:17:06 -0400 Subject: [PATCH 23/67] next tick needed to prevent test failure on vue.beta --- src/components/table/table.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index efb1f9a7106..7c98cd96f52 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -906,14 +906,16 @@ export default { newIsFiltered = true } } - // Store a reference of the filtered items in data for later use by the emitter - this.filteredItems = filtered - // We set some flags if filtering is active, or if filtering state has changed. + // If filtering has change, we store some data and set sme flags if (newIsFiltered || newIsFiltered !== oldIsFiltered) { - // We toggle this flag to trigger the emit of 'filtered' event - this.filteredTrigger = !this.filteredTrigger - // Set a flag for showing that filtering is active - this.isFiltered = newIsFiltered + this.$nextTick(() => { + // Store a reference of the filtered items in data for later use by the emitter + this.filteredItems = filtered + // We toggle this flag to trigger the emit of 'filtered' event + this.filteredTrigger = !this.filteredTrigger + // Set a flag for showing that filtering is active + this.isFiltered = newIsFiltered + }) } // Return the possibly filtered items return filtered From f823c0b46b83602e38b88be7d84b4020ebe17b07 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 03:10:09 -0400 Subject: [PATCH 24/67] table re-work using watchers Vue beta release has a few changes to reactivity in certain situations (such as setting data inside a computed watcher. --- src/components/table/table.js | 146 ++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 70 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 7c98cd96f52..e507c8dd466 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -10,7 +10,7 @@ import { arrayIncludes, isArray } from '../../utils/array' import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' -// Import styles +// Import styles for busy and stacked import './table.css' // Object of item keys that should be ignored for headers and stringification and filter events @@ -101,6 +101,28 @@ function processField (key, value) { return field } +// Determine if two given arrays are *not* loosely equal +function arraysNotEqual(left = [] , right = []) { + if (left === right) { + // If left reference is equal to the right reference, then they are equal + return false + } else if (left.length === 0 && right.length === 0) { + // If they are both zero length then they are equal + return true + } else if (left.length !== right.length) { + // If they have different lengths then they are definitely not equal + return true + } else { + const equal = left.every((item, index) => { + // We compare left array, row by row, until we find a row that is not equal in the same + // row index in the right array. + // Note: This process can be slow for rather large datasets! + return looseEquals(sanitizeRow(item), sanitizeRow(right[index])) + }) + return !equal + } +} + export default { mixins: [idMixin, listenOnRootMixin], render (h) { @@ -411,17 +433,6 @@ export default { ? h('div', { class: this.responsiveClass }, [table]) : table }, - data () { - return { - localSortBy: this.sortBy || '', - localSortDesc: this.sortDesc || false, - localBusy: false, - localItems: Array.isArray(this.items) ? this.items.slice() : [], - filteredItems: [], - isFiltered: false, - filteredTrigger: false - } - }, props: { items: { type: [Array, Function], @@ -610,20 +621,39 @@ export default { default: '' } }, + data () { + return { + localSortBy: this.sortBy || '', + localSortDesc: this.sortDesc || false, + localBusy: false, + localItems: isArray(this.items) ? this.items : [], + filteredItems: isArray(this.items) ? this.items : [], + isFiltered: false + } + }, watch: { - items (newVal, oldVal) { - if (oldVal !== newVal) { - if (this.hasProvider) { - this._providerUpdate() - } else { - this.localItems = this.items.slice() - } + items (newItems = []) { + if (this.hasProvider || newItems instanceof Function) { + this.$nextTick(this._providerUpdate) + } else if (isArray(newItems)){ + // Set localItems/filteredItems to a copy of the provided array + this.localItems = this.filteredItems = this.newItems.slice() + } else { + this.localItems = this.filteredItems = [] } }, - filteredTrigger (newVal, oldVal) { - // Whenever this value changes state (true=>false, false=>true), we emit a filtered event. - if (newVal !== oldVal) { - this.$emit('filtered', this.filteredItems.slice()) + filteredItems (newItems = [], oldItems = []) { + // These comparisons can be computationally intensive on large datasets! + if (arraysNotEqual(newItems, oldItems) || arraysNotEqual(newItems, this.localItems)) { + this.isFiltered = true + this.$emit('filtered', newItems, newItems.length) + } else { + if (this.isFiltered === true) { + // We need to emit a filtered event if isFiltered transitions to false so that + // users can update their pagination controls. + this.$emit('filtered', newItems, newItems.length) + } + this.isFiltered = false } }, context (newVal, oldVal) { @@ -685,11 +715,13 @@ export default { this.localSortBy = this.sortBy this.localSortDesc = this.sortDesc if (this.hasProvider) { + // Fetch on mount this._providerUpdate() } + // Listen for global messages to tell us to force refresh the table this.listenOnRoot('bv::refresh::table', id => { if (id === this.id || id === this) { - this._providerUpdate() + this.refresh() } }) }, @@ -822,7 +854,7 @@ export default { computedItems () { let items = this.localItems || [] // Grab some values to help trigger reactivity to prop changes, since - // all of the grunt work is done via methods (which arent reactive) + // all of the grunt work is done via methods (which aren't reactive) const sortBy = this.localSortBy const sortDesc = this.localSortDesc const sortCompare = this.sortCompare @@ -833,15 +865,16 @@ export default { if (this.computedBusy) { return items } - // Apply local filter (returns a new array if filtering) + // Apply (optional) local filter (returns a new array if filtering) if (filter) { - items = this.filterItems(items) + // We also set this.filteredItems for triggering filtered events + items = this.filteredItems = this.filterItems(items) } - // Apply local sort (returns a new array if sorting) + // Apply (optional) local sort (returns a new array if sorting) if (sortBy || typeof sortDesc === 'boolean' || sortCompare) { items = this.sortItems(items) } - // Apply local pagination (returns new array if paginating) + // Apply (optional) local pagination (returns new array if paginating) if (currPage || perPage) { items = this.paginateItems(items) } @@ -882,43 +915,11 @@ export default { }, methods: { keys, - filterItems (items) { - if (!this.localFiltering) { - // If the provider is filtering, we just set the state of this.isFiltered based on the - // truthy-ness of the fitler prop, and then return the original items as-is. - // We dont emit filtered events for non-local filtering. - this.isFiltered = Boolean(this.filter) - return items - } - let filtered = items || [] - const oldIsFiltered = this.isFiltered - let newIsFiltered = false - if (!!this.computedFilter && items && items.length) { - filtered = items.filter(this.computedFilter) || [] - // Determine if the filtered items are indeed filtered/different from items - if (filtered.length !== items.length) { - // If the length of the filterd items is not the same as the original items, - // then we flag the table as filtered. We do this test first for performance reasons - newIsFiltered = true - } else if (!(filtered.every((item, idx) => looseEqual(item, items[idx])))) { - // We compare row-by-row until the first loosely non-equal row is found. - // A differing row at some index was found, so we flag the table as filtered. - newIsFiltered = true - } - } - // If filtering has change, we store some data and set sme flags - if (newIsFiltered || newIsFiltered !== oldIsFiltered) { - this.$nextTick(() => { - // Store a reference of the filtered items in data for later use by the emitter - this.filteredItems = filtered - // We toggle this flag to trigger the emit of 'filtered' event - this.filteredTrigger = !this.filteredTrigger - // Set a flag for showing that filtering is active - this.isFiltered = newIsFiltered - }) + filterItems (items = []) { + if (this.localFiltering && !!this.computedFilter && items && items.length) { + return = items.filter(this.computedFilter) || [] } - // Return the possibly filtered items - return filtered + return items }, sortItems (items) { // Sorts the items and returns a new array of the sorted items @@ -1086,11 +1087,13 @@ export default { refresh () { // Expose refresh method if (this.hasProvider) { - this._providerUpdate() + this.$nextTick(this._providerUpdate) + } else { + this.localItems = this.filteredItems = isArray(this.items) ? this.items.slice() : [] } }, _providerSetLocal (items) { - this.localItems = items && items.length > 0 ? items.slice() : [] + this.localItems = this.filteredItems = isArray(items) ? items.slice() : [] this.localBusy = false this.$emit('refreshed') // New root emit @@ -1099,7 +1102,10 @@ export default { } }, _providerUpdate () { - // Refresh the provider items + // Refresh the provider function items. + // This method should be debounced with lodash.debounce to minimize network requests + // with a 100ms default debounce period (i.e. 100ms wait after the last update before + // the new update is called) if (this.computedBusy || !this.hasProvider) { // Don't refresh remote data if we are 'busy' or if no provider return @@ -1114,11 +1120,11 @@ export default { // Provider returned Promise data.then(items => { // Provider resolved with items - this._providerSetLocal(Array.isArray(items) ? items : []) + this._providerSetLocal(items) }) } else { // Provider returned Array data - this._providerSetLocal(Array.isArray(data) ? data : []) + this._providerSetLocal(data) } }) }, From 888bc0adee1a0f1a92203a4be2f7e54883749b6a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 03:18:16 -0400 Subject: [PATCH 25/67] lint --- src/components/table/table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index e507c8dd466..d33f89a6d11 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -916,8 +916,8 @@ export default { methods: { keys, filterItems (items = []) { - if (this.localFiltering && !!this.computedFilter && items && items.length) { - return = items.filter(this.computedFilter) || [] + if (this.localFiltering && !!this.computedFilter && isArray(items) && items.length) { + return items.filter(this.computedFilter) || [] } return items }, From ece5e9eacd4c537cfe2302a3b0669df0d2b63ace Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 03:22:27 -0400 Subject: [PATCH 26/67] lint --- src/components/table/table.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index d33f89a6d11..178cb01aa72 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -102,7 +102,7 @@ function processField (key, value) { } // Determine if two given arrays are *not* loosely equal -function arraysNotEqual(left = [] , right = []) { +function arraysNotEqual (left = [], right = []) { if (left === right) { // If left reference is equal to the right reference, then they are equal return false @@ -115,9 +115,9 @@ function arraysNotEqual(left = [] , right = []) { } else { const equal = left.every((item, index) => { // We compare left array, row by row, until we find a row that is not equal in the same - // row index in the right array. + // row index in the right array. // Note: This process can be slow for rather large datasets! - return looseEquals(sanitizeRow(item), sanitizeRow(right[index])) + return looseEqual(sanitizeRow(item), sanitizeRow(right[index])) }) return !equal } @@ -635,7 +635,7 @@ export default { items (newItems = []) { if (this.hasProvider || newItems instanceof Function) { this.$nextTick(this._providerUpdate) - } else if (isArray(newItems)){ + } else if (isArray(newItems)) { // Set localItems/filteredItems to a copy of the provided array this.localItems = this.filteredItems = this.newItems.slice() } else { From 01954c089e0afebb24e4b31bf4024f9cd3dd9263 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 03:31:24 -0400 Subject: [PATCH 27/67] typo --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 178cb01aa72..e8a63ac3175 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -637,7 +637,7 @@ export default { this.$nextTick(this._providerUpdate) } else if (isArray(newItems)) { // Set localItems/filteredItems to a copy of the provided array - this.localItems = this.filteredItems = this.newItems.slice() + this.localItems = this.filteredItems = newItems.slice() } else { this.localItems = this.filteredItems = [] } From c2f27767b19f38233c44114a361d6e3d20b9ef7b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 04:43:23 -0400 Subject: [PATCH 28/67] Update table.js --- src/components/table/table.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index e8a63ac3175..924e4cc1516 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -642,16 +642,16 @@ export default { this.localItems = this.filteredItems = [] } }, - filteredItems (newItems = [], oldItems = []) { - // These comparisons can be computationally intensive on large datasets! - if (arraysNotEqual(newItems, oldItems) || arraysNotEqual(newItems, this.localItems)) { + filteredCheck ({filteredItems, localItems}) { + // This comparisons can be computationally intensive on large datasets! + if (arraysNotEqual(filteredItems, localItems)) { this.isFiltered = true - this.$emit('filtered', newItems, newItems.length) + this.$emit('filtered', filteredItems, filteredItems.length) } else { if (this.isFiltered === true) { // We need to emit a filtered event if isFiltered transitions to false so that // users can update their pagination controls. - this.$emit('filtered', newItems, newItems.length) + this.$emit('filtered', filteredItems, filteredItems.length) } this.isFiltered = false } @@ -911,6 +911,13 @@ export default { } else { return null } + }, + filteredCheck () { + // For watching changes to filtered items + return { + filteredItems: this.filteredItems, + localItems: this.localItems + } } }, methods: { From 6e3078bcf6b82796ef8f43dc9656e79435aab6af Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 05:06:03 -0400 Subject: [PATCH 29/67] Update table.js --- src/components/table/table.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 924e4cc1516..5da61ad3609 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -642,13 +642,13 @@ export default { this.localItems = this.filteredItems = [] } }, - filteredCheck ({filteredItems, localItems}) { + filteredCheck ({filteredItems, localItems, isFiltered}) { // This comparisons can be computationally intensive on large datasets! if (arraysNotEqual(filteredItems, localItems)) { this.isFiltered = true this.$emit('filtered', filteredItems, filteredItems.length) } else { - if (this.isFiltered === true) { + if (isFiltered === true) { // We need to emit a filtered event if isFiltered transitions to false so that // users can update their pagination controls. this.$emit('filtered', filteredItems, filteredItems.length) @@ -916,7 +916,8 @@ export default { // For watching changes to filtered items return { filteredItems: this.filteredItems, - localItems: this.localItems + localItems: this.localItems, + isFiltered: this.isFiltered } } }, From f57022fe7c2ef46defab1582ef1a5bb7086307f7 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 05:15:40 -0400 Subject: [PATCH 30/67] Update table.js --- src/components/table/table.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/table/table.js b/src/components/table/table.js index 5da61ad3609..f52500c91f2 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -656,6 +656,15 @@ export default { this.isFiltered = false } }, + isFiltered (newVal, oldVal) { + if (newVal === false && oldVal === true) { + // We need to emit a filtered event if isFiltered transitions from true to + // false so that users can update their pagination controls. + this.$nextTick(() => { + this.$emit('filtered', this.filteredItems, this.filteredItems.length) + } + } + }, context (newVal, oldVal) { if (!looseEqual(newVal, oldVal)) { this.$emit('context-changed', newVal) From 1958b7864166fda9290fa332452f2ba04bba0111 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 05:17:56 -0400 Subject: [PATCH 31/67] Update table.js --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index f52500c91f2..002ef920f74 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -662,7 +662,7 @@ export default { // false so that users can update their pagination controls. this.$nextTick(() => { this.$emit('filtered', this.filteredItems, this.filteredItems.length) - } + }) } }, context (newVal, oldVal) { From 9cff40adadf5ad7830177a6cbe28683bd7184dd3 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 05:30:22 -0400 Subject: [PATCH 32/67] Update table.js --- src/components/table/table.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 002ef920f74..e6cf1729204 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -648,11 +648,6 @@ export default { this.isFiltered = true this.$emit('filtered', filteredItems, filteredItems.length) } else { - if (isFiltered === true) { - // We need to emit a filtered event if isFiltered transitions to false so that - // users can update their pagination controls. - this.$emit('filtered', filteredItems, filteredItems.length) - } this.isFiltered = false } }, @@ -660,9 +655,7 @@ export default { if (newVal === false && oldVal === true) { // We need to emit a filtered event if isFiltered transitions from true to // false so that users can update their pagination controls. - this.$nextTick(() => { - this.$emit('filtered', this.filteredItems, this.filteredItems.length) - }) + this.$emit('filtered', this.localItems, this.localItems.length) } }, context (newVal, oldVal) { From 2288c51bd8c94b2f78270887bb5f4ad5241a8bed Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 05:36:18 -0400 Subject: [PATCH 33/67] Update table.js --- src/components/table/table.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index e6cf1729204..3686d5fec65 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -648,7 +648,9 @@ export default { this.isFiltered = true this.$emit('filtered', filteredItems, filteredItems.length) } else { - this.isFiltered = false + this.$nextTick(() => { + this.isFiltered = false + }) } }, isFiltered (newVal, oldVal) { @@ -918,8 +920,7 @@ export default { // For watching changes to filtered items return { filteredItems: this.filteredItems, - localItems: this.localItems, - isFiltered: this.isFiltered + localItems: this.localItems } } }, From c1f9efdee2f6f277029be5f133ea8d6adeec74f5 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 8 Nov 2018 14:09:09 -0400 Subject: [PATCH 34/67] Update table.js --- src/components/table/table.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 3686d5fec65..8ce80278d4a 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -872,8 +872,9 @@ export default { // Apply (optional) local filter (returns a new array if filtering) if (filter) { // We also set this.filteredItems for triggering filtered events - items = this.filteredItems = this.filterItems(items) + items = this.filterItems(items) } + this.filteredItems = items // Apply (optional) local sort (returns a new array if sorting) if (sortBy || typeof sortDesc === 'boolean' || sortCompare) { items = this.sortItems(items) From 0eb8a6c8a890e90980f83c49e531912e993417f7 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Nov 2018 02:52:41 -0400 Subject: [PATCH 35/67] Switched to computed props for filtering, sorting and pagination Also included a few ARIA updates --- src/components/table/table.js | 423 +++++++++++++++++++--------------- 1 file changed, 241 insertions(+), 182 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 8ce80278d4a..4603cbaa59d 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -107,16 +107,18 @@ function arraysNotEqual (left = [], right = []) { // If left reference is equal to the right reference, then they are equal return false } else if (left.length === 0 && right.length === 0) { - // If they are both zero length then they are equal + // If they are both zero length then they are considered equal return true } else if (left.length !== right.length) { // If they have different lengths then they are definitely not equal return true } else { const equal = left.every((item, index) => { - // We compare left array, row by row, until we find a row that is not equal in the same - // row index in the right array. + // We compare left array, row by row, with a row at the same index in the right + // array until we find a row that is not equal at the same row index. // Note: This process can be slow for rather large datasets! + // We try and optimize the usage by targettign the upper conditions if at all + // possible (i.e. setting left and right variables to the same reference when possible) return looseEqual(sanitizeRow(item), sanitizeRow(right[index])) }) return !equal @@ -133,8 +135,14 @@ export default { // Build the caption let caption = h(false) + let captionId = null if (this.caption || $slots['table-caption']) { - const data = { style: this.captionStyles } + captionId = this.isStacked ? this.safeId('_caption_') : null + const data = { + key: 'caption', + id: captionId, + style: this.captionStyles + } if (!$slots['table-caption']) { data.domProps = { innerHTML: stripScripts(this.caption) } } @@ -143,7 +151,7 @@ export default { // Build the colgroup const colgroup = $slots['table-colgroup'] - ? h('colgroup', {}, $slots['table-colgroup']) + ? h('colgroup', { key: 'colgroup' }, $slots['table-colgroup']) : h(false) // factory function for thead and tfoot cells (th's) @@ -201,7 +209,7 @@ export default { let thead = h(false) if (this.isStacked !== true) { // If in always stacked mode (this.isStacked === true), then we don't bother rendering the thead - thead = h('thead', { class: this.headClasses }, [ + thead = h('thead', { key: 'thead', class: this.headClasses }, [ h('tr', { class: this.theadTrClass }, makeHeadCells(false)) ]) } @@ -210,7 +218,7 @@ export default { let tfoot = h(false) if (this.footClone && this.isStacked !== true) { // If in always stacked mode (this.isStacked === true), then we don't bother rendering the tfoot - tfoot = h('tfoot', { class: this.footClasses }, [ + tfoot = h('tfoot', { key: 'tfoot', class: this.footClasses }, [ h('tr', { class: this.tfootTrClass }, makeHeadCells(true)) ]) } @@ -224,7 +232,11 @@ export default { rows.push( h( 'tr', - { key: 'top-row', class: ['b-table-top-row', typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(null, 'row-top') : this.tbodyTrClass] }, + { + key: 'top-row', + staticClass: 'b-table-top-row', + class: [typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(null, 'row-top') : this.tbodyTrClass] + }, [$scoped['top-row']({ columns: fields.length, fields: fields })] ) ) @@ -236,6 +248,7 @@ export default { items.forEach((item, rowIndex) => { const detailsSlot = $scoped['row-details'] const rowShowDetails = Boolean(item._showDetails && detailsSlot) + // Details ID needed for aria-describedby when details showing const detailsId = rowShowDetails ? this.safeId(`_details_${rowIndex}_`) : null @@ -246,6 +259,7 @@ export default { } // For each item data field in row const tds = fields.map((field, colIndex) => { + const formatted = this.getFormattedValue(item, field) const data = { key: `row-${rowIndex}-cell-${colIndex}`, class: this.tdClasses(field, item), @@ -260,7 +274,7 @@ export default { index: rowIndex, field: field, unformatted: get(item, field.key), - value: this.getFormattedValue(item, field), + value: formatted, toggleDetails: toggleDetailsFn, detailsShowing: Boolean(item._showDetails) }) @@ -270,7 +284,6 @@ export default { childNodes = [h('div', {}, [childNodes])] } } else { - const formatted = this.getFormattedValue(item, field) if (this.isStacked) { // We wrap in a DIV to ensure rendered as a single cell when visually stacked! childNodes = [h('div', formatted)] @@ -303,21 +316,11 @@ export default { role: this.isStacked ? 'row' : null }, on: { - click: evt => { - this.rowClicked(evt, item, rowIndex) - }, - contextmenu: evt => { - this.rowContextmenu(evt, item, rowIndex) - }, - dblclick: evt => { - this.rowDblClicked(evt, item, rowIndex) - }, - mouseenter: evt => { - this.rowHovered(evt, item, rowIndex) - }, - mouseleave: evt => { - this.rowUnhovered(evt, item, rowIndex) - } + click: evt => { this.rowClicked(evt, item, rowIndex) }, + contextmenu: evt => { this.rowContextmenu(evt, item, rowIndex) }, + dblclick: evt => { this.rowDblClicked(evt, item, rowIndex) }, + mouseenter: evt => { this.rowHovered(evt, item, rowIndex) }, + mouseleave: evt => { this.rowUnhovered(evt, item, rowIndex) } } }, tds @@ -344,7 +347,8 @@ export default { 'tr', { key: `details-${rowIndex}`, - class: ['b-table-details', typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(item, 'row-details') : this.tbodyTrClass], + staticClass: 'b-table-details', + class: [typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(item, 'row-details') : this.tbodyTrClass], attrs: trAttrs }, [details] @@ -380,7 +384,8 @@ export default { 'tr', { key: 'empty-row', - class: ['b-table-empty-row', typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(null, 'row-empty') : this.tbodyTrClass], + staticClass: 'b-table-empty-row', + class: [typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(null, 'row-empty') : this.tbodyTrClass], attrs: this.isStacked ? { role: 'row' } : {} }, [empty] @@ -396,7 +401,11 @@ export default { rows.push( h( 'tr', - { key: 'bottom-row', class: ['b-table-bottom-row', typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(null, 'row-bottom') : this.tbodyTrClass] }, + { + key: 'bottom-row', + staticClass: 'b-table-bottom-row', + class: [typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(null, 'row-bottom') : this.tbodyTrClass] + }, [$scoped['bottom-row']({ columns: fields.length, fields: fields })] ) ) @@ -415,14 +424,16 @@ export default { const table = h( 'table', { + key: 'b-table', class: this.tableClasses, attrs: { id: this.safeId(), role: this.isStacked ? 'table' : null, + 'aria-describedby': captionId, 'aria-busy': this.computedBusy ? 'true' : 'false', 'aria-colcount': String(fields.length), 'aria-rowcount': this.$attrs['aria-rowcount'] || - this.items.length > this.perPage ? this.items.length : null + this.items.length > this.perPage ? String(this.items.length) : null } }, [caption, colgroup, thead, tfoot, tbody] @@ -430,7 +441,7 @@ export default { // Add responsive wrapper if needed and return table return this.isResponsive - ? h('div', { class: this.responsiveClass }, [table]) + ? h('div', { key: 'responsive-wrap', class: this.responsiveClass }, [table]) : table }, props: { @@ -560,9 +571,23 @@ export default { default: 1 }, filter: { - type: [String, RegExp, Function], + type: [String, RegExp, Object, Array, Function], default: null }, + filterFunction: { + type: Function, + default () { + if (typeof this.filter === 'function') { + // Deprecate setting 'filter' prop to a function + warn( + "b-table: setting prop 'filter' to a function has been deprecated. Use 'filter-function' instead" + ) + return this.filter + } else { + return null + } + } + }, sortCompare: { type: Function, default: null @@ -626,13 +651,15 @@ export default { localSortBy: this.sortBy || '', localSortDesc: this.sortDesc || false, localBusy: false, + // Our local copy of the items. Must be an array localItems: isArray(this.items) ? this.items : [], - filteredItems: isArray(this.items) ? this.items : [], + // Flag for displaying which empty slot to show, and for some event triggering. isFiltered: false } }, watch: { - items (newItems = []) { + // Watch props for changes and update local values + items (newItems) { if (this.hasProvider || newItems instanceof Function) { this.$nextTick(this._providerUpdate) } else if (isArray(newItems)) { @@ -642,76 +669,66 @@ export default { this.localItems = this.filteredItems = [] } }, - filteredCheck ({filteredItems, localItems, isFiltered}) { - // This comparisons can be computationally intensive on large datasets! - if (arraysNotEqual(filteredItems, localItems)) { - this.isFiltered = true - this.$emit('filtered', filteredItems, filteredItems.length) - } else { - this.$nextTick(() => { - this.isFiltered = false - }) - } - }, - isFiltered (newVal, oldVal) { - if (newVal === false && oldVal === true) { - // We need to emit a filtered event if isFiltered transitions from true to - // false so that users can update their pagination controls. - this.$emit('filtered', this.localItems, this.localItems.length) - } - }, - context (newVal, oldVal) { - if (!looseEqual(newVal, oldVal)) { - this.$emit('context-changed', newVal) - } - }, sortDesc (newVal, oldVal) { if (newVal === this.localSortDesc) { return } this.localSortDesc = newVal || false }, - localSortDesc (newVal, oldVal) { - // Emit update to sort-desc.sync - if (newVal !== oldVal) { - this.$emit('update:sortDesc', newVal) - if (!this.noProviderSorting) { - this._providerUpdate() - } - } - }, sortBy (newVal, oldVal) { if (newVal === this.localSortBy) { return } this.localSortBy = newVal || null }, + // Update .sync props + localSortDesc (newVal, oldVal) { + // Emit update to sort-desc.sync + if (newVal !== oldVal) { + this.$emit('update:sortDesc', newVal) + } + }, localSortBy (newVal, oldVal) { if (newVal !== oldVal) { this.$emit('update:sortBy', newVal) - if (!this.noProviderSorting) { - this._providerUpdate() - } } }, - perPage (newVal, oldVal) { - if (oldVal !== newVal && !this.noProviderPaging) { - this._providerUpdate() + localBusy (newVal, oldVal) { + if (newVal !== oldVal) { + this.$emit('update:busy', newVal) + } + }, + // Watch for changes to the filtered items (vs localItems). + // And set visual state and emit events as required + filteredCheck ({filteredItems, localItems}) { + // This comparison can potentially be computationally intensive on large datasets! + // i.e. when a filtered dataset is equal to the entire localItems. + // Large data set users shoud use provider sorting and filtering. + if (arraysNotEqual(filteredItems, localItems)) { + this.isFiltered = true + this.$emit('filtered', filteredItems, filteredItems.length) + } else { + this.isFiltered = false } }, - currentPage (newVal, oldVal) { - if (oldVal !== newVal && !this.noProviderPaging) { - this._providerUpdate() + isFiltered (newVal, oldVal) { + if (newVal === false && oldVal === true) { + // We need to emit a filtered event if isFiltered transitions from true to + // false so that users can update their pagination controls. + this.$emit('filtered', this.localItems, this.localItems.length) } }, - filter (newVal, oldVal) { - if (oldVal !== newVal && !this.noProviderFiltering) { - this._providerUpdate() + context (newVal, oldVal) { + // Emit context info for enternal paging/filtering/sorting handling + if (!looseEqual(newVal, oldVal)) { + this.$emit('context-changed', newVal) } }, - localBusy (newVal, oldVal) { - if (newVal !== oldVal) { - this.$emit('update:busy', newVal) + // Provider update triggering + providerTriggerContext (newVal, oldVal) { + // Trigger the provider to update as the relevant context values have changed. + if (!looseEqual(newVal, oldVal)) { + this.$nextTick(this._providerUpdate) } } }, @@ -730,6 +747,7 @@ export default { }) }, computed: { + // Layout related computed props isStacked () { return this.stacked === '' ? true : this.stacked }, @@ -775,6 +793,7 @@ export default { // Move caption to top return this.captionTop ? { captionSide: 'top' } : {} }, + // Items related computed props hasProvider () { return this.items instanceof Function }, @@ -788,15 +807,38 @@ export default { return this.hasProvider ? this.noProviderPaging : true }, context () { + // Current state of sorting, filtering and pagination return { - perPage: this.perPage, - currentPage: this.currentPage, - filter: this.filter, + filter: typeof this.filter === 'function' ? '' : this.filter, sortBy: this.localSortBy, sortDesc: this.localSortDesc, + perPage: this.perPage, + currentPage: this.currentPage, apiUrl: this.apiUrl } }, + providerTriggerContext () { + // Used to trigger the provider function via a watcher. + // The regular this.context is sent to the provider during fetches + const ctx = { + apiUrl: this.apiUrl + } + if (!this.noProviderFiltering) { + ctx.filter = typeof this.filter === 'function' : '' : this.filter + } + if (!this.noProviderSorting) { + ctx.sortBy = this.localSortBy + ctx.sortDesc = this.localSortDesc + } + if (!this.noProviderPaging) { + ctx.perPage = this.perPage + ctx.currentPage = this.currentPage + } + return ctx + }, + computedBusy () { + return this.busy || this.localBusy + }, computedFields () { // We normalize fields into an array of objects // [ { key:..., label:..., ...}, {...}, ..., {..}] @@ -836,8 +878,8 @@ export default { }) } // If no field provided, take a sample from first record (if exits) - if (fields.length === 0 && this.computedItems.length > 0) { - const sample = this.computedItems[0] + if (fields.length === 0 && this.localItems.length > 0) { + const sample = this.localItems[0] keys(sample).forEach(k => { if (!IGNORED_FIELD_KEYS[k]) { fields.push({ key: k, label: startCase(k) }) @@ -855,86 +897,87 @@ export default { return false }) }, - computedItems () { - let items = this.localItems || [] - // Grab some values to help trigger reactivity to prop changes, since - // all of the grunt work is done via methods (which aren't reactive) - const sortBy = this.localSortBy - const sortDesc = this.localSortDesc - const sortCompare = this.sortCompare - const filter = this.computedFilter - const currPage = this.currentPage - const perPage = this.perPage - // If table is busy, just return the current localItems - if (this.computedBusy) { - return items - } - // Apply (optional) local filter (returns a new array if filtering) - if (filter) { - // We also set this.filteredItems for triggering filtered events - items = this.filterItems(items) - } - this.filteredItems = items - // Apply (optional) local sort (returns a new array if sorting) - if (sortBy || typeof sortDesc === 'boolean' || sortCompare) { - items = this.sortItems(items) + computedFilterFn () { + // Factory for building filter functions. + // We do this as a computed prop so that we can be reactive to the filterFn. + let criteria = this.filter + let filterFn = this.filterFunction + // Handle deprecation of passing function to prop filter + if (typeof criteria === 'function') { + filterFn = criteria + criteria = '' } - // Apply (optional) local pagination (returns new array if paginating) - if (currPage || perPage) { - items = this.paginateItems(items) - } - // Update the value model with the filtered/sorted/paginated data set. - this.$emit('input', items) - // send the (possibly) updated items to the table. - return items - }, - computedBusy () { - return this.busy || this.localBusy - }, - computedFilter () { - if (this.filter instanceof Function) { - // filter the items array using the function set in the filter prop - // Wrap it in a function so we can prevent the filter function from mutating the top level data + if (typeof filterFn === 'function') { + // If we have a user supplied filter function, we use it. + // We wrap the filter function inside an anonymous function + // to pass context and to trigger reactive changes when using + // the 'filter-functon' prop in combination with the the 'filter' prop. return (item) => { - return this.filter(assign({}, item)) + // We provide the filter function a shallow copy of the item, and pass a + // second argument which is the filter criteria (value of the filter prop) + // passed as an object (as we may add more to this object in the future) + // Criteria (i.e. this.filter when not a function), could be a string, + // array, object or regex. It is up to the + return filterFn(assign({}, item), { filter: criteria }) + } + } else if (criteria) { + // Handle filter criteria when no filter-function specified + if (!filterFn && typeof criteria !== 'string' && !(criteria instanceof RegExp)) { + // Currently we can't use an object (or array) when using the built-in filter + criteria = '' } - } else if (this.filter) { + // Start building a filter function based on the string or regex passed let regex = null - if (this.filter instanceof RegExp) { - regex = this.filter + if (criteria instanceof RegExp) { + regex = criteria } else { // Escape special RegExp characters in the string - const string = this.filter.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + const string = criteria + .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + .replace(/\s+/g, '\\s+') + // Build the RegExp regex = new RegExp(`.*${string}.*`, 'ig') } - // return a filter function + // Return the generated filter function return (item) => { + // This searches the entire row values (excluding special _ prefixed keys), + // even ones that are not visible (not specified in this.fields). + // TODO: enable searching on formatted fields and scoped slots + // TODO: allow for searching on specific fields/key const test = regex.test(recToString(item)) regex.lastIndex = 0 return test } } else { + // no filtering return null } }, filteredCheck () { - // For watching changes to filtered items + // For watching changes to filteredItems vs localItems return { filteredItems: this.filteredItems, localItems: this.localItems } - } - }, - methods: { - keys, - filterItems (items = []) { - if (this.localFiltering && !!this.computedFilter && isArray(items) && items.length) { - return items.filter(this.computedFilter) || [] + }, + filteredItems () { + // Returns the records in localItems that match the filter criteria + let items = this.localItems || [] + const filter = this.computedFilterFn + items = isArray(items) ? items : [] + // If table is busy, just return the current localItems + if (this.computedBusy) { + return items + } + if (this.localFiltering && !!this.computedFilterFn && items.length) { + items = items.filter(this.computedFilterFn) || [] } return items }, - sortItems (items) { - // Sorts the items and returns a new array of the sorted items + sortedItems () { + // Sorts the filtered items and returns a new array of the sorted items + // or the original items array if not sorted. + let items = this.filteredItems const sortBy = this.localSortBy const sortDesc = this.localSortDesc const sortCompare = this.sortCompare @@ -947,8 +990,8 @@ export default { // Call user provided sortCompare routine result = sortCompare(a, b, sortBy, sortDesc) } - if (result === null || result === undefined) { - // Fallback to defaultSortCompare if sortCompare not defined or returns null + if (result === null || result === undefined || result === false) { + // Fallback to built-in defaultSortCompare if sortCompare not defined or returns null/false result = defaultSortCompare(a, b, sortBy) } // Negate result if sorting in descending order @@ -957,17 +1000,29 @@ export default { } return items }, - paginateItems (items) { - let currentPage = this.currentPage - const perPage = this.perPage - const localPaging = this.localPaging + paginatedItems () { + let items = this.sortedItems || [] + const currentPage = Math.max(parseInt(this.currentPage, 10) || 1, 1) + const perPage = Math.max(parseInt(this.perPage, 10) || 0, 0) + const maxPages = Math.max(Math.ceil(items.length / perPage), 1) // Apply local pagination - if (!!perPage && localPaging) { + if (this.localPaging && !!perPage) { // Grab the current page of data (which may be past filtered items limit) - return items.slice((currentPage - 1) * perPage, currentPage * perPage) + items = items.slice((currentPage - 1) * perPage, currentPage * perPage) } + // update the v-model view + this.$emit('input', items) + // Return the items to display return items }, + computedItems () { + reutrn this.paginatedItems || [] + } + }, + methods: { + // Convenience method for render function (old SFC templates) + keys, + // Methods for computing classes, attributes and styles for table cells fieldClasses (field) { return [ field.sortable ? 'sorting' : '', @@ -1017,6 +1072,39 @@ export default { typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(item, 'row') : this.tbodyTrClass ] }, + getTdValues (item, key, tdValue, defValue) { + const parent = this.$parent + if (tdValue) { + if (typeof tdValue === 'function') { + let value = get(item, key) + return tdValue(value, key, item) + } else if (typeof tdValue === 'string' && typeof parent[tdValue] === 'function') { + let value = get(item, key) + return parent[tdValue](value, key, item) + } + return tdValue + } + return defValue + }, + // Method to get the value for a field + getFormattedValue (item, field) { + const key = field.key + const formatter = field.formatter + const parent = this.$parent + let value = get(item, key) + if (formatter) { + if (typeof formatter === 'function') { + value = formatter(value, key, item) + } else if ( + typeof formatter === 'string' && + typeof parent[formatter] === 'function' + ) { + value = parent[formatter](value, key, item) + } + } + return (value === null || typeof value === 'undefined') ? '' : value + }, + // Event handlers rowClicked (e, item, index) { if (this.stopIfBusy(e)) { // If table is busy (via provider) then don't propagate @@ -1096,6 +1184,7 @@ export default { } return false }, + // Exposed method(s) refresh () { // Expose refresh method if (this.hasProvider) { @@ -1104,6 +1193,7 @@ export default { this.localItems = this.filteredItems = isArray(this.items) ? this.items.slice() : [] } }, + // Provider related methods _providerSetLocal (items) { this.localItems = this.filteredItems = isArray(items) ? items.slice() : [] this.localBusy = false @@ -1115,9 +1205,9 @@ export default { }, _providerUpdate () { // Refresh the provider function items. - // This method should be debounced with lodash.debounce to minimize network requests - // with a 100ms default debounce period (i.e. 100ms wait after the last update before - // the new update is called) + // TODO: this method should be debounced with lodash.debounce to minimize network requests, + // with a 100ms default debounce period (i.e. 100ms holdtime after the last update before + // the new update is called). Debounce period should be a prop if (this.computedBusy || !this.hasProvider) { // Don't refresh remote data if we are 'busy' or if no provider return @@ -1139,37 +1229,6 @@ export default { this._providerSetLocal(data) } }) - }, - getTdValues (item, key, tdValue, defValue) { - const parent = this.$parent - if (tdValue) { - if (typeof tdValue === 'function') { - let value = get(item, key) - return tdValue(value, key, item) - } else if (typeof tdValue === 'string' && typeof parent[tdValue] === 'function') { - let value = get(item, key) - return parent[tdValue](value, key, item) - } - return tdValue - } - return defValue - }, - getFormattedValue (item, field) { - const key = field.key - const formatter = field.formatter - const parent = this.$parent - let value = get(item, key) - if (formatter) { - if (typeof formatter === 'function') { - value = formatter(value, key, item) - } else if ( - typeof formatter === 'string' && - typeof parent[formatter] === 'function' - ) { - value = parent[formatter](value, key, item) - } - } - return (value === null || typeof value === 'undefined') ? '' : value } } } From 9e42bba5183ef48506e3e945d74a919cbde6dc0b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Nov 2018 03:02:59 -0400 Subject: [PATCH 36/67] lint --- src/components/table/table.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 4603cbaa59d..95f1a721fcc 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -819,12 +819,13 @@ export default { }, providerTriggerContext () { // Used to trigger the provider function via a watcher. + // Only the fields that are needed for triggering an update are included. // The regular this.context is sent to the provider during fetches const ctx = { apiUrl: this.apiUrl } if (!this.noProviderFiltering) { - ctx.filter = typeof this.filter === 'function' : '' : this.filter + ctx.filter = typeof this.filter === 'function' ? '' : this.filter } if (!this.noProviderSorting) { ctx.sortBy = this.localSortBy From 96422c7b7472b2ba42ffb6e0e6c187479dd4b22a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Nov 2018 03:06:07 -0400 Subject: [PATCH 37/67] lint --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 95f1a721fcc..57650ce9d5b 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1017,7 +1017,7 @@ export default { return items }, computedItems () { - reutrn this.paginatedItems || [] + return this.paginatedItems || [] } }, methods: { From 5d589246151cac02a2db9ba310fee264dd574be0 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Nov 2018 03:34:44 -0400 Subject: [PATCH 38/67] Update table.js --- src/components/table/table.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 57650ce9d5b..1db0f84b623 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -366,7 +366,7 @@ export default { if (!empty) { empty = h('div', { class: ['text-center', 'my-2'], - domProps: { innerHTML: stripScripts(this.filter ? this.emptyFilteredText : this.emptyText) } + domProps: { innerHTML: stripScripts(this.isFiltered ? this.emptyFilteredText : this.emptyText) } }) } empty = h( @@ -965,13 +965,10 @@ export default { // Returns the records in localItems that match the filter criteria let items = this.localItems || [] const filter = this.computedFilterFn + const localFiltering = this.localFiltering items = isArray(items) ? items : [] - // If table is busy, just return the current localItems - if (this.computedBusy) { - return items - } - if (this.localFiltering && !!this.computedFilterFn && items.length) { - items = items.filter(this.computedFilterFn) || [] + if (localFiltering && !!filter && items.length) { + items = items.filter(filter) || [] } return items }, @@ -1005,7 +1002,6 @@ export default { let items = this.sortedItems || [] const currentPage = Math.max(parseInt(this.currentPage, 10) || 1, 1) const perPage = Math.max(parseInt(this.perPage, 10) || 0, 0) - const maxPages = Math.max(Math.ceil(items.length / perPage), 1) // Apply local pagination if (this.localPaging && !!perPage) { // Grab the current page of data (which may be past filtered items limit) @@ -1013,7 +1009,7 @@ export default { } // update the v-model view this.$emit('input', items) - // Return the items to display + // Return the items to display in the table return items }, computedItems () { From 043413f5cbc678d0b78b7968049f459406d0bd3b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Nov 2018 04:12:52 -0400 Subject: [PATCH 39/67] Update table.js --- src/components/table/table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 1db0f84b623..ce935df061e 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -577,7 +577,7 @@ export default { filterFunction: { type: Function, default () { - if (typeof this.filter === 'function') { + if (this && typeof this.filter === 'function') { // Deprecate setting 'filter' prop to a function warn( "b-table: setting prop 'filter' to a function has been deprecated. Use 'filter-function' instead" @@ -975,7 +975,7 @@ export default { sortedItems () { // Sorts the filtered items and returns a new array of the sorted items // or the original items array if not sorted. - let items = this.filteredItems + let items = this.filteredItems || [] const sortBy = this.localSortBy const sortDesc = this.localSortDesc const sortCompare = this.sortCompare From 2ae4cf4f94eb2421790608fbf589af94ed2daff8 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Nov 2018 04:17:49 -0400 Subject: [PATCH 40/67] Update table.js --- src/components/table/table.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index ce935df061e..b6347e0a0ee 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -664,9 +664,9 @@ export default { this.$nextTick(this._providerUpdate) } else if (isArray(newItems)) { // Set localItems/filteredItems to a copy of the provided array - this.localItems = this.filteredItems = newItems.slice() + this.localItems = newItems.slice() } else { - this.localItems = this.filteredItems = [] + this.localItems = [] } }, sortDesc (newVal, oldVal) { @@ -1187,12 +1187,12 @@ export default { if (this.hasProvider) { this.$nextTick(this._providerUpdate) } else { - this.localItems = this.filteredItems = isArray(this.items) ? this.items.slice() : [] + this.localItems = isArray(this.items) ? this.items.slice() : [] } }, // Provider related methods _providerSetLocal (items) { - this.localItems = this.filteredItems = isArray(items) ? items.slice() : [] + this.localItems = isArray(items) ? items.slice() : [] this.localBusy = false this.$emit('refreshed') // New root emit From 4bd512e604fc9605319b8767bf8b1741e33a5baa Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Nov 2018 04:57:05 -0400 Subject: [PATCH 41/67] Update table.js --- src/components/table/table.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index b6347e0a0ee..bf7619bd0d7 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -425,6 +425,7 @@ export default { 'table', { key: 'b-table', + staticClass: ['table', 'b-table'], class: this.tableClasses, attrs: { id: this.safeId(), @@ -652,7 +653,7 @@ export default { localSortDesc: this.sortDesc || false, localBusy: false, // Our local copy of the items. Must be an array - localItems: isArray(this.items) ? this.items : [], + localItems: isArray(this.items) ? this.items.slice() : [], // Flag for displaying which empty slot to show, and for some event triggering. isFiltered: false } @@ -738,6 +739,8 @@ export default { if (this.hasProvider) { // Fetch on mount this._providerUpdate() + } else { + this.localItems = isArray(this.items) ? this.items.slice() : [] } // Listen for global messages to tell us to force refresh the table this.listenOnRoot('bv::refresh::table', id => { @@ -762,8 +765,6 @@ export default { }, tableClasses () { return [ - 'table', - 'b-table', this.striped ? 'table-striped' : '', this.hover ? 'table-hover' : '', this.dark ? 'table-dark' : '', @@ -966,8 +967,7 @@ export default { let items = this.localItems || [] const filter = this.computedFilterFn const localFiltering = this.localFiltering - items = isArray(items) ? items : [] - if (localFiltering && !!filter && items.length) { + if (localFiltering && !!filter && isArray(items) && items.length) { items = items.filter(filter) || [] } return items @@ -1017,8 +1017,6 @@ export default { } }, methods: { - // Convenience method for render function (old SFC templates) - keys, // Methods for computing classes, attributes and styles for table cells fieldClasses (field) { return [ From a2702c3d1df25893489bdc8f7ff88203c51369c2 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 00:14:25 -0400 Subject: [PATCH 42/67] fix filtering and optimize a few functions --- src/components/table/table.js | 373 ++++++++++++++++++++-------------- 1 file changed, 217 insertions(+), 156 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index bf7619bd0d7..46461347a12 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1,5 +1,5 @@ -import startCase from 'lodash.startcase' -import get from 'lodash.get' +import _startCase from 'lodash.startcase' +import _get from 'lodash.get' import looseEqual from '../../utils/loose-equal' import stableSort from '../../utils/stable-sort' import KeyCodes from '../../utils/key-codes' @@ -21,6 +21,7 @@ const IGNORED_FIELD_KEYS = { } // Return a copy of a row after all reserved fields have been filtered out +// TODO: add option to specify which fields to include function sanitizeRow (row) { return keys(row).reduce((obj, key) => { // Ignore special fields that start with _ @@ -40,7 +41,7 @@ function toString (v) { return '' } if (v instanceof Object) { - // Arrays are also object, and keys just returns the array + // Arrays are also object, and keys just returns the array indexes return keys(v) .sort() /* sort to prevent SSR issues on pre-rendered sorted tables */ .map(k => toString(v[k])) @@ -49,7 +50,8 @@ function toString (v) { return String(v) } -// Stringifies the values of a record, ignoring any special top level keys +// Stringifies the values of a record, ignoring any special top level field keys +// TODO: add option to strigify formatted/scopedSlot items, and only specific fields function recToString (row) { if (!(row instanceof Object)) { return '' @@ -57,15 +59,11 @@ function recToString (row) { return toString(sanitizeRow(row)) } +// Default sort compare routine +// TODO: add option to sort by multiple columns function defaultSortCompare (a, b, sortBy) { - if (sortBy.indexOf('.') < 0) { - return sort(a[sortBy], b[sortBy]) - } else { - return sort(getNestedValue(a, sortBy), getNestedValue(b, sortBy)) - } -} - -function sort (a, b) { + a = _get(a, sortBy, '') + b = _get(b, sortBy, '') if (typeof a === 'number' && typeof b === 'number') { return (a < b && -1) || (a > b && 1) || 0 } @@ -74,15 +72,29 @@ function sort (a, b) { }) } +// Helper function to determine if a value is defined +function isDefined (value) { + retutrn typeof value !== 'undefined' +} + +// Returns a value from an object using doted string notation. +// If no value found at pathReturns an empty string if no value is found function getNestedValue (obj, path) { + // Split the path by the dots path = path.split('.') - var value = obj - for (var i = 0; i < path.length; i++) { + // Sanity check!! + if (path.length !== path.filter(p => p).length) { + // return empty string if too many dots (malformed path) + return '' + } + let value = obj + for (let i = 0; i < path.length && isDefined(value); i++) { value = value[path[i]] } - return value + return isDefined(value) ? value : '' } +// Helper function to massage field entry into common object format function processField (key, value) { let field = null if (typeof value === 'string') { @@ -114,17 +126,18 @@ function arraysNotEqual (left = [], right = []) { return true } else { const equal = left.every((item, index) => { - // We compare left array, row by row, with a row at the same index in the right - // array until we find a row that is not equal at the same row index. + // We compare left array with the right array, row by row, until we find + // a row that is not equal at the same row index. // Note: This process can be slow for rather large datasets! - // We try and optimize the usage by targettign the upper conditions if at all - // possible (i.e. setting left and right variables to the same reference when possible) + // We try and optimize the usage by targetting the upper conditions first if at all + // possible (i.e. setting left and right arrays to the same reference when possible) return looseEqual(sanitizeRow(item), sanitizeRow(right[index])) }) return !equal } } +// b-table component definition export default { mixins: [idMixin, listenOnRootMixin], render (h) { @@ -298,7 +311,7 @@ export default { // Calculate the row number in the dataset (indexed from 1) let ariaRowIndex = null if (this.currentPage && this.perPage && this.perPage > 0) { - ariaRowIndex = (this.currentPage - 1) * this.perPage + rowIndex + 1 + ariaRowIndex = String((this.currentPage - 1) * this.perPage + rowIndex + 1) } // Assemble and add the row rows.push( @@ -312,6 +325,7 @@ export default { ], attrs: { 'aria-describedby': detailsId, + 'aria-owns': detailsId, 'aria-rowindex': ariaRowIndex, role: this.isStacked ? 'row' : null }, @@ -425,16 +439,18 @@ export default { 'table', { key: 'b-table', - staticClass: ['table', 'b-table'], + staticClass: 'table b-table', class: this.tableClasses, attrs: { + ...this.$attrs, id: this.safeId(), role: this.isStacked ? 'table' : null, 'aria-describedby': captionId, 'aria-busy': this.computedBusy ? 'true' : 'false', 'aria-colcount': String(fields.length), - 'aria-rowcount': this.$attrs['aria-rowcount'] || - this.items.length > this.perPage ? String(this.items.length) : null + // Only set aria-rowcount if provided in $attrs or if localItems > shown items + 'aria-rowcount': this.$attrs['aria-rowcount'] || (this.filteredItems.length > items.length) ? + String(this.filteredItems.length) : null } }, [caption, colgroup, thead, tfoot, tbody] @@ -442,7 +458,7 @@ export default { // Add responsive wrapper if needed and return table return this.isResponsive - ? h('div', { key: 'responsive-wrap', class: this.responsiveClass }, [table]) + ? h('div', { key: 'b-table-responsive', class: this.responsiveClass }, [table]) : table }, props: { @@ -577,17 +593,7 @@ export default { }, filterFunction: { type: Function, - default () { - if (this && typeof this.filter === 'function') { - // Deprecate setting 'filter' prop to a function - warn( - "b-table: setting prop 'filter' to a function has been deprecated. Use 'filter-function' instead" - ) - return this.filter - } else { - return null - } - } + default null }, sortCompare: { type: Function, @@ -618,8 +624,11 @@ export default { default: false }, value: { + // v-model for retreiving the current displayed rows type: Array, - default: () => [] + default () { + return [] + } }, labelSortAsc: { type: String, @@ -652,12 +661,26 @@ export default { localSortBy: this.sortBy || '', localSortDesc: this.sortDesc || false, localBusy: false, - // Our local copy of the items. Must be an array - localItems: isArray(this.items) ? this.items.slice() : [], + // Our local copy of the items. Must be an array + localItems: isArray(this.items) ? this.items : [], // Flag for displaying which empty slot to show, and for some event triggering. isFiltered: false } }, + mounted () { + this.localSortBy = this.sortBy + this.localSortDesc = this.sortDesc + if (this.hasProvider && (!this.localItems || this.localItems.length === 0)) { + // Fetch on mount if localItems is empty + this._providerUpdate() + } + // Listen for global messages to tell us to force refresh the table + this.listenOnRoot('bv::refresh::table', id => { + if (id === this.id || id === this) { + this.refresh() + } + }) + }, watch: { // Watch props for changes and update local values items (newItems) { @@ -699,13 +722,13 @@ export default { this.$emit('update:busy', newVal) } }, - // Watch for changes to the filtered items (vs localItems). + // Watch for changes to the filtered items vs localItems). // And set visual state and emit events as required - filteredCheck ({filteredItems, localItems}) { + filteredCheck ({filteredItems, localItems, localFilter}) { // This comparison can potentially be computationally intensive on large datasets! // i.e. when a filtered dataset is equal to the entire localItems. // Large data set users shoud use provider sorting and filtering. - if (arraysNotEqual(filteredItems, localItems)) { + if (arraysNotEqual(filteredItems, localItems) || !!localFilter) { this.isFiltered = true this.$emit('filtered', filteredItems, filteredItems.length) } else { @@ -733,22 +756,6 @@ export default { } } }, - mounted () { - this.localSortBy = this.sortBy - this.localSortDesc = this.sortDesc - if (this.hasProvider) { - // Fetch on mount - this._providerUpdate() - } else { - this.localItems = isArray(this.items) ? this.items.slice() : [] - } - // Listen for global messages to tell us to force refresh the table - this.listenOnRoot('bv::refresh::table', id => { - if (id === this.id || id === this) { - this.refresh() - } - }) - }, computed: { // Layout related computed props isStacked () { @@ -764,18 +771,18 @@ export default { : this.isResponsive ? `table-responsive-${this.responsive}` : '' }, tableClasses () { - return [ - this.striped ? 'table-striped' : '', - this.hover ? 'table-hover' : '', - this.dark ? 'table-dark' : '', - this.bordered ? 'table-bordered' : '', - this.small ? 'table-sm' : '', - this.outlined ? 'border' : '', - this.fixed ? 'b-table-fixed' : '', - this.isStacked === true - ? 'b-table-stacked' - : this.isStacked ? `b-table-stacked-${this.stacked}` : '' - ] + return { + 'table-striped': this.striped, + 'table-hover': this.hover, + 'table-dark': this.dark, + 'table-bordered': this.bordered, + 'table-sm': this.small, + 'border': this.outlined, + // The following are b-table custom styles + 'b-table-fixed': this.fixed, + 'b-table-stacked': this.stacked === true || this.stacked === '', + [`b-table-stacked-${this.stacked}`]: this.stacked !== true && this.stacked + } }, headClasses () { return [ @@ -799,18 +806,18 @@ export default { return this.items instanceof Function }, localFiltering () { - return this.hasProvider ? this.noProviderFiltering : true + return this.hasProvider ? !!this.noProviderFiltering : true }, localSorting () { - return this.hasProvider ? this.noProviderSorting : !this.noLocalSorting + return this.hasProvider ? !!this.noProviderSorting : !this.noLocalSorting }, localPaging () { - return this.hasProvider ? this.noProviderPaging : true + return this.hasProvider ? !!this.noProviderPaging : true }, context () { - // Current state of sorting, filtering and pagination + // Current state of sorting, filtering and pagination props/values return { - filter: typeof this.filter === 'function' ? '' : this.filter, + filter: this.localFilter, sortBy: this.localSortBy, sortDesc: this.localSortDesc, perPage: this.perPage, @@ -819,14 +826,16 @@ export default { } }, providerTriggerContext () { - // Used to trigger the provider function via a watcher. - // Only the fields that are needed for triggering an update are included. - // The regular this.context is sent to the provider during fetches + // Used to trigger the provider function via a watcher. Only the fields that + // are needed for triggering a provider update are included. Note that the + // regular this.context is sent to the provider during fetches though, as they + // may neeed all the prop info. const ctx = { apiUrl: this.apiUrl } if (!this.noProviderFiltering) { - ctx.filter = typeof this.filter === 'function' ? '' : this.filter + // Either a string, or could be an object or array. + ctx.filter = this.localFilter } if (!this.noProviderSorting) { ctx.sortBy = this.localSortBy @@ -849,7 +858,7 @@ export default { // Normalize array Form this.fields.filter(f => f).forEach(f => { if (typeof f === 'string') { - fields.push({ key: f, label: startCase(f) }) + fields.push({ key: f, label: _startCase(f) }) } else if ( typeof f === 'object' && f.key && @@ -884,7 +893,7 @@ export default { const sample = this.localItems[0] keys(sample).forEach(k => { if (!IGNORED_FIELD_KEYS[k]) { - fields.push({ key: k, label: startCase(k) }) + fields.push({ key: k, label: _startCase(k) }) } }) } @@ -893,82 +902,63 @@ export default { return fields.filter(f => { if (!memo[f.key]) { memo[f.key] = true - f.label = typeof f.label === 'string' ? f.label : startCase(f.key) + f.label = typeof f.label === 'string' ? f.label : _startCase(f.key) return true } return false }) }, - computedFilterFn () { - // Factory for building filter functions. - // We do this as a computed prop so that we can be reactive to the filterFn. - let criteria = this.filter - let filterFn = this.filterFunction - // Handle deprecation of passing function to prop filter - if (typeof criteria === 'function') { - filterFn = criteria - criteria = '' - } - if (typeof filterFn === 'function') { - // If we have a user supplied filter function, we use it. - // We wrap the filter function inside an anonymous function - // to pass context and to trigger reactive changes when using - // the 'filter-functon' prop in combination with the the 'filter' prop. - return (item) => { - // We provide the filter function a shallow copy of the item, and pass a - // second argument which is the filter criteria (value of the filter prop) - // passed as an object (as we may add more to this object in the future) - // Criteria (i.e. this.filter when not a function), could be a string, - // array, object or regex. It is up to the - return filterFn(assign({}, item), { filter: criteria }) - } - } else if (criteria) { - // Handle filter criteria when no filter-function specified - if (!filterFn && typeof criteria !== 'string' && !(criteria instanceof RegExp)) { - // Currently we can't use an object (or array) when using the built-in filter - criteria = '' - } - // Start building a filter function based on the string or regex passed - let regex = null - if (criteria instanceof RegExp) { - regex = criteria - } else { - // Escape special RegExp characters in the string - const string = criteria - .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') - .replace(/\s+/g, '\\s+') - // Build the RegExp - regex = new RegExp(`.*${string}.*`, 'ig') - } - // Return the generated filter function - return (item) => { - // This searches the entire row values (excluding special _ prefixed keys), - // even ones that are not visible (not specified in this.fields). - // TODO: enable searching on formatted fields and scoped slots - // TODO: allow for searching on specific fields/key - const test = regex.test(recToString(item)) - regex.lastIndex = 0 - return test - } - } else { - // no filtering - return null - } - }, filteredCheck () { // For watching changes to filteredItems vs localItems return { filteredItems: this.filteredItems, - localItems: this.localItems + localItems: this.localItems, + localFilter: this.localFilter + } + }, + localFilter () { + // Returns a sanitized/normalized version of filter prop + if (typeof this.filter === 'function') { + // this.localFilterFn will contain the correct function ref. + // Deprecate setting prop filter to a function + return '' + } else if (typeof this.filterFunction !== 'function') && + !(typeof this.filter === 'string' || this.filter instanceof RegExp) { + // Using internal filter function, which only acccepts string or regexp at the moment + return '' + } else { + // Could be astring, object or array, as needed by external filter function + return this.filter + } + }, + localFilterFn () { + let filter = this.filter + let filterFn = this.filterFunction + + // Sanitized/normalize filter-function prop + if typeof filterFn === 'funtion') { + return filterFn + } else if (typeof filter === 'function') { + // Deprecate setting prop filter to a function + return = filter + } else { + // no filterFunction, so signal to use internal filter function + return null } }, filteredItems () { - // Returns the records in localItems that match the filter criteria + // Returns the records in localItems that match the filter criteria. + // Returns the original localItems array if not sorting let items = this.localItems || [] - const filter = this.computedFilterFn - const localFiltering = this.localFiltering - if (localFiltering && !!filter && isArray(items) && items.length) { - items = items.filter(filter) || [] + const criteria = this.localFilter + const filterFn = + this.filterFnFactory(this.localFilterFn, criteria) || + this.defaultFilterFnFactory(criteria) + + // We only do local filtering if requested, and if the are records to filter and + // if a filter criteria was specified + if (this.localFiltering && !!filterFn && !!criteria && isArray[items] && items.length > 0) { + items = items.filter(filterFn) } return items }, @@ -1071,10 +1061,10 @@ export default { const parent = this.$parent if (tdValue) { if (typeof tdValue === 'function') { - let value = get(item, key) + let value = _get(item, key) return tdValue(value, key, item) } else if (typeof tdValue === 'string' && typeof parent[tdValue] === 'function') { - let value = get(item, key) + let value = _get(item, key) return parent[tdValue](value, key, item) } return tdValue @@ -1086,7 +1076,7 @@ export default { const key = field.key const formatter = field.formatter const parent = this.$parent - let value = get(item, key) + let value = _get(item, key) if (formatter) { if (typeof formatter === 'function') { value = formatter(value, key, item) @@ -1099,6 +1089,58 @@ export default { } return (value === null || typeof value === 'undefined') ? '' : value }, + // Filter Function factories + filterFnFactory (filterFn, criteria) { + // Wrap the provided filter-function and return a new function. + // returns null if no filter-function defined or if criteria is falsey. + // Rather than directly grabbing this.computedLocalFilterFn or this.filterFunction + // We have it passed, so that the caller computed prop will be reactive to changes + // in the original filter-function (as this routine is a method) + if (!filterFn || !criteria || typeof filterFn !== 'function') { + return null + } + + // Return the wrapped filter test function + return (item) => { + // Generated function returns true if the crieria matches part of the serialzed data, otherwise false + return filterFn(item, criteria) + } + }, + defaultFilterFnFactory (criteria) { + // Generates the default filter function, using the given criteria + if (!criteria || !(typeof criteria === 'string' || criteria instanceof RegExp)) { + return null + } + + // Build the regexp needed for filtering + let regex = criteria + if (tyepof regex === 'string') { + // Escape special RegExp characters in the string and convert contiguous + // whitespace to \s+ matches + const string = criteria + .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + .replace(/[\s\uFEFF\xA0]+/g, '\\s+') + // Build the RegExp (no need for global flag, as we only need to find the value once in the string) + regex = new RegExp(`.*${string}.*`, 'i') + } + + // Return the generated filter test function + return (item) => { + // This searches all row values (and sub property values) in the entire (excluding + // special _ prefixed keys), because we convert the record to a space-separated + // string containing all the value properties (recursively), even ones that are + // not visible (not specified in this.fields). + // + // TODO: enable searching on formatted fields and scoped slots + // TODO: should we filter only on visible fields (i.e. ones in this.fields) by default? + // TODO: allow for searching on specific fields/key, this could be combined with the previous TODO + // TODO: give recToString extra options for filtering (i.e. passing the fields definition + // and a reference to $scopedSlots) + // + // Generated function returns true if the crieria matches part of the serialzed data, otherwise false + return regex.test(recToString(item)) + } + }, // Event handlers rowClicked (e, item, index) { if (this.stopIfBusy(e)) { @@ -1182,11 +1224,16 @@ export default { // Exposed method(s) refresh () { // Expose refresh method + if (this.computedBusy) { + // Can't force an update when busy + return false + } if (this.hasProvider) { this.$nextTick(this._providerUpdate) } else { this.localItems = isArray(this.items) ? this.items.slice() : [] } + return true }, // Provider related methods _providerSetLocal (items) { @@ -1212,16 +1259,30 @@ export default { // Call provider function with context and optional callback after DOM is fully updated this.$nextTick(function () { - const data = this.items(this.context, this._providerSetLocal) - if (data && data.then && typeof data.then === 'function') { - // Provider returned Promise - data.then(items => { - // Provider resolved with items - this._providerSetLocal(items) - }) - } else { - // Provider returned Array data - this._providerSetLocal(data) + try { + // Call provider function passing it the context and optional callback + const data = this.items(this.context, this._providerSetLocal) + if (data && data.then && typeof data.then === 'function') { + // Provider returned Promise + data.then(items => { + // Provider resolved with items + this._providerSetLocal(items) + }) + } else if isArray(data) { + // Provider returned Array data + this._providerSetLocal(data) + } else if (this.items.length !== 2) { + // Check number of arguments provider function requested + // Provider not using callback (didn't request second argument), so we clear + // busy state as most likely there was an error in the provider function + warn('b-table provider function didnt request calback and did not return a promise or data') + this.localBusy = false + } + } catch (e) { + // Provider function borked on us, so we spew out a warning + // console.error(`b-table provider function error [${e.name}]: ${e.message}`, e.stack) + // and clear the busy state + this.localBusy = false } }) } From 133e98155a7296a07d73453d96dd1cb607204c9b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 00:16:08 -0400 Subject: [PATCH 43/67] lint --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 46461347a12..5feab96ad53 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -74,7 +74,7 @@ function defaultSortCompare (a, b, sortBy) { // Helper function to determine if a value is defined function isDefined (value) { - retutrn typeof value !== 'undefined' + return typeof value !== 'undefined' } // Returns a value from an object using doted string notation. From bca22eeaa91fb107a8d193ef5003ea142aa3c194 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 00:25:01 -0400 Subject: [PATCH 44/67] lint --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 5feab96ad53..8cea4bd2949 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -593,7 +593,7 @@ export default { }, filterFunction: { type: Function, - default null + default: null }, sortCompare: { type: Function, From 6ab6c8baaeeecdf0090cd10ea40101f1c93e6879 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:04:11 -0400 Subject: [PATCH 45/67] more lint --- src/components/table/table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 8cea4bd2949..1c3faac473f 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -922,8 +922,8 @@ export default { // this.localFilterFn will contain the correct function ref. // Deprecate setting prop filter to a function return '' - } else if (typeof this.filterFunction !== 'function') && - !(typeof this.filter === 'string' || this.filter instanceof RegExp) { + } else if ((typeof this.filterFunction !== 'function') && + !(typeof this.filter === 'string' || this.filter instanceof RegExp)) { // Using internal filter function, which only acccepts string or regexp at the moment return '' } else { From 49c2add858e4c597989f12de3f4f257d0c6b515c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:07:10 -0400 Subject: [PATCH 46/67] extra linty --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 1c3faac473f..81d6ae44c68 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -936,7 +936,7 @@ export default { let filterFn = this.filterFunction // Sanitized/normalize filter-function prop - if typeof filterFn === 'funtion') { + if (typeof filterFn === 'funtion') { return filterFn } else if (typeof filter === 'function') { // Deprecate setting prop filter to a function From 306bfc94c881345be6becf1b2345564d84fd9fe9 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:17:39 -0400 Subject: [PATCH 47/67] Update table.js --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 81d6ae44c68..3ca300236a8 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -940,7 +940,7 @@ export default { return filterFn } else if (typeof filter === 'function') { // Deprecate setting prop filter to a function - return = filter + return filter } else { // no filterFunction, so signal to use internal filter function return null From 5031ca999e98923498803290a150426356c3e8a4 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:20:59 -0400 Subject: [PATCH 48/67] Update table.js --- src/components/table/table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 3ca300236a8..035fb289fd0 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1114,7 +1114,7 @@ export default { // Build the regexp needed for filtering let regex = criteria - if (tyepof regex === 'string') { + if (typeof regex === 'string') { // Escape special RegExp characters in the string and convert contiguous // whitespace to \s+ matches const string = criteria @@ -1275,7 +1275,7 @@ export default { // Check number of arguments provider function requested // Provider not using callback (didn't request second argument), so we clear // busy state as most likely there was an error in the provider function - warn('b-table provider function didnt request calback and did not return a promise or data') + warn('b-table provider function didn\'t request calback and did not return a promise or data') this.localBusy = false } } catch (e) { From db64f990529ef65d6493373f97814e091e0794a6 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:23:28 -0400 Subject: [PATCH 49/67] Update table.js --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 035fb289fd0..4b2613e7a38 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1268,7 +1268,7 @@ export default { // Provider resolved with items this._providerSetLocal(items) }) - } else if isArray(data) { + } else if (isArray(data)) { // Provider returned Array data this._providerSetLocal(data) } else if (this.items.length !== 2) { From 80973349fe83c818a66600e0d01f590a217bf481 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:30:14 -0400 Subject: [PATCH 50/67] Update table.js --- src/components/table/table.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 4b2613e7a38..4121638df24 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -286,7 +286,7 @@ export default { item: item, index: rowIndex, field: field, - unformatted: get(item, field.key), + unformatted: _get(item, field.key), value: formatted, toggleDetails: toggleDetailsFn, detailsShowing: Boolean(item._showDetails) @@ -449,8 +449,8 @@ export default { 'aria-busy': this.computedBusy ? 'true' : 'false', 'aria-colcount': String(fields.length), // Only set aria-rowcount if provided in $attrs or if localItems > shown items - 'aria-rowcount': this.$attrs['aria-rowcount'] || (this.filteredItems.length > items.length) ? - String(this.filteredItems.length) : null + 'aria-rowcount': this.$attrs['aria-rowcount'] || + (this.filteredItems.length > items.length) ? String(this.filteredItems.length) : null } }, [caption, colgroup, thead, tfoot, tbody] @@ -778,7 +778,7 @@ export default { 'table-bordered': this.bordered, 'table-sm': this.small, 'border': this.outlined, - // The following are b-table custom styles + // The following are b-table custom styles 'b-table-fixed': this.fixed, 'b-table-stacked': this.stacked === true || this.stacked === '', [`b-table-stacked-${this.stacked}`]: this.stacked !== true && this.stacked @@ -934,9 +934,8 @@ export default { localFilterFn () { let filter = this.filter let filterFn = this.filterFunction - // Sanitized/normalize filter-function prop - if (typeof filterFn === 'funtion') { + if (typeof filterFn === 'function') { return filterFn } else if (typeof filter === 'function') { // Deprecate setting prop filter to a function @@ -1272,11 +1271,11 @@ export default { // Provider returned Array data this._providerSetLocal(data) } else if (this.items.length !== 2) { - // Check number of arguments provider function requested - // Provider not using callback (didn't request second argument), so we clear - // busy state as most likely there was an error in the provider function - warn('b-table provider function didn\'t request calback and did not return a promise or data') - this.localBusy = false + // Check number of arguments provider function requested + // Provider not using callback (didn't request second argument), so we clear + // busy state as most likely there was an error in the provider function + warn('b-table provider function didn\'t request calback and did not return a promise or data') + this.localBusy = false } } catch (e) { // Provider function borked on us, so we spew out a warning From 0123dc0bc86e8c12a361b21bb92055ab3204d939 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:31:48 -0400 Subject: [PATCH 51/67] Update table.js --- src/components/table/table.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 4121638df24..22cb3d48b23 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -77,23 +77,6 @@ function isDefined (value) { return typeof value !== 'undefined' } -// Returns a value from an object using doted string notation. -// If no value found at pathReturns an empty string if no value is found -function getNestedValue (obj, path) { - // Split the path by the dots - path = path.split('.') - // Sanity check!! - if (path.length !== path.filter(p => p).length) { - // return empty string if too many dots (malformed path) - return '' - } - let value = obj - for (let i = 0; i < path.length && isDefined(value); i++) { - value = value[path[i]] - } - return isDefined(value) ? value : '' -} - // Helper function to massage field entry into common object format function processField (key, value) { let field = null From 66144fe6033ac0a1c9aa3147a8decfe043dce7fe Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 01:33:27 -0400 Subject: [PATCH 52/67] Update table.js --- src/components/table/table.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 22cb3d48b23..71e2d9057f3 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -72,11 +72,6 @@ function defaultSortCompare (a, b, sortBy) { }) } -// Helper function to determine if a value is defined -function isDefined (value) { - return typeof value !== 'undefined' -} - // Helper function to massage field entry into common object format function processField (key, value) { let field = null From b8c463908ff9c8d8453b38412e10705771ad7340 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 02:58:25 -0400 Subject: [PATCH 53/67] add a few temp test statements for debugging --- src/components/table/table.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 71e2d9057f3..3a9ac82fdbf 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1095,14 +1095,15 @@ export default { // Escape special RegExp characters in the string and convert contiguous // whitespace to \s+ matches const string = criteria - .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') - .replace(/[\s\uFEFF\xA0]+/g, '\\s+') + // Commented out to test + // .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + // .replace(/[\s\uFEFF\xA0]+/g, '\\s+') // Build the RegExp (no need for global flag, as we only need to find the value once in the string) regex = new RegExp(`.*${string}.*`, 'i') } // Return the generated filter test function - return (item) => { + return function (item) { // This searches all row values (and sub property values) in the entire (excluding // special _ prefixed keys), because we convert the record to a space-separated // string containing all the value properties (recursively), even ones that are @@ -1115,8 +1116,9 @@ export default { // and a reference to $scopedSlots) // // Generated function returns true if the crieria matches part of the serialzed data, otherwise false + console.log(recToString(item)) return regex.test(recToString(item)) - } + }.bind(this) }, // Event handlers rowClicked (e, item, index) { From 4f7791c686db6a93caefc7172cf1ac426199c8cc Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 03:20:43 -0400 Subject: [PATCH 54/67] more debugging --- src/components/table/table.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 3a9ac82fdbf..225649a37fe 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1102,6 +1102,7 @@ export default { regex = new RegExp(`.*${string}.*`, 'i') } + console.log('Regex', regex) // Return the generated filter test function return function (item) { // This searches all row values (and sub property values) in the entire (excluding @@ -1116,9 +1117,9 @@ export default { // and a reference to $scopedSlots) // // Generated function returns true if the crieria matches part of the serialzed data, otherwise false - console.log(recToString(item)) + console.log('recordToString:', recToString(item)) return regex.test(recToString(item)) - }.bind(this) + } }, // Event handlers rowClicked (e, item, index) { From 1d569e9059560c150f39ce0f1a02f8761963d54d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 03:55:28 -0400 Subject: [PATCH 55/67] debugging tests --- src/components/table/table.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 225649a37fe..1c8494e6217 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -935,6 +935,7 @@ export default { // We only do local filtering if requested, and if the are records to filter and // if a filter criteria was specified if (this.localFiltering && !!filterFn && !!criteria && isArray[items] && items.length > 0) { + console.log('Filtering items using filterFn', filterFn) items = items.filter(filterFn) } return items @@ -1102,9 +1103,9 @@ export default { regex = new RegExp(`.*${string}.*`, 'i') } - console.log('Regex', regex) - // Return the generated filter test function - return function (item) { + console.log('Outside Regex', regex) + // Generate teh test function to use + const fn = (item) => { // This searches all row values (and sub property values) in the entire (excluding // special _ prefixed keys), because we convert the record to a space-separated // string containing all the value properties (recursively), even ones that are @@ -1117,9 +1118,13 @@ export default { // and a reference to $scopedSlots) // // Generated function returns true if the crieria matches part of the serialzed data, otherwise false + console.log('Inside Item', item) + console.log('Inside Regex', regex) console.log('recordToString:', recToString(item)) return regex.test(recToString(item)) } + + return fn }, // Event handlers rowClicked (e, item, index) { From c4d745a84d4c63a35a4b5f24c2405838f6c48f55 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 04:21:33 -0400 Subject: [PATCH 56/67] more debugging --- src/components/table/table.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 1c8494e6217..286125f30bd 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -934,7 +934,8 @@ export default { // We only do local filtering if requested, and if the are records to filter and // if a filter criteria was specified - if (this.localFiltering && !!filterFn && !!criteria && isArray[items] && items.length > 0) { + console.log('Before Filter values:', this.localFiltering, filterFn, items.length) + if (this.localFiltering && filterFn && items.length > 0) { console.log('Filtering items using filterFn', filterFn) items = items.filter(filterFn) } @@ -1069,6 +1070,7 @@ export default { }, // Filter Function factories filterFnFactory (filterFn, criteria) { + // Wrapper factory for external filter functions. // Wrap the provided filter-function and return a new function. // returns null if no filter-function defined or if criteria is falsey. // Rather than directly grabbing this.computedLocalFilterFn or this.filterFunction @@ -1096,15 +1098,14 @@ export default { // Escape special RegExp characters in the string and convert contiguous // whitespace to \s+ matches const string = criteria - // Commented out to test - // .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') - // .replace(/[\s\uFEFF\xA0]+/g, '\\s+') + .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + .replace(/[\s\uFEFF\xA0]+/g, '\\s+') // Build the RegExp (no need for global flag, as we only need to find the value once in the string) regex = new RegExp(`.*${string}.*`, 'i') } console.log('Outside Regex', regex) - // Generate teh test function to use + // Generate the test function to use const fn = (item) => { // This searches all row values (and sub property values) in the entire (excluding // special _ prefixed keys), because we convert the record to a space-separated @@ -1121,8 +1122,8 @@ export default { console.log('Inside Item', item) console.log('Inside Regex', regex) console.log('recordToString:', recToString(item)) - return regex.test(recToString(item)) - } + return this.test(recToString(item)) + }.bind(regex) return fn }, From 9ea42ba2c6d5af0f3fb0768216f75ccb608b9ed4 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 04:24:12 -0400 Subject: [PATCH 57/67] Update table.js --- src/components/table/table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 286125f30bd..e13e72379aa 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1123,9 +1123,9 @@ export default { console.log('Inside Regex', regex) console.log('recordToString:', recToString(item)) return this.test(recToString(item)) - }.bind(regex) + } - return fn + return fn.bind(regex) }, // Event handlers rowClicked (e, item, index) { From 2ca4b40ff18e54f82572e1e8e1054c455afc6040 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 04:37:48 -0400 Subject: [PATCH 58/67] Update table.js --- src/components/table/table.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index e13e72379aa..2b1536dbf8d 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -936,7 +936,6 @@ export default { // if a filter criteria was specified console.log('Before Filter values:', this.localFiltering, filterFn, items.length) if (this.localFiltering && filterFn && items.length > 0) { - console.log('Filtering items using filterFn', filterFn) items = items.filter(filterFn) } return items @@ -1104,7 +1103,6 @@ export default { regex = new RegExp(`.*${string}.*`, 'i') } - console.log('Outside Regex', regex) // Generate the test function to use const fn = (item) => { // This searches all row values (and sub property values) in the entire (excluding @@ -1122,10 +1120,13 @@ export default { console.log('Inside Item', item) console.log('Inside Regex', regex) console.log('recordToString:', recToString(item)) - return this.test(recToString(item)) + // We set lastIndex = 0 on regex in case someone uses the /g global flag + regexp.lastIndex = 0 + return regex.test(recToString(item)) } - return fn.bind(regex) + // Return the generated function + return fn }, // Event handlers rowClicked (e, item, index) { From 19702edf4b1c3d716a199c1b14bfcd28aa29069b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 04:45:17 -0400 Subject: [PATCH 59/67] Update table.js --- src/components/table/table.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 2b1536dbf8d..af24cd602d1 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1092,7 +1092,7 @@ export default { } // Build the regexp needed for filtering - let regex = criteria + let regexp = criteria if (typeof regex === 'string') { // Escape special RegExp characters in the string and convert contiguous // whitespace to \s+ matches @@ -1100,7 +1100,7 @@ export default { .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') .replace(/[\s\uFEFF\xA0]+/g, '\\s+') // Build the RegExp (no need for global flag, as we only need to find the value once in the string) - regex = new RegExp(`.*${string}.*`, 'i') + regexp = new RegExp(`.*${string}.*`, 'i') } // Generate the test function to use @@ -1118,11 +1118,11 @@ export default { // // Generated function returns true if the crieria matches part of the serialzed data, otherwise false console.log('Inside Item', item) - console.log('Inside Regex', regex) + console.log('Inside Regex', regexp) console.log('recordToString:', recToString(item)) // We set lastIndex = 0 on regex in case someone uses the /g global flag regexp.lastIndex = 0 - return regex.test(recToString(item)) + return regexp.test(recToString(item)) } // Return the generated function From fdf53ecff70d065b9153b12e79f72524a27dc8d1 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 04:57:12 -0400 Subject: [PATCH 60/67] Update table.js --- src/components/table/table.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index af24cd602d1..be715f1fba5 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1093,14 +1093,14 @@ export default { // Build the regexp needed for filtering let regexp = criteria - if (typeof regex === 'string') { + if (typeof regexp === 'string') { // Escape special RegExp characters in the string and convert contiguous // whitespace to \s+ matches - const string = criteria + const pattern = criteria .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') .replace(/[\s\uFEFF\xA0]+/g, '\\s+') // Build the RegExp (no need for global flag, as we only need to find the value once in the string) - regexp = new RegExp(`.*${string}.*`, 'i') + regexp = new RegExp(`.*${pattern}.*`, 'i') } // Generate the test function to use From 83248e1c9b9172608131d083f41f07d1a4ea89da Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 06:03:50 -0400 Subject: [PATCH 61/67] update isFiltered determination --- src/components/table/table.js | 57 ++++++++++++----------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index be715f1fba5..53c18a5486d 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -60,7 +60,9 @@ function recToString (row) { } // Default sort compare routine -// TODO: add option to sort by multiple columns +// TODO: add option to sort by multiple columns (tri-state per column, plus order of columns in sort) +// where sprtBy could be an array of objects [ {key: 'foo', sortDir: 'asc'}, {key:'bar', sortDir: 'desc'} ...] +// or an array of arrays [ ['foo','asc'], ['bar','desc'] ] function defaultSortCompare (a, b, sortBy) { a = _get(a, sortBy, '') b = _get(b, sortBy, '') @@ -91,30 +93,6 @@ function processField (key, value) { return field } -// Determine if two given arrays are *not* loosely equal -function arraysNotEqual (left = [], right = []) { - if (left === right) { - // If left reference is equal to the right reference, then they are equal - return false - } else if (left.length === 0 && right.length === 0) { - // If they are both zero length then they are considered equal - return true - } else if (left.length !== right.length) { - // If they have different lengths then they are definitely not equal - return true - } else { - const equal = left.every((item, index) => { - // We compare left array with the right array, row by row, until we find - // a row that is not equal at the same row index. - // Note: This process can be slow for rather large datasets! - // We try and optimize the usage by targetting the upper conditions first if at all - // possible (i.e. setting left and right arrays to the same reference when possible) - return looseEqual(sanitizeRow(item), sanitizeRow(right[index])) - }) - return !equal - } -} - // b-table component definition export default { mixins: [idMixin, listenOnRootMixin], @@ -700,18 +678,27 @@ export default { this.$emit('update:busy', newVal) } }, - // Watch for changes to the filtered items vs localItems). + // Watch for changes to the filter criteria and filtered items vs localItems). // And set visual state and emit events as required filteredCheck ({filteredItems, localItems, localFilter}) { - // This comparison can potentially be computationally intensive on large datasets! - // i.e. when a filtered dataset is equal to the entire localItems. - // Large data set users shoud use provider sorting and filtering. - if (arraysNotEqual(filteredItems, localItems) || !!localFilter) { - this.isFiltered = true - this.$emit('filtered', filteredItems, filteredItems.length) + // Determine if the dataset is filtered or not + let isFiltered + if (!localFilter) { + // If filter criteria is falsey + isFiltered = false + } else if (looseEqual(localFilter, []) || looseEqual(localFilter, {})) { + // If filter criteria is an empty array or object + isFiltered = false + } else if (!!localFilter) { + // if Filter criteria is truthy + isFiltered = true } else { - this.isFiltered = false + isFiltered = false + } + if (isFiltered) { + this.$emit('filtered', filteredItems, filteredItems.length) } + this.isFiltered = isFiltered }, isFiltered (newVal, oldVal) { if (newVal === false && oldVal === true) { @@ -934,7 +921,6 @@ export default { // We only do local filtering if requested, and if the are records to filter and // if a filter criteria was specified - console.log('Before Filter values:', this.localFiltering, filterFn, items.length) if (this.localFiltering && filterFn && items.length > 0) { items = items.filter(filterFn) } @@ -1117,9 +1103,6 @@ export default { // and a reference to $scopedSlots) // // Generated function returns true if the crieria matches part of the serialzed data, otherwise false - console.log('Inside Item', item) - console.log('Inside Regex', regexp) - console.log('recordToString:', recToString(item)) // We set lastIndex = 0 on regex in case someone uses the /g global flag regexp.lastIndex = 0 return regexp.test(recToString(item)) From 45360a96ea8c3562aadc3d4c7b9d9a26dca65f2e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 06:16:19 -0400 Subject: [PATCH 62/67] lint --- src/components/table/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 53c18a5486d..4d0d6c213b2 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -689,7 +689,7 @@ export default { } else if (looseEqual(localFilter, []) || looseEqual(localFilter, {})) { // If filter criteria is an empty array or object isFiltered = false - } else if (!!localFilter) { + } else if (localFilter) { // if Filter criteria is truthy isFiltered = true } else { From 55197f51ffa002c35313637df98ba82a4a8808f4 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 07:06:37 -0400 Subject: [PATCH 63/67] set fallback default parameter for lodash.get calls To prevent undefined values from creeping in --- src/components/table/table.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 4d0d6c213b2..b3abdb64cff 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -242,7 +242,7 @@ export default { item: item, index: rowIndex, field: field, - unformatted: _get(item, field.key), + unformatted: _get(item, field.key, ''), value: formatted, toggleDetails: toggleDetailsFn, detailsShowing: Boolean(item._showDetails) @@ -1024,11 +1024,10 @@ export default { getTdValues (item, key, tdValue, defValue) { const parent = this.$parent if (tdValue) { + const value = _get(item, key, '') if (typeof tdValue === 'function') { - let value = _get(item, key) return tdValue(value, key, item) } else if (typeof tdValue === 'string' && typeof parent[tdValue] === 'function') { - let value = _get(item, key) return parent[tdValue](value, key, item) } return tdValue @@ -1040,7 +1039,7 @@ export default { const key = field.key const formatter = field.formatter const parent = this.$parent - let value = _get(item, key) + let value = _get(item, key, null) if (formatter) { if (typeof formatter === 'function') { value = formatter(value, key, item) From 6aea6681c232290db5b7212a50fb6a320a27047a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 15:57:35 -0400 Subject: [PATCH 64/67] Add field key to header column aria-label when label is blank string When the fields's label was set to a blank string, this left non-sighted users at a disadvantage as to what the column is about. This adds the field's key (humanized) to the `aria-label` to provides hint to non-sighted users. Alternatively if a field title is provided, then the aria-label is not altered. Addresses ARIA issues introduced by PR #1587 --- src/components/table/table.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index b3abdb64cff..3756f0c7de6 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -126,6 +126,22 @@ export default { // factory function for thead and tfoot cells (th's) const makeHeadCells = (isFoot = false) => { return fields.map((field, colIndex) => { + let ariaLabel = '' + if (!(field.label.trim()) && !field.headerTitle) { + // In case field's label and title are empty/balnk + // We need to add a hint about what the column is about for non-dighted users + ariaLabel = _startCase(field.key) + } + const ariaLabelSorting = field.sortable + ? this.localSortDesc && this.localSortBy === field.key + ? this.labelSortAsc + : this.labelSortDesc + : null + // Assemble the aria-label + ariaLabel = [ariaLabel, ariaLabelSorting].filter(a => a).join(': ') || null + const ariaSort = field.sortable && this.localSortBy === field.key + ? (this.localSortDesc ? 'descending' : 'ascending') + : null const data = { key: field.key, class: this.fieldClasses(field), @@ -135,15 +151,8 @@ export default { abbr: field.headerAbbr || null, title: field.headerTitle || null, 'aria-colindex': String(colIndex + 1), - 'aria-label': field.sortable - ? this.localSortDesc && this.localSortBy === field.key - ? this.labelSortAsc - : this.labelSortDesc - : null, - 'aria-sort': - field.sortable && this.localSortBy === field.key - ? this.localSortDesc ? 'descending' : 'ascending' - : null + 'aria-label': ariaLabel, + 'aria-sort': ariaSort }, on: { click: evt => { From a1a0839725e824b75172edfda708ab27dbd53a74 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 16:09:58 -0400 Subject: [PATCH 65/67] minor code updates --- src/components/table/table.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 3756f0c7de6..71d4a641979 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1073,15 +1073,19 @@ export default { return null } - // Return the wrapped filter test function - return (item) => { + // Build the wrapped filter test function, passing the criteria to the provided function + const fn = (item) => { // Generated function returns true if the crieria matches part of the serialzed data, otherwise false return filterFn(item, criteria) } + + // return the wrapped function + return fn }, defaultFilterFnFactory (criteria) { - // Generates the default filter function, using the given criteria + // Generates the default filter function, using the given flter criteria if (!criteria || !(typeof criteria === 'string' || criteria instanceof RegExp)) { + // Bult in filter can only support strings or RegExp criteria (at the moment) return null } @@ -1097,7 +1101,7 @@ export default { regexp = new RegExp(`.*${pattern}.*`, 'i') } - // Generate the test function to use + // Generate the wrapped filter test function to use const fn = (item) => { // This searches all row values (and sub property values) in the entire (excluding // special _ prefixed keys), because we convert the record to a space-separated From 82fd037e33eaa159f1c03d365e7e4b6930c823c8 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 16:11:40 -0400 Subject: [PATCH 66/67] lint --- src/components/table/table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/table/table.js b/src/components/table/table.js index 71d4a641979..061f6185e3b 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -140,8 +140,8 @@ export default { // Assemble the aria-label ariaLabel = [ariaLabel, ariaLabelSorting].filter(a => a).join(': ') || null const ariaSort = field.sortable && this.localSortBy === field.key - ? (this.localSortDesc ? 'descending' : 'ascending') - : null + ? (this.localSortDesc ? 'descending' : 'ascending') + : null const data = { key: field.key, class: this.fieldClasses(field), From 97b9ade871ca7d127a6ba72588fb8b1666a0ec03 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 10 Nov 2018 17:11:26 -0400 Subject: [PATCH 67/67] Update documentation --- src/components/table/README.md | 100 ++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/src/components/table/README.md b/src/components/table/README.md index 2a2532ac274..ad87647c8f2 100755 --- a/src/components/table/README.md +++ b/src/components/table/README.md @@ -273,7 +273,9 @@ The following field properties are recognized: | Property | Type | Description | ---------| ---- | ----------- | `key` | String | The key for selecting data from the record in the items array. Required when setting the `fields` from as an array of objects. -| `label` | String | Appears in the columns table header (and footer if `foot-clone` is set). Defaults to the field's key (in humanized format) if not provided. It's possible to use empty labels by assigning an empty string `""` +| `label` | String | Appears in the columns table header (and footer if `foot-clone` is set). Defaults to the field's key (in humanized format) if not provided. It's possible to use empty labels by assigning an empty string `""` but be sure you also set `headerTitle` to provide non-sighted users a hint about the column contents. +| `headerTitle` | Text to place on the fields header `` attribute `title`. Defaults to no `title` attribute. +| `headerAbbr` | Text to place on the fields header `` attribute `abbr`. Set this to the unabbreviated version of the label (or title) if label (or title) is an abbreviation. Defaults to no `abbr` attribute. | `class` | String or Array | Class name (or array of class names) to add to `` **and** `` in the column. | `formatter` | String or Function | A formatter callback function, can be used instead of (or in conjunction with) slots for real table fields (i.e. fields, that have corresponding data at items array). Refer to [**Custom Data Rendering**](#custom-data-rendering) for more details. | `sortable` | Boolean | Enable sorting on this column. Refer to the [**Sorting**](#sorting) Section for more details. @@ -486,9 +488,8 @@ export default { ``` ->**Responsive table notes:** -> - _Possible vertical clipping/truncation_. Responsive tables make use of `overflow-y: hidden`, which clips off any content that goes beyond the bottom or top edges of the table. In particular, this can clip off dropdown menus and other third-party widgets. -> - When in responsive mode the table will lose it's width of 100%. This is a known issue with bootstrap V4 css and placing the `table-responsive` class on the `` element as recommended by Bootstrap. +**Responsive table notes:** +- _Possible vertical clipping/truncation_. Responsive tables make use of `overflow-y: hidden`, which clips off any content that goes beyond the bottom or top edges of the table. In particular, this may clip off dropdown menus and other third-party widgets. ## Stacked tables @@ -680,12 +681,12 @@ The slot's scope variable (`data` in the above sample) will have the following p | `toggleDetails` | Function | Can be called to toggle the visibility of the rows `row-details` scoped slot. See section [**Row details support**](#row-details-support) below for additional information ->**Notes:** ->- _`index` will not always be the actual row's index number, as it is +**Notes:** +- _`index` will not always be the actual row's index number, as it is computed after pagination and filtering have been applied to the original table data. The `index` value will refer to the **displayed row number**. This number will align with the indexes from the optional `v-model` bound variable._ ->- _When placing inputs, buttons, selects or links within a data cell scoped slot, +- _When placing inputs, buttons, selects or links within a data cell scoped slot, be sure to add a `@click.stop` (or `@click.native.stop` if needed) handler (which can be empty) to prevent the click on the input, button, select, or link, from triggering the `row-clicked` event:_ @@ -698,8 +699,8 @@ the `row-clicked` event:_ ``` #### Displaying raw HTML -By default `b-table` escapes HTML tags in items, if you need to display raw HTML code in `b-table`, you should use -`v-html` directive on an element in a in scoped field slot +By default `b-table` escapes HTML tags in items data and results of formatter functions, if you need to display +raw HTML code in `b-table`, you should use `v-html` directive on an element in a in scoped field slot ```html