diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d805659f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = false +trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc index 1f883e38..2afe7349 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,6 +13,8 @@ "Sortable": true }, "rules": { + "cypress/no-unnecessary-waiting": "off", + "cypress/unsafe-to-chain-command": "off", "no-prototype-builtins": [0] } } \ No newline at end of file diff --git a/cypress/integration/example-0070-plugin-state.spec.js b/cypress/integration/example-0070-plugin-state.spec.js new file mode 100644 index 00000000..96ccf853 --- /dev/null +++ b/cypress/integration/example-0070-plugin-state.spec.js @@ -0,0 +1,114 @@ + +describe('Example 0070 - Grid State using Local Storage', () => { + const originalTitles = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']; + + beforeEach(() => { + cy.restoreLocalStorage(); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseExampleUrl')}/example-0070-plugin-state.html`); + cy.get('h2 + p').should('contain', 'Slick.State'); + + cy.clearLocalStorage(); + cy.get('[data-test="clear-storage"]').click(); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(originalTitles[index])); + }); + + it('should sort ascending column "D"', () => { + cy.get('.slick-header-columns .slick-header-column:nth(3)') + .click({ force: true }); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + }); + + it('should drag column "A" to be after column "C" in the grid', () => { + const expectedTitles = ['A', 'C', 'D', 'B', 'E', 'F', 'G', 'H', 'I', 'J']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .contains('B') + .drag('.slick-header-column:nth(3)'); + + cy.get('.slick-header-column:nth(3)').contains('B'); + + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + it('should resize 1st column and make it wider', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .should('contain', 'C'); + + cy.get('.slick-resizable-handle:nth(1)') + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(2)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + cy.get('.slick-header-column:nth(1)') + .should(($el) => { + const expectedWidth = 170; // calculate with a calculated width including a (+/-)5px precision + expect($el.width()).greaterThan(expectedWidth - 5); + expect($el.width()).lessThan(expectedWidth + 5); + }); + }); + + it('should reload the page', () => { + cy.reload().wait(50); + }); + + it('should reload page and expect same columns orders, 2nd column wider and 3rd column sorted ascending', () => { + const expectedTitles = ['A', 'C', 'D', 'B', 'E', 'F', 'G', 'H', 'I', 'J']; + + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + + cy.get('.slick-header-column:nth(1)') + .should(($el) => { + const expectedWidth = 170; // calculate with a calculated width including a (+/-)5px precision + expect($el.width()).greaterThan(expectedWidth - 5); + expect($el.width()).lessThan(expectedWidth + 5); + }); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + }); + + it('should clear local storage & reload the page', () => { + cy.clearLocalStorage(); + cy.get('[data-test="clear-storage"]').click(); + + cy.reload().wait(50); + }); + + it('should expect grid to be reset to default', () => { + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(originalTitles[index])); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('not.exist'); + }); +}); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 51479eb1..07343c3e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -35,4 +35,18 @@ Cypress.Commands.add("getCell", (row, col, viewport = 'topLeft', { parentSelecto const canvasSelectorY = position.y ? `.grid-canvas-${position.y}` : ''; return cy.get(`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="top:${row * rowHeight}px"] > .slick-cell:nth(${col})`); -}) \ No newline at end of file +}); + +let LOCAL_STORAGE_MEMORY = {}; + +Cypress.Commands.add('saveLocalStorage', () => { + Object.keys(localStorage).forEach(key => { + LOCAL_STORAGE_MEMORY[key] = localStorage[key]; + }); +}); + +Cypress.Commands.add('restoreLocalStorage', () => { + Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => { + localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]); + }); +}); \ No newline at end of file diff --git a/examples/example-0070-plugin-state.html b/examples/example-0070-plugin-state.html index 8c7e6406..440ec368 100644 --- a/examples/example-0070-plugin-state.html +++ b/examples/example-0070-plugin-state.html @@ -2,6 +2,7 @@ + SlickGrid example: Plugin: State @@ -38,12 +39,12 @@

About

This example demonstrates using the Slick.State plugin persisting grid column width, order, visibility and sort order using Local Storage. For this demo, we use Local Storage, but you could technically save this data into a database and reload at any time your previous session.

+ +

Options:

+ - - - @@ -85,7 +86,12 @@

About

} } - $(function () { + function clearLocalStorage() { + localStorage.clear(); + window.location.reload(); + } + + (function () { var dataView = new Slick.Data.DataView(); var statePersistor = new Slick.State({ cid: 'sample-grid', @@ -144,9 +150,9 @@

About

statePersistor.restore() .then(function (state) { - grid.setSortColumns(state.sortcols); + grid.setSortColumns(state.sortcols || []); var columns = grid.getColumns(); - var sortCols = $.map(grid.getSortColumns(), function (col) { + var sortCols = (grid.getSortColumns() || []).map(function (col) { return { sortCol: columns[grid.getColumnIndex(col.columnId)], sortAsc: col.sortAsc @@ -154,7 +160,7 @@

About

}); sortDataView(sortCols); }); - }); + })(); diff --git a/examples/example-row-detail-selection-and-move.html b/examples/example-row-detail-selection-and-move.html index 3c82c577..387b9689 100644 --- a/examples/example-row-detail-selection-and-move.html +++ b/examples/example-row-detail-selection-and-move.html @@ -116,9 +116,7 @@

Selected Titles:

- - - + @@ -225,7 +223,7 @@

Selected Titles:

return (x == y ? 0 : (x > y ? 1 : -1)); } - $(function () { + (function () { // prepare the data for (var i = 0; i < 1000; i++) { data[i] = new DataItem(i); @@ -358,7 +356,7 @@

Selected Titles:

return item && item.title || ''; }); } - $('#selectedTitles').text(selectedTitles.toString()); + document.querySelector('#selectedTitles').textContent = selectedTitles.toString(); }); grid.onSort.subscribe(function (e, args) { @@ -380,7 +378,7 @@

Selected Titles:

dataView.onSelectedRowIdsChanged.subscribe(function (e, args) { console.log('onSelectedRowIdsChanged', args) }); - }); + })(); diff --git a/plugins/slick.state.js b/plugins/slick.state.js index a2d5c637..412a7530 100644 --- a/plugins/slick.state.js +++ b/plugins/slick.state.js @@ -1,6 +1,6 @@ -(function ($) { +(function (window) { // register namespace - $.extend(true, window, { + Slick.Utils.extend(true, window, { Slick: { State: State } @@ -15,17 +15,19 @@ return { get: function(key) { - return $.Deferred(function(dfd) { - if (!localStorage) return dfd.reject("missing localStorage"); + return new Promise((resolve, reject) => { + if (!localStorage) { + reject("missing localStorage"); + return + } try { var d = localStorage.getItem(key); if (d) { - return dfd.resolve(JSON.parse(d)); + return resolve(JSON.parse(d)); } - dfd.resolve(); - } - catch (exc) { - dfd.reject(exc); + resolve({}); + } catch (exc) { + reject(exc); } }); }, @@ -46,7 +48,7 @@ }; function State(options) { - options = $.extend(true, {}, defaults, options); + options = Slick.Utils.extend(true, {}, defaults, options); var _grid, _cid, _store = options.storage, @@ -61,18 +63,18 @@ _grid = grid; _cid = grid.cid || options.cid; if (_cid) { - grid.onColumnsResized.subscribe(save); - grid.onColumnsReordered.subscribe(save); - grid.onSort.subscribe(save); + _grid.onColumnsResized.subscribe(save); + _grid.onColumnsReordered.subscribe(save); + _grid.onSort.subscribe(save); } else { console.warn("grid has no client id. state persisting is disabled."); } } function destroy() { - grid.onSort.unsubscribe(save); - grid.onColumnsReordered.unsubscribe(save); - grid.onColumnsResized.unsubscribe(save); + _grid.onSort.unsubscribe(save); + _grid.onColumnsReordered.unsubscribe(save); + _grid.onColumnsResized.unsubscribe(save); save(); } @@ -86,7 +88,7 @@ }; state.userData = userData.current; - + setUserDataFromState(state.userData); onStateChanged.notify(state); @@ -95,15 +97,21 @@ } function restore() { - return $.Deferred(function(dfd) { - if (!_cid) { return dfd.reject("missing client id"); } - if (!_store) { return dfd.reject("missing store"); } + return new Promise((resolve, reject) => { + if (!_cid) { + reject("missing client id"); + return; + } + if (!_store) { + reject("missing store"); + return; + } _store.get(options.key_prefix + _cid) .then(function success(state) { - if (state) { + if (state) { if (state.sortcols) { - _grid.setSortColumns(state.sortcols); + _grid.setSortColumns(state.sortcols || []); } if (state.viewport && options.scrollRowIntoView) { _grid.scrollRowIntoView(state.viewport.top, true); @@ -112,14 +120,14 @@ var defaultColumns = options.defaultColumns; if (defaultColumns) { var defaultColumnsLookup = {}; - $.each(defaultColumns, function(idx, colDef) { + defaultColumns.forEach(function (colDef) { defaultColumnsLookup[colDef.id] = colDef; }); var cols = []; - $.each(state.columns, function(idx, columnDef) { + (state.columns || []).forEach(function (columnDef) { if (defaultColumnsLookup[columnDef.id]) { - cols.push($.extend(true, {}, defaultColumnsLookup[columnDef.id], { + cols.push(Slick.Utils.extend(true, {}, defaultColumnsLookup[columnDef.id], { width: columnDef.width, headerCssClass: columnDef.headerCssClass })); @@ -133,8 +141,11 @@ } setUserDataFromState(state.userData); } - dfd.resolve(state); - }, dfd.reject); + resolve(state); + }) + .catch(function (e) { + reject(e); + }) }); } @@ -171,7 +182,7 @@ return userData.current; } - /** + /** * returns user-data found in saved state * * @return {Object} @@ -192,7 +203,7 @@ } function getColumns() { - return $.map(_grid.getColumns(), function(col) { + return _grid.getColumns().map(function (col) { return { id: col.id, width: col.width @@ -212,7 +223,7 @@ /* * API */ - $.extend(this, { + Slick.Utils.extend(this, { "init": init, "destroy": destroy, "save": save, @@ -225,4 +236,4 @@ "reset": reset }); } -})(jQuery); +})(window); diff --git a/slick.grid.js b/slick.grid.js index 2cfc47b3..441ee873 100644 --- a/slick.grid.js +++ b/slick.grid.js @@ -112,7 +112,7 @@ if (typeof Slick === "undefined") { maxSupportedCssHeight: 1000000000, sanitizer: undefined, // sanitize function, built in basic sanitizer is: Slick.RegexSanitizer(dirtyHtml) logSanitizedHtml: false // log to console when sanitised - recommend true for testing of dev and production - }; + }; var columnDefaults = { name: "", @@ -321,7 +321,7 @@ if (typeof Slick === "undefined") { } else { _container = container; } - + if (!_container) { throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM."); } @@ -964,7 +964,7 @@ if (typeof Slick === "undefined") { } viewportHasHScroll = (canvasWidth >= viewportW - scrollbarDimensions.width); - + utils.width(_headerRowSpacerL, canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0)); utils.width(_headerRowSpacerR, canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0)); @@ -992,7 +992,7 @@ if (typeof Slick === "undefined") { let supportedHeight = 1000000; // FF reports the height back but still renders blank after ~6M px //var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000; - const testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? options.ffMaxSupportedCssHeight : options.maxSupportedCssHeight; + const testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? options.ffMaxSupportedCssHeight : options.maxSupportedCssHeight; const div = utils.template("
", document.body); while (true) { @@ -1079,7 +1079,7 @@ if (typeof Slick === "undefined") { header.setAttribute("title", toolTip || ""); if(title !== undefined) header.children[0].innerHTML = title; - + trigger(self.onHeaderCellRendered, { "node": header, "column": columnDef, @@ -1100,7 +1100,7 @@ if (typeof Slick === "undefined") { var idx = (typeof columnIdOrIdx === "number" ? columnIdOrIdx : getColumnIndex(columnIdOrIdx)); var targetHeader = hasFrozenColumns() ? ((idx <= options.frozenColumn) ? _headerL : _headerR) : _headerL; var targetIndex = hasFrozenColumns() ? ((idx <= options.frozenColumn) ? idx : idx - options.frozenColumn - 1) : idx; - + return targetHeader.children[targetIndex]; } @@ -1183,7 +1183,7 @@ if (typeof Slick === "undefined") { if (className) { footerRowCell.classList.add(className); } - + utils.storage.put(footerRowCell, "column", m); trigger(self.onFooterRowCellRendered, { @@ -1360,7 +1360,7 @@ if (typeof Slick === "undefined") { if (m.sortable) { header.classList.add("slick-header-sortable"); utils.template("", header); - if (options.numberedMultiColumnSort && options.sortColNumberInSeparateSpan) { + if (options.numberedMultiColumnSort && options.sortColNumberInSeparateSpan) { utils.template("", header); } } @@ -1456,8 +1456,8 @@ if (typeof Slick === "undefined") { sortColumns.splice(i, 1); sortColumn = null; } - if (!options.multiColumnSort) { - sortColumns = []; + if (!options.multiColumnSort) { + sortColumns = []; } if (sortColumn && (!hadSortCol || !options.multiColumnSort)) { sortColumns.push(sortColumn); @@ -1594,7 +1594,7 @@ if (typeof Slick === "undefined") { _viewportScrollContainerX.scrollLeft = _viewportScrollContainerX.scrollLeft - 10; } - var canDragScroll; + var canDragScroll; var sortableOptions = { animation: 50, direction: 'horizontal', @@ -1725,7 +1725,7 @@ if (typeof Slick === "undefined") { if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) { continue; } - + const resizeableHandle = utils.template("
", colElm); _bindingEventService.bind(resizeableHandle, "dblclick", handleResizeableHandleDoubleClick); @@ -2299,7 +2299,7 @@ if (typeof Slick === "undefined") { } /** - * call destroy method, when exists, on all the instance(s) it found + * call destroy method, when exists, on all the instance(s) it found * @params instances - can be a single instance or a an array of instances */ function destroyAllInstances(inputInstances) { @@ -2975,7 +2975,7 @@ if (typeof Slick === "undefined") { if (!initialized) { return; } - + let columnIndex = 0; _headers.forEach(function (header) { for (let i = 0; i < header.children.length; i++, columnIndex++) { @@ -3030,7 +3030,7 @@ if (typeof Slick === "undefined") { function setSortColumns(cols) { sortColumns = cols; - + const numberCols = options.numberedMultiColumnSort && sortColumns.length > 1; _headers.forEach(function (header) { let indicators = header.querySelectorAll(".slick-header-column-sorted"); @@ -3048,7 +3048,7 @@ if (typeof Slick === "undefined") { el.textContent = ""; }); }); - + let i = 1; sortColumns.forEach(function (col) { if (col.sortAsc == null) { @@ -3062,7 +3062,7 @@ if (typeof Slick === "undefined") { column.classList.add("slick-header-column-sorted"); let indicator = column.querySelector(".slick-sort-indicator"); indicator.classList.add(col.sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc"); - + if (numberCols) { indicator = column.querySelector(".slick-sort-indicator-numbered"); indicator.textContent = i; @@ -4462,7 +4462,7 @@ if (typeof Slick === "undefined") { }, 0); } - // autoheight suppresses vertical scrolling, but editors can create a div larger than + // autoheight suppresses vertical scrolling, but editors can create a div larger than // the row vertical size, which can lead to a vertical scroll bar appearing temporarily // while the editor is displayed. this is not part of the grid scrolling, so we should ignore it if (vScrollDist && !options.autoHeight) { @@ -4974,7 +4974,7 @@ if (typeof Slick === "undefined") { if (columnResizeDragging) { return; } - + var header = e.target.closest(".slick-header-column"); var column = header && utils.storage.get(header, "column"); if (column) { @@ -5246,8 +5246,8 @@ if (typeof Slick === "undefined") { // this optimisation causes trouble - MLeibman #329 //if (activeCellChanged) { - if (!suppressActiveCellChangedEvent) { - trigger(self.onActiveCellChanged, getActiveCell()); + if (!suppressActiveCellChangedEvent) { + trigger(self.onActiveCellChanged, getActiveCell()); } //} } @@ -6184,11 +6184,11 @@ if (typeof Slick === "undefined") { if (!options.sanitizer || typeof dirtyHtml !== 'string') { return dirtyHtml; } - + var cleanHtml = options.sanitizer(dirtyHtml); - + if (!suppressLogging && options.logSanitizedHtml && logMessageCount <= logMessageMaxCount && cleanHtml !== dirtyHtml) { - console.log("sanitizer altered html: " + dirtyHtml + " --> " + cleanHtml); + console.log("sanitizer altered html: " + dirtyHtml + " --> " + cleanHtml); if (logMessageCount === logMessageMaxCount) { console.log("sanitizer: silencing messages after first " + logMessageMaxCount); }