diff --git a/lib/xlsx/xform/table/custom-filter-xform.js b/lib/xlsx/xform/table/custom-filter-xform.js new file mode 100644 index 000000000..d0e9d272f --- /dev/null +++ b/lib/xlsx/xform/table/custom-filter-xform.js @@ -0,0 +1,33 @@ +const BaseXform = require('../base-xform'); + +class CustomFilterXform extends BaseXform { + get tag() { + return 'customFilter'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + val: model.val, + operator: model.operator, + }); + } + + parseOpen(node) { + if (node.name === this.tag) { + this.model = { + val: node.attributes.val, + operator: node.attributes.operator, + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = CustomFilterXform; diff --git a/lib/xlsx/xform/table/filter-column-xform.js b/lib/xlsx/xform/table/filter-column-xform.js index 20fd24628..c900996d4 100644 --- a/lib/xlsx/xform/table/filter-column-xform.js +++ b/lib/xlsx/xform/table/filter-column-xform.js @@ -1,6 +1,29 @@ const BaseXform = require('../base-xform'); +const ListXform = require('../list-xform'); + +const CustomFilterXform = require('./custom-filter-xform'); +const FilterXform = require('./filter-xform'); class FilterColumnXform extends BaseXform { + constructor() { + super(); + + this.map = { + customFilters: new ListXform({ + tag: 'customFilters', + count: false, + empty: true, + childXform: new CustomFilterXform(), + }), + filters: new ListXform({ + tag: 'filters', + count: false, + empty: true, + childXform: new FilterXform(), + }), + }; + } + get tag() { return 'filterColumn'; } @@ -10,6 +33,17 @@ class FilterColumnXform extends BaseXform { } render(xmlStream, model) { + if (model.customFilters) { + xmlStream.openNode(this.tag, { + colId: model.colId, + hiddenButton: model.filterButton ? '0' : '1', + }); + + this.map.customFilters.render(xmlStream, model.customFilters); + + xmlStream.closeNode(); + return true; + } xmlStream.leafNode(this.tag, { colId: model.colId, hiddenButton: model.filterButton ? '0' : '1', @@ -18,20 +52,44 @@ class FilterColumnXform extends BaseXform { } parseOpen(node) { - if (node.name === this.tag) { - const {attributes} = node; - this.model = { - filterButton: attributes.hiddenButton === '0', - }; + if (this.parser) { + this.parser.parseOpen(node); return true; } - return false; + const {attributes} = node; + switch (node.name) { + case this.tag: + this.model = { + filterButton: attributes.hiddenButton === '0', + }; + return true; + default: + this.parser = this.map[node.name]; + if (this.parser) { + this.parseOpen(node); + return true; + } + throw new Error(`Unexpected xml node in parseOpen: ${JSON.stringify(node)}`); + } } parseText() {} - parseClose() { - return false; + parseClose(name) { + if (this.parser) { + if (!this.parser.parseClose(name)) { + this.parser = undefined; + } + return true; + } + switch (name) { + case this.tag: + this.model.customFilters = this.map.customFilters.model; + return false; + default: + // could be some unrecognised tags + return true; + } } } diff --git a/lib/xlsx/xform/table/filter-xform.js b/lib/xlsx/xform/table/filter-xform.js new file mode 100644 index 000000000..91a620a02 --- /dev/null +++ b/lib/xlsx/xform/table/filter-xform.js @@ -0,0 +1,31 @@ +const BaseXform = require('../base-xform'); + +class FilterXform extends BaseXform { + get tag() { + return 'filter'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + val: model.val, + }); + } + + parseOpen(node) { + if (node.name === this.tag) { + this.model = { + val: node.attributes.val, + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = FilterXform; diff --git a/spec/integration/data/test-issue-1669.xlsx b/spec/integration/data/test-issue-1669.xlsx new file mode 100644 index 000000000..cc9dab8f1 Binary files /dev/null and b/spec/integration/data/test-issue-1669.xlsx differ diff --git a/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js b/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js new file mode 100644 index 000000000..3a030c090 --- /dev/null +++ b/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js @@ -0,0 +1,8 @@ +const ExcelJS = verquire('exceljs'); + +describe('github issues', () => { + it('issue 1669 - optional autofilter and custom autofilter on tables', () => { + const wb = new ExcelJS.Workbook(); + return wb.xlsx.readFile('./spec/integration/data/test-issue-1669.xlsx'); + }).timeout(6000); +}); diff --git a/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js b/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js new file mode 100644 index 000000000..971ac53b7 --- /dev/null +++ b/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js @@ -0,0 +1,30 @@ +const testXformHelper = require('../test-xform-helper'); + +const CustomFilterXform = verquire('xlsx/xform/table/custom-filter-xform'); + +const expectations = [ + { + title: 'custom filter', + create() { + return new CustomFilterXform(); + }, + preparedModel: {val: '*brandywine*'}, + xml: '', + parsedModel: {val: '*brandywine*'}, + tests: ['render', 'renderIn', 'parse'], + }, + { + title: 'custom filter with operator', + create() { + return new CustomFilterXform(); + }, + preparedModel: {operator: 'notEqual', val: '4'}, + xml: '', + parsedModel: {operator: 'notEqual', val: '4'}, + tests: ['render', 'renderIn', 'parse'], + }, +]; + +describe('CustomFilterXform', () => { + testXformHelper(expectations); +}); diff --git a/spec/unit/xlsx/xform/table/filter-column-xform.spec.js b/spec/unit/xlsx/xform/table/filter-column-xform.spec.js index 2d9900c17..c15f802fe 100644 --- a/spec/unit/xlsx/xform/table/filter-column-xform.spec.js +++ b/spec/unit/xlsx/xform/table/filter-column-xform.spec.js @@ -31,6 +31,25 @@ const expectations = [ tests: ['prepare', 'render', 'renderIn', 'parse'], options: {index: 1}, }, + { + title: 'with custom filter', + create() { + return new FilterColumnXform(); + }, + initialModel: {filterButton: false, customFilters: [{val: '*brandywine*'}]}, + preparedModel: { + colId: '0', + filterButton: false, + customFilters: [{val: '*brandywine*'}], + }, + xml: + '', + get parsedModel() { + return this.initialModel; + }, + tests: ['prepare', 'render', 'renderIn', 'parse'], + options: {index: 0}, + }, ]; describe('FilterColumnXform', () => {