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.
- package/package.json +26 -0
- package/src/core/render.js +400 -0
- package/src/core/state.js +183 -0
- package/src/core-utils/defaults.js +4 -0
- package/src/core-utils/rect.js +49 -0
- package/src/core-utils/rect.test.js +111 -0
- package/src/core-utils/roundToPixels.js +3 -0
- package/src/core-utils/stringifyId.js +27 -0
- package/src/core-utils/stringifyId.test.js +27 -0
- package/src/index.js +595 -0
- package/src/state-utils/getActive.js +17 -0
- package/src/state-utils/getCellSection.js +22 -0
- package/src/state-utils/getCellType.js +7 -0
- package/src/state-utils/getClipboardData.js +53 -0
- package/src/state-utils/getColumnIndex.js +25 -0
- package/src/state-utils/getCombinedCells.js +6 -0
- package/src/state-utils/getDataFormatting.js +46 -0
- package/src/state-utils/getEditableCells.js +23 -0
- package/src/state-utils/getEditedCellsAndFilters.js +4 -0
- package/src/state-utils/getEdition.js +6 -0
- package/src/state-utils/getFilterFormatting.js +3 -0
- package/src/state-utils/getFiltered.js +61 -0
- package/src/state-utils/getFilteringRules.js +5 -0
- package/src/state-utils/getFixedSize.js +8 -0
- package/src/state-utils/getFormatResolver.js +5 -0
- package/src/state-utils/getFormattingRules.js +5 -0
- package/src/state-utils/getHighlightedCells.js +40 -0
- package/src/state-utils/getHoveredCell.js +43 -0
- package/src/state-utils/getInputFormatting.js +3 -0
- package/src/state-utils/getInputPlacement.js +51 -0
- package/src/state-utils/getInvoked.js +5 -0
- package/src/state-utils/getIsTextValid.js +3 -0
- package/src/state-utils/getKeys.js +3 -0
- package/src/state-utils/getLookup.js +3 -0
- package/src/state-utils/getMeasureFormatting.js +3 -0
- package/src/state-utils/getMeasured.js +113 -0
- package/src/state-utils/getMousePosition.js +8 -0
- package/src/state-utils/getNewSortBy.js +20 -0
- package/src/state-utils/getPinned.js +8 -0
- package/src/state-utils/getPlaced.js +45 -0
- package/src/state-utils/getReducedCells.js +6 -0
- package/src/state-utils/getRenderFormatting.js +122 -0
- package/src/state-utils/getResizable.js +49 -0
- package/src/state-utils/getResolved.js +42 -0
- package/src/state-utils/getResolvedFilters.js +7 -0
- package/src/state-utils/getResolvedSortBy.js +7 -0
- package/src/state-utils/getRowIndex.js +25 -0
- package/src/state-utils/getScrollRect.js +41 -0
- package/src/state-utils/getSections.js +101 -0
- package/src/state-utils/getSelection.js +5 -0
- package/src/state-utils/getSorted.js +130 -0
- package/src/state-utils/getSortingFormatting.js +3 -0
- package/src/state-utils/getSortingRules.js +5 -0
- package/src/state-utils/getTextResolver.js +5 -0
- package/src/state-utils/getToggledValue.js +11 -0
- package/src/state-utils/getTotalSize.js +6 -0
- package/src/state-utils/getUnfolded.js +86 -0
- package/src/types/Edition.js +37 -0
- package/src/types/FilteringRules.js +48 -0
- package/src/types/FormatResolver.js +19 -0
- package/src/types/FormattingRules.js +120 -0
- package/src/types/FormattingRules.test.js +90 -0
- package/src/types/RulesLookup.js +118 -0
- package/src/types/Selection.js +25 -0
- package/src/types/SortingRules.js +62 -0
- package/src/types/TextResolver.js +60 -0
- 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
|
+
}
|