From 43a41b1fcdb1d9f3f3603638ab966077c9a04c9b Mon Sep 17 00:00:00 2001 From: Joe Bordes Date: Thu, 15 Apr 2021 03:22:38 +0200 Subject: [PATCH 01/14] docs: add coreBOS to used by (#1303) * docs: add coreBOS to used by * docs: change URL of coreBOS to home site --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 661c39974..e30c052d0 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ For more information on PR's step, please see links of Contributing section. - [Shop by](https://www.godo.co.kr/shopby/main.gd) - [Payco](https://www.payco.com/) - [YES24 - Movie Management System (Admin Tools)](http://m.movie.yes24.com/Movie/CurrentMovie.aspx) +- [coreBOS](https://corebos.com) ## πŸ“œ License From 18c4aef5bb1fccd6308d3febbd65fdd175c39e8d Mon Sep 17 00:00:00 2001 From: jaesung-lee Date: Thu, 15 Apr 2021 16:54:10 +0900 Subject: [PATCH 02/14] fix: copy the data without encoding (#1310) * fix: copy the data without encoding * chore: innerHTML => textContent * chore: apply code review --- packages/toast-ui.grid/src/query/clipboard.ts | 12 ++++-------- packages/toast-ui.grid/src/view/clipboard.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/toast-ui.grid/src/query/clipboard.ts b/packages/toast-ui.grid/src/query/clipboard.ts index a1f8484ca..a057f288b 100644 --- a/packages/toast-ui.grid/src/query/clipboard.ts +++ b/packages/toast-ui.grid/src/query/clipboard.ts @@ -27,8 +27,8 @@ function getTextWithCopyOptionsApplied( if (copyOptions) { if (copyOptions.customValue) { text = getCustomValue(copyOptions.customValue, valueMap.value, rawData, column); - } else if (copyOptions.useListItemText && editorOptions) { - const { listItems } = (editorOptions as unknown) as ListItemOptions; + } else if (copyOptions.useListItemText && editorOptions?.listItems) { + const { listItems } = editorOptions as ListItemOptions; const { value } = valueMap; let valueList = [value]; const result: CellValue[] = []; @@ -94,12 +94,8 @@ function getValuesToString(store: Store) { return rowList .map(({ valueMap }) => columnInRange - .map(({ name }, index) => - getTextWithCopyOptionsApplied( - valueMap[name], - filteredRawData, - visibleColumnsWithRowHeader[index] - ) + .map((targetColumn) => + getTextWithCopyOptionsApplied(valueMap[targetColumn.name], filteredRawData, targetColumn) ) .join('\t') ) diff --git a/packages/toast-ui.grid/src/view/clipboard.tsx b/packages/toast-ui.grid/src/view/clipboard.tsx index 14c8ce11f..b91d1d1dc 100644 --- a/packages/toast-ui.grid/src/view/clipboard.tsx +++ b/packages/toast-ui.grid/src/view/clipboard.tsx @@ -82,7 +82,7 @@ class ClipboardComp extends Component { return; } const { store } = this.context; - this.el.innerHTML = getText(store); + this.el.textContent = getText(store); if (isSupportWindowClipboardData()) { setClipboardSelection(this.el.childNodes[0]); @@ -109,12 +109,12 @@ class ClipboardComp extends Component { let data; if (html && html.indexOf('table') !== -1) { // step 1: Append copied data on contenteditable element to parsing correctly table data. - el.innerHTML = html; + el.textContent = html; // step 2: Make grid data from cell data of appended table element. const { rows } = el.querySelector('tbody')!; data = convertTableToData(rows); // step 3: Empty contenteditable element to reset. - el.innerHTML = ''; + el.textContent = ''; } else { data = convertTextToData(clipboardData.getData('text/plain')); } @@ -175,7 +175,7 @@ class ClipboardComp extends Component { if (!this.el) { return; } - const text = this.el.innerHTML; + const text = this.el.textContent!; if (isSupportWindowClipboardData()) { (window as WindowWithClipboard).clipboardData!.setData('Text', text); } else if (ev.clipboardData) { From 2a8fffc7e66ae96d0ebba86553380db5051daeba Mon Sep 17 00:00:00 2001 From: jaesung-lee Date: Thu, 15 Apr 2021 17:17:57 +0900 Subject: [PATCH 03/14] fix: cannot show the dummy cells at the farthest right in scrollX (#1312) * fix: duplcated component key * fix: wrong offsetTop, offsetLeft style * chore: add dummy story(show the cell with scroll) * chore: add integration test for dummy rows and remove story * chore: fix broken test case --- .../cypress/integration/dummyRows.spec.ts | 79 +++++++++++++------ packages/toast-ui.grid/src/view/bodyArea.tsx | 4 +- .../toast-ui.grid/src/view/bodyDummyRow.tsx | 2 +- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/packages/toast-ui.grid/cypress/integration/dummyRows.spec.ts b/packages/toast-ui.grid/cypress/integration/dummyRows.spec.ts index 6396240b0..73aed44d8 100644 --- a/packages/toast-ui.grid/cypress/integration/dummyRows.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/dummyRows.spec.ts @@ -1,35 +1,70 @@ +import { cls } from '@/helper/dom'; + before(() => { cy.visit('/dist'); }); -beforeEach(() => { - const columns = [{ name: 'id', editor: 'text' }, { name: 'name' }]; - const data = [ - { id: 1, name: 'Kim', score: 90, grade: 'A' }, - { id: 2, name: 'Lee', score: 80, grade: 'B' }, - ]; - cy.createGrid({ data, columns, bodyHeight: 400, showDummyRows: true }); -}); +describe('basic case', () => { + beforeEach(() => { + const columns = [{ name: 'id', editor: 'text' }, { name: 'name' }]; + const data = [ + { id: 1, name: 'Kim', score: 90, grade: 'A' }, + { id: 2, name: 'Lee', score: 80, grade: 'B' }, + ]; + cy.createGrid({ data, columns, bodyHeight: 400, showDummyRows: true }); + }); -it('should render dummy rows', () => { - cy.getByCls('cell-dummy').should('exist'); -}); + it('should render dummy rows', () => { + cy.getByCls('cell-dummy').should('exist'); + }); + + it('should initialize focus and selection layer', () => { + cy.gridInstance().invoke('focusAt', 0, 0); + cy.gridInstance().invoke('setSelectionRange', { start: [0, 0], end: [0, 0] }); + + cy.getByCls('cell-dummy').first().click(); + + cy.getByCls('layer-focus').should('not.exist'); + cy.getByCls('layer-selection').should('not.visible'); + }); -it('should inialize focus and selection layer', () => { - cy.gridInstance().invoke('focusAt', 0, 0); - cy.gridInstance().invoke('setSelectionRange', { start: [0, 0], end: [0, 0] }); + it('should save the editing result when clicking the dummy cell', () => { + cy.gridInstance().invoke('startEditing', 0, 'id'); + cy.getByCls('content-text').type('test'); - cy.getByCls('cell-dummy').first().click(); + cy.getByCls('cell-dummy').first().click(); - cy.getByCls('layer-focus').should('not.exist'); - cy.getByCls('layer-selection').should('not.visible'); + cy.getCell(0, 'id').should('have.text', 'test'); + }); }); -it('should save the editing result when clicking the dummy cell', () => { - cy.gridInstance().invoke('startEditing', 0, 'id'); - cy.getByCls('content-text').type('test'); +describe('The grid with filters', () => { + beforeEach(() => { + const columns = [ + { name: 'name', minWidth: 200, filter: 'text' }, + { name: 'artist', minWidth: 200 }, + { name: 'type', minWidth: 200 }, + { name: 'price', minWidth: 200 }, + { name: 'release', minWidth: 200 }, + { name: 'genre', minWidth: 200 }, + ]; + const data = [ + { name: 'Beautiful Lies', artist: 'Birdy' }, + { name: 'X', artist: 'Ed Sheeran' }, + ]; + cy.createGrid({ data, columns, bodyHeight: 400, showDummyRows: true }); + }); - cy.getByCls('cell-dummy').first().click(); + it('should show dummy cells when scrollX is located at the farthest right', () => { + cy.gridInstance().invoke('filter', 'name', [{ code: 'eq', value: 'text' }]); + cy.getRsideBody() + .scrollTo('right') + .should(($el) => { + setTimeout(() => { + const leftPos = $el.find(`.${cls('table-container')}`).css('left'); - cy.getCell(0, 'id').should('have.text', 'test'); + expect(leftPos).eq('200px'); + }); + }); + }); }); diff --git a/packages/toast-ui.grid/src/view/bodyArea.tsx b/packages/toast-ui.grid/src/view/bodyArea.tsx index 95946733e..2c55a28a9 100644 --- a/packages/toast-ui.grid/src/view/bodyArea.tsx +++ b/packages/toast-ui.grid/src/view/bodyArea.tsx @@ -213,8 +213,8 @@ class BodyAreaComp extends Component { areaStyle.overflowY = 'hidden'; } const tableContainerStyle = { - top: totalRowHeight ? offsetTop : 0, - left: totalRowHeight ? offsetLeft : 0, + top: offsetTop, + left: offsetLeft, height: dummyRowCount ? bodyHeight - scrollXHeight : '', overflow: dummyRowCount ? 'hidden' : 'visible', }; diff --git a/packages/toast-ui.grid/src/view/bodyDummyRow.tsx b/packages/toast-ui.grid/src/view/bodyDummyRow.tsx index 825c5e825..de6f76e54 100644 --- a/packages/toast-ui.grid/src/view/bodyDummyRow.tsx +++ b/packages/toast-ui.grid/src/view/bodyDummyRow.tsx @@ -25,7 +25,7 @@ const BodyDummyRowComp = ({ columnNames, rowHeight, index }: Props) => { return ( ); From 8abdd5e1b90cf470cd4554c213d1aeb44f8c8c96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 15:11:53 +0900 Subject: [PATCH 04/14] chore(deps): bump y18n from 3.2.1 to 3.2.2 (#1293) Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/yargs/y18n/releases) - [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md) - [Commits](https://github.com/yargs/y18n/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/toast-ui.react-grid/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/toast-ui.react-grid/package-lock.json b/packages/toast-ui.react-grid/package-lock.json index e95413cc5..83f375c5c 100644 --- a/packages/toast-ui.react-grid/package-lock.json +++ b/packages/toast-ui.react-grid/package-lock.json @@ -15379,9 +15379,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { @@ -15411,9 +15411,9 @@ "dev": true }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", "dev": true } } From 01f7acccd55f5c518ba6c51ed8c0999357b3cec0 Mon Sep 17 00:00:00 2001 From: jaesung-lee Date: Thu, 22 Apr 2021 16:35:07 +0900 Subject: [PATCH 05/14] fix: selectbox, checkbox editor height (#1322) * fix: add select box max-height * fix: set max-height to checkbox editor --- packages/toast-ui.grid/src/css/grid.css | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/toast-ui.grid/src/css/grid.css b/packages/toast-ui.grid/src/css/grid.css index e4b496d16..55324f8aa 100644 --- a/packages/toast-ui.grid/src/css/grid.css +++ b/packages/toast-ui.grid/src/css/grid.css @@ -843,11 +843,17 @@ outline: none; } +.tui-grid-container .tui-select-box-dropdown { + max-height: 180px; +} + .tui-grid-editor-checkbox-list-layer { position: absolute; background-color: #fff; border: 1px solid #aaa; z-index: 100; + max-height: 180px; + overflow: hidden auto; } .tui-grid-editor-checkbox-list-layer * { @@ -855,8 +861,12 @@ } .tui-grid-editor-checkbox-list-layer .tui-grid-editor-checkbox { - height: 32px; line-height: 32px; + height: 32px; +} + +.tui-grid-editor-checkbox-list-layer .tui-grid-editor-checkbox:last-child { + margin-bottom: 1px; } .tui-grid-editor-checkbox-hovered { @@ -927,4 +937,4 @@ .tui-grid-container .tui-calendar-month .tui-calendar-body, .tui-grid-container .tui-calendar-year .tui-calendar-body { width: 220px; -} \ No newline at end of file +} From 08df83a32c43e4026bc47c79fa2863cc79644035 Mon Sep 17 00:00:00 2001 From: jaesung-lee Date: Thu, 22 Apr 2021 16:58:41 +0900 Subject: [PATCH 06/14] fix: emit script errors when data is not in the select box list (#1326) --- .../toast-ui.grid/cypress/integration/editor.spec.ts | 12 +++++++++++- packages/toast-ui.grid/src/editor/select.ts | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/toast-ui.grid/cypress/integration/editor.spec.ts b/packages/toast-ui.grid/cypress/integration/editor.spec.ts index 67416c1e0..03c0a5b2a 100644 --- a/packages/toast-ui.grid/cypress/integration/editor.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/editor.spec.ts @@ -334,7 +334,7 @@ it('should do synchronous rendering of the editing cell', () => { describe('select, checkbox, radio editor', () => { function createGridWithType(type: string) { - const data = [{ name: '1' }, { name: '2' }, { name: '3' }]; + const data = [{ name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }]; const columns = [ { name: 'name', @@ -391,6 +391,16 @@ describe('select, checkbox, radio editor', () => { } }); }); + + it('should return a empty string when selecting a value that is not in the select box', () => { + createGridWithType('select'); + + cy.gridInstance().invoke('startEditing', 3, 'name'); + // finish editing by clicking the another cell + cy.getCell(0, 'name').click(); + + cy.getCell(3, 'name').should('have.text', ''); + }); }); // @TODO: There is a issue that the checkbox is unchecked when cypress type. Only the hover style is tested. diff --git a/packages/toast-ui.grid/src/editor/select.ts b/packages/toast-ui.grid/src/editor/select.ts index 9aade225b..297b0826d 100644 --- a/packages/toast-ui.grid/src/editor/select.ts +++ b/packages/toast-ui.grid/src/editor/select.ts @@ -98,7 +98,7 @@ export class SelectEditor implements CellEditor { } public getValue() { - return this.selectBoxEl.getSelectedItem().getValue(); + return this.selectBoxEl.getSelectedItem()?.getValue() ?? ''; } public mounted() { From b37b5832942b611164c86e533f11d2f57d9636cc Mon Sep 17 00:00:00 2001 From: jaesung-lee Date: Thu, 22 Apr 2021 17:19:03 +0900 Subject: [PATCH 07/14] fix: add i18n for filter select option (#1325) * fix: add i18n for filter select option * chore: add filter button i18n * chore: remove unnecessary blank line --- packages/toast-ui.grid/src/helper/filter.ts | 58 +++++++++++-------- packages/toast-ui.grid/src/i18n/index.ts | 28 +++++++++ .../src/view/datePickerFilter.tsx | 5 +- .../src/view/filterLayerInner.tsx | 5 +- .../toast-ui.grid/src/view/selectFilter.tsx | 3 +- .../toast-ui.grid/src/view/textFilter.tsx | 5 +- packages/toast-ui.grid/types/options.d.ts | 14 +++++ 7 files changed, 86 insertions(+), 32 deletions(-) diff --git a/packages/toast-ui.grid/src/helper/filter.ts b/packages/toast-ui.grid/src/helper/filter.ts index 8928ea889..64ecd2bae 100644 --- a/packages/toast-ui.grid/src/helper/filter.ts +++ b/packages/toast-ui.grid/src/helper/filter.ts @@ -7,6 +7,7 @@ import { } from '@t/store/filterLayerState'; import { CellValue } from '@t/store/data'; import { isString, endsWith, startsWith } from './common'; +import i18n from '../i18n'; interface FilterSelectOption { number: { [key in NumberFilterCode]: string }; @@ -14,31 +15,38 @@ interface FilterSelectOption { date: { [key in DateFilterCode]: string }; } -export const filterSelectOption: FilterSelectOption = { - number: { - eq: '=', - lt: '<', - gt: '>', - lte: '<=', - gte: '>=', - ne: '!=', - }, - text: { - contain: 'Contains', - eq: 'Equals', - ne: 'Not equals', - start: 'Starts with', - end: 'Ends with', - }, - date: { - eq: 'Equals', - ne: 'Not equals', - after: 'After', - afterEq: 'After or Equal', - before: 'Before', - beforeEq: 'Before or Equal', - }, -}; +let filterSelectOption: FilterSelectOption; + +export function createFilterSelectOption(): FilterSelectOption { + if (!filterSelectOption) { + filterSelectOption = { + number: { + eq: '=', + lt: '<', + gt: '>', + lte: '<=', + gte: '>=', + ne: '!=', + }, + text: { + contain: i18n.get('filter.contains'), + eq: i18n.get('filter.eq'), + ne: i18n.get('filter.ne'), + start: i18n.get('filter.start'), + end: i18n.get('filter.end'), + }, + date: { + eq: i18n.get('filter.eq'), + ne: i18n.get('filter.ne'), + after: i18n.get('filter.after'), + afterEq: i18n.get('filter.afterEq'), + before: i18n.get('filter.before'), + beforeEq: i18n.get('filter.beforeEq'), + }, + }; + } + return filterSelectOption; +} export function getUnixTime(value: CellValue) { return parseInt((new Date(String(value)).getTime() / 1000).toFixed(0), 10); diff --git a/packages/toast-ui.grid/src/i18n/index.ts b/packages/toast-ui.grid/src/i18n/index.ts index b9dea29f4..a706ea283 100644 --- a/packages/toast-ui.grid/src/i18n/index.ts +++ b/packages/toast-ui.grid/src/i18n/index.ts @@ -26,6 +26,20 @@ const messages: OptI18nLanguage = { noDataToModify: 'No data to modify.', failResponse: 'An error occurred while requesting data.\nPlease try again.', }, + filter: { + contains: 'Contains', + eq: 'Equals', + ne: 'Not equals', + start: 'Starts with', + end: 'Ends with', + after: 'After', + afterEq: 'After or Equal', + before: 'Before', + beforeEq: 'Before or Equal', + apply: 'Apply', + clear: 'Clear', + selectAll: 'Select All', + }, }, ko: { display: { @@ -45,6 +59,20 @@ const messages: OptI18nLanguage = { noDataToModify: 'μ²˜λ¦¬ν•  데이터가 μ—†μŠ΅λ‹ˆλ‹€.', failResponse: '데이터 μš”μ²­ 쀑에 μ—λŸ¬κ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€.\nλ‹€μ‹œ μ‹œλ„ν•˜μ—¬ μ£Όμ‹œκΈ° λ°”λžλ‹ˆλ‹€.', }, + filter: { + contains: 'Contains', + eq: 'Equals', + ne: 'Not equals', + start: 'Starts with', + end: 'Ends with', + after: 'After', + afterEq: 'After or Equal', + before: 'Before', + beforeEq: 'Before or Equal', + apply: 'Apply', + clear: 'Clear', + selectAll: 'Select All', + }, }, }; diff --git a/packages/toast-ui.grid/src/view/datePickerFilter.tsx b/packages/toast-ui.grid/src/view/datePickerFilter.tsx index 73784117e..1e166726d 100644 --- a/packages/toast-ui.grid/src/view/datePickerFilter.tsx +++ b/packages/toast-ui.grid/src/view/datePickerFilter.tsx @@ -13,7 +13,7 @@ import { DispatchProps } from '../dispatch/create'; import Grid from '../grid'; import { getInstance } from '../instance'; import { cls } from '../helper/dom'; -import { filterSelectOption } from '../helper/filter'; +import { createFilterSelectOption } from '../helper/filter'; import { debounce, deepMergedCopy, isString } from '../helper/common'; import { keyNameMap, isNonPrintableKey, KeyNameMap } from '../helper/keyboard'; import { FILTER_DEBOUNCE_TIME } from '../helper/constant'; @@ -132,7 +132,8 @@ class DatePickerFilterComp extends Component { const { columnInfo } = this.props; const { options } = columnInfo.filter!; const showIcon = !(options && options.showIcon === false); - const selectOption = filterSelectOption.date; + const filterSelectOptions = createFilterSelectOption(); + const selectOption = filterSelectOptions.date; const { value, code } = this.getPreviousValue(); return ( diff --git a/packages/toast-ui.grid/src/view/filterLayerInner.tsx b/packages/toast-ui.grid/src/view/filterLayerInner.tsx index 5ad858421..590739f81 100644 --- a/packages/toast-ui.grid/src/view/filterLayerInner.tsx +++ b/packages/toast-ui.grid/src/view/filterLayerInner.tsx @@ -9,6 +9,7 @@ import { DatePickerFilter } from './datePickerFilter'; import { FilterOperator } from './filterOperator'; import { SelectFilter } from './selectFilter'; import { some } from '../helper/common'; +import i18n from '../i18n'; interface StoreProps { filters: Filter[] | null; @@ -86,7 +87,7 @@ export class FilterLayerInnerComp extends Component { dispatch('clearActiveFilterState'); }} > - Clear + {i18n.get('filter.clear')} )} {showApplyBtn && ( @@ -96,7 +97,7 @@ export class FilterLayerInnerComp extends Component { dispatch('applyActiveFilterState'); }} > - Apply + {i18n.get('filter.apply')} )} diff --git a/packages/toast-ui.grid/src/view/selectFilter.tsx b/packages/toast-ui.grid/src/view/selectFilter.tsx index d194afbb8..10e3c0788 100644 --- a/packages/toast-ui.grid/src/view/selectFilter.tsx +++ b/packages/toast-ui.grid/src/view/selectFilter.tsx @@ -10,6 +10,7 @@ import { cls } from '../helper/dom'; import { some, debounce } from '../helper/common'; import { getUniqColumnData } from '../query/data'; import { FILTER_DEBOUNCE_TIME } from '../helper/constant'; +import i18n from '../i18n'; interface ColumnData { value: CellValue; @@ -76,7 +77,7 @@ class SelectFilterComp extends Component { onChange={this.toggleAllColumnCheckbox} checked={isAllSelected} /> - Select All + {i18n.get('filter.selectAll')}
    diff --git a/packages/toast-ui.grid/src/view/textFilter.tsx b/packages/toast-ui.grid/src/view/textFilter.tsx index 6434aa1cc..cad559a49 100644 --- a/packages/toast-ui.grid/src/view/textFilter.tsx +++ b/packages/toast-ui.grid/src/view/textFilter.tsx @@ -9,7 +9,7 @@ import { ColumnInfo } from '@t/store/column'; import { connect } from './hoc'; import { DispatchProps } from '../dispatch/create'; import { cls } from '../helper/dom'; -import { filterSelectOption } from '../helper/filter'; +import { createFilterSelectOption } from '../helper/filter'; import { debounce } from '../helper/common'; import { keyNameMap, isNonPrintableKey, KeyNameMap } from '../helper/keyboard'; import { FILTER_DEBOUNCE_TIME } from '../helper/constant'; @@ -74,7 +74,8 @@ class TextFilterComp extends Component { public render() { const { columnInfo } = this.props; const { code, value } = this.getPreviousValue(); - const selectOption = filterSelectOption[ + const filterSelectOptions = createFilterSelectOption(); + const selectOption = filterSelectOptions[ columnInfo.filter!.type as 'number' | 'text' ] as SelectOption; diff --git a/packages/toast-ui.grid/types/options.d.ts b/packages/toast-ui.grid/types/options.d.ts index 728509fd6..599174f82 100644 --- a/packages/toast-ui.grid/types/options.d.ts +++ b/packages/toast-ui.grid/types/options.d.ts @@ -438,6 +438,20 @@ export interface OptI18nData { noDataToModify?: string; failResponse?: string; }; + filter?: { + contains?: string; + eq?: string; + ne?: string; + start?: string; + end?: string; + after?: string; + afterEq?: string; + before?: string; + beforeEq?: string; + apply?: string; + clear?: string; + selectAll?: string; + }; } export interface SortStateResetOption { From 646211c872cc01203f6f8a3c98688c9aacff6920 Mon Sep 17 00:00:00 2001 From: Charyum Park Date: Fri, 23 Apr 2021 11:40:17 +0900 Subject: [PATCH 08/14] fix: Remove empty values from select filter (#1245) (#1267) * fix: Remove empty values from select filter * fix: Nil value handling when selectAll is selected * chore: add test case(filter empty value in select filter) Co-authored-by: js87zz --- .../cypress/integration/filter.spec.ts | 62 +++++++++++++++++++ packages/toast-ui.grid/src/dispatch/filter.ts | 4 +- packages/toast-ui.grid/src/i18n/index.ts | 2 + packages/toast-ui.grid/src/query/data.ts | 8 ++- .../toast-ui.grid/src/view/selectFilter.tsx | 33 ++++++---- packages/toast-ui.grid/types/options.d.ts | 1 + 6 files changed, 96 insertions(+), 14 deletions(-) diff --git a/packages/toast-ui.grid/cypress/integration/filter.spec.ts b/packages/toast-ui.grid/cypress/integration/filter.spec.ts index dcb036c57..85a5f7bc3 100644 --- a/packages/toast-ui.grid/cypress/integration/filter.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/filter.spec.ts @@ -395,6 +395,68 @@ describe('apply filter (type: select)', () => { }); }); +describe('apply filter (type: select) with nil(null, undefined) or empty string value', () => { + const columns = [{ name: 'id', filter: 'select' }, { name: 'name' }]; + const data = [ + { id: 'player1', name: 'Choi' }, + { id: 'player2', name: 'Kim' }, + { id: 'player3', name: 'Ryu' }, + { id: null, name: 'Pyo' }, + { id: '', name: 'Oh' }, + // eslint-disable-next-line no-undefined + { id: undefined, name: 'Kwag' }, + ]; + + beforeEach(() => { + cy.createGrid({ data, columns }); + }); + + it(`should display 'Empty value' in select filter`, () => { + clickFilterBtn(); + + const selectFilterList = getFilterListItem(); + const expected = ['Select All', 'player1', 'player2', 'player3', 'Empty Value']; + + selectFilterList.each(($el, index) => { + cy.wrap($el).should('have.text', expected[index]); + }); + }); + + ['API', 'UI'].forEach((method) => { + it(`should filter all empty values(null, undefined, empty string) by ${method}`, () => { + if (method === 'API') { + invokeFilter('id', [ + { code: 'eq', value: 'player1' }, + { code: 'eq', value: 'player2' }, + { code: 'eq', value: 'player3' }, + ]); + } else { + applyFilterBySelectUI(4); + } + cy.getRsideBody().should('have.cellData', [ + ['player1', 'Choi'], + ['player2', 'Kim'], + ['player3', 'Ryu'], + ]); + }); + }); + + it(`should toggle all empty values(null, undefined, empty string)`, () => { + applyFilterBySelectUI(4); + + toggleSelectFilter(4); + + cy.getRsideBody().should('have.cellData', [ + ['player1', 'Choi'], + ['player2', 'Kim'], + ['player3', 'Ryu'], + ['', 'Pyo'], + ['', 'Oh'], + ['', 'Kwag'], + ]); + }); +}); + describe('apply filter (type: datepicker)', () => { const columns = [{ name: 'id' }, { name: 'date', filter: 'date' }]; const data = [ diff --git a/packages/toast-ui.grid/src/dispatch/filter.ts b/packages/toast-ui.grid/src/dispatch/filter.ts index 3a98785c8..063c7de70 100644 --- a/packages/toast-ui.grid/src/dispatch/filter.ts +++ b/packages/toast-ui.grid/src/dispatch/filter.ts @@ -131,7 +131,7 @@ export function applyActiveFilterState(store: Store) { return; } - filterLayerState.activeFilterState!.state = validState; + filterLayerState.activeFilterState!.state = state; if (type === 'select') { const columnData = getUniqColumnData(data.rawData, column, columnName); @@ -141,7 +141,7 @@ export function applyActiveFilterState(store: Store) { } } - const fns = validState.map(({ code, value }) => getFilterConditionFn(code!, value, type)); + const fns = state.map(({ code, value }) => getFilterConditionFn(code!, value, type)); filter(store, columnName, composeConditionFn(fns, operator), state); } diff --git a/packages/toast-ui.grid/src/i18n/index.ts b/packages/toast-ui.grid/src/i18n/index.ts index a706ea283..1917c9ae9 100644 --- a/packages/toast-ui.grid/src/i18n/index.ts +++ b/packages/toast-ui.grid/src/i18n/index.ts @@ -39,6 +39,7 @@ const messages: OptI18nLanguage = { apply: 'Apply', clear: 'Clear', selectAll: 'Select All', + emptyValue: 'Empty Value', }, }, ko: { @@ -72,6 +73,7 @@ const messages: OptI18nLanguage = { apply: 'Apply', clear: 'Clear', selectAll: 'Select All', + emptyValue: 'Empty Value', }, }, }; diff --git a/packages/toast-ui.grid/src/query/data.ts b/packages/toast-ui.grid/src/query/data.ts index 6156bdedc..4bd0acadf 100644 --- a/packages/toast-ui.grid/src/query/data.ts +++ b/packages/toast-ui.grid/src/query/data.ts @@ -130,7 +130,13 @@ export function findRowByRowKey( export function getUniqColumnData(targetData: Row[], column: Column, columnName: string) { const columnInfo = column.allColumnMap[columnName]; - const uniqColumnData = uniqByProp(columnName, targetData); + const uniqColumnData = uniqByProp( + columnName, + targetData.map((data) => ({ + ...data, + [columnName]: isNil(data[columnName]) ? '' : data[columnName], + })) + ); return uniqColumnData.map((row) => { const value = row[columnName]; diff --git a/packages/toast-ui.grid/src/view/selectFilter.tsx b/packages/toast-ui.grid/src/view/selectFilter.tsx index 10e3c0788..151ef9d45 100644 --- a/packages/toast-ui.grid/src/view/selectFilter.tsx +++ b/packages/toast-ui.grid/src/view/selectFilter.tsx @@ -7,13 +7,14 @@ import { DispatchProps } from '../dispatch/create'; import Grid from '../grid'; import { getInstance } from '../instance'; import { cls } from '../helper/dom'; -import { some, debounce } from '../helper/common'; +import { some, debounce, isEmpty } from '../helper/common'; import { getUniqColumnData } from '../query/data'; import { FILTER_DEBOUNCE_TIME } from '../helper/constant'; import i18n from '../i18n'; interface ColumnData { value: CellValue; + text: string; checked: boolean; } @@ -37,11 +38,11 @@ class SelectFilterComp extends Component { searchInput: '', }; - private handleChange = debounce((ev: Event, id: string) => { + private handleChange = debounce((ev: Event, value: string) => { const { dispatch } = this.props; const { checked } = ev.target as HTMLInputElement; - dispatch('setActiveSelectFilterState', id, checked); + dispatch('setActiveSelectFilterState', value, checked); }, FILTER_DEBOUNCE_TIME); private toggleAllColumnCheckbox = debounce((ev: Event) => { @@ -82,8 +83,7 @@ class SelectFilterComp extends Component {
      {data.map((item) => { - const { value, checked } = item; - const text = String(value); + const { value, text, checked } = item; return (
    • { this.handleChange(ev, text)} + onChange={(ev) => this.handleChange(ev, value)} /> - {value} + {text}
    • ); @@ -116,10 +116,21 @@ export const SelectFilter = connect( const { name: columnName } = columnAddress; const uniqueColumnData = getUniqColumnData(rawData, column, columnName); - const columnData = uniqueColumnData.map((value) => ({ - value, - checked: some((item) => value === item.value, state), - })); + const columnData = uniqueColumnData + .filter((value) => value) + .map((value) => ({ + value, + text: String(value), + checked: some((item) => value === item.value, state), + })); + const isExistEmptyValue = uniqueColumnData.some((value) => isEmpty(value)); + if (isExistEmptyValue) { + columnData.push({ + value: '', + text: i18n.get('filter.emptyValue'), + checked: some((item) => isEmpty(item.value), state), + }); + } return { grid: getInstance(id), diff --git a/packages/toast-ui.grid/types/options.d.ts b/packages/toast-ui.grid/types/options.d.ts index 599174f82..a76356587 100644 --- a/packages/toast-ui.grid/types/options.d.ts +++ b/packages/toast-ui.grid/types/options.d.ts @@ -451,6 +451,7 @@ export interface OptI18nData { apply?: string; clear?: string; selectAll?: string; + emptyValue?: string; }; } From 8647ee5cf3df221226820ac8ae24aa0044bd35e5 Mon Sep 17 00:00:00 2001 From: jaesung-lee Date: Fri, 23 Apr 2021 17:38:01 +0900 Subject: [PATCH 09/14] fix: the partial of the filter layer is hidden when window.innerWidth is smaller than the filter layer left position (#1327) --- .../src/view/filterLayerInner.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/toast-ui.grid/src/view/filterLayerInner.tsx b/packages/toast-ui.grid/src/view/filterLayerInner.tsx index 590739f81..71f88eb60 100644 --- a/packages/toast-ui.grid/src/view/filterLayerInner.tsx +++ b/packages/toast-ui.grid/src/view/filterLayerInner.tsx @@ -26,6 +26,10 @@ interface OwnProps { type Props = StoreProps & OwnProps & DispatchProps; export class FilterLayerInnerComp extends Component { + private el!: HTMLElement; + + state = { left: this.props.columnAddress.left }; + private renderFilter = (index: number) => { const { columnAddress, filterState, columnInfo } = this.props; const type = columnInfo.filter!.type; @@ -51,9 +55,19 @@ export class FilterLayerInnerComp extends Component { } }; + componentDidMount() { + const { left } = this.el.getBoundingClientRect(); + const { clientWidth } = this.el; + const { innerWidth } = window; + + if (innerWidth < left + clientWidth) { + const orgLeft = this.state.left; + this.setState({ left: orgLeft - (left + clientWidth - innerWidth) }); + } + } + public render() { const { - columnAddress, columnInfo, renderSecondFilter, dispatch, @@ -61,10 +75,16 @@ export class FilterLayerInnerComp extends Component { filterState, } = this.props; const { showApplyBtn, showClearBtn } = columnInfo.filter!; - const { left } = columnAddress; + const { left } = this.state; return ( -
      +
      { + this.el = el; + }} + >
      Date: Tue, 27 Apr 2021 12:07:17 +0900 Subject: [PATCH 10/14] feat: drag and drop to move row (#1314) * feat: add draggableRow * chore: wrong comment for grid instance * feat: add floating row css for draggable * feat: add draggable functionality * chore: add test case(D&D move row) * chore: apply code review * feat: tree drag and drop (#1324) * feat: add tree drag operation * refactor: tree floating cell * refactor: add tree parent-cell class name * feat: add drag event * feat: move tree row * fix: add '_isLeaf' prop for checking leaf node properly * feat: change moveRow API for moving the tree row * chore: add test case(move tree row) * chore: add drag event test case * docs: add event description * chore: fix broken test case * chore: fix floating row css * feat: add custom draggable renderer * chore: apply review * chore: apply code review * chore: apply code review * feat: add appended property in drop event * fix: wrong property(isLeaf => leaf) * chore: fix the move to last row * chore: add z-index to floating-line css * feat: move floating row as X position --- packages/toast-ui.grid/cypress/helper/util.ts | 9 + .../cypress/integration/data.spec.ts | 67 ++++- .../cypress/integration/eventBus.spec.ts | 57 ++++- .../cypress/integration/tree.spec.ts | 241 ++++++++++++++++++ .../toast-ui.grid/docs/en/custom-event.md | 3 + .../toast-ui.grid/docs/ko/custom-event.md | 3 + packages/toast-ui.grid/src/css/grid.css | 62 +++++ packages/toast-ui.grid/src/dispatch/tree.ts | 90 ++++++- packages/toast-ui.grid/src/grid.tsx | 31 ++- packages/toast-ui.grid/src/helper/column.ts | 4 +- packages/toast-ui.grid/src/helper/dom.ts | 8 +- packages/toast-ui.grid/src/query/draggable.ts | 203 +++++++++++++++ packages/toast-ui.grid/src/query/tree.ts | 10 +- .../src/renderer/rowHeaderDraggable.ts | 37 +++ packages/toast-ui.grid/src/store/column.ts | 51 +++- packages/toast-ui.grid/src/store/create.ts | 2 + .../toast-ui.grid/src/store/helper/tree.ts | 30 ++- packages/toast-ui.grid/src/view/bodyArea.tsx | 205 ++++++++++++++- packages/toast-ui.grid/types/event/index.d.ts | 3 + packages/toast-ui.grid/types/index.d.ts | 3 +- packages/toast-ui.grid/types/options.d.ts | 14 +- .../toast-ui.grid/types/renderer/index.d.ts | 2 +- .../toast-ui.grid/types/store/column.d.ts | 2 +- packages/toast-ui.grid/types/store/data.d.ts | 1 + 24 files changed, 1094 insertions(+), 44 deletions(-) create mode 100644 packages/toast-ui.grid/src/query/draggable.ts create mode 100644 packages/toast-ui.grid/src/renderer/rowHeaderDraggable.ts diff --git a/packages/toast-ui.grid/cypress/helper/util.ts b/packages/toast-ui.grid/cypress/helper/util.ts index c76513332..d8d537d02 100644 --- a/packages/toast-ui.grid/cypress/helper/util.ts +++ b/packages/toast-ui.grid/cypress/helper/util.ts @@ -1,3 +1,5 @@ +import { RowKey } from '@t/store/data'; + type Address = [number, number]; export function clipboardType(key: string) { @@ -34,3 +36,10 @@ export function setSelectionUsingMouse(start: Address, end: Address) { .trigger('mouseup'); }); } + +export function dragAndDrop(rowKey: RowKey, position: number) { + cy.getCell(rowKey, '_draggable') + .trigger('mousedown') + .trigger('mousemove', { pageY: position, force: true }) + .trigger('mouseup', { force: true }); +} diff --git a/packages/toast-ui.grid/cypress/integration/data.spec.ts b/packages/toast-ui.grid/cypress/integration/data.spec.ts index 3d8127a4a..204dc266e 100644 --- a/packages/toast-ui.grid/cypress/integration/data.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/data.spec.ts @@ -2,7 +2,7 @@ import { OptGrid } from '@t/options'; import { Row, RowKey } from '@t/store/data'; import { cls } from '@/helper/dom'; import { FormatterProps } from '@t/store/column'; -import { invokeFilter, applyAliasHeaderCheckbox } from '../helper/util'; +import { invokeFilter, applyAliasHeaderCheckbox, dragAndDrop } from '../helper/util'; import { assertGridHasRightRowNumber, assertHeaderCheckboxStatus } from '../helper/assert'; function assertHeaderCheckboxDisabled(disable: boolean) { @@ -997,3 +997,68 @@ describe('getFormattedValue()', () => { cy.gridInstance().invoke('getFormattedValue', 0, 'none').should('eq', null); }); }); + +describe('D&D', () => { + function getActiveFocusLayer() { + return cy.getByCls('layer-focus'); + } + + beforeEach(() => { + const largeData = [ + { name: 'Kim', age: 10 }, + { name: 'Lee', age: 20 }, + { name: 'Ryu', age: 30 }, + { name: 'Han', age: 40 }, + ]; + const columns = [ + { name: 'name', editor: 'text', sortable: true, filter: 'text' }, + { name: 'age', editor: 'text', sortable: true }, + ]; + + cy.createGrid({ data: largeData, columns, scrollY: true, bodyHeight: 400, draggable: true }); + }); + + it('should move the row by dragging the row(bottom direction)', () => { + cy.getRsideBody().should('have.cellData', [ + ['Kim', '10'], + ['Lee', '20'], + ['Ryu', '30'], + ['Han', '40'], + ]); + + dragAndDrop(1, 140); + + cy.getRsideBody().should('have.cellData', [ + ['Kim', '10'], + ['Ryu', '30'], + ['Lee', '20'], + ['Han', '40'], + ]); + }); + + it('should move the row by dragging the row(top direction)', () => { + cy.getRsideBody().should('have.cellData', [ + ['Kim', '10'], + ['Lee', '20'], + ['Ryu', '30'], + ['Han', '40'], + ]); + + dragAndDrop(1, 40); + + cy.getRsideBody().should('have.cellData', [ + ['Lee', '20'], + ['Kim', '10'], + ['Ryu', '30'], + ['Han', '40'], + ]); + }); + + it('should remove the focus when triggering mousedown to drag element', () => { + cy.gridInstance().invoke('focus', 1, 'name'); + + dragAndDrop(1, 40); + + getActiveFocusLayer().should('not.exist'); + }); +}); diff --git a/packages/toast-ui.grid/cypress/integration/eventBus.spec.ts b/packages/toast-ui.grid/cypress/integration/eventBus.spec.ts index ad91154d4..d6fb79210 100644 --- a/packages/toast-ui.grid/cypress/integration/eventBus.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/eventBus.spec.ts @@ -20,6 +20,7 @@ beforeEach(() => { cy.createGrid({ data, columns, + draggable: true, bodyHeight: 150, width: 500, rowHeaders: ['rowNum', 'checkbox'], @@ -171,9 +172,9 @@ it('onGridBeforeDestroy', () => { }); it('columnResize', () => { - const callback = cy.stub(); + const stub = cy.stub(); - cy.gridInstance().invoke('on', 'columnResize', callback); + cy.gridInstance().invoke('on', 'columnResize', stub); cy.getByCls('column-resize-handle') .first() @@ -181,8 +182,8 @@ it('columnResize', () => { .trigger('mousemove', { pageX: 400 }) .trigger('mouseup'); - cy.wrap(callback).should('be.calledWithMatch', { - resizedColumns: [{ columnName: 'name', width: 311 }], + cy.wrap(stub).should('be.calledWithMatch', { + resizedColumns: [{ columnName: 'name', width: 271 }], }); }); @@ -679,3 +680,51 @@ describe('beforeChange, afterChange', () => { }); // @TODO: add test case when pasting the data }); + +describe('D&D', () => { + it('dragStart event', () => { + const stub = cy.stub(); + + cy.gridInstance().invoke('on', 'dragStart', stub); + cy.getCell(0, '_draggable') + .trigger('mousedown') + .then(() => { + cy.getByCls('floating-row').then((floatingRow) => { + cy.wrap(stub).should('be.calledWithMatch', { + rowKey: 0, + floatingRow: floatingRow[0], + }); + }); + }); + }); + + it('drag event', () => { + const stub = cy.stub(); + cy.gridInstance().invoke('on', 'drag', stub); + + cy.getCell(0, '_draggable') + .trigger('mousedown') + .trigger('mousemove', { pageY: 100, force: true }); + + cy.wrap(stub).should('be.calledWithMatch', { + rowKey: 0, + targetRowKey: 1, + }); + }); + + it('drop event', () => { + const stub = cy.stub(); + cy.gridInstance().invoke('on', 'drop', stub); + + cy.getCell(0, '_draggable') + .trigger('mousedown') + .trigger('mousemove', { pageY: 100, force: true }) + .trigger('mouseup', { force: true }); + + cy.wrap(stub).should('be.calledWithMatch', { + rowKey: 0, + targetRowKey: 1, + appended: false, + }); + }); +}); diff --git a/packages/toast-ui.grid/cypress/integration/tree.spec.ts b/packages/toast-ui.grid/cypress/integration/tree.spec.ts index b8eb22d3b..efee14ee6 100644 --- a/packages/toast-ui.grid/cypress/integration/tree.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/tree.spec.ts @@ -9,6 +9,7 @@ import { assertToggleButtonExpanded, assertModifiedRowsLength, } from '../helper/assert'; +import { dragAndDrop } from '../helper/util'; const columns: OptColumn[] = [{ name: 'c1', editor: 'text' }, { name: 'c2' }]; @@ -998,3 +999,243 @@ describe('origin data', () => { assertToggleButtonCollapsed(0, 'c1'); }); }); + +describe('move tree row', () => { + beforeEach(() => { + const treeData = [ + { + c1: 'foo', + _children: [ + { + c1: 'bar', + _attributes: { + expanded: true, + }, + _children: [ + { + c1: 'baz', + _attributes: { + expanded: false, + }, + _children: [ + { + c1: 'qux', + }, + { + c1: 'quxx', + _children: [], + }, + ], + }, + ], + }, + ], + }, + { + c1: 'foo_2', + _children: [ + { + c1: 'bar_2', + }, + ], + }, + { c1: 'baz_2' }, + ]; + + cy.createGrid({ + data: treeData, + draggable: true, + columns: [{ name: 'c1' }], + treeColumnOptions: { + name: 'c1', + }, + }); + cy.gridInstance().invoke('expandAll'); + }); + + ['UI(D&D)', 'API'].forEach((type) => { + it(`should move the row as the child of an another row by ${type}`, () => { + if (type === 'API') { + cy.gridInstance().invoke('moveRow', 1, 5, { appended: true }); + } else { + // move 'bar' row to 'foo_2' first child + dragAndDrop(1, 280); + } + + cy.getRsideBody().should('have.cellData', [ + ['foo'], + ['foo_2'], + ['bar_2'], + ['bar'], + ['baz'], + ['qux'], + ['quxx'], + ['baz_2'], + ]); + // 'foo' row + assertHasChildren(0, 'c1', false); + // 'foo_2' row + assertHasChildren(5, 'c1', true); + }); + + it(`should move the row to an another row's index by ${type}`, () => { + if (type === 'API') { + cy.gridInstance().invoke('moveRow', 6, 1); + } else { + // move 'bar_2' row to first index of 'foo' row + dragAndDrop(6, 90); + } + + cy.getRsideBody().should('have.cellData', [ + ['foo'], + ['bar_2'], + ['bar'], + ['baz'], + ['qux'], + ['quxx'], + ['foo_2'], + ['baz_2'], + ]); + // 'foo_2' row + assertHasChildren(5, 'c1', false); + }); + + it(`should move the row to root index by ${type}`, () => { + if (type === 'API') { + cy.gridInstance().invoke('moveRow', 6, 0); + } else { + // move 'bar' row to first index of root + dragAndDrop(6, 48); + } + + cy.getRsideBody().should('have.cellData', [ + ['bar_2'], + ['foo'], + ['bar'], + ['baz'], + ['qux'], + ['quxx'], + ['foo_2'], + ['baz_2'], + ]); + // 'foo_2' row + assertHasChildren(5, 'c1', false); + }); + + it(`should move the row to an another leaf node by ${type}`, () => { + if (type === 'API') { + cy.gridInstance().invoke('moveRow', 6, 3, { appended: true }); + } else { + // move 'bar_2' row to leaf node(qux row) + dragAndDrop(6, 180); + } + + cy.gridInstance().invoke('expandAll'); + + cy.getRsideBody().should('have.cellData', [ + ['foo'], + ['bar'], + ['baz'], + ['qux'], + ['bar_2'], + ['quxx'], + ['foo_2'], + ['baz_2'], + ]); + // 'qux' row + assertHasChildren(3, 'c1', true); + }); + + it(`should move the row to last node by ${type}`, () => { + if (type === 'API') { + cy.gridInstance().invoke('moveRow', 0, 7); + } else { + // move 'foo' row to last node(baz_2 row) + dragAndDrop(0, 600); + } + + cy.gridInstance().invoke('expandAll'); + + cy.getRsideBody().should('have.cellData', [ + ['foo_2'], + ['bar_2'], + ['baz_2'], + ['foo'], + ['bar'], + ['baz'], + ['qux'], + ['quxx'], + ]); + }); + + it(`should not move the disabled row by ${type}`, () => { + cy.gridInstance().invoke('disableRow', 0); + + if (type === 'API') { + cy.gridInstance().invoke('moveRow', 0, 7); + } else { + dragAndDrop(0, 600); + } + + cy.getRsideBody().should('have.cellData', [ + ['foo'], + ['bar'], + ['baz'], + ['qux'], + ['quxx'], + ['foo_2'], + ['bar_2'], + ['baz_2'], + ]); + }); + + it(`should not append the row to disabled row by ${type}`, () => { + cy.gridInstance().invoke('disableRow', 7); + + if (type === 'API') { + cy.gridInstance().invoke('moveRow', 0, 7, { appended: true }); + } else { + dragAndDrop(0, 340); + } + + cy.getRsideBody().should('have.cellData', [ + ['foo'], + ['bar'], + ['baz'], + ['qux'], + ['quxx'], + ['foo_2'], + ['bar_2'], + ['baz_2'], + ]); + }); + }); + + it('should pass the `appended: false` prop when triggering drop event on moving index', () => { + const stub = cy.stub(); + cy.gridInstance().invoke('on', 'drop', stub); + + // move 'bar' row to first index of root + dragAndDrop(6, 48); + + cy.wrap(stub).should('be.calledWithMatch', { + rowKey: 6, + targetRowKey: 0, + appended: false, + }); + }); + + it('should pass the `appended: true` prop when triggering drop event on appending to an another node', () => { + const stub = cy.stub(); + cy.gridInstance().invoke('on', 'drop', stub); + + // move 'bar_2' row to leaf node(qux row) + dragAndDrop(6, 180); + + cy.wrap(stub).should('be.calledWithMatch', { + rowKey: 6, + targetRowKey: 3, + appended: true, + }); + }); +}); diff --git a/packages/toast-ui.grid/docs/en/custom-event.md b/packages/toast-ui.grid/docs/en/custom-event.md index 23d64159c..76c7069bc 100644 --- a/packages/toast-ui.grid/docs/en/custom-event.md +++ b/packages/toast-ui.grid/docs/en/custom-event.md @@ -119,6 +119,9 @@ grid.on('mousedown', function(ev) { - `scrollEnd` : When scrolling at the bottommost - `beforeChange`: Before one or more cells is changed - `afterChange`: After one or more cells is changed +- `dragStart`: Drag to start the movement of the row (only occurs if the `dragable` option is enabled) +- `drag`: Dragging to move row (only occurs if the `dragable` option is enabled) +- `drop`: When the drag is over and the row movement is complete. (only occurs if the `dragable` option is enabled) There are other events that can be used when using `DataSource`. diff --git a/packages/toast-ui.grid/docs/ko/custom-event.md b/packages/toast-ui.grid/docs/ko/custom-event.md index ea2927970..04ae9fe7d 100644 --- a/packages/toast-ui.grid/docs/ko/custom-event.md +++ b/packages/toast-ui.grid/docs/ko/custom-event.md @@ -119,6 +119,9 @@ grid.on('mousedown', (ev) => { - `scrollEnd` : 슀크둀 μœ„μΉ˜κ°€ κ°€μž₯ ν•˜λ‹¨μ— λ„λ‹¬ν•œ 경우 - `beforeChange`: ν•˜λ‚˜ λ˜λŠ” μ—¬λŸ¬ 개의 μ…€ 값이 λ³€κ²½λ˜κΈ° μ „ - `afterChange`: ν•˜λ‚˜ λ˜λŠ” μ—¬λŸ¬ 개의 μ…€ 값이 λ³€κ²½λœ ν›„ +- `dragStart`: λ“œλž˜κ·Έν•˜μ—¬ 둜우의 이동을 μ‹œμž‘ν–ˆμ„ λ•Œ(`draggable` μ˜΅μ…˜μ΄ ν™œμ„±ν™”λœ 경우만 λ°œμƒ) +- `drag`: λ“œλž˜κ·Έν•˜μ—¬ 둜우λ₯Ό μ΄λ™ν•˜λŠ” 쀑(`draggable` μ˜΅μ…˜μ΄ ν™œμ„±ν™”λœ 경우만 λ°œμƒ) +- `drop`: λ“œλž˜κ·Έκ°€ λλ‚˜κ³  둜우 이동을 μ™„λ£Œν•˜μ˜€μ„ λ•Œ(`draggable` μ˜΅μ…˜μ΄ ν™œμ„±ν™”λœ 경우만 λ°œμƒ) `DataSource`λ₯Ό μ΄μš©ν•  λ•Œ μ‚¬μš©ν•  수 μžˆλŠ” μ΄λ²€νŠΈλŠ” λ‹€μŒκ³Ό κ°™λ‹€. diff --git a/packages/toast-ui.grid/src/css/grid.css b/packages/toast-ui.grid/src/css/grid.css index 55324f8aa..a1e9f7339 100644 --- a/packages/toast-ui.grid/src/css/grid.css +++ b/packages/toast-ui.grid/src/css/grid.css @@ -938,3 +938,65 @@ .tui-grid-container .tui-calendar-year .tui-calendar-body { width: 220px; } + +.tui-grid-row-header-draggable { + text-align: center; + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; +} + +.tui-grid-row-header-draggable span { + display: inline-block; + width: 1px; + height: 1px; + margin: 1px; + line-height: 0; + background: #5a6268; +} + +.tui-grid-floating-row { + z-Index: 15; + background: #fff; + border: 1px solid #ddd; + color: #5a6268; + min-width: 200px; + position: absolute; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08); + border-radius: 3px; + overflow: hidden; +} + +.tui-grid-floating-cell { + display: inline-block; +} + +.tui-grid-floating-tree-cell { + padding: 0 10px; +} + +.tui-grid-floating-tree-cell-content { + margin-left: 10px; + vertical-align: middle +} + +.tui-grid-floating-tree-cell .tui-grid-tree-icon { + position: relative; + margin-top: -14px; + display: inline-block; +} + +.tui-grid-floating-line { + position: absolute; + height: 1px; + background: #00a9ff; + display: none; + z-Index: 15; +} + +.tui-grid-cell.dragging { + opacity: 0.5; +} + +.tui-grid-cell.parent-cell { + background-color: rgba(0, 169, 255, 0.15) +} \ No newline at end of file diff --git a/packages/toast-ui.grid/src/dispatch/tree.ts b/packages/toast-ui.grid/src/dispatch/tree.ts index 8e509dbd4..b8c33c151 100644 --- a/packages/toast-ui.grid/src/dispatch/tree.ts +++ b/packages/toast-ui.grid/src/dispatch/tree.ts @@ -1,12 +1,19 @@ import { Row, RowKey } from '@t/store/data'; import { Store } from '@t/store'; -import { OptRow, OptAppendTreeRow } from '@t/options'; +import { OptRow, OptAppendTreeRow, OptMoveRow } from '@t/options'; import { Column, ColumnInfo } from '@t/store/column'; import { ColumnCoords } from '@t/store/columnCoords'; import { Dimension } from '@t/store/dimension'; import { createViewRow } from '../store/data'; -import { getRowHeight, findIndexByRowKey, findRowByRowKey, getLoadingState } from '../query/data'; -import { notify, batchObserver } from '../helper/observable'; +import { + getRowHeight, + findIndexByRowKey, + findRowByRowKey, + getLoadingState, + isSorted, + isFiltered, +} from '../query/data'; +import { notify, batchObserver, getOriginObject, Observable } from '../helper/observable'; import { getDataManager } from '../instance'; import { isUpdatableRowAttr, @@ -30,7 +37,7 @@ import { import { getEventBus } from '../event/eventBus'; import GridEvent from '../event/gridEvent'; import { flattenTreeData, getTreeIndentWidth } from '../store/helper/tree'; -import { findProp, findPropIndex, removeArrayItem } from '../helper/common'; +import { findProp, findPropIndex, removeArrayItem, some } from '../helper/common'; import { getComputedFontStyle, getTextWidth } from '../helper/dom'; import { fillMissingColumnData } from './lazyObservable'; import { getColumnSide } from '../query/column'; @@ -264,6 +271,15 @@ function removeChildRowKey(row: Row, rowKey: RowKey) { if (tree) { removeArrayItem(rowKey, tree.childRowKeys); + if (row._children) { + const index = findPropIndex('rowKey', rowKey, row._children); + if (index !== -1) { + row._children.splice(index, 1); + } + } + if (!tree.childRowKeys.length) { + row._leaf = true; + } notify(tree, 'childRowKeys'); } } @@ -335,10 +351,11 @@ export function appendTreeRow(store: Store, row: OptRow, options: OptAppendTreeR fillMissingColumnData(column, rawRows); + const viewRows = rawRows.map((rawRow) => createViewRow(id, rawRow, rawData, column)); + batchObserver(() => { rawData.splice(startIdx, 0, ...rawRows); }); - const viewRows = rawRows.map((rawRow) => createViewRow(id, rawRow, rawData, column)); viewData.splice(startIdx, 0, ...viewRows); const rowHeights = rawRows.map((rawRow) => { @@ -391,3 +408,66 @@ function postUpdateAfterManipulation(store: Store, rowIndex: number, rows?: Row[ setCheckedAllRows(store); setAutoResizingColumnWidths(store, rows); } + +export function moveTreeRow( + store: Store, + rowKey: RowKey, + targetIndex: number, + options: OptMoveRow & { moveToLast?: boolean } +) { + const { data, column, id } = store; + const { rawData } = data; + const targetRow = rawData[targetIndex]; + + if (!targetRow || isSorted(data) || isFiltered(data)) { + return; + } + + const currentIndex = findIndexByRowKey(data, column, id, rowKey, false); + const row = rawData[currentIndex]; + + if ( + currentIndex === -1 || + currentIndex === targetIndex || + row._attributes.disabled || + (targetRow._attributes.disabled && options.appended) + ) { + return; + } + + const childRows = getDescendantRows(store, rowKey); + const minIndex = Math.min(currentIndex, targetIndex); + const moveToChild = some((childRow) => childRow.rowKey === targetRow.rowKey, childRows); + + if (!moveToChild) { + removeTreeRow(store, rowKey); + const originRow = getOriginObject(row as Observable); + + getDataManager(id).push('UPDATE', targetRow, true); + getDataManager(id).push('UPDATE', row, true); + + if (options.appended) { + appendTreeRow(store, originRow, { parentRowKey: targetRow.rowKey }); + } else { + const { parentRowKey } = targetRow._attributes.tree!; + const parentIndex = findIndexByRowKey(data, column, id, parentRowKey); + let offset: number; + + // calculate the moving index for considering removed rows + if (targetIndex > currentIndex) { + const indexWithoutRemovedRows = targetIndex - (childRows.length + 1); + offset = + parentIndex === -1 ? indexWithoutRemovedRows : indexWithoutRemovedRows - parentIndex - 1; + } else { + offset = parentIndex === -1 ? targetIndex : targetIndex - parentIndex - 1; + } + + // to resolve the index for moving last index + if (options.moveToLast) { + offset += 1; + } + appendTreeRow(store, originRow, { parentRowKey, offset }); + } + postUpdateAfterManipulation(store, minIndex); + } +} diff --git a/packages/toast-ui.grid/src/grid.tsx b/packages/toast-ui.grid/src/grid.tsx index 7b58add54..25c660819 100644 --- a/packages/toast-ui.grid/src/grid.tsx +++ b/packages/toast-ui.grid/src/grid.tsx @@ -17,6 +17,7 @@ import { OptFilter, LifeCycleEventName, ResetOptions, + OptMoveRow, } from '@t/options'; import { Store } from '@t/store'; import { RowKey, CellValue, Row, InvalidRow } from '@t/store/data'; @@ -156,9 +157,8 @@ if ((module as any).hot) { * @param {function} [options.rowHeaders.renderer] - Sets the custom renderer to customize the header content. * @param {Array} options.columns - The configuration of the grid columns. * @param {string} options.columns.name - The name of the column. - * @deprecated * @param {boolean} [options.columns.ellipsis=false] - If set to true, ellipsis will be used - * for overflowing content. + * for overflowing content.(This option will be deprecated) * @param {string} [options.columns.align=left] - Horizontal alignment of the column content. * Available values are 'left', 'center', 'right'. * @param {string} [options.columns.valign=middle] - Vertical alignment of the column content. @@ -233,12 +233,11 @@ if ((module as any).hot) { * @param {function} [options.columns.relations.listItems] - The function whose return * value specifies the option list for the 'select', 'radio', 'checkbox' type. * The options list of target columns will be replaced with the return value of this function. - * @deprecated * @param {string} [options.columns.whiteSpace='nowrap'] - If set to 'normal', the text line is broken * by fitting to the column's width. If set to 'pre', spaces are preserved and the text is braken by * new line characters. If set to 'pre-wrap', spaces are preserved, the text line is broken by * fitting to the column's width and new line characters. If set to 'pre-line', spaces are merged, - * the text line is broken by fitting to the column's width and new line characters. + * the text line is broken by fitting to the column's width and new line characters.(This option will be deprecated) * @param {Object} [options.summary] - The object for configuring summary area. * @param {number} [options.summary.height] - The height of the summary area. * @param {string} [options.summary.position='bottom'] - The position of the summary area. ('bottom', 'top') @@ -269,6 +268,7 @@ if ((module as any).hot) { * @param {function} [options.onGridMounted] - The function that will be called after rendering the grid. * @param {function} [options.onGridUpdated] - The function that will be called after updating the all data of the grid and rendering the grid. * @param {function} [options.onGridBeforeDestroy] - The function that will be called before destroying the grid. + * @param {boolean} [options.draggable] - Whether to enable to drag the row for changing the order of rows. */ export default class Grid implements TuiGrid { private el: HTMLElement; @@ -1636,10 +1636,27 @@ export default class Grid implements TuiGrid { * Move the row identified by the specified rowKey to target index. * If data is sorted or filtered, this couldn't be used. * @param {number|string} rowKey - The unique key of the row - * @param {number} targetIndex - target index for moving + * @param {number} targetIndex - Target index for moving + * @param {Object} [options] - Options + * @param {number} [options.appended] - This option for only tree data. Whether the row is appended to other row as the child. */ - public moveRow(rowKey: RowKey, targetIndex: number) { - this.dispatch('moveRow', rowKey, targetIndex); + public moveRow(rowKey: RowKey, targetIndex: number, options: OptMoveRow = { appended: false }) { + const { column, data } = this.store; + + if (column.treeColumnName) { + let moveToLast = false; + + if (!options.appended) { + if (targetIndex === data.rawData.length - 1) { + moveToLast = true; + } else if (this.getIndexOfRow(rowKey) < targetIndex) { + targetIndex += 1; + } + } + this.dispatch('moveTreeRow', rowKey, targetIndex, { ...options, moveToLast }); + } else { + this.dispatch('moveRow', rowKey, targetIndex); + } } /** diff --git a/packages/toast-ui.grid/src/helper/column.ts b/packages/toast-ui.grid/src/helper/column.ts index 42f1e5d13..989a65fa7 100644 --- a/packages/toast-ui.grid/src/helper/column.ts +++ b/packages/toast-ui.grid/src/helper/column.ts @@ -1,5 +1,7 @@ +import { includes } from './common'; + export function isRowHeader(columnName: string) { - return ['_number', '_checked'].indexOf(columnName) > -1; + return includes(['_number', '_checked', '_draggable'], columnName); } export function isRowNumColumn(columnName: string) { diff --git a/packages/toast-ui.grid/src/helper/dom.ts b/packages/toast-ui.grid/src/helper/dom.ts index 6d370a98b..b96e58ba9 100644 --- a/packages/toast-ui.grid/src/helper/dom.ts +++ b/packages/toast-ui.grid/src/helper/dom.ts @@ -128,7 +128,13 @@ export type ClassNameType = | 'editor-label-icon-checkbox-checked' | 'editor-label-icon-radio' | 'editor-label-icon-radio-checked' - | 'editor-datepicker-layer'; + | 'editor-datepicker-layer' + | 'row-header-draggable' + | 'floating-row' + | 'floating-cell' + | 'floating-tree-cell' + | 'floating-tree-cell-content' + | 'floating-line'; const CLS_PREFIX = 'tui-grid-'; diff --git a/packages/toast-ui.grid/src/query/draggable.ts b/packages/toast-ui.grid/src/query/draggable.ts new file mode 100644 index 000000000..97f02f811 --- /dev/null +++ b/packages/toast-ui.grid/src/query/draggable.ts @@ -0,0 +1,203 @@ +import { Store } from '@t/store'; +import { RowKey, ViewRow, Row } from '@t/store/data'; +import { findOffsetIndex, fromArray, clamp } from '../helper/common'; +import { cls } from '../helper/dom'; +import { findIndexByRowKey } from './data'; + +export interface PosInfo { + pageX: number; + pageY: number; + left: number; + top: number; + scrollLeft: number; + scrollTop: number; + container?: HTMLElement; +} + +export interface DraggableInfo { + row: HTMLElement; + rowKey: RowKey; + line: HTMLElement; + targetRow?: Row; +} + +export interface MovedIndexAndPosInfo { + index: number; + height: number; + offsetLeft: number; + offsetTop: number; + targetRow: Row; + moveToLast: boolean; +} + +export interface FloatingRowSize { + width: number; + height: number; +} + +interface FloatingRowOffsets { + offsetLeft: number; + offsetTop: number; +} + +const EXCEED_RATIO = 0.8; +const ADDITIONAL_HEIGHT = 10; + +function createRow(height: string) { + const row = document.createElement('div'); + + row.className = cls('floating-row'); + row.style.height = height; + row.style.lineHeight = height; + row.style.width = 'auto'; + + return row; +} + +function createCells(cell: Element) { + const childLen = cell.childNodes.length; + const el = document.createElement('div'); + el.className = cls('floating-cell'); + el.style.width = window.getComputedStyle(cell).width; + + for (let i = 0; i < childLen; i += 1) { + // the cell is not complex structure, so there is no the performance problem + el.appendChild(cell.childNodes[i].cloneNode(true)); + } + + return el; +} + +function createTreeCell(treeColumnName: string, viewRow: ViewRow) { + const cell = document.createElement('div'); + const iconStyle = viewRow.treeInfo!.leaf ? '' : 'background-position: -39px -35px'; + + const span = document.createElement('span'); + span.className = cls('floating-tree-cell-content'); + span.textContent = String(viewRow.valueMap[treeColumnName].value); + + cell.className = cls('floating-tree-cell'); + cell.innerHTML = ` + + + + `; + cell.appendChild(span); + + return cell; +} + +function createFloatingDraggableRow( + store: Store, + rowKey: RowKey, + offsetLeft: number, + offsetTop: number, + posInfo: PosInfo +) { + const { data, column, id } = store; + const { treeColumnName } = column; + const cells = fromArray(posInfo.container!.querySelectorAll(`[data-row-key="${rowKey}"]`)); + + // get original table row height + const height = `${cells[0].parentElement!.clientHeight}px`; + const row = createRow(height); + + row.style.left = `${offsetLeft}px`; + row.style.top = `${offsetTop}px`; + + if (treeColumnName) { + const index = findIndexByRowKey(data, column, id, rowKey); + const viewRow = data.viewData[index]; + + row.appendChild(createTreeCell(treeColumnName, viewRow)); + } else { + cells.forEach((cell) => { + row.appendChild(createCells(cell)); + }); + } + + return row; +} + +export function createDraggableInfo(store: Store, posInfo: PosInfo): DraggableInfo | null { + const { data, dimension } = store; + const { rawData, filters } = data; + + // if there is any filter condition, cannot drag the row + if (!rawData.length || filters?.length) { + return null; + } + + const { offsetLeft, offsetTop, index } = getMovedPosAndIndex(store, posInfo); + const { rowKey, _attributes } = rawData[index]; + const row = createFloatingDraggableRow(store, rowKey, offsetLeft, offsetTop, posInfo); + + return _attributes.disabled + ? null + : { + row, + rowKey, + line: createFloatingLine(dimension.scrollYWidth), + }; +} + +export function getMovedPosAndIndex( + store: Store, + { pageX, pageY, left, top, scrollTop }: PosInfo +): MovedIndexAndPosInfo { + const { rowCoords, dimension, column, data } = store; + const { heights, offsets } = rowCoords; + const { rawData } = data; + const { headerHeight } = dimension; + const offsetLeft = pageX - left; + const offsetTop = pageY - top + scrollTop; + let index = findOffsetIndex(rowCoords.offsets, offsetTop); + + // move to next index when exceeding the height with ratio + if (!column.treeColumnName) { + if (index < rawData.length - 1 && offsetTop - offsets[index] > heights[index] * EXCEED_RATIO) { + index += 1; + } + } + + let height = offsets[index] - scrollTop + headerHeight; + let moveToLast = false; + // resolve the height for moving to last index with tree data + if (column.treeColumnName) { + if (rawData.length - 1 === index && offsetTop > offsets[index] + heights[index]) { + height += heights[index]; + moveToLast = true; + } + } + + return { + index, + height, + offsetLeft, + offsetTop: offsetTop - scrollTop + headerHeight, + targetRow: rawData[index], + moveToLast, + }; +} + +export function createFloatingLine(scrollYWidth: number) { + const line = document.createElement('div'); + + line.className = cls('floating-line'); + line.style.width = `calc(100% - ${scrollYWidth}px)`; + + return line; +} + +export function getResolvedOffsets( + { dimension }: Store, + { offsetLeft, offsetTop }: FloatingRowOffsets, + { width }: FloatingRowSize +) { + const { width: bodyWidth, bodyHeight, scrollXHeight } = dimension; + + return { + offsetLeft: clamp(offsetLeft, 0, bodyWidth - width), + offsetTop: clamp(offsetTop, 0, bodyHeight + scrollXHeight + ADDITIONAL_HEIGHT), + }; +} diff --git a/packages/toast-ui.grid/src/query/tree.ts b/packages/toast-ui.grid/src/query/tree.ts index b111660e9..8e975d6ec 100644 --- a/packages/toast-ui.grid/src/query/tree.ts +++ b/packages/toast-ui.grid/src/query/tree.ts @@ -105,16 +105,16 @@ export function getChildRowKeys(row: Row) { return tree ? tree.childRowKeys.slice() : []; } -export function isHidden(row: Row) { - const { tree } = row._attributes; +export function isHidden({ _attributes }: Row) { + const { tree } = _attributes; return !!(tree && tree.hidden); } -export function isLeaf(row: Row) { - const { tree } = row._attributes; +export function isLeaf({ _attributes, _leaf }: Row) { + const { tree } = _attributes; - return !!tree && !tree.childRowKeys.length && isUndefined(tree.expanded); + return !!tree && !tree.childRowKeys.length && !!_leaf; } export function isExpanded(row: Row) { diff --git a/packages/toast-ui.grid/src/renderer/rowHeaderDraggable.ts b/packages/toast-ui.grid/src/renderer/rowHeaderDraggable.ts new file mode 100644 index 000000000..037293e86 --- /dev/null +++ b/packages/toast-ui.grid/src/renderer/rowHeaderDraggable.ts @@ -0,0 +1,37 @@ +import { CellRenderer } from '@t/renderer'; +import { cls } from '../helper/dom'; + +const ROW_COUNT = 3; +const COL_COUNT = 3; + +export class RowHeaderDraggableRenderer implements CellRenderer { + private el: HTMLDivElement; + + constructor() { + const el = document.createElement('div'); + + el.className = cls('row-header-draggable'); + + this.el = el; + this.renderDraggableIcon(); + } + + getElement() { + return this.el; + } + + private renderDraggableIcon() { + for (let i = 0; i < ROW_COUNT; i += 1) { + const wrapper = document.createElement('div'); + + wrapper.style.lineHeight = '0'; + + for (let j = 0; j < COL_COUNT; j += 1) { + const square = document.createElement('span'); + + wrapper.appendChild(square); + } + this.el.appendChild(wrapper); + } + } +} diff --git a/packages/toast-ui.grid/src/store/column.ts b/packages/toast-ui.grid/src/store/column.ts index 6ded31e64..31a604d96 100644 --- a/packages/toast-ui.grid/src/store/column.ts +++ b/packages/toast-ui.grid/src/store/column.ts @@ -8,6 +8,7 @@ import { OptComplexColumnInfo, Dictionary, OptFilter, + OptRowHeaderColumn, } from '@t/options'; import { ColumnOptions, @@ -37,10 +38,12 @@ import { findProp, uniq, isEmpty, + findIndex, } from '../helper/common'; import { DefaultRenderer } from '../renderer/default'; import { editorMap } from '../editor/manager'; import { RowHeaderInputRenderer } from '../renderer/rowHeaderInput'; +import { RowHeaderDraggableRenderer } from '../renderer/rowHeaderDraggable'; const DEF_ROW_HEADER_INPUT = ''; const ROW_HEADER = 40; @@ -48,6 +51,7 @@ const COLUMN = 50; const rowHeadersMap = { rowNum: '_number', checkbox: '_checked', + draggable: '_draggable', }; export function validateRelationColumn(columnInfos: ColumnInfo[]) { @@ -345,6 +349,31 @@ function createComplexColumnHeaders( }); } +function createDraggableRowHeader(rowHeaderColumn: OptRowHeader | null) { + const renderer = isObject(rowHeaderColumn) + ? rowHeaderColumn.renderer + : { type: RowHeaderDraggableRenderer }; + + const draggableColumn: ColumnInfo = { + name: '_draggable', + header: '', + hidden: false, + resizable: false, + align: 'center', + valign: 'middle', + renderer: createRendererOptions(renderer), + baseWidth: ROW_HEADER, + minWidth: ROW_HEADER, + fixedWidth: true, + autoResizing: false, + escapeHTML: false, + headerAlign: 'center', + headerVAlign: 'middle', + }; + + return draggableColumn; +} + interface ColumnOption { columns: OptColumn[]; columnOptions: ColumnOptions; @@ -357,6 +386,7 @@ interface ColumnOption { valign: VAlignType; columnHeaders: OptColumnHeaderInfo[]; disabled: boolean; + draggable: boolean; } export function create({ @@ -371,6 +401,7 @@ export function create({ valign, columnHeaders, disabled, + draggable, }: ColumnOption) { const relationColumns = columns.reduce((acc: string[], { relations }) => { acc = acc.concat(createRelationColumns(relations || [])); @@ -378,8 +409,24 @@ export function create({ }, []); const columnHeaderInfo = { columnHeaders, align, valign }; - const rowHeaderInfos = rowHeaders.map((rowHeader) => - createRowHeader(rowHeader, columnHeaderInfo) + const rowHeaderInfos = []; + + if (draggable) { + let rowHeaderColumn: OptRowHeader | null = null; + const index = findIndex( + (rowHeader) => + (isString(rowHeader) && rowHeader === 'draggable') || + (rowHeader as OptRowHeaderColumn).type === 'draggable', + rowHeaders + ); + if (index !== -1) { + [rowHeaderColumn] = rowHeaders.splice(index, 1); + } + rowHeaderInfos.push(createDraggableRowHeader(rowHeaderColumn)); + } + + rowHeaders.forEach((rowHeader) => + rowHeaderInfos.push(createRowHeader(rowHeader, columnHeaderInfo)) ); const columnInfos = columns.map((column) => diff --git a/packages/toast-ui.grid/src/store/create.ts b/packages/toast-ui.grid/src/store/create.ts index c2b21f42d..04d679f34 100644 --- a/packages/toast-ui.grid/src/store/create.ts +++ b/packages/toast-ui.grid/src/store/create.ts @@ -42,6 +42,7 @@ export function createStore(id: number, options: OptGrid): Store { treeColumnOptions = { name: '' }, header = {}, disabled = false, + draggable = false, } = options; const { frozenBorderWidth } = columnOptions; const { height: summaryHeight, position: summaryPosition } = summaryOptions; @@ -64,6 +65,7 @@ export function createStore(id: number, options: OptGrid): Store { valign, columnHeaders, disabled, + draggable, }); const data = createData({ data: Array.isArray(options.data) ? options.data : [], diff --git a/packages/toast-ui.grid/src/store/helper/tree.ts b/packages/toast-ui.grid/src/store/helper/tree.ts index e55c7ce2b..549b02962 100644 --- a/packages/toast-ui.grid/src/store/helper/tree.ts +++ b/packages/toast-ui.grid/src/store/helper/tree.ts @@ -4,7 +4,7 @@ import { Column } from '../../../types/store/column'; import { createRawRow } from '../data'; import { isExpanded, getDepth, isLeaf, isHidden } from '../../query/tree'; import { observable, observe } from '../../helper/observable'; -import { includes, isUndefined } from '../../helper/common'; +import { includes, isUndefined, someProp } from '../../helper/common'; import { TREE_INDENT_WIDTH } from '../../helper/constant'; interface TreeDataOption { @@ -31,20 +31,30 @@ function generateTreeRowKey() { return treeRowKey; } -function addChildRowKey(row: Row, rowKey: RowKey) { +function addChildRowKey(row: Row, childRow: Row) { const { tree } = row._attributes; + const { rowKey } = childRow; if (tree && !includes(tree.childRowKeys, rowKey)) { tree.childRowKeys.push(rowKey); } + if (!someProp('rowKey', rowKey, row._children!)) { + row._children!.push(childRow); + } + row._leaf = false; } -function insertChildRowKey(row: Row, rowKey: RowKey, offset: number) { +function insertChildRowKey(row: Row, childRow: Row, offset: number) { const { tree } = row._attributes; + const { rowKey } = childRow; if (tree && !includes(tree.childRowKeys, rowKey)) { tree.childRowKeys.splice(offset, 0, rowKey); } + if (!someProp('rowKey', rowKey, row._children!)) { + row._children!.splice(offset, 0, childRow); + } + row._leaf = false; } function getTreeCellInfo(rawData: Row[], row: Row, useIcon?: boolean) { @@ -71,6 +81,11 @@ export function createTreeRawRow( childRowKeys = row._attributes.tree.childRowKeys as RowKey[]; } const { keyColumnName, offset, lazyObservable = false, disabled = false } = options; + + if (!row._children) { + row._children = []; + row._leaf = true; + } // generate new tree rowKey when row doesn't have rowKey const targetTreeRowKey = isUndefined(row.rowKey) ? generateTreeRowKey() : Number(row.rowKey); const rawRow = createRawRow(id, row, targetTreeRowKey, column, { @@ -78,7 +93,6 @@ export function createTreeRawRow( lazyObservable, disabled, }); - const { rowKey } = rawRow; const defaultAttributes = { parentRowKey: parentRow ? parentRow.rowKey : null, childRowKeys, @@ -87,17 +101,15 @@ export function createTreeRawRow( if (parentRow) { if (!isUndefined(offset)) { - insertChildRowKey(parentRow, rowKey, offset); + insertChildRowKey(parentRow, rawRow, offset); } else { - addChildRowKey(parentRow, rowKey); + addChildRowKey(parentRow, rawRow); } } const tree = { ...defaultAttributes, - ...((Array.isArray(row._children) || childRowKeys.length) && { - expanded: !!row._attributes!.expanded, - }), + expanded: row._attributes!.expanded, }; rawRow._attributes.tree = lazyObservable ? tree : observable(tree); diff --git a/packages/toast-ui.grid/src/view/bodyArea.tsx b/packages/toast-ui.grid/src/view/bodyArea.tsx index 2c55a28a9..d058cd44c 100644 --- a/packages/toast-ui.grid/src/view/bodyArea.tsx +++ b/packages/toast-ui.grid/src/view/bodyArea.tsx @@ -1,5 +1,15 @@ import { h, Component } from 'preact'; import { Side } from '@t/store/focus'; +import { + createDraggableInfo, + DraggableInfo, + getMovedPosAndIndex, + MovedIndexAndPosInfo, + PosInfo, + getResolvedOffsets, + FloatingRowSize, +} from '../query/draggable'; +import { RowKey } from '@t/store/data'; import { PagePosition, DragStartData } from '@t/store/selection'; import { BodyRows } from './bodyRows'; import { ColGroup } from './colGroup'; @@ -10,6 +20,7 @@ import { hasClass, isDatePickerElement, findParent, + getCellAddress, } from '../helper/dom'; import { DispatchProps } from '../dispatch/create'; import { connect } from './hoc'; @@ -38,6 +49,7 @@ interface StoreProps { scrollY: boolean; cellBorderWidth: number; eventBus: EventBus; + hasTreeColumn: boolean; } type Props = OwnProps & StoreProps & DispatchProps; @@ -50,6 +62,13 @@ interface AreaStyle { overflowY?: Overflow; } +interface MovedIndexInfo { + index: number; + rowKey: RowKey; + moveToLast?: boolean; + appended?: boolean; +} + // only updates when these props are changed // for preventing unnecessary rendering when scroll changes const PROPS_FOR_UPDATE: (keyof StoreProps)[] = [ @@ -61,6 +80,10 @@ const PROPS_FOR_UPDATE: (keyof StoreProps)[] = [ ]; // Minimum distance (pixel) to detect if user wants to drag when moving mouse with button pressed. const MIN_DISTANCE_FOR_DRAG = 10; +const ADDITIONAL_RANGE = 3; +const DRAGGING_CLASS = 'dragging'; +const PARENT_CELL_CLASS = 'parent-cell'; +const DRAGGABLE_COLUMN_NAME = '_draggable'; class BodyAreaComp extends Component { private el?: HTMLElement; @@ -74,6 +97,15 @@ class BodyAreaComp extends Component { private prevScrollLeft = 0; + // draggable info when start to move the row + private draggableInfo: DraggableInfo | null = null; + + // floating row width and height for dragging + private floatingRowSize: FloatingRowSize | null = null; + + // the index info to move row through drag + private movedIndexInfo: MovedIndexInfo | null = null; + private scrollToNextDebounced = debounce(() => { this.props.dispatch('scrollToNext'); }, 200); @@ -104,6 +136,112 @@ class BodyAreaComp extends Component { } }; + private dragRow = (ev: MouseEvent) => { + const [pageX, pageY] = getCoordinateWithOffset(ev.pageX, ev.pageY); + + if (this.moveEnoughToTriggerDragEvent({ pageX, pageY })) { + const { el, boundingRect, props } = this; + const { scrollTop, scrollLeft } = el!; + const movedPosAndIndex = getMovedPosAndIndex(this.context.store, { + scrollLeft, + scrollTop, + left: boundingRect!.left, + top: boundingRect!.top, + pageX, + pageY, + }); + const { index, targetRow } = movedPosAndIndex; + const rowKeyToMove = targetRow.rowKey; + const { row, rowKey } = this.draggableInfo!; + const { offsetLeft, offsetTop } = getResolvedOffsets( + this.context.store, + movedPosAndIndex, + this.floatingRowSize! + ); + + row.style.left = `${offsetLeft}px`; + row.style.top = `${offsetTop}px`; + + const gridEvent = new GridEvent({ rowKey, targetRowKey: rowKeyToMove }); + /** + * Occurs when dragging the row + * @event Grid#drag + * @property {Grid} instance - Current grid instance + * @property {RowKey} rowKey - The rowKey of the dragging row + * @property {RowKey} targetRowKey - The rowKey of the row at current dragging position + */ + this.props.eventBus.trigger('drag', gridEvent); + + if (props.hasTreeColumn) { + this.setTreeMovedIndexInfo(movedPosAndIndex); + } else { + // move the row to next index + this.movedIndexInfo = { index, rowKey: rowKeyToMove }; + this.props.dispatch('moveRow', rowKey, index); + } + } + }; + + private setTreeMovedIndexInfo(movedPosAndIndex: MovedIndexAndPosInfo) { + const { line } = this.draggableInfo!; + const { index, offsetTop, height, targetRow, moveToLast } = movedPosAndIndex; + const { rowKey } = targetRow; + + if (this.movedIndexInfo) { + this.props.dispatch('removeRowClassName', this.movedIndexInfo!.rowKey, PARENT_CELL_CLASS); + } + // display line border to mark the index to move + if (Math.abs(height - offsetTop) < ADDITIONAL_RANGE || moveToLast) { + line.style.top = `${height}px`; + line.style.display = 'block'; + this.movedIndexInfo = { index, rowKey, moveToLast }; + // show the background color to mark parent row + } else { + line.style.display = 'none'; + this.movedIndexInfo = { index, rowKey, appended: true }; + this.props.dispatch('addRowClassName', rowKey, PARENT_CELL_CLASS); + } + } + + private startToDragRow = (posInfo: PosInfo) => { + const container = this.el!.parentElement!.parentElement!; + posInfo.container = container; + const draggableInfo = createDraggableInfo(this.context.store, posInfo); + + if (draggableInfo) { + const { row, rowKey, line } = draggableInfo; + const gridEvent = new GridEvent({ rowKey, floatingRow: row }); + /** + * Occurs when starting to drag the row + * @event Grid#dragStart + * @property {Grid} instance - Current grid instance + * @property {RowKey} rowKey - The rowKey of the row to drag + * @property {HTMLElement} floatingRow - The floating row DOM element + */ + this.props.eventBus.trigger('dragStart', gridEvent); + + if (!gridEvent.isStopped()) { + container.appendChild(row); + + const { clientWidth, clientHeight } = row; + + this.floatingRowSize = { width: clientWidth, height: clientHeight }; + this.draggableInfo = draggableInfo; + + if (this.props.hasTreeColumn) { + container!.appendChild(line); + } + + this.props.dispatch('addRowClassName', rowKey, DRAGGING_CLASS); + this.props.dispatch('setFocusInfo', null, null, false); + + document.addEventListener('mousemove', this.dragRow); + document.addEventListener('mouseup', this.dropRow); + document.addEventListener('selectstart', this.handleSelectStart); + } + } + }; + private handleMouseDown = (ev: MouseEvent) => { const targetElement = ev.target as HTMLElement; if (!this.el || targetElement === this.el) { @@ -126,6 +264,11 @@ class BodyAreaComp extends Component { const { top, left } = el.getBoundingClientRect(); this.boundingRect = { top, left }; + if (getCellAddress(targetElement)?.columnName === DRAGGABLE_COLUMN_NAME) { + this.startToDragRow({ pageX, pageY, left, top, scrollLeft, scrollTop }); + return; + } + if (!isDatePickerElement(targetElement) && !findParent(targetElement, 'layer-editing')) { dispatch( 'mouseDownBody', @@ -169,6 +312,61 @@ class BodyAreaComp extends Component { } }; + private dropRow = () => { + const { hasTreeColumn } = this.props; + const { rowKey } = this.draggableInfo!; + + if (this.movedIndexInfo) { + const { + index, + rowKey: targetRowKey, + appended = false, + moveToLast = false, + } = this.movedIndexInfo; + const gridEvent = new GridEvent({ rowKey, targetRowKey, appended }); + /** + * Occurs when dropping the row + * @event Grid#drop + * @property {Grid} instance - Current grid instance + * @property {RowKey} rowKey - The rowKey of the dragging row + * @property {RowKey} targetRowKey - The rowKey of the row at current dragging position + * @property {boolean} appended - Whether the row is appended to other row as the child in tree data. + */ + this.props.eventBus.trigger('drop', gridEvent); + + if (!gridEvent.isStopped()) { + if (hasTreeColumn) { + this.props.dispatch('moveTreeRow', rowKey, index, { appended, moveToLast }); + } else { + this.props.dispatch('moveRow', rowKey, index); + } + } + } + this.props.dispatch('removeRowClassName', rowKey, DRAGGING_CLASS); + if (this.movedIndexInfo) { + this.props.dispatch('removeRowClassName', this.movedIndexInfo.rowKey, PARENT_CELL_CLASS); + } + // clear floating element and draggable info + this.clearDraggableInfo(); + }; + + private clearDraggableInfo() { + const { row, line } = this.draggableInfo!; + + row.parentElement!.removeChild(row); + + if (this.props.hasTreeColumn) { + line.parentElement!.removeChild(line); + } + + this.draggableInfo = null; + this.movedIndexInfo = null; + + document.removeEventListener('mousemove', this.dragRow); + document.removeEventListener('mouseup', this.dropRow); + document.removeEventListener('selectstart', this.handleSelectStart); + } + private clearDocumentEvents = () => { this.dragStartData = { pageX: null, pageY: null }; this.props.dispatch('dragEnd'); @@ -179,13 +377,13 @@ class BodyAreaComp extends Component { document.removeEventListener('selectstart', this.handleSelectStart); }; - public shouldComponentUpdate(nextProps: Props) { + shouldComponentUpdate(nextProps: Props) { const currProps = this.props; return some((propName) => nextProps[propName] !== currProps[propName], PROPS_FOR_UPDATE); } - public componentWillReceiveProps(nextProps: Props) { + componentWillReceiveProps(nextProps: Props) { const { scrollTop, scrollLeft } = nextProps; this.el!.scrollTop = scrollTop; @@ -250,7 +448,7 @@ class BodyAreaComp extends Component { } export const BodyArea = connect((store, { side }) => { - const { columnCoords, rowCoords, dimension, viewport, id } = store; + const { columnCoords, rowCoords, dimension, viewport, id, column } = store; const { totalRowHeight } = rowCoords; const { totalColumnWidth } = columnCoords; const { bodyHeight, scrollXHeight, scrollX, scrollY, cellBorderWidth } = dimension; @@ -270,5 +468,6 @@ export const BodyArea = connect((store, { side }) => { scrollY, cellBorderWidth, eventBus: getEventBus(id), + hasTreeColumn: !!column.treeColumnName, }; })(BodyAreaComp); diff --git a/packages/toast-ui.grid/types/event/index.d.ts b/packages/toast-ui.grid/types/event/index.d.ts index f7da851fc..320324c59 100644 --- a/packages/toast-ui.grid/types/event/index.d.ts +++ b/packages/toast-ui.grid/types/event/index.d.ts @@ -37,6 +37,9 @@ export interface GridEventProps { page?: number; origin?: Origin; changes?: CellChange[]; + floatingRow?: HTMLElement; + targetRowKey?: RowKey | null; + appended?: boolean; } export class TuiGridEvent { diff --git a/packages/toast-ui.grid/types/index.d.ts b/packages/toast-ui.grid/types/index.d.ts index 9e051567a..81ded6217 100644 --- a/packages/toast-ui.grid/types/index.d.ts +++ b/packages/toast-ui.grid/types/index.d.ts @@ -20,6 +20,7 @@ import { OptRow, OptColumn, ResetOptions, + OptMoveRow, } from './options'; import { ModifiedRowsOptions, @@ -262,7 +263,7 @@ declare namespace tui { public setRow(rowKey: RowKey, row: OptRow): void; - public moveRow(rowKey: RowKey, targetIndex: number): void; + public moveRow(rowKey: RowKey, targetIndex: number, options: OptMoveRow): void; public setRequestParams(params: Dictionary): void; diff --git a/packages/toast-ui.grid/types/options.d.ts b/packages/toast-ui.grid/types/options.d.ts index a76356587..8ad28cb7b 100644 --- a/packages/toast-ui.grid/types/options.d.ts +++ b/packages/toast-ui.grid/types/options.d.ts @@ -73,7 +73,10 @@ export type GridEventName = | 'beforePageMove' | 'afterPageMove' | 'beforeChange' - | 'afterChange'; + | 'afterChange' + | 'dragStart' + | 'drag' + | 'drop'; export type GridEventListener = (gridEvent: TuiGridEvent) => void; export interface OptGrid { @@ -106,6 +109,7 @@ export interface OptGrid { onGridMounted?: GridEventListener; onGridUpdated?: GridEventListener; onGridBeforeDestroy?: GridEventListener; + draggable?: boolean; } export interface OptRow { @@ -117,7 +121,7 @@ export interface OptRow { export interface OptAppendRow { at?: number; focus?: boolean; - parentRowKey?: RowKey; + parentRowKey?: RowKey | null; extendPrevRowSpan?: boolean; } @@ -131,11 +135,15 @@ export interface OptRemoveRow { } export interface OptAppendTreeRow { - parentRowKey?: RowKey; + parentRowKey?: RowKey | null; offset?: number; focus?: boolean; } +export interface OptMoveRow { + appended?: boolean; +} + export interface OptTree { name: string; useIcon?: boolean; diff --git a/packages/toast-ui.grid/types/renderer/index.d.ts b/packages/toast-ui.grid/types/renderer/index.d.ts index 81dea8c14..9b1cb16a4 100644 --- a/packages/toast-ui.grid/types/renderer/index.d.ts +++ b/packages/toast-ui.grid/types/renderer/index.d.ts @@ -18,7 +18,7 @@ export interface CellRenderer { getElement(): Element; focused?(): void; mounted?(parent: HTMLElement): void; - render(props: CellRendererProps): void; + render?(props: CellRendererProps): void; beforeDestroy?(): void; } diff --git a/packages/toast-ui.grid/types/store/column.d.ts b/packages/toast-ui.grid/types/store/column.d.ts index ece81ff7f..85a727320 100644 --- a/packages/toast-ui.grid/types/store/column.d.ts +++ b/packages/toast-ui.grid/types/store/column.d.ts @@ -12,7 +12,7 @@ export type CustomValue = export type VAlignType = 'top' | 'middle' | 'bottom'; export type AlignType = 'left' | 'center' | 'right'; export type Formatter = ((props: FormatterProps) => string) | string; -export type RowHeaderType = 'rowNum' | 'checkbox'; +export type RowHeaderType = 'rowNum' | 'checkbox' | 'draggable'; export type SortingType = 'asc' | 'desc'; export type ErrorCode = | 'REQUIRED' diff --git a/packages/toast-ui.grid/types/store/data.d.ts b/packages/toast-ui.grid/types/store/data.d.ts index 648fe7fbb..05df1e361 100644 --- a/packages/toast-ui.grid/types/store/data.d.ts +++ b/packages/toast-ui.grid/types/store/data.d.ts @@ -18,6 +18,7 @@ export type Row = Dictionary & { _relationListItemMap: Dictionary; _disabledPriority: DisabledPriority; _children?: Row[]; + _leaf?: boolean; }; export type RowSpanAttributeValue = RowSpanAttribute[keyof RowSpanAttribute]; export type DisabledPriority = Dictionary<'ROW' | 'COLUMN'>; From 1759386c15db0c8c0dd3828ab18ca24b94aee1e2 Mon Sep 17 00:00:00 2001 From: jaesung-lee Date: Tue, 27 Apr 2021 12:37:02 +0900 Subject: [PATCH 11/14] fix: emit script error when copying unobservable rows (#1329) * fix: emit script error when copying unobservable rows * chore: apply code review --- packages/toast-ui.grid/src/dispatch/data.ts | 7 ++++-- packages/toast-ui.grid/src/query/clipboard.ts | 24 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/toast-ui.grid/src/dispatch/data.ts b/packages/toast-ui.grid/src/dispatch/data.ts index 0f67229d1..6f0845049 100644 --- a/packages/toast-ui.grid/src/dispatch/data.ts +++ b/packages/toast-ui.grid/src/dispatch/data.ts @@ -87,7 +87,7 @@ export function updateHeights(store: Store) { : filteredRawData.map((row) => getRowHeight(row, rowHeight)); } -export function makeObservable(store: Store, rowIndex: number) { +export function makeObservable(store: Store, rowIndex: number, silent = false) { const { data, column, id } = store; const { rawData, viewData } = data; const { treeColumnName } = column; @@ -104,7 +104,10 @@ export function makeObservable(store: Store, rowIndex: number) { rawData[rowIndex] = createRawRow(id, rawRow, rowIndex, column); } viewData[rowIndex] = createViewRow(id, rawData[rowIndex], rawData, column); - notify(data, 'rawData', 'filteredRawData', 'viewData', 'filteredViewData'); + + if (!silent) { + notify(data, 'rawData', 'filteredRawData', 'viewData', 'filteredViewData'); + } } export function setValue( diff --git a/packages/toast-ui.grid/src/query/clipboard.ts b/packages/toast-ui.grid/src/query/clipboard.ts index a057f288b..b15a9fd7a 100644 --- a/packages/toast-ui.grid/src/query/clipboard.ts +++ b/packages/toast-ui.grid/src/query/clipboard.ts @@ -1,9 +1,11 @@ import { CustomValue, ColumnInfo } from '@t/store/column'; -import { CellValue, Row, CellRenderData } from '@t/store/data'; +import { CellValue, Row, CellRenderData, ViewRow } from '@t/store/data'; import { ListItemOptions } from '@t/editor'; import { Store } from '@t/store'; import { SelectionRange } from '@t/store/selection'; import { find, isNull } from '../helper/common'; +import { makeObservable } from '../dispatch/data'; +import { isObservable, notify } from '../helper/observable'; function getCustomValue( customValue: CustomValue, @@ -75,6 +77,23 @@ function getValueToString(store: Store) { ); } +function getObservableList(store: Store, filteredViewData: ViewRow[], start: number, end: number) { + const rowList = []; + + for (let i = start; i <= end; i += 1) { + if (!isObservable(filteredViewData[i].valueMap)) { + makeObservable(store, i, true); + + if (i === end) { + notify(store.data, 'rawData', 'filteredRawData', 'viewData', 'filteredViewData'); + } + } + rowList.push(filteredViewData[i]); + } + + return rowList; +} + function getValuesToString(store: Store) { const { selection: { originalRange }, @@ -87,8 +106,7 @@ function getValuesToString(store: Store) { } const { row, column } = originalRange!; - - const rowList = filteredViewData.slice(row[0], row[1] + 1); + const rowList = getObservableList(store, filteredViewData, ...row); const columnInRange = visibleColumnsWithRowHeader.slice(column[0], column[1] + 1); return rowList From f63b7eef0aaf983bbf1be5c2ca5ac6c0fe3d894b Mon Sep 17 00:00:00 2001 From: js87zz Date: Tue, 27 Apr 2021 14:13:01 +0900 Subject: [PATCH 12/14] chore: update version to v4.17.0 --- lerna.json | 2 +- packages/toast-ui.grid/package-lock.json | 2 +- packages/toast-ui.grid/package.json | 2 +- packages/toast-ui.grid/types/index.d.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lerna.json b/lerna.json index d24deb5de..51cad5331 100644 --- a/lerna.json +++ b/lerna.json @@ -1,4 +1,4 @@ { "packages": ["packages/*"], - "version": "4.16.1" + "version": "4.17.0" } diff --git a/packages/toast-ui.grid/package-lock.json b/packages/toast-ui.grid/package-lock.json index 3a2cc875c..64d53b899 100644 --- a/packages/toast-ui.grid/package-lock.json +++ b/packages/toast-ui.grid/package-lock.json @@ -1,6 +1,6 @@ { "name": "tui-grid", - "version": "4.16.1", + "version": "4.17.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/toast-ui.grid/package.json b/packages/toast-ui.grid/package.json index 84042748b..cd8d8bd4e 100644 --- a/packages/toast-ui.grid/package.json +++ b/packages/toast-ui.grid/package.json @@ -1,6 +1,6 @@ { "name": "tui-grid", - "version": "4.16.1", + "version": "4.17.0", "description": "TOAST UI Grid : Powerful data grid control supported by TOAST UI", "main": "dist/tui-grid.js", "types": "types/index.d.ts", diff --git a/packages/toast-ui.grid/types/index.d.ts b/packages/toast-ui.grid/types/index.d.ts index 81ded6217..7b66ba593 100644 --- a/packages/toast-ui.grid/types/index.d.ts +++ b/packages/toast-ui.grid/types/index.d.ts @@ -1,4 +1,4 @@ -// Type definitions for TOAST UI Grid v4.16.1 +// Type definitions for TOAST UI Grid v4.17.0 // TypeScript Version: 3.9.5 import { CellValue, RowKey, Row, SortState, RowSpan, InvalidRow } from './store/data'; From 32ac3f41376d325cf998afc4c73293578197d31a Mon Sep 17 00:00:00 2001 From: js87zz Date: Tue, 27 Apr 2021 14:23:33 +0900 Subject: [PATCH 13/14] chore: update version of wrappers to v4.17.0 --- packages/toast-ui.react-grid/package-lock.json | 14 +++++++------- packages/toast-ui.react-grid/package.json | 4 ++-- packages/toast-ui.vue-grid/package-lock.json | 14 +++++++------- packages/toast-ui.vue-grid/package.json | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/toast-ui.react-grid/package-lock.json b/packages/toast-ui.react-grid/package-lock.json index 83f375c5c..586e989a9 100644 --- a/packages/toast-ui.react-grid/package-lock.json +++ b/packages/toast-ui.react-grid/package-lock.json @@ -1,6 +1,6 @@ { "name": "@toast-ui/react-grid", - "version": "4.16.1", + "version": "4.17.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -13993,17 +13993,17 @@ "integrity": "sha512-6jGbM/m7A2L59lJSripwMVp87awrWgJXezlLV8GuAha3s0k01E4+MndoU5WlXd4dauVRgzHhKguTVslx/jMehw==" }, "tui-date-picker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tui-date-picker/-/tui-date-picker-4.1.0.tgz", - "integrity": "sha512-ls/8yGuWe9MPa9SzL5iQiuyVpmDCdcJgyfg5O73U0sw+ba6Y1NLOuyRDrSFaT4tKg5jm3zeLD98I9jhYCT7P/g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tui-date-picker/-/tui-date-picker-4.2.0.tgz", + "integrity": "sha512-SDI3RRWOimhIAVRC23fpwxpp7q1wJu+fzNZYgOw2OyAYTa+R2oOjqUHzHEIK4OBMiasvHwWUyIztnAXoizQUeA==", "requires": { "tui-time-picker": "^2.0.3" } }, "tui-grid": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/tui-grid/-/tui-grid-4.16.1.tgz", - "integrity": "sha512-XCM9kWYdrN9/eK23506p+zRXaPerWIf89gu7pwug65zKF0hkR/8eCN7T9Cz1k29szPWBN2hSRL6FZntv8Ymc7g==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tui-grid/-/tui-grid-4.17.0.tgz", + "integrity": "sha512-Y5fzy1TP9h6+Ymej07w1cams34w2zlKr2qe5/3O0R1hGtLjLH84ZRocxq82b4BWk+C3p0LOVLmBdc2OoweKcCg==", "requires": { "tui-date-picker": "^4.1.0", "tui-pagination": "^3.4.0" diff --git a/packages/toast-ui.react-grid/package.json b/packages/toast-ui.react-grid/package.json index 80ac13c53..d9187469c 100644 --- a/packages/toast-ui.react-grid/package.json +++ b/packages/toast-ui.react-grid/package.json @@ -1,6 +1,6 @@ { "name": "@toast-ui/react-grid", - "version": "4.16.1", + "version": "4.17.0", "description": "TOAST UI Grid for React", "main": "dist/toastui-react-grid.js", "files": [ @@ -52,6 +52,6 @@ "xhr-mock": "^2.4.1" }, "dependencies": { - "tui-grid": "^4.16.1" + "tui-grid": "^4.17.0" } } diff --git a/packages/toast-ui.vue-grid/package-lock.json b/packages/toast-ui.vue-grid/package-lock.json index 10979fcc4..1cecf9648 100644 --- a/packages/toast-ui.vue-grid/package-lock.json +++ b/packages/toast-ui.vue-grid/package-lock.json @@ -1,6 +1,6 @@ { "name": "@toast-ui/vue-grid", - "version": "4.16.1", + "version": "4.17.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -13474,17 +13474,17 @@ "integrity": "sha512-6jGbM/m7A2L59lJSripwMVp87awrWgJXezlLV8GuAha3s0k01E4+MndoU5WlXd4dauVRgzHhKguTVslx/jMehw==" }, "tui-date-picker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tui-date-picker/-/tui-date-picker-4.1.0.tgz", - "integrity": "sha512-ls/8yGuWe9MPa9SzL5iQiuyVpmDCdcJgyfg5O73U0sw+ba6Y1NLOuyRDrSFaT4tKg5jm3zeLD98I9jhYCT7P/g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tui-date-picker/-/tui-date-picker-4.2.0.tgz", + "integrity": "sha512-SDI3RRWOimhIAVRC23fpwxpp7q1wJu+fzNZYgOw2OyAYTa+R2oOjqUHzHEIK4OBMiasvHwWUyIztnAXoizQUeA==", "requires": { "tui-time-picker": "^2.0.3" } }, "tui-grid": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/tui-grid/-/tui-grid-4.16.1.tgz", - "integrity": "sha512-XCM9kWYdrN9/eK23506p+zRXaPerWIf89gu7pwug65zKF0hkR/8eCN7T9Cz1k29szPWBN2hSRL6FZntv8Ymc7g==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tui-grid/-/tui-grid-4.17.0.tgz", + "integrity": "sha512-Y5fzy1TP9h6+Ymej07w1cams34w2zlKr2qe5/3O0R1hGtLjLH84ZRocxq82b4BWk+C3p0LOVLmBdc2OoweKcCg==", "requires": { "tui-date-picker": "^4.1.0", "tui-pagination": "^3.4.0" diff --git a/packages/toast-ui.vue-grid/package.json b/packages/toast-ui.vue-grid/package.json index f5b5a60f6..4214533ae 100644 --- a/packages/toast-ui.vue-grid/package.json +++ b/packages/toast-ui.vue-grid/package.json @@ -1,6 +1,6 @@ { "name": "@toast-ui/vue-grid", - "version": "4.16.1", + "version": "4.17.0", "description": "TOAST UI Grid for Vue", "main": "dist/toastui-vue-grid.js", "files": [ @@ -42,6 +42,6 @@ "vue": "^2.5.0" }, "dependencies": { - "tui-grid": "^4.16.1" + "tui-grid": "^4.17.0" } } From b0ae3758fe2c36fa8b356d9b03da1a5a9558b26f Mon Sep 17 00:00:00 2001 From: Aravindha1234u Date: Mon, 3 May 2021 22:00:05 +0530 Subject: [PATCH 14/14] Fixed XSS #1 --- packages/toast-ui.grid/src/dispatch/data.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/toast-ui.grid/src/dispatch/data.ts b/packages/toast-ui.grid/src/dispatch/data.ts index 6f0845049..7ba4d954b 100644 --- a/packages/toast-ui.grid/src/dispatch/data.ts +++ b/packages/toast-ui.grid/src/dispatch/data.ts @@ -165,7 +165,12 @@ export function setValue( return; } - value = change.nextValue; + value = String(change.nextValue) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); const { rowSpanMap } = targetRow; const { columns } = sortState; const index = findPropIndex('columnName', columnName, columns);