@neovici/cosmoz-omnitable 7.3.0 → 8.0.0-beta.4

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 (51) hide show
  1. package/README.md +25 -0
  2. package/cosmoz-omnitable-column-amount.js +89 -320
  3. package/cosmoz-omnitable-column-autocomplete.js +36 -47
  4. package/cosmoz-omnitable-column-boolean.js +107 -209
  5. package/cosmoz-omnitable-column-date.js +89 -102
  6. package/cosmoz-omnitable-column-datetime.js +86 -119
  7. package/cosmoz-omnitable-column-list-data.js +4 -1
  8. package/cosmoz-omnitable-column-list-horizontal.js +20 -38
  9. package/cosmoz-omnitable-column-list-mixin.js +133 -140
  10. package/cosmoz-omnitable-column-list.js +19 -28
  11. package/cosmoz-omnitable-column-mixin.js +69 -447
  12. package/cosmoz-omnitable-column-number.js +91 -183
  13. package/cosmoz-omnitable-column-time.js +77 -162
  14. package/cosmoz-omnitable-column.js +49 -93
  15. package/cosmoz-omnitable-group-row.js +1 -5
  16. package/cosmoz-omnitable-header-row.js +9 -6
  17. package/cosmoz-omnitable-item-expand.js +0 -3
  18. package/cosmoz-omnitable-item-row.js +5 -8
  19. package/cosmoz-omnitable-styles.js +1 -5
  20. package/cosmoz-omnitable.js +74 -770
  21. package/lib/cosmoz-omnitable-amount-range-input.js +295 -0
  22. package/{cosmoz-omnitable-column-date-mixin.js → lib/cosmoz-omnitable-date-input-mixin.js} +4 -26
  23. package/lib/cosmoz-omnitable-date-range-input.js +81 -0
  24. package/lib/cosmoz-omnitable-datetime-range-input.js +75 -0
  25. package/lib/cosmoz-omnitable-number-range-input.js +159 -0
  26. package/{cosmoz-omnitable-column-range-mixin.js → lib/cosmoz-omnitable-range-input-mixin.js} +45 -123
  27. package/lib/cosmoz-omnitable-settings.js +7 -4
  28. package/lib/cosmoz-omnitable-time-range-input.js +130 -0
  29. package/lib/generic-sorter.js +2 -2
  30. package/lib/invoke.js +1 -0
  31. package/lib/memoize.js +54 -0
  32. package/lib/polymer-haunted-render-mixin.js +19 -0
  33. package/lib/save-as-csv-action.js +32 -0
  34. package/lib/save-as-xlsx-action.js +25 -0
  35. package/lib/use-canvas-width.js +1 -1
  36. package/lib/use-dom-columns.js +138 -0
  37. package/lib/use-fast-layout.js +21 -2
  38. package/lib/use-hash-state.js +59 -0
  39. package/lib/use-layout.js +1 -1
  40. package/lib/use-omnitable.js +26 -14
  41. package/lib/use-processed-items.js +132 -0
  42. package/lib/use-sort-and-group-options.js +30 -0
  43. package/lib/utils-amount.js +147 -0
  44. package/lib/utils-data.js +36 -0
  45. package/lib/utils-date.js +204 -0
  46. package/lib/utils-datetime.js +71 -0
  47. package/lib/utils-number.js +112 -0
  48. package/lib/utils-time.js +115 -0
  49. package/package.json +1 -1
  50. package/lib/use-force-render.js +0 -8
  51. package/lib/use-render-on-column-updates.js +0 -18
@@ -0,0 +1,138 @@
1
+ import { useEffect, useState } from 'haunted';
2
+ import { memooize } from './memoize';
3
+ import { columnSymbol } from './normalize-settings';
4
+
5
+ const
6
+ verifyColumnSetup = columns => {
7
+ let ok = true;
8
+ const columnNames = columns.map(c => c.name);
9
+ // Check if column names are set
10
+ columns.forEach(column => {
11
+ if (column.name != null) {
12
+ return;
13
+ }
14
+ ok = false;
15
+ // eslint-disable-next-line no-console
16
+ console.error('The name attribute needs to be set on all columns! Missing on column', column);
17
+ });
18
+
19
+ columns.forEach(column => {
20
+ if (columnNames.indexOf(name) === columnNames.lastIndexOf(name)) {
21
+ return;
22
+ }
23
+ ok = false;
24
+ // eslint-disable-next-line no-console
25
+ console.error('The name attribute needs to be unique among all columns! Not unique on column', column);
26
+ });
27
+ return ok;
28
+ },
29
+
30
+ // eslint-disable-next-line max-lines-per-function
31
+ domColumnsToConfig = (host, { enabledColumns }) => {
32
+ const domColumns = host.shadowRoot.querySelector('#columnsSlot')
33
+ .assignedElements({ flatten: true })
34
+ .filter(child => child.isOmnitableColumn && !child.hidden);
35
+
36
+ if (!verifyColumnSetup(domColumns)) {
37
+ return [];
38
+ }
39
+
40
+ const columns = Array.isArray(enabledColumns)
41
+ ? domColumns.filter(column => enabledColumns.includes(column.name))
42
+ : domColumns.filter(column => !column.disabled);
43
+
44
+ return columns.map(column => ({
45
+ name: column.name,
46
+ title: column.title,
47
+
48
+ valuePath: column.valuePath ?? column.name,
49
+ groupOn: column.groupOn ?? column.valuePath,
50
+ sortOn: column.sortOn ?? column.valuePath,
51
+
52
+ minWidth: column.minWidth,
53
+ width: column.width,
54
+ flex: column.flex,
55
+ priority: column.priority,
56
+
57
+ getString: column.getString,
58
+ getComparableValue: column.getComparableValue,
59
+ serializeFilter: column.serializeFilter,
60
+ deserializeFilter: column.deserializeFilter,
61
+ toXlsxValue: column.toXlsxValue,
62
+
63
+ renderHeader: column.renderHeader,
64
+ renderCell: column.renderCell,
65
+ renderEditCell: column.renderEditCell,
66
+ renderGroup: column.renderGroup,
67
+ cellTitleFn: column.cellTitleFn,
68
+ getFilterFn: column.getFilterFn,
69
+ cellClass: column.cellClass,
70
+
71
+ editable: column.editable,
72
+
73
+ values: column.values,
74
+ source: memooize((column, data) => column.externalValues || typeof column.values === 'function'
75
+ ? column.values
76
+ : column.computeSource(column, data)),
77
+
78
+ // @deprecated
79
+ loading: column.loading,
80
+ externalValues: column.externalValues,
81
+ computeSource: column.computeSource,
82
+
83
+ // boolean columns
84
+ trueLabel: column.trueLabel,
85
+ falseLabel: column.falseLabel,
86
+
87
+ // list columns
88
+ valueProperty: column.valueProperty,
89
+ textProperty: column.textProperty,
90
+ emptyLabel: column.emptyLabel,
91
+ emptyValue: column.emptyValue,
92
+
93
+ // range columns
94
+ min: column.min,
95
+ max: column.max,
96
+ locale: column.locale,
97
+ autoupdate: column.autoupdate,
98
+
99
+ // number columns
100
+ maximumFractionDigits: column.maximumFractionDigits,
101
+ minimumFractionDigits: column.minimumFractionDigits,
102
+
103
+ // amount columns
104
+ currency: column.currency,
105
+ rates: column.rates,
106
+ autodetect: column.autodetect,
107
+
108
+ // treenode columns
109
+ ownerTree: column.ownerTree,
110
+ showMaxNodes: column.showMaxNodes,
111
+ keyProperty: column.keyProperty,
112
+
113
+ [columnSymbol]: column
114
+ }));
115
+ };
116
+
117
+ export const useDOMColumns = (host, { enabledColumns }) => {
118
+ const
119
+ [columns, setColumns] = useState(() => domColumnsToConfig(host, { enabledColumns }));
120
+
121
+ useEffect(() => {
122
+ const
123
+ slot = host.shadowRoot.querySelector('#columnsSlot'),
124
+ handler = () => {
125
+ host.suppressNextScrollReset();
126
+ setColumns(domColumnsToConfig(host, { enabledColumns }));
127
+ };
128
+
129
+ slot.addEventListener('slotchange', handler);
130
+ host.addEventListener('cosmoz-column-prop-changed', handler);
131
+ return () => {
132
+ slot.removeEventListener('slotchange', handler);
133
+ host.removeEventListener('cosmoz-column-prop-changed', handler);
134
+ };
135
+ }, []);
136
+
137
+ return columns;
138
+ };
@@ -1,10 +1,11 @@
1
- import { useMemo } from 'haunted';
1
+ import { useEffect, useLayoutEffect, useMemo } from 'haunted';
2
2
  import { toCss } from './compute-layout';
3
3
  import { useResizableColumns } from './use-resizable-columns';
4
4
  import { useCanvasWidth } from './use-canvas-width';
5
5
  import { useTweenArray } from './use-tween-array';
6
6
  import { useLayout } from './use-layout';
7
7
  import { columnSymbol } from './normalize-settings';
8
+ import { render } from 'lit-html';
8
9
 
9
10
  export const useFastLayout = ({ host, settings, setSettings, groupOnColumn, resizeSpeedFactor }) => {
10
11
  const
@@ -26,8 +27,26 @@ export const useFastLayout = ({ host, settings, setSettings, groupOnColumn, resi
26
27
 
27
28
  useResizableColumns({ host, canvasWidth, layout, setSettings: update => setSettings(update(settings)) });
28
29
 
30
+ useLayoutEffect(() => render(layoutCss, host.$.layoutStyle), [layoutCss]);
31
+
32
+ // force iron-list to render when the omnitable becomes visible
33
+ useEffect(() => {
34
+ let lastWidth = 0;
35
+
36
+ const
37
+ onResize = ([entry]) => {
38
+ if (lastWidth === 0) {
39
+ requestAnimationFrame(() => host.$.groupedList.$.list._render());
40
+ }
41
+ lastWidth = entry.contentRect?.width;
42
+ },
43
+ observer = new ResizeObserver(onResize);
44
+
45
+ observer.observe(host);
46
+ return () => observer.unobserve(host);
47
+ }, []);
48
+
29
49
  return {
30
- layoutCss,
31
50
  collapsedColumns
32
51
  };
33
52
  };
@@ -0,0 +1,59 @@
1
+ import { useCallback, useState } from 'haunted';
2
+ import { navigate } from '@neovici/cosmoz-page-router/lib/use-routes';
3
+ import { invoke } from './invoke';
4
+ import { identity } from '@neovici/cosmoz-utils/lib/function';
5
+
6
+ const
7
+ hashUrl = () => new URL(location.hash.replace(/^#!?/iu, '').replace('%23', '#'), location.origin),
8
+
9
+ singleParse = (hashParam, codec = identity) => codec(new URLSearchParams(hashUrl().hash.replace('#', '')).get(hashParam)),
10
+ multiParse = (hashParam, codec = identity) => {
11
+ const params = Array.from(new URLSearchParams(hashUrl().hash.replace('#', '')).entries())
12
+ .filter(([param]) => param.startsWith(hashParam))
13
+ .map(([param, value]) => codec([param.replace(hashParam, ''), value]))
14
+ .filter(([, value]) => value != null);
15
+
16
+ return Object.fromEntries(params);
17
+ },
18
+
19
+ makeLinker = parameterize => (hashParam, value, codec = identity) => {
20
+ const
21
+ url = hashUrl(),
22
+ searchParams = new URLSearchParams(url.hash.replace('#', ''));
23
+
24
+ parameterize(hashParam, value, codec, searchParams);
25
+
26
+ return '#!' + Object.assign(url, { hash: searchParams }).href.replace(location.origin, '');
27
+ },
28
+ isEmpty = v => v == null || v === '',
29
+ singleLink = makeLinker((hashParam, value, codec, searchParams) =>
30
+ !isEmpty(codec(value))
31
+ ? searchParams.set(hashParam, codec(value))
32
+ : searchParams.delete(hashParam)),
33
+ multiLink = makeLinker((hashParam, value, codec, searchParams) =>
34
+ Object.entries(value)
35
+ .map(codec)
36
+ .forEach(([key, value]) => !isEmpty(value)
37
+ ? searchParams.set(hashParam + key, value)
38
+ : searchParams.delete(hashParam + key)));
39
+
40
+ export const useHashState = (initial, param, { suffix = '', read, write, multi } = {}) => {
41
+ const
42
+ link = multi ? multiLink : singleLink,
43
+ parseHash = multi ? multiParse : singleParse,
44
+ [state, _setState] = useState(() => param == null
45
+ ? initial
46
+ : parseHash(param + suffix, read) ?? initial),
47
+
48
+ setState = useCallback(state => _setState(oldState => {
49
+ const newState = invoke(state, oldState);
50
+
51
+ if (param != null) {
52
+ navigate(link(param + suffix, newState, write), null, { notify: false });
53
+ }
54
+
55
+ return newState;
56
+ }), [param, suffix, link, write]);
57
+
58
+ return [state, setState];
59
+ };
package/lib/use-layout.js CHANGED
@@ -2,7 +2,7 @@ import { useMemo } from 'haunted';
2
2
  import { computeLayout } from './compute-layout';
3
3
 
4
4
  export const useLayout = ({ canvasWidth, groupOnColumn, config }) => useMemo(() => {
5
- if (!Array.isArray(config) || canvasWidth == null) {
5
+ if (!Array.isArray(config) || canvasWidth == null || canvasWidth === 0) {
6
6
  return [];
7
7
  }
8
8
 
@@ -1,33 +1,39 @@
1
- import { useMemo, useState } from 'haunted';
1
+ import { useCallback, useMemo, useState } from 'haunted';
2
2
  import { columnSymbol, normalizeSettings } from './normalize-settings';
3
+ import { useProcessedItems } from './use-processed-items';
3
4
  import { useFastLayout } from './use-fast-layout';
4
5
  import { useSavedSettings } from './use-saved-settings';
6
+ import { useDOMColumns } from './use-dom-columns';
7
+ import { useSortAndGroupOptions } from './use-sort-and-group-options';
8
+ import { onItemChange } from './utils-data';
5
9
 
6
10
  // eslint-disable-next-line max-lines-per-function
7
11
  export const useOmnitable = host => {
8
12
  const
9
- { columns, groupOnColumn, resizeSpeedFactor, settingsId, sortedFilteredGroupedItems } = host,
13
+ { enabledColumns, hashParam } = host,
14
+ columns = useDOMColumns(host, { enabledColumns }),
15
+ sortAndGroupOptions = useSortAndGroupOptions(columns, hashParam, host.sortOn, host.groupOn),
16
+ { groupOnColumn, groupOnDescending, sortOnColumn, descending } = sortAndGroupOptions,
17
+ { data, resizeSpeedFactor, settingsId } = host,
18
+ // TODO: drop filterFunctions
19
+ { processedItems, filters, setFilterState, numProcessedItems, filterFunctions, groupsCount } =
20
+ useProcessedItems({ data, columns, groupOnColumn, groupOnDescending, sortOnColumn, descending, hashParam }),
10
21
  [settings, setSettings] = useState([]),
11
22
  { savedSettings, onSettingsSave, onSettingsReset, hasChangedSettings } = useSavedSettings(settingsId, settings, setSettings),
12
23
  normalizedSettings = useMemo(() => normalizeSettings(columns, settings, savedSettings), [columns, settings, savedSettings]),
13
24
  normalizedColumns = useMemo(() => normalizedSettings.map(s => s[columnSymbol]), [columns, ...normalizedSettings.map(s => s.name)]),
14
25
  { layoutCss, collapsedColumns } = useFastLayout({ host, settings: normalizedSettings, setSettings, groupOnColumn, resizeSpeedFactor }),
15
- filterFunctions = useMemo(() => {
16
- if (!columns) {
17
- return {};
18
- }
19
- return Object.fromEntries(columns
20
- .map(col => [col.name, col.getFilterFn()])
21
- .filter(([, fn]) => fn !== undefined));
22
- }, [sortedFilteredGroupedItems]),
23
- columnsWithFilters = useMemo(() => Object.keys(filterFunctions), [filterFunctions]),
24
26
  hasHiddenFilter = useMemo(
25
27
  () => [groupOnColumn, ...collapsedColumns, ...normalizedSettings.filter(s => s.disabled)]
26
- .some(column => column && columnsWithFilters.includes(column.name)),
27
- [columnsWithFilters, normalizedSettings, collapsedColumns]
28
+ .some(column => column && Object.keys(filterFunctions).includes(column.name)),
29
+ [filterFunctions, normalizedSettings, collapsedColumns]
28
30
  );
29
31
 
30
32
  return {
33
+ columns,
34
+
35
+ ...sortAndGroupOptions,
36
+
31
37
  layoutCss,
32
38
  setSettings,
33
39
  normalizedSettings,
@@ -36,6 +42,12 @@ export const useOmnitable = host => {
36
42
  hasHiddenFilter,
37
43
  onSettingsSave,
38
44
  onSettingsReset,
39
- hasChangedSettings
45
+ hasChangedSettings,
46
+ filters,
47
+ setFilterState,
48
+ onItemChange: useCallback((column, item) => value => onItemChange(host, column, item, value), []),
49
+ numProcessedItems,
50
+ groupsCount,
51
+ sortedFilteredGroupedItems: processedItems
40
52
  };
41
53
  };
@@ -0,0 +1,132 @@
1
+ import { useCallback, useMemo } from 'haunted';
2
+ import { genericSorter } from './generic-sorter';
3
+ import { invoke } from './invoke';
4
+ import { columnSymbol } from './normalize-settings';
5
+ import { useHashState } from './use-hash-state';
6
+
7
+ const
8
+ sortBy = (valueFn, descending) => (a, b) => genericSorter(valueFn(a), valueFn(b)) * (descending ? -1 : 1),
9
+ kebab = input => input.replace(/([a-z0-9])([A-Z])/gu, '$1-$2').toLowerCase(),
10
+ notifyChanges = (column, changes) => {
11
+ if (!column || !changes) {
12
+ return;
13
+ }
14
+
15
+ Object.entries(changes).forEach(([key, value]) => {
16
+ column[columnSymbol][key] = value;
17
+ column[columnSymbol].dispatchEvent(new CustomEvent(`${ kebab(key) }-changed`, { bubbles: true, value }));
18
+ });
19
+ };
20
+
21
+ // eslint-disable-next-line max-lines-per-function
22
+ export const useProcessedItems = ({ data, columns, groupOnColumn, groupOnDescending, sortOnColumn, descending, hashParam }) => {
23
+ const
24
+ write = useCallback(([filter, value]) => {
25
+ const column = columns.find(({ name }) => name === filter);
26
+ if (column == null) {
27
+ return [filter, undefined];
28
+ }
29
+ return [filter, value.filter && column.serializeFilter(column, value.filter)];
30
+ }, [columns]),
31
+
32
+ read = useCallback(([filter, value]) => {
33
+ const column = columns.find(({ name }) => name === filter);
34
+ if (column == null) {
35
+ return [filter, undefined];
36
+ }
37
+ // TODO: add try-catch
38
+ const state = { filter: column.deserializeFilter(column, value) };
39
+ notifyChanges(column, state);
40
+ return [filter, state];
41
+ }, [columns]),
42
+
43
+ [filters, setFilters] = useHashState({}, hashParam, { multi: true, suffix: '-filter--', write, read }),
44
+ // TODO: drop extra info from state
45
+ setFilterState = useCallback((name, state) => setFilters(filters => {
46
+ const newState = invoke(state, filters[name]);
47
+
48
+ notifyChanges(columns.find(c => c.name === name), newState);
49
+
50
+ return {
51
+ ...filters,
52
+ [name]: {
53
+ ...filters[name],
54
+ ...newState
55
+ }
56
+ };
57
+ }), [columns, setFilters]),
58
+
59
+ filterValues = useMemo(() => Object.values(filters).map(f => f.filter), [filters]),
60
+ filterFunctions = useMemo(() => {
61
+ return Object.fromEntries(columns
62
+ .map(col => [col.name, col.getFilterFn(col, filters[col.name]?.filter)])
63
+ .filter(([, fn]) => fn !== undefined));
64
+ }, [columns, ...filterValues]),
65
+
66
+ filteredItems = useMemo(() => {
67
+ if (!Array.isArray(data) || data.length === 0) {
68
+ return [];
69
+ }
70
+
71
+ if (Object.entries(filterFunctions).length === 0) {
72
+ return data.slice();
73
+ }
74
+
75
+ return data.filter(item => Object.values(filterFunctions).every(filterFn => filterFn(item)));
76
+ }, [data, filterFunctions]),
77
+
78
+ // todo: extract function
79
+ processedItems = useMemo(() => {
80
+ if (!groupOnColumn && sortOnColumn != null && sortOnColumn.sortOn != null) {
81
+ return filteredItems.slice().sort(sortBy(a => sortOnColumn.getComparableValue(a, sortOnColumn.sortOn, sortOnColumn), descending));
82
+ }
83
+
84
+ if (groupOnColumn != null && groupOnColumn.groupOn != null) {
85
+ const groupedResults = filteredItems.reduce((acc, item) => {
86
+ const gval = groupOnColumn.getComparableValue(item, groupOnColumn.groupOn, groupOnColumn);
87
+
88
+ if (gval === undefined) {
89
+ return acc;
90
+ }
91
+
92
+ let group = acc.find(g => g.id === gval);
93
+
94
+ if (!group) {
95
+ group = {
96
+ id: gval,
97
+ name: gval,
98
+ items: [item]
99
+ };
100
+ return [...acc, group];
101
+ }
102
+
103
+ group.items.push(item);
104
+ return acc;
105
+ }, []);
106
+
107
+ groupedResults.sort(sortBy(a => groupOnColumn.getComparableValue(a.items[0], groupOnColumn.groupOn, groupOnColumn), groupOnDescending));
108
+
109
+ if (!sortOnColumn) {
110
+ return groupedResults;
111
+ }
112
+
113
+ return groupedResults
114
+ .filter(group => Array.isArray(group.items))
115
+ .map(group => {
116
+ group.items.sort(sortBy(a => sortOnColumn.getComparableValue(a, sortOnColumn.sortOn, sortOnColumn), descending));
117
+ return group;
118
+ });
119
+ }
120
+
121
+ return filteredItems;
122
+ }, [filteredItems, groupOnColumn, groupOnDescending, sortOnColumn, descending]);
123
+
124
+ return {
125
+ processedItems,
126
+ filters,
127
+ filterFunctions,
128
+ setFilterState,
129
+ numProcessedItems: filteredItems.length,
130
+ groupsCount: processedItems[0]?.items != null ? processedItems.length : 0
131
+ };
132
+ };
@@ -0,0 +1,30 @@
1
+ import { useMemo } from 'haunted';
2
+ import { useHashState } from './use-hash-state';
3
+
4
+ const
5
+ parseBool = bool => [true, 'true', 1, 'yes', 'on'].includes(bool),
6
+ boolParam = p => p === '' || parseBool(p);
7
+
8
+ export const useSortAndGroupOptions = (columns, hashParam, initialSortOn, initialGroupOn) => {
9
+ const
10
+ [sortOn, setSortOn] = useHashState(initialSortOn, hashParam, { suffix: '-sortOn' }),
11
+ [descending, setDescending] = useHashState(false, hashParam, { suffix: '-descending', read: boolParam }),
12
+ [groupOn, setGroupOn] = useHashState(initialGroupOn, hashParam, { suffix: '-groupOn' }),
13
+ [groupOnDescending, setGroupOnDescending] = useHashState(false, hashParam, { suffix: '-groupOnDescending', read: boolParam }),
14
+ sortOnColumn = useMemo(() => columns.find(column => column.name === sortOn), [columns, sortOn]),
15
+ groupOnColumn = useMemo(() => columns.find(column => column.name === groupOn), [columns, groupOn]);
16
+
17
+ return {
18
+ groupOn,
19
+ setGroupOn,
20
+ groupOnColumn,
21
+ groupOnDescending,
22
+ setGroupOnDescending,
23
+
24
+ sortOn,
25
+ setSortOn,
26
+ sortOnColumn,
27
+ descending,
28
+ setDescending
29
+ };
30
+ };
@@ -0,0 +1,147 @@
1
+ import { get } from '@polymer/polymer/lib/utils/path';
2
+ import { toNumber } from './utils-number';
3
+
4
+ export const
5
+ /**
6
+ * Converts a value to an amount object optionaly limiting it.
7
+ *
8
+ * @param {Object} rates The rates
9
+ * @param {Object} value The value to convert to number
10
+ * @param {Object} limit The value used to limit the number
11
+ * @param {Function} limitFunc The function used to limit the number (Math.min|Math.max)
12
+ * @returns {Object|void} Value converted to Number or void
13
+ */
14
+ toAmount = (rates = {}, value, limit, limitFunc) => { // eslint-disable-line max-statements
15
+ if (value == null || value === '') {
16
+ return;
17
+ }
18
+
19
+ if (typeof value !== 'object' || value.currency == null || value.currency === '') {
20
+ return null;
21
+ }
22
+
23
+ const number = toNumber(value.amount);
24
+ if (number == null || Number.isNaN(number)) {
25
+ return null;
26
+ }
27
+ const amount = {
28
+ currency: value.currency,
29
+ amount: number
30
+ };
31
+
32
+ if (limitFunc == null || limit == null) {
33
+ return amount;
34
+ }
35
+ const lAmount = toAmount(rates, limit);
36
+ if (lAmount == null) {
37
+ return amount;
38
+ }
39
+
40
+ // calculate value and limit amounts with rates
41
+ const
42
+ valAmount = amount.amount * (rates[amount.currency] || 1),
43
+ limAmount = lAmount.amount * (rates[lAmount.currency] || 1),
44
+ lNumber = toNumber(valAmount, limAmount, limitFunc);
45
+ return lNumber === valAmount ? amount : lAmount;
46
+ },
47
+
48
+ getComparableValue = (item, valuePath, { rates } = {}) => {
49
+ if (item == null) {
50
+ return;
51
+ }
52
+
53
+ let value = item;
54
+
55
+ if (valuePath != null) {
56
+ value = get(item, valuePath);
57
+ }
58
+ value = toAmount(rates, value);
59
+
60
+ if (value == null) {
61
+ return;
62
+ }
63
+ const amount = toNumber(value.amount);
64
+
65
+ if (rates == null) {
66
+ return amount;
67
+ }
68
+
69
+ return amount * (rates[value.currency] || 1);
70
+ },
71
+
72
+ applySingleFilter = (column, filter) => item => {
73
+ const value = getComparableValue(item, column.valuePath, column);
74
+
75
+ if (value == null) {
76
+ return false;
77
+ }
78
+
79
+ const
80
+ min = getComparableValue(filter, 'min', column),
81
+ max = getComparableValue(filter, 'max', column);
82
+
83
+ return !(value < min || value > max);
84
+ },
85
+
86
+ formatters = {},
87
+ getFormatter = (currency, locale) => {
88
+ const id = locale ? locale : '',
89
+ key = currency + id || '';
90
+
91
+ if (formatters[key]) {
92
+ return formatters[key];
93
+ }
94
+
95
+ formatters[key] = new Intl.NumberFormat(locale || undefined, {
96
+ style: 'currency',
97
+ currency
98
+ });
99
+
100
+ return formatters[key];
101
+ },
102
+
103
+ renderValue = (rates, value, locale) => {
104
+ const amount = toAmount(rates, value);
105
+ if (amount == null) {
106
+ return '';
107
+ }
108
+ return getFormatter(amount.currency, locale)
109
+ .format(value.amount);
110
+ },
111
+
112
+
113
+ getString = ({ valuePath, rates, locale }, item) => {
114
+ const value = toAmount(rates, get(item, valuePath));
115
+ if (value === undefined) {
116
+ return '';
117
+ }
118
+ if (value === null) {
119
+ return 'Invalid value';
120
+ }
121
+ return renderValue(rates, value, locale);
122
+ },
123
+
124
+ toHashString = value => {
125
+ if (!value) {
126
+ return '';
127
+ }
128
+ return value.amount + value.currency;
129
+ },
130
+
131
+ fromHashString = value => {
132
+ if (value == null || value === '') {
133
+ return;
134
+ }
135
+ const params = value.match(/^(-?[\d]+)([\D]+?)$/iu);
136
+ if (!Array.isArray(params) || params.length < 0) {
137
+ return null;
138
+ }
139
+ return {
140
+ amount: params[1],
141
+ currency: params[2]
142
+ };
143
+ },
144
+
145
+ getCurrency = ({ valuePath }, item) => get(item, valuePath)?.currency,
146
+
147
+ getInputString = ({ valuePath }, item) => get(item, valuePath)?.amount;
@@ -0,0 +1,36 @@
1
+ import { get, set } from '@polymer/polymer/lib/utils/path';
2
+ import { columnSymbol } from './normalize-settings';
3
+
4
+ export const
5
+ valuesFrom = (data, valuePath) => data
6
+ ?.map(item => get(item, valuePath))
7
+ .filter((value, index, self) =>
8
+ value != null && self.indexOf(value) === index
9
+ ),
10
+
11
+ onItemChange = (host, column, item, value) => {
12
+ const
13
+ { valuePath } = column,
14
+ oldValue = get(item, valuePath);
15
+
16
+ if (value === oldValue) {
17
+ return;
18
+ }
19
+
20
+ set(item, valuePath, value);
21
+
22
+ const
23
+ change = {
24
+ item,
25
+ valuePath,
26
+ value,
27
+ oldValue,
28
+ column: column[columnSymbol]
29
+ };
30
+
31
+ host.dispatchEvent(new CustomEvent('column-item-changed', {
32
+ bubbles: true,
33
+ composed: true,
34
+ detail: change
35
+ }));
36
+ };