adore-datatable 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ import { isNumber, stripHTML } from './utils';
2
+ import CellManager from './cellmanager';
3
+
4
+ export default function filterRows(rows, filters, data) {
5
+ let filteredRowIndices = [];
6
+
7
+ if (Object.keys(filters).length === 0) {
8
+ return rows.map(row => row.meta.rowIndex);
9
+ }
10
+
11
+ for (let colIndex in filters) {
12
+ const keyword = filters[colIndex];
13
+
14
+ const filteredRows = filteredRowIndices.length ?
15
+ filteredRowIndices.map(i => rows[i]) :
16
+ rows;
17
+
18
+ const cells = filteredRows.map(row => row[colIndex]);
19
+
20
+ let filter = guessFilter(keyword);
21
+ let filterMethod = getFilterMethod(rows, data, filter);
22
+
23
+ if (filterMethod) {
24
+ filteredRowIndices = filterMethod(filter.text, cells);
25
+ } else {
26
+ filteredRowIndices = cells.map(cell => cell.rowIndex);
27
+ }
28
+ }
29
+
30
+ return filteredRowIndices;
31
+ };
32
+
33
+ function getFilterMethod(rows, allData, filter) {
34
+ const getFormattedValue = cell => {
35
+ let formatter = CellManager.getCustomCellFormatter(cell);
36
+ let rowData = rows[cell.rowIndex];
37
+ if (allData && allData.data && allData.data.length) {
38
+ rowData = allData.data[cell.rowIndex];
39
+ }
40
+ if (formatter && cell.content) {
41
+ cell.html = formatter(cell.content, rows[cell.rowIndex], cell.column, rowData, filter);
42
+ return stripHTML(cell.html);
43
+ }
44
+ return cell.content || '';
45
+ };
46
+
47
+ const stringCompareValue = cell =>
48
+ String(stripHTML(cell.html || '') || getFormattedValue(cell)).toLowerCase();
49
+
50
+ const numberCompareValue = cell => parseFloat(cell.content);
51
+
52
+ const getCompareValues = (cell, keyword) => {
53
+ if (cell.column.compareValue) {
54
+ const compareValues = cell.column.compareValue(cell, keyword);
55
+ if (compareValues && Array.isArray(compareValues)) return compareValues;
56
+ }
57
+
58
+ // check if it can be converted to number
59
+ const float = numberCompareValue(cell);
60
+ if (!isNaN(float)) {
61
+ return [float, keyword];
62
+ }
63
+
64
+ return [stringCompareValue(cell), keyword];
65
+ };
66
+
67
+ let filterMethodMap = {
68
+ contains(keyword, cells) {
69
+ return cells
70
+ .filter(cell => {
71
+ const needle = (keyword || '').toLowerCase();
72
+ return !needle ||
73
+ (cell.content || '').toLowerCase().includes(needle) ||
74
+ stringCompareValue(cell).includes(needle);
75
+ })
76
+ .map(cell => cell.rowIndex);
77
+ },
78
+
79
+ greaterThan(keyword, cells) {
80
+ return cells
81
+ .filter(cell => {
82
+ const [compareValue, keywordValue] = getCompareValues(cell, keyword);
83
+ return compareValue > keywordValue;
84
+ })
85
+ .map(cell => cell.rowIndex);
86
+ },
87
+
88
+ lessThan(keyword, cells) {
89
+ return cells
90
+ .filter(cell => {
91
+ const [compareValue, keywordValue] = getCompareValues(cell, keyword);
92
+ return compareValue < keywordValue;
93
+ })
94
+ .map(cell => cell.rowIndex);
95
+ },
96
+
97
+ equals(keyword, cells) {
98
+ return cells
99
+ .filter(cell => {
100
+ const value = parseFloat(cell.content);
101
+ return value === keyword;
102
+ })
103
+ .map(cell => cell.rowIndex);
104
+ },
105
+
106
+ notEquals(keyword, cells) {
107
+ return cells
108
+ .filter(cell => {
109
+ const value = parseFloat(cell.content);
110
+ return value !== keyword;
111
+ })
112
+ .map(cell => cell.rowIndex);
113
+ },
114
+
115
+ range(rangeValues, cells) {
116
+ return cells
117
+ .filter(cell => {
118
+ const values1 = getCompareValues(cell, rangeValues[0]);
119
+ const values2 = getCompareValues(cell, rangeValues[1]);
120
+ const value = values1[0];
121
+ return value >= values1[1] && value <= values2[1];
122
+ })
123
+ .map(cell => cell.rowIndex);
124
+ },
125
+
126
+ containsNumber(keyword, cells) {
127
+ return cells
128
+ .filter(cell => {
129
+ let number = parseFloat(keyword, 10);
130
+ let string = keyword;
131
+ let hayNumber = numberCompareValue(cell);
132
+ let hayString = stringCompareValue(cell);
133
+
134
+ return number === hayNumber || hayString.includes(string);
135
+ })
136
+ .map(cell => cell.rowIndex);
137
+ }
138
+ };
139
+
140
+ return filterMethodMap[filter.type];
141
+ }
142
+
143
+ function guessFilter(keyword = '') {
144
+ if (keyword.length === 0) return {};
145
+
146
+ let compareString = keyword;
147
+
148
+ if (['>', '<', '='].includes(compareString[0])) {
149
+ compareString = keyword.slice(1);
150
+ } else if (compareString.startsWith('!=')) {
151
+ compareString = keyword.slice(2);
152
+ }
153
+
154
+ if (keyword.startsWith('>')) {
155
+ if (compareString) {
156
+ return {
157
+ type: 'greaterThan',
158
+ text: compareString.trim()
159
+ };
160
+ }
161
+ }
162
+
163
+ if (keyword.startsWith('<')) {
164
+ if (compareString) {
165
+ return {
166
+ type: 'lessThan',
167
+ text: compareString.trim()
168
+ };
169
+ }
170
+ }
171
+
172
+ if (keyword.startsWith('=')) {
173
+ if (isNumber(compareString)) {
174
+ return {
175
+ type: 'equals',
176
+ text: Number(keyword.slice(1).trim())
177
+ };
178
+ }
179
+ }
180
+
181
+ if (isNumber(compareString)) {
182
+ return {
183
+ type: 'containsNumber',
184
+ text: compareString
185
+ };
186
+ }
187
+
188
+ if (keyword.startsWith('!=')) {
189
+ if (isNumber(compareString)) {
190
+ return {
191
+ type: 'notEquals',
192
+ text: Number(keyword.slice(2).trim())
193
+ };
194
+ }
195
+ }
196
+
197
+ if (keyword.split(':').length === 2 && keyword.split(':').every(v => isNumber(v.trim()))) {
198
+ compareString = keyword.split(':');
199
+ return {
200
+ type: 'range',
201
+ text: compareString.map(v => v.trim())
202
+ };
203
+ }
204
+
205
+ return {
206
+ type: 'contains',
207
+ text: compareString.toLowerCase()
208
+ };
209
+ }
package/src/icons.js ADDED
@@ -0,0 +1,10 @@
1
+ /* eslint-disable max-len */
2
+
3
+ // Icons from https://feathericons.com/
4
+
5
+ let icons = {
6
+ chevronDown: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>',
7
+ chevronRight: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>'
8
+ };
9
+
10
+ export default icons;
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import DataTable from './datatable.js';
2
+ import packageJson from '../package.json';
3
+ DataTable.__version__ = packageJson.version;
4
+
5
+ export default DataTable;
@@ -0,0 +1,59 @@
1
+ import $ from './dom';
2
+
3
+ const KEYCODES = {
4
+ 13: 'enter',
5
+ 91: 'meta',
6
+ 16: 'shift',
7
+ 17: 'ctrl',
8
+ 18: 'alt',
9
+ 37: 'left',
10
+ 38: 'up',
11
+ 39: 'right',
12
+ 40: 'down',
13
+ 9: 'tab',
14
+ 27: 'esc',
15
+ 67: 'c',
16
+ 70: 'f',
17
+ 86: 'v'
18
+ };
19
+
20
+ export default class Keyboard {
21
+ constructor(element) {
22
+ this.listeners = {};
23
+ $.on(element, 'keydown', this.handler.bind(this));
24
+ }
25
+
26
+ handler(e) {
27
+ let key = KEYCODES[e.keyCode];
28
+
29
+ if (e.shiftKey && key !== 'shift') {
30
+ key = 'shift+' + key;
31
+ }
32
+
33
+ if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) {
34
+ key = 'ctrl+' + key;
35
+ }
36
+
37
+ const listeners = this.listeners[key];
38
+
39
+ if (listeners && listeners.length > 0) {
40
+ for (let listener of listeners) {
41
+ const preventBubbling = listener(e);
42
+ if (preventBubbling === undefined || preventBubbling === true) {
43
+ e.preventDefault();
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ on(key, listener) {
50
+ const keys = key.split(',').map(k => k.trim());
51
+
52
+ keys.map(key => {
53
+ this.listeners[key] = this.listeners[key] || [];
54
+ this.listeners[key].push(listener);
55
+ });
56
+ }
57
+ }
58
+
59
+ export let keyCode = KEYCODES;
@@ -0,0 +1,369 @@
1
+ import $ from './dom';
2
+ import {
3
+ makeDataAttributeString,
4
+ nextTick,
5
+ ensureArray,
6
+ linkProperties,
7
+ uniq,
8
+ numberSortAsc
9
+ } from './utils';
10
+
11
+ export default class RowManager {
12
+ constructor(instance) {
13
+ this.instance = instance;
14
+ linkProperties(this, this.instance, [
15
+ 'options',
16
+ 'fireEvent',
17
+ 'wrapper',
18
+ 'bodyScrollable',
19
+ 'bodyRenderer',
20
+ 'style'
21
+ ]);
22
+
23
+ this.bindEvents();
24
+ this.refreshRows = nextTick(this.refreshRows, this);
25
+ }
26
+
27
+ get datamanager() {
28
+ return this.instance.datamanager;
29
+ }
30
+
31
+ get cellmanager() {
32
+ return this.instance.cellmanager;
33
+ }
34
+
35
+ bindEvents() {
36
+ this.bindCheckbox();
37
+ }
38
+
39
+ bindCheckbox() {
40
+ if (!this.options.checkboxColumn) return;
41
+
42
+ // map of checked rows
43
+ this.checkMap = [];
44
+
45
+ $.on(this.wrapper, 'click', '.dt-cell--col-0 [type="checkbox"]', (e, $checkbox) => {
46
+ const $cell = $checkbox.closest('.dt-cell');
47
+ const {
48
+ rowIndex,
49
+ isHeader
50
+ } = $.data($cell);
51
+ const checked = $checkbox.checked;
52
+
53
+ if (isHeader) {
54
+ this.checkAll(checked);
55
+ } else {
56
+ this.checkRow(rowIndex, checked);
57
+ }
58
+ });
59
+ }
60
+
61
+ refreshRows() {
62
+ this.instance.renderBody();
63
+ this.instance.setDimensions();
64
+ }
65
+
66
+ refreshRow(row, rowIndex) {
67
+ const _row = this.datamanager.updateRow(row, rowIndex);
68
+
69
+ _row.forEach(cell => {
70
+ this.cellmanager.refreshCell(cell, true);
71
+ });
72
+ }
73
+
74
+ getCheckedRows() {
75
+ if (!this.checkMap) {
76
+ return [];
77
+ }
78
+
79
+ let out = [];
80
+ for (let rowIndex in this.checkMap) {
81
+ const checked = this.checkMap[rowIndex];
82
+ if (checked === 1) {
83
+ out.push(rowIndex);
84
+ }
85
+ }
86
+
87
+ return out;
88
+ }
89
+
90
+ highlightCheckedRows() {
91
+ this.getCheckedRows()
92
+ .map(rowIndex => this.checkRow(rowIndex, true));
93
+ }
94
+
95
+ checkRow(rowIndex, toggle) {
96
+ const value = toggle ? 1 : 0;
97
+ const selector = rowIndex => `.dt-cell--0-${rowIndex} [type="checkbox"]`;
98
+ // update internal map
99
+ this.checkMap[rowIndex] = value;
100
+ // set checkbox value explicitly
101
+ $.each(selector(rowIndex), this.bodyScrollable)
102
+ .map(input => {
103
+ input.checked = toggle;
104
+ });
105
+ // highlight row
106
+ this.highlightRow(rowIndex, toggle);
107
+ this.showCheckStatus();
108
+ this.fireEvent('onCheckRow', this.datamanager.getRow(rowIndex));
109
+ }
110
+
111
+ checkAll(toggle) {
112
+ const value = toggle ? 1 : 0;
113
+
114
+ // update internal map
115
+ if (toggle) {
116
+ if (this.datamanager._filteredRows) {
117
+ this.datamanager._filteredRows.forEach(f => {
118
+ this.checkRow(f, toggle);
119
+ });
120
+ } else {
121
+ this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value);
122
+ }
123
+ } else {
124
+ this.checkMap = [];
125
+ }
126
+ // set checkbox value
127
+ $.each('.dt-cell--col-0 [type="checkbox"]', this.bodyScrollable)
128
+ .map(input => {
129
+ input.checked = toggle;
130
+ });
131
+ // highlight all
132
+ this.highlightAll(toggle);
133
+ this.showCheckStatus();
134
+ this.fireEvent('onCheckRow');
135
+ }
136
+
137
+ showCheckStatus() {
138
+ if (!this.options.checkedRowStatus) return;
139
+ const checkedRows = this.getCheckedRows();
140
+ const count = checkedRows.length;
141
+ if (count > 0) {
142
+ let message = this.instance.translate('{count} rows selected', {
143
+ count: count
144
+ });
145
+ this.bodyRenderer.showToastMessage(message);
146
+ } else {
147
+ this.bodyRenderer.clearToastMessage();
148
+ }
149
+ }
150
+
151
+ highlightRow(rowIndex, toggle = true) {
152
+ const $row = this.getRow$(rowIndex);
153
+ if (!$row) return;
154
+
155
+ if (!toggle && this.bodyScrollable.classList.contains('dt-scrollable--highlight-all')) {
156
+ $row.classList.add('dt-row--unhighlight');
157
+ return;
158
+ }
159
+
160
+ if (toggle && $row.classList.contains('dt-row--unhighlight')) {
161
+ $row.classList.remove('dt-row--unhighlight');
162
+ }
163
+
164
+ this._highlightedRows = this._highlightedRows || {};
165
+
166
+ if (toggle) {
167
+ $row.classList.add('dt-row--highlight');
168
+ this._highlightedRows[rowIndex] = $row;
169
+ } else {
170
+ $row.classList.remove('dt-row--highlight');
171
+ delete this._highlightedRows[rowIndex];
172
+ }
173
+ }
174
+
175
+ highlightAll(toggle = true) {
176
+ if (toggle) {
177
+ this.bodyScrollable.classList.add('dt-scrollable--highlight-all');
178
+ } else {
179
+ this.bodyScrollable.classList.remove('dt-scrollable--highlight-all');
180
+ for (const rowIndex in this._highlightedRows) {
181
+ const $row = this._highlightedRows[rowIndex];
182
+ $row.classList.remove('dt-row--highlight');
183
+ }
184
+ this._highlightedRows = {};
185
+ }
186
+ }
187
+
188
+ showRows(rowIndices) {
189
+ rowIndices = ensureArray(rowIndices);
190
+ const rows = rowIndices.map(rowIndex => this.datamanager.getRow(rowIndex));
191
+ this.bodyRenderer.renderRows(rows);
192
+ }
193
+
194
+ showAllRows() {
195
+ const rowIndices = this.datamanager.getAllRowIndices();
196
+ this.showRows(rowIndices);
197
+ }
198
+
199
+ getChildrenToShowForNode(rowIndex) {
200
+ const row = this.datamanager.getRow(rowIndex);
201
+ row.meta.isTreeNodeClose = false;
202
+
203
+ return this.datamanager.getImmediateChildren(rowIndex);
204
+ }
205
+
206
+ openSingleNode(rowIndex) {
207
+ const childrenToShow = this.getChildrenToShowForNode(rowIndex);
208
+ const visibleRowIndices = this.bodyRenderer.visibleRowIndices;
209
+ const rowsToShow = uniq([...childrenToShow, ...visibleRowIndices]).sort(numberSortAsc);
210
+
211
+ this.showRows(rowsToShow);
212
+ }
213
+
214
+ getChildrenToHideForNode(rowIndex) {
215
+ const row = this.datamanager.getRow(rowIndex);
216
+ row.meta.isTreeNodeClose = true;
217
+
218
+ const rowsToHide = this.datamanager.getChildren(rowIndex);
219
+ rowsToHide.forEach(rowIndex => {
220
+ const row = this.datamanager.getRow(rowIndex);
221
+ if (!row.meta.isLeaf) {
222
+ row.meta.isTreeNodeClose = true;
223
+ }
224
+ });
225
+
226
+ return rowsToHide;
227
+ }
228
+
229
+ closeSingleNode(rowIndex) {
230
+ const rowsToHide = this.getChildrenToHideForNode(rowIndex);
231
+ const visibleRows = this.bodyRenderer.visibleRowIndices;
232
+ const rowsToShow = visibleRows
233
+ .filter(rowIndex => !rowsToHide.includes(rowIndex))
234
+ .sort(numberSortAsc);
235
+
236
+ this.showRows(rowsToShow);
237
+ }
238
+
239
+ expandAllNodes() {
240
+ let rows = this.datamanager.getRows();
241
+ let rootNodes = rows.filter(row => !row.meta.isLeaf);
242
+
243
+ const childrenToShow = rootNodes.map(row => this.getChildrenToShowForNode(row.meta.rowIndex)).flat();
244
+ const visibleRowIndices = this.bodyRenderer.visibleRowIndices;
245
+ const rowsToShow = uniq([...childrenToShow, ...visibleRowIndices]).sort(numberSortAsc);
246
+
247
+ this.showRows(rowsToShow);
248
+ }
249
+
250
+ collapseAllNodes() {
251
+ let rows = this.datamanager.getRows();
252
+ let rootNodes = rows.filter(row => row.meta.indent === 0);
253
+
254
+ const rowsToHide = rootNodes.map(row => this.getChildrenToHideForNode(row.meta.rowIndex)).flat();
255
+ const visibleRows = this.bodyRenderer.visibleRowIndices;
256
+ const rowsToShow = visibleRows
257
+ .filter(rowIndex => !rowsToHide.includes(rowIndex))
258
+ .sort(numberSortAsc);
259
+
260
+ this.showRows(rowsToShow);
261
+ }
262
+
263
+ setTreeDepth(depth) {
264
+ let rows = this.datamanager.getRows();
265
+
266
+ const rowsToOpen = rows.filter(row => row.meta.indent < depth);
267
+ const rowsToClose = rows.filter(row => row.meta.indent >= depth);
268
+ const rowsToHide = rowsToClose.filter(row => row.meta.indent > depth);
269
+
270
+ rowsToClose.forEach(row => {
271
+ if (!row.meta.isLeaf) {
272
+ row.meta.isTreeNodeClose = true;
273
+ }
274
+ });
275
+ rowsToOpen.forEach(row => {
276
+ if (!row.meta.isLeaf) {
277
+ row.meta.isTreeNodeClose = false;
278
+ }
279
+ });
280
+
281
+ const rowsToShow = rows
282
+ .filter(row => !rowsToHide.includes(row))
283
+ .map(row => row.meta.rowIndex)
284
+ .sort(numberSortAsc);
285
+ this.showRows(rowsToShow);
286
+ }
287
+
288
+ getRow$(rowIndex) {
289
+ return $(this.selector(rowIndex), this.bodyScrollable);
290
+ }
291
+
292
+ getTotalRows() {
293
+ return this.datamanager.getRowCount();
294
+ }
295
+
296
+ getFirstRowIndex() {
297
+ return 0;
298
+ }
299
+
300
+ getLastRowIndex() {
301
+ return this.datamanager.getRowCount() - 1;
302
+ }
303
+
304
+ scrollToRow(rowIndex) {
305
+ rowIndex = +rowIndex;
306
+ this._lastScrollTo = this._lastScrollTo || 0;
307
+ const $row = this.getRow$(rowIndex);
308
+ if ($.inViewport($row, this.bodyScrollable)) return;
309
+
310
+ const {
311
+ height
312
+ } = $row.getBoundingClientRect();
313
+ const {
314
+ top,
315
+ bottom
316
+ } = this.bodyScrollable.getBoundingClientRect();
317
+ const rowsInView = Math.floor((bottom - top) / height);
318
+
319
+ let offset = 0;
320
+ if (rowIndex > this._lastScrollTo) {
321
+ offset = height * ((rowIndex + 1) - rowsInView);
322
+ } else {
323
+ offset = height * ((rowIndex + 1) - 1);
324
+ }
325
+
326
+ this._lastScrollTo = rowIndex;
327
+ $.scrollTop(this.bodyScrollable, offset);
328
+ }
329
+
330
+ getRowHTML(row, props) {
331
+ const dataAttr = makeDataAttributeString(props);
332
+ let rowIdentifier = props.rowIndex;
333
+
334
+ if (props.isFilter) {
335
+ row = row.map(cell => (Object.assign({}, cell, {
336
+ content: this.getFilterInput({
337
+ colIndex: cell.colIndex,
338
+ name: cell.name
339
+ }),
340
+ isFilter: 1,
341
+ isHeader: undefined,
342
+ editable: false
343
+ })));
344
+
345
+ rowIdentifier = 'filter';
346
+ }
347
+
348
+ if (props.isHeader) {
349
+ rowIdentifier = 'header';
350
+ }
351
+
352
+ return `
353
+ <div class="dt-row dt-row-${rowIdentifier}" ${dataAttr}>
354
+ ${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')}
355
+ </div>
356
+ `;
357
+ }
358
+
359
+ getFilterInput(props) {
360
+ let title = `title="Filter based on ${props.name || 'Index'}"`;
361
+ const dataAttr = makeDataAttributeString(props);
362
+ return `<input class="dt-filter dt-input" type="text" ${dataAttr} tabindex="1"
363
+ ${props.colIndex === 0 ? 'disabled' : title} />`;
364
+ }
365
+
366
+ selector(rowIndex) {
367
+ return `.dt-row-${rowIndex}`;
368
+ }
369
+ }