diff --git a/packages/toast-ui.grid/cypress/integration/relation.spec.ts b/packages/toast-ui.grid/cypress/integration/relation.spec.ts index fef3b337a..ae00395c4 100644 --- a/packages/toast-ui.grid/cypress/integration/relation.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/relation.spec.ts @@ -7,6 +7,7 @@ import { } from '../../samples/relations'; import Grid from '@/grid'; import { deepCopyArray } from '@/helper/common'; +import { OptColumn } from '@t/options'; function changeCellValues(rowKey: number) { // changed fixed value to remove unnecessary paramter for values @@ -318,3 +319,39 @@ describe(`throw error`, () => { }); }); }); + +describe('Dynamic rowSpan', () => { + const rowSpanData = deepCopyArray(data); + rowSpanData[0].category1 = '02'; + rowSpanData[0].category2 = '02_03'; + + it('should not apply dynamic rowSpan to child relation column on ordered relation columns', () => { + const orderedRelationColumnsWithRowSpan = orderedRelationColumns.map((column) => { + (column as OptColumn).rowSpan = true; + return column; + }); + cy.createGrid({ + data: rowSpanData, + columns: orderedRelationColumnsWithRowSpan, + rowSpan: 'all', + }); + + cy.getCell(0, 'category1').should('have.attr', 'rowSpan', 2); + cy.getCell(0, 'category2').should('not.have.attr', 'rowSpan'); + }); + + it('should not apply dynamic rowSpan to child relation column on unordered relation columns', () => { + const unorderedRelationColumnsWithRowSpan = unorderedRelationColumns1.map((column) => { + (column as OptColumn).rowSpan = true; + return column; + }); + cy.createGrid({ + data: rowSpanData, + columns: unorderedRelationColumnsWithRowSpan, + rowSpan: 'all', + }); + + cy.getCell(0, 'category1').should('have.attr', 'rowSpan', 2); + cy.getCell(0, 'category2').should('not.have.attr', 'rowSpan'); + }); +}); diff --git a/packages/toast-ui.grid/cypress/integration/rowSpan.spec.ts b/packages/toast-ui.grid/cypress/integration/rowSpan.spec.ts index 6f5940098..e74b61c58 100644 --- a/packages/toast-ui.grid/cypress/integration/rowSpan.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/rowSpan.spec.ts @@ -1,6 +1,8 @@ import { RowKey, RowSpan } from '@t/store/data'; import { data as sample } from '../../samples/basic'; -import { OptRow } from '@t/options'; +import { OptColumn, OptGrid, OptRow } from '@t/options'; +import { invokeFilter, dragAndDropRow } from '../helper/util'; +import { deepCopyArray } from '@/helper/common'; function createDataWithRowSpanAttr(): OptRow[] { const optRows: OptRow[] = sample.slice(); @@ -320,3 +322,247 @@ it('render rowSpan cell properly by calling setColumns API', () => { cy.getCell(3, 'name').should('have.attr', 'rowSpan', '3'); cy.getCell(4, 'artist').should('have.attr', 'rowSpan', '3'); }); + +describe('Dynamic RowSpan', () => { + const dataForDynamicRowSpan = [ + { name: 'Han', age: 10, value: 1 }, + { name: 'Kim', age: 10, value: 1 }, + { name: 'Cho', age: 20, value: 1 }, + { name: 'Ryu', age: 15, value: 1 }, + { name: 'Lee', age: 15, value: 2 }, + { name: 'Park', age: 10, value: 2 }, + ]; + const columnsForDynamicRowSpanToAll: OptColumn[] = [ + { name: 'name', rowSpan: true }, + { name: 'age', filter: 'number', sortingType: 'asc', sortable: true, rowSpan: true }, + { name: 'value', rowSpan: true }, + ]; + const columnsForDynamicRowSpanToAge: OptColumn[] = [ + { name: 'name' }, + { name: 'age', filter: 'number', sortingType: 'asc', sortable: true, rowSpan: true }, + { name: 'value' }, + ]; + + function createGridWithRowSpan( + options?: Omit, + columnsOptions?: OptColumn[] + ) { + const rowSpanColumns = columnsOptions ?? columnsForDynamicRowSpanToAge; + + cy.createGrid({ + data: dataForDynamicRowSpan, + columns: rowSpanColumns, + ...options, + }); + } + + it("should render rowSpan cell properly for all columns (rowSpan: 'all')", () => { + createGridWithRowSpan({}, columnsForDynamicRowSpanToAll); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(0, 'value').should('have.attr', 'rowSpan', '4'); + cy.getCell(4, 'value').should('have.attr', 'rowSpan', '2'); + }); + + it("should render rowSpan cell properly for specific columns (rowSpan: ['age'])", () => { + createGridWithRowSpan(); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + cy.getColumnCells('value').each(($el) => { + cy.wrap($el).should('not.have.attr', 'rowSpan'); + }); + }); + + describe('With filter', () => { + it('should render rowSpan cell properly with filter', () => { + createGridWithRowSpan(); + + invokeFilter('age', [{ code: 'eq', value: 10 }]); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '3'); + }); + + it('should render rowSpan cell properly with unfilter', () => { + createGridWithRowSpan(); + + invokeFilter('age', [{ code: 'eq', value: 10 }]); + + cy.gridInstance().invoke('unfilter', 'age'); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + }); + + describe('With row D&D', () => { + it('should reset rowSpan when row drag started', () => { + createGridWithRowSpan({ draggable: true }); + + cy.getCell(0, '_draggable').trigger('mousedown'); + + cy.getColumnCells('age').each(($el) => { + cy.wrap($el).should('not.have.attr', 'rowSpan'); + }); + }); + + it('should render rowSpan cell properly after D&D', () => { + createGridWithRowSpan({ draggable: true }); + + dragAndDropRow(0, 250); + + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(5, 'age').should('have.attr', 'rowSpan', '2'); + }); + }); + + describe('With sort', () => { + it('should render rowSpan cell properly after sorting (asc)', () => { + createGridWithRowSpan(); + + cy.gridInstance().invoke('sort', 'age', true); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '3'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after sorting (desc)', () => { + createGridWithRowSpan(); + + cy.gridInstance().invoke('sort', 'age', false); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '3'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after unsorting', () => { + createGridWithRowSpan(); + + cy.gridInstance().invoke('sort', 'age', true); + cy.gridInstance().invoke('unsort', 'age'); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + }); + + describe('With pagination', () => { + it('should render rowSpan cell properly with pagination', () => { + createGridWithRowSpan({ + pageOptions: { + useClient: true, + perPage: 5, + }, + }); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should not apply rowSpan to another page cell', () => { + createGridWithRowSpan({ + pageOptions: { + useClient: true, + perPage: 4, + }, + }); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('not.have.attr', 'rowSpan'); + }); + }); + + describe('With other data modifying APIs', () => { + it('should render rowSpan cell properly after showColumn()', () => { + const columnsWithHideAge = deepCopyArray(columnsForDynamicRowSpanToAge); + columnsWithHideAge[1].hidden = true; + createGridWithRowSpan({}, columnsWithHideAge); + + cy.gridInstance().invoke('showColumn', 'age'); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after setValue()', () => { + createGridWithRowSpan(); + + cy.gridInstance().invoke('setValue', 2, 'age', 10); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '3'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after setColumnValues()', () => { + createGridWithRowSpan(); + + cy.gridInstance().invoke('setColumnValues', 'age', 10); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '6'); + }); + + it('should render rowSpan cell properly after appendRow()', () => { + createGridWithRowSpan(); + + const appendedRow = { name: 'Choi', age: 10, value: 3 }; + + cy.gridInstance().invoke('appendRow', appendedRow); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(5, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after prependRow()', () => { + createGridWithRowSpan(); + + const prependedRow = { name: 'Choi', age: 10, value: 3 }; + + cy.gridInstance().invoke('prependRow', prependedRow); + + cy.getCell(6, 'age').should('have.attr', 'rowSpan', '3'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after removeRow()', () => { + createGridWithRowSpan(); + + cy.gridInstance().invoke('removeRow', 0); + + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after setRow()', () => { + createGridWithRowSpan(); + + const setRow = { name: 'Cho', age: 10, value: 1 }; + + cy.gridInstance().invoke('setRow', 2, setRow); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '3'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after appendRows()', () => { + createGridWithRowSpan(); + + const appendedRow = [{ name: 'Choi', age: 10, value: 3 }]; + + cy.gridInstance().invoke('appendRows', appendedRow); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(5, 'age').should('have.attr', 'rowSpan', '2'); + }); + + it('should render rowSpan cell properly after resetData()', () => { + createGridWithRowSpan(); + + cy.gridInstance().invoke('resetData', dataForDynamicRowSpan); + + cy.getCell(0, 'age').should('have.attr', 'rowSpan', '2'); + cy.getCell(3, 'age').should('have.attr', 'rowSpan', '2'); + }); + }); +}); diff --git a/packages/toast-ui.grid/cypress/integration/tree.spec.ts b/packages/toast-ui.grid/cypress/integration/tree.spec.ts index d6a2a9fa1..4016c8b2f 100644 --- a/packages/toast-ui.grid/cypress/integration/tree.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/tree.spec.ts @@ -1589,3 +1589,23 @@ it('should update row number after calling appendTreeRow()', () => { ['test1'], ]); }); + +it('should not apply dynamic rowSpan', () => { + const treeColumnsWithRowSpan = columns.map((column) => { + column.rowSpan = true; + return column; + }); + cy.createGrid({ + data, + columns: treeColumnsWithRowSpan, + treeColumnOptions: { + name: 'c1', + }, + }); + + ['c1', 'c2'].forEach((columnName) => { + cy.getColumnCells(columnName).each(($el) => { + cy.wrap($el).should('not.have.attr', 'rowSpan'); + }); + }); +}); diff --git a/packages/toast-ui.grid/docs/ko/row-span.md b/packages/toast-ui.grid/docs/ko/row-span.md index b6dc4768d..b6446ba9f 100644 --- a/packages/toast-ui.grid/docs/ko/row-span.md +++ b/packages/toast-ui.grid/docs/ko/row-span.md @@ -1,15 +1,83 @@ # 둜우 슀팬 πŸ–– -TOAST UI GridλŠ” μ—°μ†λ˜λŠ” λ‘œμš°λ“€μ˜ 데이터λ₯Ό 컬럼 λ‹¨μœ„λ‘œ 병합할 수 μžˆλŠ” 둜우 슀팬 κΈ°λŠ₯을 μ œκ³΅ν•œλ‹€. λ³‘ν•©λœ μ…€ μžμ²΄κ°€ ν•˜λ‚˜μ˜ μ…€λ‘œ κ°„μ£Όλ˜λ―€λ‘œ, `focus`, `selection` 의 κ²½μš°μ—λ„ μ—¬λŸ¬ 개의 셀이 μ•„λ‹Œ ν•˜λ‚˜μ˜ μ…€λ‘œμ„œ λ™μž‘ν•œλ‹€. +TOAST UI GridλŠ” μ—°μ†λ˜λŠ” λ‘œμš°λ“€μ˜ 데이터λ₯Ό 컬럼 λ‹¨μœ„λ‘œ 병합할 수 μžˆλŠ” rowspan κΈ°λŠ₯을 μ œκ³΅ν•œλ‹€. λ³‘ν•©λœ μ…€ μžμ²΄κ°€ ν•˜λ‚˜μ˜ μ…€λ‘œ κ°„μ£Όλ˜λ―€λ‘œ, `focus`, `selection` 의 κ²½μš°μ—λ„ μ—¬λŸ¬ 개의 셀이 μ•„λ‹Œ ν•˜λ‚˜μ˜ μ…€λ‘œμ„œ λ™μž‘ν•œλ‹€. -## 둜우 슀팬 UI +## rowspan UI -둜우 슀팬이 적용된 경우, 둜우의 νŠΉμ • 셀듀이 λ³‘ν•©λ˜μ–΄ μ•„λž˜μ²˜λŸΌ ν‘œν˜„λœλ‹€. +rowspan이 적용된 경우, 둜우의 νŠΉμ • 셀듀이 λ³‘ν•©λ˜μ–΄ μ•„λž˜μ²˜λŸΌ ν‘œν˜„λœλ‹€. ![row-span](https://user-images.githubusercontent.com/37766175/62029543-cdea7080-b21d-11e9-9411-5ed8e2a734b2.png) ## μ˜΅μ…˜ +병합 λŒ€μƒ 컬럼의 `rowSpan` 속성 값을 `true`둜 μ„€μ •ν•œλ‹€. + +```ts +const grid = new Grid({ + // ..., + columns: [ + { + header: 'Name', + name: 'name', + rowSpan: true + }, + { + header: 'Artist', + name: 'artist', + rowSpan: true + } + ], + // ... +}); +``` + +## rowspan λ™μž‘ + +### κΈ°λ³Έ 데이터 +κ·Έλ¦¬λ“œ 생성 μ‹œ μ„ νƒν•œ μ»¬λŸΌμ— λŒ€ν•΄ rowspan을 μ μš©ν•œλ‹€. + + + +### ν•„ν„° +ν•„ν„° 적용 μ‹œ λ³€κ²½λœ 데이터에 λŒ€ν•΄ rowspan을 μž¬μ μš©ν•œλ‹€. + +![](https://user-images.githubusercontent.com/41339744/145195895-bd62f5c7-12e0-44eb-9468-b8f957593ac5.gif) + +### D&D +λ“œλž˜κ·Έ μ‹œμž‘ μ‹œ λͺ¨λ“  rowspan을 μ΄ˆκΈ°ν™” ν•œ ν›„ λ“œλ‘­ μ‹œ rowspan을 재적용 ν•œλ‹€. +![](https://user-images.githubusercontent.com/41339744/145196027-8e4e2b36-d051-47ed-bc72-e0b09e2ac476.gif) + +### μ •λ ¬ +μ •λ ¬ μ‹œ λ³€κ²½λœ 데이터에 λŒ€ν•΄ rowspan을 μž¬μ μš©ν•œλ‹€. + +![](https://user-images.githubusercontent.com/41339744/145196155-a51b211e-1a86-455c-968d-a747089115ab.gif) + +### νŽ˜μ΄μ§€λ„€μ΄μ…˜ +νŽ˜μ΄μ§€κ°€ λ‹€λ₯Έ 경우 μ΄μ–΄μ§€λŠ” 값이 같더라도 λ³„λ„μ˜ rowspan을 μ μš©ν•œλ‹€. + +![](https://user-images.githubusercontent.com/41339744/145196260-f9223857-43ad-4a8e-9452-d95c0dec3f70.gif) + +### μ—λ””νŠΈ +μ…€ 값이 λ³€κ²½λœ 경우 그에 따라 rowspan을 μž¬μ μš©ν•œλ‹€. rowspan이 적용된 μ…€ 값을 λ³€κ²½ν•˜λ©΄ rowspan으둜 묢인 둜우 쀑 μ΅œμƒλ‹¨ 둜우의 μ…€ κ°’λ§Œ λ³€κ²½λœλ‹€. + +![](https://user-images.githubusercontent.com/41339744/145196389-b67242db-9b97-4433-ac5a-03a890f85e0a.gif) + +### 컬럼 관계 +컬럼 관계 λ‚΄ μ΅œμƒμœ„ κ΄€κ³„μ˜ μ»¬λŸΌμ„ μ œμ™Έν•œ ν•˜μœ„ μ»¬λŸΌμ—λŠ” 동적 rowspan을 μ μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. + + + +### 트리 +트리 λ°μ΄ν„°μ˜ 경우 rowspan을 μ μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. + +## 예제 + +rowspan μ˜ˆμ œλŠ” [링크](http://nhn.github.io/tui.grid/latest/tutorial-example29-dynamic-row-span)λ₯Ό 톡해 확인할 수 μžˆλ‹€. + +## 데이터 _attributes의 μ˜΅μ…˜μœΌλ‘œ 적용 +> **deprecated**, 컬럼 μ˜΅μ…˜ μ‚¬μš© +### μ˜΅μ…˜ + `_attributes.rowSpan` μ˜΅μ…˜μ— 병합 λŒ€μƒ 컬럼과 개수λ₯Ό 각각 킀와 μ†μ„±μœΌλ‘œ μ„€μ •ν•œλ‹€. ```js @@ -26,6 +94,7 @@ const grid = new Grid({ name: '19', artist: 'Adele', _attributes: { + // deprecated rowSpan: { // Merge rows artist: 3 } @@ -53,15 +122,15 @@ const grid = new Grid({ }); ``` -## API +### API -`appendRow` 와 `removeRow` APIμ—λŠ” 둜우 슀팬이 적용된 경우만 λ™μž‘ν•˜λŠ” μ˜΅μ…˜μ΄ μžˆλ‹€. +`appendRow` 와 `removeRow` APIμ—λŠ” rowspan이 적용된 경우만 λ™μž‘ν•˜λŠ” μ˜΅μ…˜μ΄ μžˆλ‹€. | API | μ˜΅μ…˜ | μ„€λͺ… | | --- | --- | --- | | `appendRow`| `extendPrevRowSpan` | μΆ”κ°€λœ 둜우의 이전(μœ„) λ‘œμš°κ°€ λ³‘ν•©λœ μ…€μ˜ κ°€μž₯ ν•˜λ‹¨ 둜우인 경우, 이λ₯Ό ν™•μž₯ν•˜μ—¬ μΆ”κ°€ 병합할 지 κ²°μ •ν•˜λŠ” μ˜΅μ…˜μ΄λ‹€. λ§Œμ•½ λ‘œμš°κ°€ λ³‘ν•©λœ μ…€μ˜ 쀑간에 μΆ”κ°€λ˜λŠ” κ²½μš°λŠ” μ˜΅μ…˜ κ°’κ³Ό 상관없이 μΆ”κ°€ λ³‘ν•©λœλ‹€. | -| `removeRow`| `keepRowSpanData` | 둜우 슀팬이 적용된 κ°€μž₯ μƒλ‹¨μ˜ λ‘œμš°κ°€ μ‚­μ œλœ 경우(λ³‘ν•©λœ μ…€μ˜ κ°€μž₯ 상단 둜우), λ‹€μŒ 둜우λ₯Ό κΈ°μ€€μœΌλ‘œ 둜우 μŠ€νŒ¬μ„ μœ μ§€ν•  지 κ²°μ •ν•˜λŠ” μ˜΅μ…˜μ΄λ‹€. λ§Œμ•½ λ³‘ν•©λœ μ…€μ˜ 쀑간에 μžˆλŠ” λ‘œμš°κ°€ μ‚­μ œλœ κ²½μš°λŠ” μ˜΅μ…˜ κ°’κ³Ό 상관없이 κΈ°μ‘΄ 병합 μƒνƒœλ₯Ό μœ μ§€ν•œλ‹€.| +| `removeRow`| `keepRowSpanData` | rowspan이 적용된 κ°€μž₯ μƒλ‹¨μ˜ λ‘œμš°κ°€ μ‚­μ œλœ 경우(λ³‘ν•©λœ μ…€μ˜ κ°€μž₯ 상단 둜우), λ‹€μŒ 둜우λ₯Ό κΈ°μ€€μœΌλ‘œ rowspan을 μœ μ§€ν•  지 κ²°μ •ν•˜λŠ” μ˜΅μ…˜μ΄λ‹€. λ§Œμ•½ λ³‘ν•©λœ μ…€μ˜ 쀑간에 μžˆλŠ” λ‘œμš°κ°€ μ‚­μ œλœ κ²½μš°λŠ” μ˜΅μ…˜ κ°’κ³Ό 상관없이 κΈ°μ‘΄ 병합 μƒνƒœλ₯Ό μœ μ§€ν•œλ‹€.| -## 예제 +### 예제 -[μ—¬κΈ°](http://nhn.github.io/tui.grid/latest/tutorial-example06-attributes)μ„œ 둜우 슀팬 예제λ₯Ό 확인할 수 μžˆλ‹€. +rowspan μ˜ˆμ œλŠ” [링크](http://nhn.github.io/tui.grid/latest/tutorial-example06-attributes)λ₯Ό 톡해 확인할 수 μžˆλ‹€. diff --git a/packages/toast-ui.grid/examples/example29-dynamic-row-span.html b/packages/toast-ui.grid/examples/example29-dynamic-row-span.html new file mode 100644 index 000000000..cb26ec4b9 --- /dev/null +++ b/packages/toast-ui.grid/examples/example29-dynamic-row-span.html @@ -0,0 +1,73 @@ + + + + + 29. Dynamic rowSpan + + + + + +
+ You can see the tutorial + here +
+ The example code can be slower than your environment because the code is transpiled by babel-standalone in runtime. +
+ +
+
+
+ + + + + + diff --git a/packages/toast-ui.grid/src/dispatch/column.ts b/packages/toast-ui.grid/src/dispatch/column.ts index a5188aea1..b9a9877fb 100644 --- a/packages/toast-ui.grid/src/dispatch/column.ts +++ b/packages/toast-ui.grid/src/dispatch/column.ts @@ -25,6 +25,7 @@ import { import { getTreeIndentWidth } from '../store/helper/tree'; import { getDepth, isTreeColumnName } from '../query/tree'; import { TREE_CELL_HORIZONTAL_PADDING } from '../helper/constant'; +import { updateRowSpan } from './rowSpan'; import { isRowHeader } from '../helper/column'; import { isAllColumnsVisible } from '../query/column'; @@ -193,8 +194,9 @@ export function hideColumn(store: Store, columnName: string) { setColumnsHiddenValue(column, columnName, true); } -export function showColumn({ column }: Store, columnName: string) { - setColumnsHiddenValue(column, columnName, false); +export function showColumn(store: Store, columnName: string) { + setColumnsHiddenValue(store.column, columnName, false); + updateRowSpan(store); } export function setComplexColumnHeaders(store: Store, complexColumnHeaders: ComplexColumnInfo[]) { diff --git a/packages/toast-ui.grid/src/dispatch/create.ts b/packages/toast-ui.grid/src/dispatch/create.ts index 9ea5b6131..f45573ca9 100644 --- a/packages/toast-ui.grid/src/dispatch/create.ts +++ b/packages/toast-ui.grid/src/dispatch/create.ts @@ -14,6 +14,7 @@ import * as filter from './filter'; import * as pagination from './pagination'; import * as contextMenu from './contextMenu'; import * as exportData from './export'; +import * as rowSpan from './rowSpan'; import { Store } from '@t/store'; const dispatchMap = { @@ -33,6 +34,7 @@ const dispatchMap = { ...pagination, ...contextMenu, ...exportData, + ...rowSpan, }; type DispatchMap = typeof dispatchMap; diff --git a/packages/toast-ui.grid/src/dispatch/data.ts b/packages/toast-ui.grid/src/dispatch/data.ts index 12a80121f..26b168a9d 100644 --- a/packages/toast-ui.grid/src/dispatch/data.ts +++ b/packages/toast-ui.grid/src/dispatch/data.ts @@ -61,7 +61,12 @@ import { initFilter, resetFilterState } from './filter'; import { initScrollPosition } from './viewport'; import { isCheckboxColumn, isDragColumn, isRowHeader, isRowNumColumn } from '../helper/column'; import { updatePageOptions, updatePageWhenRemovingRow, resetPageState } from './pagination'; -import { updateRowSpanWhenAppending, updateRowSpanWhenRemoving } from './rowSpan'; +import { + resetRowSpan, + updateRowSpan, + updateRowSpanWhenAppending, + updateRowSpanWhenRemoving, +} from './rowSpan'; import { createObservableData } from './lazyObservable'; import { removeUniqueInfoMap, @@ -163,6 +168,8 @@ export function setValue( } } + resetRowSpan(store); + const targetColumn = findProp('name', columnName, columnsWithoutRowHeader); const orgValue = targetRow[columnName]; @@ -172,6 +179,7 @@ export function setValue( targetColumn.onBeforeChange(gridEvent); if (gridEvent.isStopped()) { + updateRowSpan(store); return; } } @@ -188,6 +196,7 @@ export function setValue( */ eventBus.trigger('beforeChange', gridEvent); if (gridEvent.isStopped()) { + updateRowSpan(store); return; } @@ -211,7 +220,7 @@ export function setValue( updateSummaryValueByCell(store, columnName, { orgValue, value }); getDataManager(id).push('UPDATE', targetRow); - if (!isEmpty(rowSpanMap) && rowSpanMap[columnName] && isRowSpanEnabled(sortState)) { + if (!isEmpty(rowSpanMap) && rowSpanMap[columnName] && isRowSpanEnabled(sortState, column)) { const { spanCount } = rowSpanMap[columnName]; // update sub rows value for (let count = 1; count < spanCount; count += 1) { @@ -241,6 +250,8 @@ export function setValue( * @property {Grid} instance - Current grid instance */ eventBus.trigger('afterChange', gridEvent); + + updateRowSpan(store); } export function isUpdatableRowAttr(name: keyof RowAttributes, checkDisabled: boolean) { @@ -324,6 +335,7 @@ export function setColumnValues( updateSummaryValueByColumn(store, columnName, { value }); forceValidateUniquenessOfColumn(data.rawData, column, columnName); setAutoResizingColumnWidths(store); + updateRowSpan(store); } export function check(store: Store, rowKey: RowKey) { @@ -534,9 +546,9 @@ export function setRowCheckDisabled(store: Store, disabled: boolean, rowKey: Row } export function appendRow(store: Store, row: OptRow, options: OptAppendRow) { - const { data, id } = store; + const { data, column, id } = store; const { rawData, viewData, sortState, pageOptions } = data; - const { at = rawData.length } = options; + const { at = rawData.length, extendPrevRowSpan } = options; const { rawRow, viewRow, prevRow } = getCreatedRowInfo(store, at, row); const inserted = at !== rawData.length; @@ -552,8 +564,11 @@ export function appendRow(store: Store, row: OptRow, options: OptAppendRow) { sortByCurrentState(store); - if (prevRow && isRowSpanEnabled(sortState)) { - updateRowSpanWhenAppending(rawData, prevRow, options.extendPrevRowSpan || false); + if (isRowSpanEnabled(sortState, column)) { + if (prevRow) { + updateRowSpanWhenAppending(rawData, prevRow, extendPrevRowSpan || false); + } + updateRowSpan(store); } getDataManager(id).push('CREATE', rawRow, inserted); @@ -587,7 +602,7 @@ export function removeRow(store: Store, rowKey: RowKey, options: OptRemoveRow) { } initSelection(store); - if (nextRow && isRowSpanEnabled(sortState)) { + if (nextRow && isRowSpanEnabled(sortState, column)) { updateRowSpanWhenRemoving(rawData, removedRow, nextRow, options.keepRowSpanData || false); } @@ -648,6 +663,7 @@ export function resetData(store: Store, inputData: OptRow[], options: ResetOptio getDataManager(id).setOriginData(inputData); getDataManager(id).clearAll(); setColumnWidthsByText(store); + updateRowSpan(store); setTimeout(() => { /** @@ -786,7 +802,7 @@ export function setRow(store: Store, rowIndex: number, row: OptRow) { sortByCurrentState(store); - if (prevRow && isRowSpanEnabled(sortState)) { + if (prevRow && isRowSpanEnabled(sortState, column)) { updateRowSpanWhenAppending(rawData, prevRow, false); } @@ -797,6 +813,7 @@ export function setRow(store: Store, rowIndex: number, row: OptRow) { }); updateSummaryValueByRow(store, rawRow, { type: 'SET', orgRow }); postUpdateAfterManipulation(store, rowIndex, 'DONE'); + updateRowSpan(store); } export function moveRow(store: Store, rowKey: RowKey, targetIndex: number) { @@ -851,15 +868,19 @@ export function scrollToNext(store: Store) { export function appendRows(store: Store, inputData: OptRow[]) { const { data, column, id } = store; + const startIndex = data.rawData.length; + const { rawData, viewData } = createData(id, inputData, column, { lazyObservable: true }); + if (!column.keyColumnName) { const rowKey = getMaxRowKey(data); - inputData.forEach((row, index) => { + rawData.forEach((row, index) => { row.rowKey = rowKey + index; }); - } - const startIndex = data.rawData.length; - const { rawData, viewData } = createData(id, inputData, column, { lazyObservable: true }); + viewData.forEach((row, index) => { + row.rowKey = rowKey + index; + }); + } const newRawData = data.rawData.concat(rawData); const newViewData = data.viewData.concat(viewData); @@ -871,6 +892,7 @@ export function appendRows(store: Store, inputData: OptRow[]) { sortByCurrentState(store); updateHeights(store); postUpdateAfterManipulation(store, startIndex, 'DONE', rawData); + updateRowSpan(store); } export function removeRows(store: Store, targetRows: RemoveTargetRows) { @@ -888,7 +910,7 @@ export function removeRows(store: Store, targetRows: RemoveTargetRows) { removeUniqueInfoMap(id, removedRow, column); if (nextRow) { - if (isRowSpanEnabled(sortState)) { + if (isRowSpanEnabled(sortState, column)) { updateRowSpanWhenRemoving(rawData, removedRow, nextRow, false); } } diff --git a/packages/toast-ui.grid/src/dispatch/export.ts b/packages/toast-ui.grid/src/dispatch/export.ts index c54a37748..6f76faba0 100644 --- a/packages/toast-ui.grid/src/dispatch/export.ts +++ b/packages/toast-ui.grid/src/dispatch/export.ts @@ -160,6 +160,7 @@ function exportCallback( exportCSV(fileName, targetText); } else { if (!XLSX?.writeFile) { + // eslint-disable-next-line no-console console.error( '[tui/grid] - Not found the dependency "xlsx". You should install the "xlsx" to export the data as Excel format' ); @@ -204,6 +205,7 @@ export function execExport(store: Store, format: 'csv' | 'xlsx', options?: OptEx exportCSV(fileName, targetText); } else { if (!XLSX?.writeFile) { + // eslint-disable-next-line no-console console.error( '[tui/grid] - Not found the dependency "xlsx". You should install the "xlsx" to export the data as Excel format' ); diff --git a/packages/toast-ui.grid/src/dispatch/filter.ts b/packages/toast-ui.grid/src/dispatch/filter.ts index 063c7de70..c5e83a838 100644 --- a/packages/toast-ui.grid/src/dispatch/filter.ts +++ b/packages/toast-ui.grid/src/dispatch/filter.ts @@ -21,6 +21,7 @@ import { setCheckedAllRows, updateHeights } from './data'; import { updateAllSummaryValues } from './summary'; import { createFilterEvent, EventType, EventParams } from '../query/filter'; import { updatePageOptions } from './pagination'; +import { updateRowSpan } from './rowSpan'; function initLayerAndScrollAfterFiltering(store: Store) { const { data } = store; @@ -197,6 +198,8 @@ export function filter( initLayerAndScrollAfterFiltering(store); updateAllSummaryValues(store); emitAfterFilter(store, 'afterFilter', columnName); + + updateRowSpan(store); } export function updateFilters({ data }: Store, columnName: string, nextColumnFilterState: Filter) { @@ -259,6 +262,8 @@ export function unfilter(store: Store, columnName?: string) { updateAllSummaryValues(store); emitAfterFilter(store, 'afterUnfilter', columnName); } + + updateRowSpan(store); } export function setFilter( diff --git a/packages/toast-ui.grid/src/dispatch/focus.ts b/packages/toast-ui.grid/src/dispatch/focus.ts index 8beb084e7..5441f42b1 100644 --- a/packages/toast-ui.grid/src/dispatch/focus.ts +++ b/packages/toast-ui.grid/src/dispatch/focus.ts @@ -121,7 +121,7 @@ export function changeFocus( if (!gridEvent.isStopped()) { let focusRowKey = rowKey; - if (rowKey && columnName && isRowSpanEnabled(sortState)) { + if (rowKey && columnName && isRowSpanEnabled(sortState, column)) { const rowSpan = getRowSpanByRowKey(rowKey, columnName, rawData); if (rowSpan) { focusRowKey = rowSpan.mainRowKey; diff --git a/packages/toast-ui.grid/src/dispatch/keyboard.ts b/packages/toast-ui.grid/src/dispatch/keyboard.ts index 8aabb889c..88096476c 100644 --- a/packages/toast-ui.grid/src/dispatch/keyboard.ts +++ b/packages/toast-ui.grid/src/dispatch/keyboard.ts @@ -90,13 +90,8 @@ export function moveTabFocus(store: Store, command: TabCommandType) { } export function moveSelection(store: Store, command: KeyboardEventCommandType) { - const { - selection, - focus, - data, - column: { visibleColumnsWithRowHeader, rowHeaderCount }, - id, - } = store; + const { selection, focus, data, column, id } = store; + const { visibleColumnsWithRowHeader, rowHeaderCount } = column; const { filteredViewData, sortState } = data; const { rowIndex: focusRowIndex, totalColumnIndex: totalFocusColumnIndex } = focus; let { inputRange: currentInputRange } = selection; @@ -126,7 +121,7 @@ export function moveSelection(store: Store, command: KeyboardEventCommandType) { nextCellIndexes = [rowLength - 1, columnLength - 1]; } else { nextCellIndexes = getNextCellIndex(store, command, [rowIndex, columnIndex]); - if (isRowSpanEnabled(sortState)) { + if (isRowSpanEnabled(sortState, column)) { nextCellIndexes = getNextCellIndexWithRowSpan( store, command, @@ -146,7 +141,7 @@ export function moveSelection(store: Store, command: KeyboardEventCommandType) { [startRowIndex, endRowIndex] = getRowRangeWithRowSpan( [startRowIndex, endRowIndex], [columnStartIndex, nextColumnIndex], - visibleColumnsWithRowHeader, + column, focus.rowIndex, data ); diff --git a/packages/toast-ui.grid/src/dispatch/mouse.ts b/packages/toast-ui.grid/src/dispatch/mouse.ts index 28e4845f2..79a53275c 100644 --- a/packages/toast-ui.grid/src/dispatch/mouse.ts +++ b/packages/toast-ui.grid/src/dispatch/mouse.ts @@ -92,7 +92,6 @@ function updateSelection(store: Store, dragData: PagePosition) { const { scrollTop, scrollLeft } = viewport; const { pageX, pageY } = dragData; const { inputRange: curInputRange } = selection; - const { visibleColumnsWithRowHeader } = column; let startRowIndex, startColumnIndex, endRowIndex; const viewInfo = { pageX, pageY, scrollTop, scrollLeft }; @@ -116,7 +115,7 @@ function updateSelection(store: Store, dragData: PagePosition) { [startRowIndex, endRowIndex] = getRowRangeWithRowSpan( [startRowIndex, endRowIndex], [startColumnIndex, endColumnIndex], - visibleColumnsWithRowHeader, + column, store.focus.rowIndex, data ); @@ -283,7 +282,7 @@ export function mouseDownRowHeader(store: Store, rowKey: RowKey) { const [startRowIndex, endRowIndex] = getRowRangeWithRowSpan( [rowIndexPerPage, rowIndexPerPage], [rowHeaderCount, endColumnIndex], - visibleColumnsWithRowHeader, + column, null, data ); @@ -303,7 +302,7 @@ export function mouseDownRowHeader(store: Store, rowKey: RowKey) { export function dragMoveRowHeader(store: Store, dragData: PagePosition) { const { viewport, selection, id, data, column } = store; const { scrollTop, scrollLeft } = viewport; - const { visibleColumnsWithRowHeader, rowHeaderCount } = column; + const { rowHeaderCount } = column; const { pageX, pageY } = dragData; const { inputRange: curInputRange } = selection; @@ -319,7 +318,7 @@ export function dragMoveRowHeader(store: Store, dragData: PagePosition) { [startRowIndex, endRowIndex] = getRowRangeWithRowSpan( [startRowIndex, endRowIndex], [rowHeaderCount, columnIndex], - visibleColumnsWithRowHeader, + column, null, data ); diff --git a/packages/toast-ui.grid/src/dispatch/rowSpan.ts b/packages/toast-ui.grid/src/dispatch/rowSpan.ts index a11a669e6..7a22ea87f 100644 --- a/packages/toast-ui.grid/src/dispatch/rowSpan.ts +++ b/packages/toast-ui.grid/src/dispatch/rowSpan.ts @@ -1,6 +1,11 @@ -import { Row } from '@t/store/data'; +import { Row, RowSpanAttributeValue } from '@t/store/data'; import { createRowSpan } from '../store/data'; -import { findProp, isEmpty, findPropIndex } from '../helper/common'; +import { findProp, isEmpty, findPropIndex, find } from '../helper/common'; +import { Store } from '@t/store'; +import { Dictionary } from '@t/options'; +import { notify } from '../helper/observable'; +import { getRowSpanOfColumn } from '../query/rowSpan'; +import { DEFAULT_PER_PAGE } from '../helper/constant'; export function updateRowSpanWhenAppending(data: Row[], prevRow: Row, extendPrevRowSpan: boolean) { const { rowSpanMap: prevRowSpanMap } = prevRow; @@ -73,6 +78,51 @@ export function updateRowSpanWhenRemoving( }); } +export function updateRowSpan(store: Store) { + const { data, column } = store; + const { filteredRawData, pageOptions } = data; + const { perPage: perPageOption } = pageOptions; + const rowSpans: Dictionary = {}; + const perPage = !isEmpty(pageOptions) && !perPageOption ? DEFAULT_PER_PAGE : perPageOption; + + if (column.visibleRowSpanEnabledColumns.length > 0) { + resetRowSpan(store, true); + + column.visibleRowSpanEnabledColumns.forEach(({ name }) => { + const rowSpanOfColumn = getRowSpanOfColumn(filteredRawData, name, perPage); + + Object.keys(rowSpanOfColumn).forEach((rowKey) => { + if (rowSpans[rowKey]) { + rowSpans[rowKey][name] = rowSpanOfColumn[rowKey][name]; + } else { + rowSpans[rowKey] = rowSpanOfColumn[rowKey]; + } + }); + }); + + Object.keys(rowSpans).forEach((rowKey) => { + const row = find(({ rowKey: key }) => `${key}` === rowKey, filteredRawData); + + updateMainRowSpan(filteredRawData, row!, rowSpans[rowKey]); + }); + + notify(data, 'rawData', 'filteredRawData', 'viewData', 'filteredViewData'); + } +} + +export function updateMainRowSpan(data: Row[], mainRow: Row, rowSpan: RowSpanAttributeValue) { + if (rowSpan) { + const { rowKey, rowSpanMap } = mainRow; + + Object.keys(rowSpan).forEach((columnName) => { + const spanCount = rowSpan[columnName]; + + rowSpanMap[columnName] = createRowSpan(true, rowKey, spanCount, spanCount); + updateSubRowSpan(data, mainRow, columnName, 1, spanCount); + }); + } +} + function updateSubRowSpan( data: Row[], mainRow: Row, @@ -87,3 +137,15 @@ function updateSubRowSpan( row.rowSpanMap[columnName] = createRowSpan(false, mainRow.rowKey, -offset, spanCount); } } + +export function resetRowSpan({ data }: Store, slient = false) { + data.rawData.forEach(({ rowSpanMap }) => { + Object.keys(rowSpanMap).forEach((columnName) => { + delete rowSpanMap[columnName]; + }); + }); + + if (!slient) { + notify(data, 'rawData', 'filteredRawData', 'viewData', 'filteredViewData'); + } +} diff --git a/packages/toast-ui.grid/src/dispatch/selection.ts b/packages/toast-ui.grid/src/dispatch/selection.ts index a5d897dae..973e4cb2a 100644 --- a/packages/toast-ui.grid/src/dispatch/selection.ts +++ b/packages/toast-ui.grid/src/dispatch/selection.ts @@ -28,12 +28,8 @@ export function changeSelectionRange( } export function setSelection(store: Store, range: { start: Range; end: Range }) { - const { - selection, - data, - column: { visibleColumnsWithRowHeader, rowHeaderCount }, - id, - } = store; + const { selection, data, column, id } = store; + const { visibleColumnsWithRowHeader, rowHeaderCount } = column; const { viewData } = data; const rowLength = viewData.length; const columnLength = visibleColumnsWithRowHeader.length; @@ -46,7 +42,7 @@ export function setSelection(store: Store, range: { start: Range; end: Range }) [startRowIndex, endRowIndex] = getRowRangeWithRowSpan( [startRowIndex, endRowIndex], [startColumnIndex, endColumnIndex], - visibleColumnsWithRowHeader, + column, null, data ); diff --git a/packages/toast-ui.grid/src/dispatch/sort.ts b/packages/toast-ui.grid/src/dispatch/sort.ts index 30d2278e6..4123a472f 100644 --- a/packages/toast-ui.grid/src/dispatch/sort.ts +++ b/packages/toast-ui.grid/src/dispatch/sort.ts @@ -10,6 +10,7 @@ import { updateRowNumber, setCheckedAllRows } from './data'; import { isSortable, isInitialSortState, isScrollPagination, isSorted } from '../query/data'; import { isComplexHeader } from '../query/column'; import { isCancelSort, createSortEvent, EventType, EventParams } from '../query/sort'; +import { updateRowSpan } from './rowSpan'; function createSoretedViewData(rawData: Row[]) { return rawData.map( @@ -165,6 +166,8 @@ export function sort( applySortedData(store); emitAfterSort(store, cancelSort, columnName); + + updateRowSpan(store); } export function unsort(store: Store, columnName = 'sortKey') { @@ -189,6 +192,8 @@ export function unsort(store: Store, columnName = 'sortKey') { } applySortedData(store); emitAfterSort(store, true, columnName); + + updateRowSpan(store); } export function initSortState(data: Data) { diff --git a/packages/toast-ui.grid/src/grid.tsx b/packages/toast-ui.grid/src/grid.tsx index 47f00daaa..6513d9075 100644 --- a/packages/toast-ui.grid/src/grid.tsx +++ b/packages/toast-ui.grid/src/grid.tsx @@ -343,6 +343,8 @@ export default class Grid implements TuiGrid { this.gridEl = render(, el); this.dispatch('setColumnWidthsByText'); + + this.dispatch('updateRowSpan'); } /** @@ -1718,6 +1720,7 @@ export default class Grid implements TuiGrid { } else { this.dispatch('moveRow', rowKey, targetIndex); } + this.dispatch('updateRowSpan'); } /** diff --git a/packages/toast-ui.grid/src/helper/constant.ts b/packages/toast-ui.grid/src/helper/constant.ts index f45df758d..e922732a2 100644 --- a/packages/toast-ui.grid/src/helper/constant.ts +++ b/packages/toast-ui.grid/src/helper/constant.ts @@ -2,6 +2,7 @@ export const FILTER_DEBOUNCE_TIME = 50; export const TREE_INDENT_WIDTH = 22; export const TREE_CELL_HORIZONTAL_PADDING = 19; export const RIGHT_MOUSE_BUTTON = 2; +export const DEFAULT_PER_PAGE = 20; export const DISABLED_PRIORITY_NONE = 'NONE'; export const DISABLED_PRIORITY_CELL = 'CELL'; export const DISABLED_PRIORITY_ROW = 'ROW'; diff --git a/packages/toast-ui.grid/src/query/data.ts b/packages/toast-ui.grid/src/query/data.ts index 1e97f6006..22165519c 100644 --- a/packages/toast-ui.grid/src/query/data.ts +++ b/packages/toast-ui.grid/src/query/data.ts @@ -17,7 +17,6 @@ import { omit, } from '../helper/common'; import { getDataManager } from '../instance'; -import { isRowSpanEnabled } from './rowSpan'; import { isHiddenColumn } from './column'; import { createRawRow, generateDataCreationKey } from '../store/data'; import { getFormattedValue as formattedValue } from '../store/helper/data'; @@ -101,12 +100,12 @@ export function findIndexByRowKey( return -1; } - const { filteredRawData, rawData, sortState } = data; + const { filteredRawData, rawData } = data; const targetData = filtered ? filteredRawData : rawData; const dataManager = getDataManager(id); const modified = dataManager ? dataManager.isMixedOrder() : false; - if (!isRowSpanEnabled(sortState) || column.keyColumnName || modified) { + if (isSorted(data) || column.keyColumnName || modified) { return findPropIndex('rowKey', rowKey, targetData); } diff --git a/packages/toast-ui.grid/src/query/keyboard.ts b/packages/toast-ui.grid/src/query/keyboard.ts index 72d249186..e5ceefbee 100644 --- a/packages/toast-ui.grid/src/query/keyboard.ts +++ b/packages/toast-ui.grid/src/query/keyboard.ts @@ -41,10 +41,10 @@ export function getNextCellIndex( ): CellIndex { const { data, - column: { visibleColumnsWithRowHeader, rowHeaderCount }, + column, rowCoords: { heights }, } = store; - + const { visibleColumnsWithRowHeader, rowHeaderCount } = column; const { sortState, filteredRawData, pageRowRange } = data; const lastRowIndex = @@ -57,13 +57,13 @@ export function getNextCellIndex( switch (command) { case 'up': - if (isRowSpanEnabled(sortState)) { + if (isRowSpanEnabled(sortState, column)) { rowIndex = getRowSpanTopIndex(rowIndex, columnName, filteredRawData); } rowIndex = getPrevRowIndex(rowIndex, heights); break; case 'down': - if (isRowSpanEnabled(sortState)) { + if (isRowSpanEnabled(sortState, column)) { rowIndex = getRowSpanBottomIndex(rowIndex, columnName, filteredRawData); } rowIndex = getNextRowIndex(rowIndex, heights); @@ -101,7 +101,7 @@ export function getNextCellIndex( break; } if (lastColumn) { - if (isRowSpanEnabled(sortState)) { + if (isRowSpanEnabled(sortState, column)) { rowIndex = getRowSpanBottomIndex(rowIndex, columnName, filteredRawData); } rowIndex = getNextRowIndex(rowIndex, heights); @@ -115,7 +115,7 @@ export function getNextCellIndex( break; } if (firstColumn) { - if (isRowSpanEnabled(sortState)) { + if (isRowSpanEnabled(sortState, column)) { rowIndex = getRowSpanTopIndex(rowIndex, columnName, filteredRawData); } rowIndex = getPrevRowIndex(rowIndex, heights); diff --git a/packages/toast-ui.grid/src/query/rowSpan.ts b/packages/toast-ui.grid/src/query/rowSpan.ts index 2a828968b..ea4730649 100644 --- a/packages/toast-ui.grid/src/query/rowSpan.ts +++ b/packages/toast-ui.grid/src/query/rowSpan.ts @@ -1,9 +1,24 @@ -import { RowSpan, Row, Data, RowKey, SortState } from '@t/store/data'; -import { ColumnInfo } from '@t/store/column'; +import { + RowSpan, + Row, + Data, + RowKey, + SortState, + RowSpanAttributeValue, + CellValue, +} from '@t/store/data'; +import { Column, ColumnInfo } from '@t/store/column'; import { RowCoords } from '@t/store/rowCoords'; import { Range } from '@t/store/selection'; import { findPropIndex, isEmpty, isNull } from '../helper/common'; import { getSortedRange } from './selection'; +import { Dictionary } from '@t/options'; + +interface OptGetRowSpan { + rowKey: RowKey; + value: CellValue; + index: number; +} function getMainRowSpan(columnName: string, rowSpan: RowSpan, data: Row[]) { const { mainRow, mainRowKey } = rowSpan; @@ -26,21 +41,22 @@ function getRowSpanRange( let [startRowIndex, endRowIndex] = rowRange; for (let index = startColumnIndex; index <= endColumnIndex; index += 1) { - const { rawData } = data; - const { rowSpanMap: startRowSpanMap } = rawData[startRowIndex]; - const { rowSpanMap: endRowSpanMap } = rawData[endRowIndex]; + const { filteredRawData } = data; + const { rowSpanMap: startRowSpanMap } = filteredRawData[startRowIndex]; + const { rowSpanMap: endRowSpanMap } = filteredRawData[endRowIndex]; const columnName = visibleColumns[index].name; // get top row index of topmost rowSpan if (startRowSpanMap[columnName]) { const { mainRowKey } = startRowSpanMap[columnName]; - const topRowSpanIndex = findPropIndex('rowKey', mainRowKey, rawData); + const topRowSpanIndex = findPropIndex('rowKey', mainRowKey, filteredRawData); startRowIndex = startRowIndex > topRowSpanIndex ? topRowSpanIndex : startRowIndex; } // get bottom row index of bottommost rowSpan if (endRowSpanMap[columnName]) { const { mainRowKey, spanCount } = endRowSpanMap[columnName]; - const bottomRowSpanIndex = findPropIndex('rowKey', mainRowKey, rawData) + spanCount - 1; + const bottomRowSpanIndex = + findPropIndex('rowKey', mainRowKey, filteredRawData) + spanCount - 1; endRowIndex = endRowIndex < bottomRowSpanIndex ? bottomRowSpanIndex : endRowIndex; } } @@ -83,12 +99,18 @@ export function getMaxRowSpanRange( export function getRowRangeWithRowSpan( rowRange: Range, colRange: Range, - visibleColumnsWithRowHeader: ColumnInfo[], + column: Column, rowIndex: number | null, data: Data ): Range { - if (isRowSpanEnabled(data.sortState)) { - return getMaxRowSpanRange(rowRange, colRange, visibleColumnsWithRowHeader, rowIndex, data); + if (isRowSpanEnabled(data.sortState, column)) { + return getMaxRowSpanRange( + rowRange, + colRange, + column.visibleColumnsWithRowHeader, + rowIndex, + data + ); } return rowRange; @@ -168,6 +190,36 @@ export function getMaxRowSpanCount(rowIndex: number, data: Row[]) { ); } -export function isRowSpanEnabled(sortState: SortState) { - return sortState.columns[0].columnName === 'sortKey'; +export function isRowSpanEnabled(sortState: SortState, column?: Column) { + return ( + sortState.columns[0].columnName === 'sortKey' || !!column?.visibleRowSpanEnabledColumns.length + ); +} + +export function getRowSpanOfColumn(data: Row[], columnName: string, perPage?: number) { + const rowSpanOfColumn: Dictionary = {}; + let rowSpan: RowSpanAttributeValue = {}; + let mainRowKey: RowKey | null = null; + let mainRowValue: CellValue = null; + + data.forEach(({ rowKey, [columnName]: value }, index) => { + const isRowInNextPage = perPage && index !== 0 && index % perPage === 0; + if (mainRowValue !== value || isRowInNextPage) { + if (!isNull(mainRowKey) && rowSpan[columnName] !== 1) { + rowSpanOfColumn[mainRowKey] = rowSpan; + } + rowSpan = {}; + rowSpan[columnName] = 1; + mainRowKey = rowKey; + mainRowValue = value; + } else { + rowSpan[columnName] += 1; + } + }); + + if (!isNull(mainRowKey) && rowSpan[columnName] !== 1) { + rowSpanOfColumn[mainRowKey] = rowSpan; + } + + return rowSpanOfColumn; } diff --git a/packages/toast-ui.grid/src/store/column.ts b/packages/toast-ui.grid/src/store/column.ts index 452c1ef92..8f8e619db 100644 --- a/packages/toast-ui.grid/src/store/column.ts +++ b/packages/toast-ui.grid/src/store/column.ts @@ -253,6 +253,11 @@ export function createColumn( columnHeaderInfo ); + const useRowSpanOption = + column.rowSpan && !treeColumnOptions.name && !includes(relationColumns, column.name); + + const rowSpan = useRowSpanOption ? column.rowSpan : false; + return observable({ name, escapeHTML, @@ -288,6 +293,7 @@ export function createColumn( disabled, comparator, autoResizing: width === 'auto', + rowSpan, }); } @@ -503,6 +509,10 @@ export function create({ }; }, + get visibleRowSpanEnabledColumns() { + return this.visibleColumns.filter(({ rowSpan }) => rowSpan); + }, + get defaultValues() { return this.allColumns .filter(({ defaultValue }) => Boolean(defaultValue)) diff --git a/packages/toast-ui.grid/src/store/data.ts b/packages/toast-ui.grid/src/store/data.ts index 0bf1a8fcc..1cf4f41b2 100644 --- a/packages/toast-ui.grid/src/store/data.ts +++ b/packages/toast-ui.grid/src/store/data.ts @@ -34,6 +34,7 @@ import { addUniqueInfoMap, getValidationCode } from './helper/validation'; import { isScrollPagination } from '../query/data'; import { getFormattedValue, setMaxTextMap } from './helper/data'; import { + DEFAULT_PER_PAGE, DISABLED_PRIORITY_CELL, DISABLED_PRIORITY_COLUMN, DISABLED_PRIORITY_NONE, @@ -363,12 +364,9 @@ export function createRawRow( options: RawRowOptions = {} ) { // this rowSpan variable is attribute option before creating rowSpanDataMap - let rowSpan: RowSpanAttributeValue; - const { keyColumnName, prevRow, lazyObservable = false, disabled = false } = options; + const rowSpan = row._attributes?.rowSpan as RowSpanAttributeValue; - if (row._attributes) { - rowSpan = row._attributes.rowSpan as RowSpanAttributeValue; - } + const { keyColumnName, prevRow, lazyObservable = false, disabled = false } = options; if (keyColumnName) { row.rowKey = row[keyColumnName]; @@ -405,6 +403,15 @@ export function createData( const { keyColumnName, treeColumnName = '' } = column; let rawData: Row[]; + // Notify when using deprecated option "_attribute.rowSpan". + const isUseRowSpanOption = data.some((row) => row._attributes?.rowSpan); + if (isUseRowSpanOption) { + // eslint-disable-next-line no-console + console.warn( + 'The option "_attribute.rowSpan" is deprecated. Please use rowSpan option of column.\nFollow example: http://nhn.github.io/tui.grid/latest/tutorial-example29-dynamic-row-span' + ); + } + if (treeColumnName) { rawData = createTreeRawData({ id, @@ -477,7 +484,7 @@ function createPageOptions(userPageOptions: PageOptions, rawData: Row[]) { : { useClient: false, page: 1, - perPage: 20, + perPage: DEFAULT_PER_PAGE, type: 'pagination', ...userPageOptions, totalCount: userPageOptions.useClient ? rawData.length : userPageOptions.totalCount!, diff --git a/packages/toast-ui.grid/src/store/focus.ts b/packages/toast-ui.grid/src/store/focus.ts index 8723019e5..3da3b6d25 100644 --- a/packages/toast-ui.grid/src/store/focus.ts +++ b/packages/toast-ui.grid/src/store/focus.ts @@ -112,7 +112,7 @@ export function create({ const bottom = top + rowCoords.heights[rowIndex]; const rowSpan = getRowSpanByRowKey(rowKey!, columnName, filteredRawData); - if (isRowSpanEnabled(sortState) && rowSpan) { + if (isRowSpanEnabled(sortState, column) && rowSpan) { const verticalPos = getVerticalPosWithRowSpan( columnName, rowSpan, diff --git a/packages/toast-ui.grid/src/store/viewport.ts b/packages/toast-ui.grid/src/store/viewport.ts index 62e363c34..8e032b4b6 100644 --- a/packages/toast-ui.grid/src/store/viewport.ts +++ b/packages/toast-ui.grid/src/store/viewport.ts @@ -19,19 +19,29 @@ interface ViewportOption { showDummyRows: boolean; } +interface CalculateRangeOption { + scrollPos: number; + totalSize: number; + offsets: number[]; + data: Data; + column: Column; + rowCalculation?: boolean; +} + function findIndexByPosition(offsets: number[], position: number) { const rowOffset = findIndex((offset) => offset > position, offsets); return rowOffset === -1 ? offsets.length - 1 : rowOffset - 1; } -function calculateRange( - scrollPos: number, - totalSize: number, - offsets: number[], - data: Data, - rowCalculation?: boolean -): Range { +function calculateRange({ + scrollPos, + totalSize, + offsets, + data, + column, + rowCalculation, +}: CalculateRangeOption): Range { // safari uses negative scroll position for bouncing effect scrollPos = Math.max(scrollPos, 0); @@ -44,7 +54,7 @@ function calculateRange( [start, end] = pageRowRange; } - if (dataLength && dataLength >= start && rowCalculation && isRowSpanEnabled(sortState)) { + if (dataLength && dataLength >= start && rowCalculation && isRowSpanEnabled(sortState, column)) { const maxRowSpanCount = getMaxRowSpanCount(start, filteredRawData); const topRowSpanIndex = start - maxRowSpanCount; @@ -94,12 +104,13 @@ export function create({ // only for right side columns get colRange() { - const range = calculateRange( - this.scrollLeft, - columnCoords.areaWidth.R, - columnCoords.offsets.R, - data - ); + const range = calculateRange({ + scrollPos: this.scrollLeft, + totalSize: columnCoords.areaWidth.R, + offsets: columnCoords.offsets.R, + data, + column, + }); return getCachedRange(this.__storage__.colRange, range); }, @@ -114,13 +125,14 @@ export function create({ }, get rowRange() { - const range = calculateRange( - this.scrollTop, - dimension.bodyHeight, - rowCoords.offsets, + const range = calculateRange({ + scrollPos: this.scrollTop, + totalSize: dimension.bodyHeight, + offsets: rowCoords.offsets, data, - true - ); + column, + rowCalculation: true, + }); return getCachedRange(this.__storage__.rowRange, range); }, diff --git a/packages/toast-ui.grid/src/view/bodyArea.tsx b/packages/toast-ui.grid/src/view/bodyArea.tsx index 0184eda21..f8b320483 100644 --- a/packages/toast-ui.grid/src/view/bodyArea.tsx +++ b/packages/toast-ui.grid/src/view/bodyArea.tsx @@ -217,6 +217,7 @@ class BodyAreaComp extends Component { private startToDragRow = (posInfo: PosInfo) => { const container = this.el.parentElement!.parentElement!; posInfo.container = container; + this.props.dispatch('resetRowSpan'); const draggableInfo = createDraggableRowInfo(this.context.store, posInfo); if (draggableInfo) { @@ -374,6 +375,7 @@ class BodyAreaComp extends Component { } // clear floating element and draggable info this.clearDraggableInfo(); + this.props.dispatch('updateRowSpan'); }; private clearDraggableInfo() { diff --git a/packages/toast-ui.grid/src/view/rowSpanCell.tsx b/packages/toast-ui.grid/src/view/rowSpanCell.tsx index e22220427..64bc2e2d0 100644 --- a/packages/toast-ui.grid/src/view/rowSpanCell.tsx +++ b/packages/toast-ui.grid/src/view/rowSpanCell.tsx @@ -4,6 +4,7 @@ import { ColumnInfo } from '@t/store/column'; import { connect } from './hoc'; import { DispatchProps } from '../dispatch/create'; import { BodyCell } from './bodyCell'; +import { isRowSpanEnabled } from '../query/rowSpan'; interface OwnProps { viewRow: ViewRow; @@ -43,10 +44,12 @@ export class RowSpanCellComp extends Component { } } -export const RowSpanCell = connect(({ data }, { viewRow, columnInfo }) => { - const { sortState } = data; - const rowSpan = (viewRow.rowSpanMap && viewRow.rowSpanMap[columnInfo.name]) || null; - const enableRowSpan = sortState.columns[0].columnName === 'sortKey'; +export const RowSpanCell = connect( + ({ data, column }, { viewRow, columnInfo }) => { + const { sortState } = data; + const rowSpan = (viewRow.rowSpanMap && viewRow.rowSpanMap[columnInfo.name]) || null; + const enableRowSpan = isRowSpanEnabled(sortState, column); - return { rowSpan, enableRowSpan }; -})(RowSpanCellComp); + return { rowSpan, enableRowSpan }; + } +)(RowSpanCellComp); diff --git a/packages/toast-ui.grid/tuidoc.config.json b/packages/toast-ui.grid/tuidoc.config.json index 4b812ce88..eb631a883 100644 --- a/packages/toast-ui.grid/tuidoc.config.json +++ b/packages/toast-ui.grid/tuidoc.config.json @@ -69,7 +69,8 @@ "example25-large-data-performance": "25. Large Data Performance", "example26-infinite-scroll": "26. infinite scroll", "example27-export": "27. Export", - "example28-drag-and-drop": "28. Drag and Drop" + "example28-drag-and-drop": "28. Drag and Drop", + "example29-dynamic-row-span": "29. Dynamic rowSpan" }, "globalErrorLogVariable": "errorLogs" }, diff --git a/packages/toast-ui.grid/types/store/column.d.ts b/packages/toast-ui.grid/types/store/column.d.ts index 95ebef45c..1d811d55f 100644 --- a/packages/toast-ui.grid/types/store/column.d.ts +++ b/packages/toast-ui.grid/types/store/column.d.ts @@ -145,6 +145,7 @@ export interface CommonColumnInfo { onBeforeChange?: GridEventListener; onAfterChange?: GridEventListener; comparator?: Comparator; + rowSpan?: boolean; } export interface ColumnInfo extends CommonColumnInfo { @@ -193,6 +194,7 @@ export interface Column { readonly visibleFrozenCount: number; readonly visibleColumnsBySide: VisibleColumnsBySide; readonly visibleColumnsBySideWithRowHeader: VisibleColumnsBySide; + readonly visibleRowSpanEnabledColumns: ColumnInfo[]; readonly defaultValues: { name: string; value: CellValue }[]; readonly validationColumns: ColumnInfo[]; readonly ignoredColumns: string[]; diff --git a/packages/toast-ui.grid/types/store/data.d.ts b/packages/toast-ui.grid/types/store/data.d.ts index 1111a7eed..9577b8ba7 100644 --- a/packages/toast-ui.grid/types/store/data.d.ts +++ b/packages/toast-ui.grid/types/store/data.d.ts @@ -1,4 +1,4 @@ -import { Dictionary } from '../options'; +import { Dictionary, RecursivePartial } from '../options'; import { Filter } from './filterLayerState'; import { InvalidColumn, Comparator, ErrorInfo } from './column'; import { Range } from './selection'; @@ -51,7 +51,7 @@ export interface RowAttributes { } export interface RowSpanAttribute { - rowSpan?: Dictionary; + rowSpan: Dictionary; } export interface RowSpan {