js-spread-grid 0.0.1

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.
Files changed (67) hide show
  1. package/package.json +26 -0
  2. package/src/core/render.js +400 -0
  3. package/src/core/state.js +183 -0
  4. package/src/core-utils/defaults.js +4 -0
  5. package/src/core-utils/rect.js +49 -0
  6. package/src/core-utils/rect.test.js +111 -0
  7. package/src/core-utils/roundToPixels.js +3 -0
  8. package/src/core-utils/stringifyId.js +27 -0
  9. package/src/core-utils/stringifyId.test.js +27 -0
  10. package/src/index.js +595 -0
  11. package/src/state-utils/getActive.js +17 -0
  12. package/src/state-utils/getCellSection.js +22 -0
  13. package/src/state-utils/getCellType.js +7 -0
  14. package/src/state-utils/getClipboardData.js +53 -0
  15. package/src/state-utils/getColumnIndex.js +25 -0
  16. package/src/state-utils/getCombinedCells.js +6 -0
  17. package/src/state-utils/getDataFormatting.js +46 -0
  18. package/src/state-utils/getEditableCells.js +23 -0
  19. package/src/state-utils/getEditedCellsAndFilters.js +4 -0
  20. package/src/state-utils/getEdition.js +6 -0
  21. package/src/state-utils/getFilterFormatting.js +3 -0
  22. package/src/state-utils/getFiltered.js +61 -0
  23. package/src/state-utils/getFilteringRules.js +5 -0
  24. package/src/state-utils/getFixedSize.js +8 -0
  25. package/src/state-utils/getFormatResolver.js +5 -0
  26. package/src/state-utils/getFormattingRules.js +5 -0
  27. package/src/state-utils/getHighlightedCells.js +40 -0
  28. package/src/state-utils/getHoveredCell.js +43 -0
  29. package/src/state-utils/getInputFormatting.js +3 -0
  30. package/src/state-utils/getInputPlacement.js +51 -0
  31. package/src/state-utils/getInvoked.js +5 -0
  32. package/src/state-utils/getIsTextValid.js +3 -0
  33. package/src/state-utils/getKeys.js +3 -0
  34. package/src/state-utils/getLookup.js +3 -0
  35. package/src/state-utils/getMeasureFormatting.js +3 -0
  36. package/src/state-utils/getMeasured.js +113 -0
  37. package/src/state-utils/getMousePosition.js +8 -0
  38. package/src/state-utils/getNewSortBy.js +20 -0
  39. package/src/state-utils/getPinned.js +8 -0
  40. package/src/state-utils/getPlaced.js +45 -0
  41. package/src/state-utils/getReducedCells.js +6 -0
  42. package/src/state-utils/getRenderFormatting.js +122 -0
  43. package/src/state-utils/getResizable.js +49 -0
  44. package/src/state-utils/getResolved.js +42 -0
  45. package/src/state-utils/getResolvedFilters.js +7 -0
  46. package/src/state-utils/getResolvedSortBy.js +7 -0
  47. package/src/state-utils/getRowIndex.js +25 -0
  48. package/src/state-utils/getScrollRect.js +41 -0
  49. package/src/state-utils/getSections.js +101 -0
  50. package/src/state-utils/getSelection.js +5 -0
  51. package/src/state-utils/getSorted.js +130 -0
  52. package/src/state-utils/getSortingFormatting.js +3 -0
  53. package/src/state-utils/getSortingRules.js +5 -0
  54. package/src/state-utils/getTextResolver.js +5 -0
  55. package/src/state-utils/getToggledValue.js +11 -0
  56. package/src/state-utils/getTotalSize.js +6 -0
  57. package/src/state-utils/getUnfolded.js +86 -0
  58. package/src/types/Edition.js +37 -0
  59. package/src/types/FilteringRules.js +48 -0
  60. package/src/types/FormatResolver.js +19 -0
  61. package/src/types/FormattingRules.js +120 -0
  62. package/src/types/FormattingRules.test.js +90 -0
  63. package/src/types/RulesLookup.js +118 -0
  64. package/src/types/Selection.js +25 -0
  65. package/src/types/SortingRules.js +62 -0
  66. package/src/types/TextResolver.js +60 -0
  67. package/src/types/VisibilityResolver.js +61 -0
@@ -0,0 +1,37 @@
1
+ import stringifyId from "../core-utils/stringifyId.js";
2
+
3
+ // TODO: write unit tests
4
+ export default class Edition {
5
+ constructor(editedCells) {
6
+ this.lookup = new Map();
7
+
8
+ editedCells.forEach(cell => {
9
+ const rowKey = stringifyId(cell.rowId);
10
+ const columnKey = stringifyId(cell.columnId);
11
+
12
+ if (!this.lookup.has(rowKey))
13
+ this.lookup.set(rowKey, new Map());
14
+
15
+ this.lookup.get(rowKey).set(columnKey, cell.value);
16
+ });
17
+ }
18
+
19
+ hasValueByKey(rowKey, columnKey) {
20
+ return this.lookup.has(rowKey) && this.lookup.get(rowKey).has(columnKey);
21
+ }
22
+
23
+ getValueByKey(rowKey, columnKey) {
24
+ if (!this.hasValueByKey(rowKey, columnKey))
25
+ return undefined;
26
+
27
+ return this.lookup.get(rowKey).get(columnKey);
28
+ }
29
+
30
+ hasValueById(rowId, columnId) {
31
+ return this.hasValueByKey(stringifyId(rowId), stringifyId(columnId));
32
+ }
33
+
34
+ getValueById(rowId, columnId) {
35
+ return this.getValueByKey(stringifyId(rowId), stringifyId(columnId));
36
+ }
37
+ }
@@ -0,0 +1,48 @@
1
+ import stringifyId from "../core-utils/stringifyId.js";
2
+ import RulesLookup from "./RulesLookup.js";
3
+
4
+ const defaultCondition = ({ text, expression }) => text.includes(expression);
5
+
6
+ export default class FilteringRules {
7
+ constructor(rules) {
8
+ this.rulesLookup = new RulesLookup();
9
+
10
+ for (const rule of rules) {
11
+ const entry = {
12
+ by: stringifyId('by' in rule ? rule.by : 'FILTER'),
13
+ condition: rule.condition || defaultCondition
14
+ };
15
+
16
+ this.rulesLookup.addRule(rule.column, rule.row, entry);
17
+ }
18
+ }
19
+
20
+ resolve(data, rows, columns, row, column, value, text, filterLookup) {
21
+ const rules = this.rulesLookup.getRules(column, row);
22
+
23
+ if (rules.length === 0) {
24
+ if (row.type !== 'DATA')
25
+ return true;
26
+ if (column.type !== 'DATA')
27
+ return true;
28
+ if (!filterLookup.has('"FILTER"'))
29
+ return true;
30
+
31
+ return defaultCondition({ text, expression: filterLookup.get('"FILTER"') });
32
+ }
33
+
34
+ let context = { data, rows, columns, row, column, value, text };
35
+
36
+ for (const rule of rules) {
37
+ if (!filterLookup.has(rule.by))
38
+ continue;
39
+
40
+ const filterContext = { ...context, expression: filterLookup.get(rule.by) };
41
+
42
+ if (!rule.condition(filterContext))
43
+ return false;
44
+ }
45
+
46
+ return true;
47
+ }
48
+ }
@@ -0,0 +1,19 @@
1
+ export default class FormatResolver {
2
+ constructor(formattingRules, data, rows, columns, edition) {
3
+ this.formattingRules = formattingRules;
4
+ this.data = data;
5
+ this.rows = rows;
6
+ this.columns = columns;
7
+ this.edition = edition;
8
+ }
9
+
10
+ resolve(row, column) {
11
+ return this.formattingRules.resolve(
12
+ this.data,
13
+ this.rows,
14
+ this.columns,
15
+ row,
16
+ column,
17
+ this.edition);
18
+ }
19
+ }
@@ -0,0 +1,120 @@
1
+ import { defaultPadding } from "../core-utils/defaults.js";
2
+ import RulesLookup from "./RulesLookup.js";
3
+
4
+ const borderTypes = ['borderTop', 'borderRight', 'borderBottom', 'borderLeft'];
5
+ // TODO: better handle default validate for toggle edit
6
+ const defaultEdit = { validate: () => true, parse: string => string };
7
+
8
+ // TODO: Don't recreate styles if they haven't changed
9
+ function indexBorders(style, index) {
10
+ const newStyle = { ...style };
11
+
12
+ if ('border' in newStyle) {
13
+ for (const borderType of borderTypes)
14
+ newStyle[borderType] = newStyle.border;
15
+ delete newStyle.border;
16
+ }
17
+
18
+ for (const borderType of borderTypes)
19
+ if (borderType in newStyle)
20
+ newStyle[borderType] = { ...newStyle[borderType], index };
21
+
22
+ return newStyle;
23
+ }
24
+
25
+ function getText(context) {
26
+ if ('text' in context)
27
+ return context.text;
28
+ if (context.value !== undefined)
29
+ return `${context.value}`;
30
+ return '';
31
+ }
32
+
33
+ // TODO: Rename to FormatResolver
34
+ // TODO: Optimize by not searching using keys that don't have correlated match rules
35
+ // TODO: Accept both a function and an object as a style (where the object is a resolved style)
36
+ // TODO: Consider removing index from the lookup
37
+ export default class FormattingRules {
38
+ constructor(rules) {
39
+ this.rulesLookup = new RulesLookup();
40
+
41
+ for (const [index, rule] of rules.entries()) {
42
+ const entry = { index };
43
+
44
+ if ('condition' in rule)
45
+ entry.condition = rule.condition;
46
+ if ('style' in rule)
47
+ entry.style = typeof rule.style === 'function' ? rule.style : () => rule.style;
48
+ if ('value' in rule)
49
+ entry.value = typeof rule.value === 'function' ? rule.value : () => rule.value;
50
+ if ('text' in rule)
51
+ entry.text = typeof rule.text === 'function' ? rule.text : () => rule.text;
52
+ if ('padding' in rule)
53
+ entry.padding = typeof rule.padding === 'function' ? rule.padding : () => rule.padding;
54
+ if ('edit' in rule)
55
+ entry.edit = rule.edit;
56
+ if ('draw' in rule)
57
+ entry.draw = rule.draw;
58
+
59
+ this.rulesLookup.addRule(rule.column, rule.row, entry);
60
+ }
61
+ }
62
+
63
+ resolve(data, rows, columns, row, column, edition) {
64
+ const rules = this.rulesLookup
65
+ .getRules(column, row)
66
+ .sort((a, b) => a.index - b.index)
67
+ .filter((value, index, array) => value.index !== array[index - 1]?.index);
68
+
69
+ let context = { data, rows, columns, row, column };
70
+ let style = {};
71
+ let draw = undefined;
72
+ let visible = true;
73
+ let padding = defaultPadding;
74
+
75
+ if (edition.hasValueByKey(row.key, column.key))
76
+ context = { ...context, newValue: edition.getValueByKey(row.key, column.key) };
77
+
78
+ for (const rule of rules) {
79
+ if ('condition' in rule && !rule.condition(context))
80
+ continue;
81
+
82
+ // TODO: Don't actually add things like `edit` to the context
83
+ if ('value' in rule)
84
+ context = { ...context, value: rule.value(context) };
85
+ if ('style' in rule)
86
+ style = { ...style, ...indexBorders(rule.style(context), rule.index) };
87
+ if ('text' in rule)
88
+ context = { ...context, text: rule.text(context) };
89
+ if ('padding' in rule)
90
+ padding = { ...padding, ...rule.padding(context) };
91
+ if ('edit' in rule)
92
+ context = { ...context, edit: rule.edit && 'edit' in context ? { ...context.edit, ...rule.edit } : { ...defaultEdit, ...rule.edit } };
93
+ if ('draw' in rule) {
94
+ const currentContext = context;
95
+ draw = (ctx) => rule.draw({ ...currentContext, ctx });
96
+ }
97
+
98
+ // TODO: Add StopPropagation
99
+ }
100
+
101
+ const text = getText(context);
102
+ const result = {
103
+ style,
104
+ visible,
105
+ text,
106
+ padding
107
+ };
108
+
109
+ if ('value' in context)
110
+ result.value = context.value;
111
+ if ('edit' in context)
112
+ result.edit = context.edit;
113
+ if (draw !== undefined)
114
+ result.draw = draw;
115
+ if ('text' in context)
116
+ result.text = context.text;
117
+
118
+ return result;
119
+ }
120
+ }
@@ -0,0 +1,90 @@
1
+ import Edition from './Edition.js';
2
+ import FormattingRules from './FormattingRules.js';
3
+ import stringifyId from '../core-utils/stringifyId.js';
4
+
5
+ // TODO: add even more test cases
6
+ describe('FormattingRules', () => {
7
+ describe('resolve', () => {
8
+ it('should return an empty object when no styles are matched', () => {
9
+ const styles = [
10
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => false, style: () => ({ color: 'red' }) },
11
+ { column: { id: 'col2' }, row: { id: 'row2' }, condition: () => false, style: () => ({ color: 'blue' }) },
12
+ ];
13
+ const resolver = new FormattingRules(styles);
14
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row3') }, { key: stringifyId('col3') }, new Edition([])).style;
15
+ expect(style).toEqual({});
16
+ });
17
+
18
+ it('should apply indexing to border styles', () => {
19
+ const styles = [
20
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => true, style: () => ({ borderTop: { width: 1 } }) },
21
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => true, style: () => ({ borderLeft: { width: 1 } }) },
22
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => true, style: () => ({ borderBottom: { width: 1 } }) },
23
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => true, style: () => ({ borderRight: { width: 1 } }) },
24
+ ];
25
+ const resolver = new FormattingRules(styles);
26
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row1') }, { key: stringifyId('col1') }, new Edition([])).style;
27
+ expect(style).toEqual({
28
+ borderTop: { width: 1, index: 0 },
29
+ borderLeft: { width: 1, index: 1 },
30
+ borderBottom: { width: 1, index: 2 },
31
+ borderRight: { width: 1, index: 3 },
32
+ });
33
+ });
34
+
35
+ it('should only apply rules for which the condition is true', () => {
36
+ const styles = [
37
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => true, style: () => ({ color: 'red' }) },
38
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => false, style: () => ({ color: 'blue' }) },
39
+ ];
40
+ const resolver = new FormattingRules(styles);
41
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row1') }, { key: stringifyId('col1') }, new Edition([])).style;
42
+ expect(style).toEqual({ color: 'red' });
43
+ });
44
+
45
+ it('should use the value for evaluating conditions', () => {
46
+ const styles = [
47
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: ({row}) => row.data, style: () => ({ color: 'red' }) },
48
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: ({row}) => !row.data, style: () => ({ color: 'blue' }) },
49
+ ];
50
+ const resolver = new FormattingRules(styles);
51
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row1'), data: true }, { key: stringifyId('col1') }, new Edition([])).style;
52
+ expect(style).toEqual({ color: 'red' });
53
+ });
54
+
55
+ const commonStyles = [
56
+ { column: { id: 'col1' }, row: { id: 'row1' }, condition: () => true, style: () => ({ color: 'red' }) },
57
+ { column: { id: 'col2' }, row: { id: 'row2' }, condition: () => true, style: () => ({ color: 'blue', background: 'black' }) },
58
+ { column: { match: 'ANY' }, row: { match: 'ANY' }, condition: () => true, style: () => ({ color: 'green' }) },
59
+ { column: { id: 'col1' }, row: { match: 'ANY' }, condition: () => true, style: () => ({ color: 'yellow' }) },
60
+ { column: { match: 'ANY' }, row: { id: 'row1' }, condition: () => true, style: () => ({ color: 'purple' }) },
61
+ { column: { match: 'ANY' }, row: { index: 0 }, condition: () => true, style: () => ({ color: 'pink' }) },
62
+ { column: { index: 0 }, row: { match: 'ANY' }, condition: () => true, style: () => ({ color: 'orange' }) },
63
+ { column: { id: 'col2' }, row: { match: 'ANY' }, condition: () => true, style: () => ({ color: 'brown' }) },
64
+ ];
65
+
66
+ it('should use ANY rules for matching undefined columns and rows', () => {
67
+ const resolver = new FormattingRules(commonStyles);
68
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row3'), type: 'DATA' }, { key: stringifyId('col3'), type: 'DATA' }, new Edition([])).style;
69
+ expect(style).toEqual({ color: 'green' });
70
+ });
71
+
72
+ it('should use latest matching rule', () => {
73
+ const resolver = new FormattingRules(commonStyles);
74
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row1'), index: 1, type: 'DATA' }, { key: stringifyId('col1'), index: 1, type: 'DATA' }, new Edition([])).style;
75
+ expect(style).toEqual({ color: 'purple' });
76
+ });
77
+
78
+ it('combine styles from multiple rules', () => {
79
+ const resolver = new FormattingRules(commonStyles);
80
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row2'), index: 2, type: 'DATA' }, { key: stringifyId('col2'), index: 2, type: 'DATA' }, new Edition([])).style;
81
+ expect(style).toEqual({ color: 'brown', background: 'black' });
82
+ });
83
+
84
+ it('should use index rules for matching columns and rows by index', () => {
85
+ const resolver = new FormattingRules(commonStyles);
86
+ const style = resolver.resolve(null, [], [], { key: stringifyId('row2'), index: 2, type: 'DATA' }, { key: stringifyId('col0'), index: 0, type: 'DATA' }, new Edition([])).style;
87
+ expect(style).toEqual({ color: 'orange' });
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,118 @@
1
+ import stringifyId from "../core-utils/stringifyId.js";
2
+
3
+ const matchMapping = {
4
+ 'HEADER': ['HEADER'],
5
+ 'FILTER': ['FILTER'],
6
+ 'DATA': ['DATA'],
7
+ 'CUSTOM': ['CUSTOM'],
8
+ 'ANY': ['HEADER', 'DATA', 'FILTER', 'CUSTOM'],
9
+ 'SPECIAL': ['HEADER', 'FILTER', 'CUSTOM'],
10
+ undefined: []
11
+ };
12
+
13
+ class Lookup {
14
+ byKey = new Map();
15
+ byIndex = new Map(); // TODO: Should this be removed?
16
+ byLabel = new Map();
17
+ byMatch = new Map();
18
+ }
19
+
20
+ export default class RulesLookup {
21
+ lookup = new Lookup();
22
+ hasRules = false;
23
+
24
+ addRule(column, row, rule) {
25
+ this.hasRules = true;
26
+
27
+ if (Array.isArray(column)) {
28
+ for (const c of column)
29
+ this.addRule(c, row, rule);
30
+ return;
31
+ }
32
+
33
+ if (Array.isArray(row)) {
34
+ for (const r of row)
35
+ this.addRule(column, r, rule);
36
+ return;
37
+ }
38
+
39
+ column = column
40
+ ? 'id' in column
41
+ ? { key: stringifyId(column.id) }
42
+ : column
43
+ : { match: 'DATA' };
44
+ row = row
45
+ ? 'id' in row
46
+ ? { key: stringifyId(row.id) }
47
+ : row
48
+ : { match: 'DATA' };
49
+
50
+ function addRowRule(lookup, key) {
51
+ if (!lookup.has(key))
52
+ lookup.set(key, []);
53
+
54
+ lookup.get(key).push(rule);
55
+ }
56
+
57
+ function addColumnRule(lookup, key) {
58
+ if (!lookup.has(key))
59
+ lookup.set(key, new Lookup());
60
+
61
+ if ('key' in row)
62
+ addRowRule(lookup.get(key).byKey, row.key);
63
+ if ('index' in row)
64
+ addRowRule(lookup.get(key).byIndex, row.index);
65
+ if ('label' in row)
66
+ addRowRule(lookup.get(key).byLabel, row.label);
67
+ for (const match of matchMapping[row.match])
68
+ addRowRule(lookup.get(key).byMatch, match);
69
+ }
70
+
71
+ if ('key' in column)
72
+ addColumnRule(this.lookup.byKey, column.key);
73
+ if ('index' in column)
74
+ addColumnRule(this.lookup.byIndex, column.index);
75
+ if ('label' in column)
76
+ addColumnRule(this.lookup.byLabel, column.label);
77
+ for (const match of matchMapping[column.match])
78
+ addColumnRule(this.lookup.byMatch, match);
79
+ }
80
+
81
+ getRules(column, row) {
82
+ const rules = [];
83
+
84
+ if (!this.hasRules)
85
+ return rules;
86
+
87
+ function gatherRules(newRules) {
88
+ for (const rule of newRules)
89
+ rules.push(rule);
90
+ }
91
+
92
+ function gatherRowRules(lookup) {
93
+ if (lookup.byKey.has(row.key))
94
+ gatherRules(lookup.byKey.get(row.key));
95
+ if (lookup.byIndex.has(row.index))
96
+ gatherRules(lookup.byIndex.get(row.index));
97
+ if (lookup.byMatch.has(row.type))
98
+ gatherRules(lookup.byMatch.get(row.type));
99
+ for (const label of row.labels) {
100
+ if (lookup.byLabel.has(label))
101
+ gatherRules(lookup.byLabel.get(label));
102
+ }
103
+ }
104
+
105
+ if (this.lookup.byKey.has(column.key))
106
+ gatherRowRules(this.lookup.byKey.get(column.key));
107
+ if (this.lookup.byIndex.has(column.index))
108
+ gatherRowRules(this.lookup.byIndex.get(column.index));
109
+ if (this.lookup.byMatch.has(column.type))
110
+ gatherRowRules(this.lookup.byMatch.get(column.type));
111
+ for (const label of column.labels) {
112
+ if (this.lookup.byLabel.has(label))
113
+ gatherRowRules(this.lookup.byLabel.get(label));
114
+ }
115
+
116
+ return rules;
117
+ }
118
+ };
@@ -0,0 +1,25 @@
1
+ import stringifyId from "../core-utils/stringifyId.js";
2
+
3
+ export default class Selection {
4
+ constructor(selectedCells) {
5
+ this.lookup = new Map();
6
+
7
+ selectedCells.forEach(cell => {
8
+ const rowKey = stringifyId(cell.rowId);
9
+ const columnKey = stringifyId(cell.columnId);
10
+
11
+ if (!this.lookup.has(rowKey))
12
+ this.lookup.set(rowKey, new Set());
13
+
14
+ this.lookup.get(rowKey).add(columnKey);
15
+ });
16
+ }
17
+
18
+ isKeySelected(rowKey, columnKey) {
19
+ return this.lookup.has(rowKey) && this.lookup.get(rowKey).has(columnKey);
20
+ }
21
+
22
+ isIdSelected(rowId, columnId) {
23
+ return this.isKeySelected(stringifyId(rowId), stringifyId(columnId));
24
+ }
25
+ }
@@ -0,0 +1,62 @@
1
+ import RulesLookup from "./RulesLookup.js";
2
+
3
+ // const defaultComparatorAsc = (lhs, rhs) => lhs.value < rhs.value;
4
+ // const defaultComparatorDesc = (lhs, rhs) => lhs.value > rhs.value;
5
+
6
+ function defaultComparatorAsc(lhs, rhs) {
7
+ if (lhs.value == undefined)
8
+ return false;
9
+ if (rhs.value == undefined)
10
+ return true;
11
+ return lhs.value < rhs.value;
12
+ }
13
+
14
+ function defaultComparatorDesc(lhs, rhs) {
15
+ if (lhs.value == undefined)
16
+ return false;
17
+ if (rhs.value == undefined)
18
+ return true;
19
+ return lhs.value > rhs.value;
20
+ }
21
+
22
+ export default class SortingRules {
23
+ constructor(rules) {
24
+ this.rulesLookup = new RulesLookup();
25
+
26
+ for (const rule of rules) {
27
+ const entry = {
28
+ by: stringifyId('by' in rule ? rule.by : 'HEADER'),
29
+ comparatorAsc: rule.comparator || defaultComparatorAsc,
30
+ comparatorDesc: (lhs, rhs) => -rule.comparator(lhs, rhs) || defaultComparatorDesc
31
+ };
32
+
33
+ this.rulesLookup.addRule(rule.column, rule.row, entry);
34
+ }
35
+ }
36
+
37
+ resolve(column, row, sortByLookup) {
38
+ const rules = this.rulesLookup.getRules(column, row);
39
+
40
+ if (rules.length === 0) {
41
+ if (row.type !== 'DATA')
42
+ return null;
43
+ if (column.type !== 'DATA')
44
+ return null;
45
+ if (!sortByLookup.has('"HEADER"'))
46
+ return null;
47
+
48
+ return sortByLookup.get('"HEADER"') === 'ASC'
49
+ ? defaultComparatorAsc
50
+ : defaultComparatorDesc;
51
+ }
52
+
53
+ if (rules.length > 1)
54
+ throw new Error('Multiple sorting rules for the same cell'); // TODO: add more context
55
+
56
+ const rule = rules[0];
57
+
58
+ return sortByLookup.get(rule.by) === 'ASC'
59
+ ? rule.comparatorAsc
60
+ : rule.comparatorDesc;
61
+ }
62
+ }
@@ -0,0 +1,60 @@
1
+ import { defaultFont } from "../core-utils/defaults.js";
2
+
3
+ export default class TextResolver
4
+ {
5
+ constructor() {
6
+ this.canvas = document.createElement('canvas');
7
+ this.context = this.canvas.getContext('2d');
8
+ this.fontMetrics = new Map();
9
+ }
10
+
11
+ measureWidth(text, font) {
12
+ const ctx = this.context;
13
+
14
+ ctx.font = font || defaultFont;
15
+
16
+ const textMetrics = ctx.measureText(text);
17
+
18
+ return textMetrics.width;
19
+ }
20
+
21
+ measureHeight(text, font) {
22
+ let lines = 1;
23
+ for (const char of text) {
24
+ if (char === '\n')
25
+ lines++;
26
+ }
27
+
28
+ return lines * this.getFontMetrics(font).height;
29
+ }
30
+
31
+ getFontMetrics(font) {
32
+ const key = font;
33
+
34
+ if (this.fontMetrics.has(key))
35
+ return this.fontMetrics.get(key);
36
+
37
+ const ctx = this.context;
38
+
39
+ // TODO: Set other font properties
40
+ ctx.font = font || defaultFont;
41
+
42
+ const textMetrics = ctx.measureText('X');
43
+
44
+ const middle = (textMetrics.actualBoundingBoxDescent - textMetrics.actualBoundingBoxAscent) / 2;
45
+ const topOffset = middle + textMetrics.fontBoundingBoxAscent;
46
+ const bottomOffset = textMetrics.fontBoundingBoxDescent - middle;
47
+ const height = textMetrics.fontBoundingBoxAscent + textMetrics.fontBoundingBoxDescent;
48
+
49
+ const fontMetrics = {
50
+ topOffset: topOffset,
51
+ middle: -middle,
52
+ bottomOffset: bottomOffset,
53
+ height: height
54
+ };
55
+
56
+ this.fontMetrics.set(key, fontMetrics);
57
+
58
+ return fontMetrics;
59
+ }
60
+ }
@@ -0,0 +1,61 @@
1
+ import Edition from "./Edition.js";
2
+
3
+ // TODO: split into two normal functions, instead of wrapping it into a class
4
+ export default class VisibilityResolver {
5
+ constructor(formattingRules, data, rows, columns, filtering) {
6
+ this.formattingRules = formattingRules;
7
+ this.data = data;
8
+ this.rows = rows;
9
+ this.columns = columns;
10
+ this.edition = new Edition([]);
11
+ this.filtering = filtering;
12
+ }
13
+
14
+ findVisibleColumns() {
15
+ if (!this.filtering.hasRowFilters())
16
+ return this.columns;
17
+
18
+ return this.columns.filter(column => {
19
+ return this.rows.every(row => {
20
+ if (!this.filtering.hasRowFiltersByKey(row.key))
21
+ return true;
22
+
23
+ const cell = this.formattingRules.resolve(
24
+ this.data,
25
+ this.rows,
26
+ this.columns,
27
+ row,
28
+ column,
29
+ this.edition,
30
+ this.filtering);
31
+
32
+ return cell.visible;
33
+ });
34
+ });
35
+ }
36
+
37
+ findVisibleRows() {
38
+ // TODO: don't include search columns in that check
39
+ if (!this.filtering.hasColumnFilters())
40
+ return this.rows;
41
+
42
+ return this.rows.filter(row => {
43
+ // TODO: iterate over columns with filters, instead of all columns
44
+ return this.columns.every(column => {
45
+ if (!this.filtering.hasColumnFiltersByKey(column.key))
46
+ return true;
47
+
48
+ const cell = this.formattingRules.resolve(
49
+ this.data,
50
+ this.rows,
51
+ this.columns,
52
+ row,
53
+ column,
54
+ this.edition,
55
+ this.filtering);
56
+
57
+ return cell.visible;
58
+ });
59
+ });
60
+ }
61
+ }