@neovici/cosmoz-omnitable 12.0.2 → 12.2.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,214 @@
1
+ /* eslint-disable max-lines-per-function */
2
+ import { html, useCallback, useEffect, useMemo, useRef } from 'haunted';
3
+ import { indexSymbol } from './utils';
4
+ import { isEmpty } from '@neovici/cosmoz-utils/template';
5
+ import { onItemChange as _onItemChange } from './utils-data';
6
+
7
+ const _getGroupRowClasses = (folded) =>
8
+ folded ? 'groupRow groupRow-folded' : 'groupRow',
9
+ _getFoldIcon = (expanded) => (expanded ? 'expand-less' : 'expand-more'),
10
+ renderItem = ({
11
+ columns,
12
+ collapsedColumns,
13
+ onItemClick,
14
+ onCheckboxChange,
15
+ dataIsValid,
16
+ groupOnColumn,
17
+ onItemChange,
18
+ }) => {
19
+ return (item, index, { selected, expanded, toggleCollapse }) => {
20
+ return html` <div class="item-row-wrapper">
21
+ <div
22
+ ?selected=${selected}
23
+ part="itemRow itemRow-${item[indexSymbol]}"
24
+ .dataIndex=${item[indexSymbol]}
25
+ .dataItem=${item}
26
+ class="itemRow"
27
+ @click=${onItemClick}
28
+ >
29
+ <input
30
+ class="checkbox"
31
+ type="checkbox"
32
+ .checked=${selected}
33
+ .dataItem=${item}
34
+ @input=${onCheckboxChange}
35
+ ?disabled=${!dataIsValid}
36
+ />
37
+ <cosmoz-omnitable-item-row
38
+ .columns=${columns}
39
+ .index=${index}
40
+ .selected=${selected}
41
+ .expanded=${expanded}
42
+ .item=${item}
43
+ .groupOnColumn=${groupOnColumn}
44
+ .onItemChange=${onItemChange}
45
+ >
46
+ </cosmoz-omnitable-item-row>
47
+ <paper-icon-button
48
+ class="expand"
49
+ ?hidden=${isEmpty(collapsedColumns.length)}
50
+ .icon=${_getFoldIcon(expanded)}
51
+ @click=${toggleCollapse}
52
+ ></paper-icon-button>
53
+ </div>
54
+ <cosmoz-omnitable-item-expand
55
+ .columns=${collapsedColumns}
56
+ .item=${item}
57
+ .index=${index}
58
+ ?selected=${selected}
59
+ ?expanded=${expanded}
60
+ .groupOnColumn=${groupOnColumn}
61
+ part="item-expand"
62
+ >
63
+ </cosmoz-omnitable-item-expand>
64
+ </div>`;
65
+ };
66
+ },
67
+ renderGroup = ({ onCheckboxChange, dataIsValid, groupOnColumn }) => {
68
+ return (item, index, { selected, folded, toggleFold }) =>
69
+ html` <div
70
+ class="${_getGroupRowClasses(folded)}"
71
+ part="groupRow groupRow-${item[indexSymbol]}"
72
+ >
73
+ <input
74
+ class="checkbox"
75
+ type="checkbox"
76
+ .checked=${selected}
77
+ .dataItem=${item}
78
+ @input=${onCheckboxChange}
79
+ ?disabled=${!dataIsValid}
80
+ />
81
+ <h3 class="groupRow-label">
82
+ <div><span>${groupOnColumn?.title}</span>: &nbsp;</div>
83
+ <cosmoz-omnitable-group-row
84
+ .column=${groupOnColumn}
85
+ .item=${item.items?.[0]}
86
+ .selected=${selected}
87
+ .folded=${folded}
88
+ .group=${item}
89
+ ></cosmoz-omnitable-group-row>
90
+ </h3>
91
+ <div class="groupRow-badge">${item.items.length}</div>
92
+ <paper-icon-button
93
+ class="fold"
94
+ .icon=${_getFoldIcon(folded)}
95
+ @click=${toggleFold}
96
+ ></paper-icon-button>
97
+ </div>`;
98
+ };
99
+
100
+ export const useList = ({
101
+ host,
102
+ dataIsValid,
103
+ processedItems,
104
+ columns,
105
+ collapsedColumns,
106
+ sortAndGroupOptions,
107
+ ...rest
108
+ }) => {
109
+ const { loading = false, displayEmptyGroups = false, compareItemsFn } = host,
110
+ keyState = useRef({ shiftKey: false, ctrlKey: false }),
111
+ onCheckboxChange = useCallback((event) => {
112
+ const item = event.target.dataItem,
113
+ selected = event.target.checked;
114
+ if (keyState.current.shiftKey) {
115
+ host.shadowRoot
116
+ .querySelector('#groupedList')
117
+ .toggleSelectTo(item, selected);
118
+ } else if (keyState.current.ctrlKey) {
119
+ event.target.checked = true;
120
+ host.shadowRoot.querySelector('#groupedList').selectOnly(item);
121
+ } else {
122
+ host.shadowRoot
123
+ .querySelector('#groupedList')
124
+ .toggleSelect(item, selected);
125
+ }
126
+
127
+ event.preventDefault();
128
+ event.stopPropagation();
129
+ }, []);
130
+
131
+ useEffect(() => {
132
+ const handler = ({ shiftKey, ctrlKey }) => {
133
+ keyState.current = { shiftKey, ctrlKey };
134
+ };
135
+ window.addEventListener('keydown', handler);
136
+ window.addEventListener('keyup', handler);
137
+ return () => {
138
+ window.removeEventListener('keydown', handler);
139
+ window.removeEventListener('keyup', handler);
140
+ };
141
+ }, []);
142
+
143
+ const onItemClick = useCallback((e) => {
144
+ const composedPath = e.composedPath(),
145
+ path = composedPath.slice(0, composedPath.indexOf(e.currentTarget));
146
+
147
+ if (path.find((e) => e.matches?.('a, .checkbox, .expand'))) {
148
+ return;
149
+ }
150
+
151
+ host.dispatchEvent(
152
+ new window.CustomEvent('omnitable-item-click', {
153
+ bubbles: true,
154
+ composed: true,
155
+ detail: {
156
+ item: e.currentTarget.dataItem,
157
+ index: e.currentTarget.dataIndex,
158
+ },
159
+ })
160
+ );
161
+ }, []);
162
+
163
+ useEffect(() => {
164
+ host.shadowRoot.querySelector('#groupedList').scrollTarget =
165
+ host.shadowRoot.querySelector('#scroller');
166
+ }, []);
167
+
168
+ const { groupOnColumn } = sortAndGroupOptions,
169
+ onItemChange = useCallback(
170
+ (column, item) => (value) => _onItemChange(host, column, item, value),
171
+ []
172
+ );
173
+
174
+ return {
175
+ ...rest,
176
+ processedItems,
177
+ dataIsValid,
178
+ filterIsTooStrict: dataIsValid && processedItems.length < 1,
179
+ loading,
180
+ compareItemsFn,
181
+ displayEmptyGroups,
182
+
183
+ renderItem: useMemo(
184
+ () =>
185
+ renderItem({
186
+ columns,
187
+ collapsedColumns,
188
+ onItemClick,
189
+ onCheckboxChange,
190
+ dataIsValid,
191
+ groupOnColumn,
192
+ onItemChange,
193
+ }),
194
+ [
195
+ columns,
196
+ collapsedColumns,
197
+ onItemClick,
198
+ onCheckboxChange,
199
+ dataIsValid,
200
+ groupOnColumn,
201
+ onItemChange,
202
+ ]
203
+ ),
204
+ renderGroup: useMemo(
205
+ () =>
206
+ renderGroup({
207
+ onCheckboxChange,
208
+ dataIsValid,
209
+ groupOnColumn,
210
+ }),
211
+ [onCheckboxChange, dataIsValid, groupOnColumn]
212
+ ),
213
+ };
214
+ };
@@ -1,17 +1,17 @@
1
- import { useCallback, useMemo, useEffect } from 'haunted';
1
+ import { useState } from 'haunted';
2
2
  import { useProcessedItems } from './use-processed-items';
3
3
  import { useFastLayout } from './use-fast-layout';
4
4
  import { useSettings } from './settings';
5
- import { useDOMColumns } from './use-dom-columns';
6
5
  import { useSortAndGroupOptions } from './use-sort-and-group-options';
7
- import { onItemChange } from './utils-data';
8
- import { useNotifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
6
+ import { usePublicInterface } from './use-public-interface';
7
+ import { useHeader } from './use-header';
8
+ import { useList } from './use-list';
9
+ import { useFooter } from './use-footer';
9
10
 
10
11
  // eslint-disable-next-line max-lines-per-function
11
12
  export const useOmnitable = (host) => {
12
- const { enabledColumns, hashParam, settingsId } = host,
13
- _columns = useDOMColumns(host, { enabledColumns }),
14
- settingS = useSettings({ columns: _columns, settingsId, initial: host }),
13
+ const { hashParam, settingsId, data, resizeSpeedFactor } = host,
14
+ settingS = useSettings({ settingsId, host }),
15
15
  { settings, setSettings, columns, resetRef } = settingS,
16
16
  sortAndGroupOptions = useSortAndGroupOptions(
17
17
  columns,
@@ -20,111 +20,69 @@ export const useOmnitable = (host) => {
20
20
  setSettings,
21
21
  resetRef
22
22
  ),
23
- { groupOnColumn, groupOnDescending, sortOnColumn, descending } =
24
- sortAndGroupOptions,
25
- { data, resizeSpeedFactor } = host,
26
23
  // TODO: drop filterFunctions
27
24
  {
28
25
  processedItems,
29
26
  visibleData,
30
27
  filters,
31
28
  setFilterState,
32
- numProcessedItems,
33
29
  filterFunctions,
34
- groupsCount,
35
30
  } = useProcessedItems({
36
31
  data,
37
32
  columns,
38
- groupOnColumn,
39
- groupOnDescending,
40
- sortOnColumn,
41
- descending,
42
33
  hashParam,
34
+ sortAndGroupOptions,
43
35
  }),
44
- layout = useFastLayout({
36
+ { collapsedColumns } = useFastLayout({
45
37
  host,
38
+ columns,
46
39
  settings,
47
40
  setSettings,
48
- groupOnColumn,
49
41
  resizeSpeedFactor,
42
+ sortAndGroupOptions,
50
43
  }),
51
- collapsedColumns = useMemo(
52
- () =>
53
- settings.columns.reduce(
54
- (acc, column, index) =>
55
- layout[index] != null ||
56
- column.name === groupOnColumn?.name ||
57
- column.disabled
58
- ? acc
59
- : [...acc, columns.find((c) => c.name === column.name)],
60
- []
61
- ),
62
- [columns, settings, layout]
63
- ),
64
- hasHiddenFilter = useMemo(
65
- () =>
66
- [
67
- groupOnColumn,
68
- ...collapsedColumns,
69
- ...settings.columns.filter((s) => s.disabled),
70
- ].some(
71
- (column) =>
72
- column && Object.keys(filterFunctions).includes(column.name)
73
- ),
74
- [filterFunctions, settings, collapsedColumns]
75
- ),
76
- settingsConfig = useMemo(
77
- () => ({
78
- ...settingS,
79
- collapsed: collapsedColumns,
80
- badge: hasHiddenFilter,
81
- filters,
82
- }),
83
- [settingS, collapsedColumns, hasHiddenFilter, filters]
84
- );
85
-
86
- useEffect(() => {
87
- const handler = (ev) =>
88
- setFilterState(ev.detail.name, (state) => ({
89
- ...state,
90
- ...ev.detail.state,
91
- }));
92
- host.addEventListener('legacy-filter-changed', handler);
93
- return () => host.removeEventListener('legacy-filter-changed', handler);
94
- }, []);
95
-
96
- useEffect(() => {
97
- const el = host.shadowRoot.querySelector('#tableContent'),
98
- observer = new ResizeObserver((entries) =>
99
- requestAnimationFrame(() => {
100
- host.style.setProperty(
101
- '--ot-height',
102
- entries[0]?.contentRect.height + 'px'
103
- );
104
- })
105
- );
106
- observer.observe(el);
107
- return () => observer.unobserve(el);
108
- }, []);
109
-
110
- useNotifyProperty('visibleData', visibleData);
111
-
112
- return {
113
- ...sortAndGroupOptions,
44
+ dataIsValid = data && Array.isArray(data) && data.length > 0,
45
+ [selectedItems, setSelectedItems] = useState([]);
114
46
 
47
+ usePublicInterface({
48
+ host,
49
+ visibleData,
50
+ sortedFilteredGroupedItems: processedItems,
115
51
  columns,
116
- collapsedColumns,
117
-
118
- settingsConfig,
119
-
120
52
  filters,
121
53
  setFilterState,
122
- onItemChange: useCallback(
123
- (column, item) => (value) => onItemChange(host, column, item, value),
124
- []
125
- ),
126
- numProcessedItems,
127
- groupsCount,
128
- sortedFilteredGroupedItems: processedItems,
54
+ selectedItems,
55
+ ...sortAndGroupOptions,
56
+ });
57
+
58
+ return {
59
+ header: useHeader({
60
+ host,
61
+ selectedItems,
62
+ sortAndGroupOptions,
63
+ dataIsValid,
64
+ data,
65
+ columns,
66
+ filters,
67
+ collapsedColumns,
68
+ settings,
69
+ filterFunctions,
70
+ settingS,
71
+ setFilterState,
72
+ }),
73
+ list: useList({
74
+ host,
75
+ dataIsValid,
76
+ processedItems,
77
+ setSelectedItems,
78
+ columns,
79
+ collapsedColumns,
80
+ sortAndGroupOptions,
81
+ }),
82
+ footer: useFooter({
83
+ host,
84
+ selectedItems,
85
+ columns,
86
+ }),
129
87
  };
130
88
  };
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo } from 'haunted';
1
+ import { useCallback, useEffect, useMemo } from 'haunted';
2
2
  import { genericSorter } from './generic-sorter';
3
3
  import { invoke } from './invoke';
4
4
  import { columnSymbol } from './use-dom-columns';
@@ -26,19 +26,19 @@ const sortBy = (valueFn, descending) => (a, b) =>
26
26
  );
27
27
  });
28
28
  },
29
- assignIndex = (item, index) => Object.assign(item, { [indexSymbol]: index });
29
+ assignIndex = (item, index) => Object.assign(item, { [indexSymbol]: index }),
30
+ unparsed = Symbol('unparsed');
30
31
 
31
32
  // eslint-disable-next-line max-lines-per-function
32
33
  export const useProcessedItems = ({
33
34
  data,
34
35
  columns,
35
- groupOnColumn,
36
- groupOnDescending,
37
- sortOnColumn,
38
- descending,
39
36
  hashParam,
37
+ sortAndGroupOptions,
40
38
  }) => {
41
- const write = useCallback(
39
+ const { groupOnColumn, groupOnDescending, sortOnColumn, descending } =
40
+ sortAndGroupOptions,
41
+ write = useCallback(
42
42
  ([filter, value]) => {
43
43
  const column = columns.find(({ name }) => name === filter);
44
44
  if (column == null) {
@@ -55,7 +55,7 @@ export const useProcessedItems = ({
55
55
  ([filter, value]) => {
56
56
  const column = columns.find(({ name }) => name === filter);
57
57
  if (column == null) {
58
- return [filter, undefined];
58
+ return [filter, { [unparsed]: value }];
59
59
  }
60
60
 
61
61
  const state = { filter: column.deserializeFilter(column, value) };
@@ -211,13 +211,12 @@ export const useProcessedItems = ({
211
211
  groupIndex = 0;
212
212
  const result = [];
213
213
  processedItems.forEach((item) => {
214
-
215
- if(Array.isArray(item.items)) {
214
+ if (Array.isArray(item.items)) {
216
215
  assignIndex(item, groupIndex++);
217
- item.items.forEach(groupItem => {
216
+ item.items.forEach((groupItem) => {
218
217
  assignIndex(groupItem, index++);
219
- result.push(groupItem);
220
- })
218
+ result.push(groupItem);
219
+ });
221
220
  return;
222
221
  }
223
222
 
@@ -227,13 +226,35 @@ export const useProcessedItems = ({
227
226
  return result;
228
227
  }, [processedItems]);
229
228
 
229
+ // parse un-parsed filter values
230
+ // filters can be left un-parsed if a column was not defined when the URL is read
231
+ useEffect(() => {
232
+ setFilters((filters) => {
233
+ const hasUnparsedFilters = Object.values(filters).some(
234
+ (value) => value[unparsed] != null
235
+ );
236
+
237
+ if (!hasUnparsedFilters) {
238
+ return filters;
239
+ }
240
+
241
+ return Object.fromEntries(
242
+ Object.entries(filters).map(([name, value]) => {
243
+ if (value[unparsed] == null) {
244
+ return [name, value];
245
+ }
246
+
247
+ return read([name, value[unparsed]]);
248
+ })
249
+ );
250
+ });
251
+ }, [read]);
252
+
230
253
  return {
231
254
  processedItems,
232
255
  visibleData,
233
256
  filters,
234
257
  filterFunctions,
235
- setFilterState,
236
- numProcessedItems: filteredItems.length,
237
- groupsCount: processedItems[0]?.items != null ? processedItems.length : 0,
258
+ setFilterState
238
259
  };
239
260
  };
@@ -0,0 +1,96 @@
1
+ /* eslint-disable max-lines-per-function */
2
+ import { useImperativeApi } from '@neovici/cosmoz-utils/hooks/use-imperative-api';
3
+ import { useEffect } from 'haunted';
4
+ import { useNotifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
5
+
6
+ export const usePublicInterface = ({ host, visibleData, ...api }) => {
7
+ useImperativeApi(api, Object.values(api));
8
+
9
+ const removeItems = (items) => {
10
+ const removedItems = [];
11
+
12
+ for (let i = items.length - 1; i >= 0; i -= 1) {
13
+ const removed = host.removeItem(items[i]);
14
+ if (removed != null) {
15
+ removedItems.push(removed);
16
+ }
17
+ }
18
+ return removedItems;
19
+ },
20
+ /**
21
+ * Helper method to remove an item from `data`.
22
+ * @param {Object} item Item to remove
23
+ * @return {Object} item removed
24
+ */
25
+ removeItem = (item) => {
26
+ const index = host.data.indexOf(item);
27
+
28
+ if (index < 0) {
29
+ return null;
30
+ }
31
+
32
+ const removed = host.data.splice(index, 1);
33
+ host.data = host.data.slice();
34
+ if (Array.isArray(removed) && removed.length > 0) {
35
+ return removed[0];
36
+ }
37
+ },
38
+ replaceItem = (oldItem, newItem) => {
39
+ const itemIndex = host.data.indexOf(oldItem);
40
+ if (itemIndex > -1) {
41
+ return host.replaceItemAtIndex(itemIndex, newItem);
42
+ }
43
+ },
44
+ replaceItemAtIndex = (index, newItem) => {
45
+ host.data.splice(index, 1, newItem);
46
+ host.data = host.data.slice();
47
+ },
48
+ selectItem = (item) => {
49
+ host.shadowRoot.querySelector('#groupedList').select(item);
50
+ },
51
+ deselectItem = (item) => {
52
+ host.shadowRoot.querySelector('#groupedList').deselect(item);
53
+ },
54
+ isItemSelected = (item) => {
55
+ return host.shadowRoot.querySelector('#groupedList').isItemSelected(item);
56
+ },
57
+ { setFilterState } = api;
58
+
59
+ useEffect(() => {
60
+ const handler = (ev) =>
61
+ setFilterState(ev.detail.name, (state) => ({
62
+ ...state,
63
+ ...ev.detail.state,
64
+ }));
65
+ host.addEventListener('legacy-filter-changed', handler);
66
+ return () => host.removeEventListener('legacy-filter-changed', handler);
67
+ }, []);
68
+
69
+ useNotifyProperty('visibleData', visibleData);
70
+ useNotifyProperty(
71
+ 'sortedFilteredGroupedItems',
72
+ api.sortedFilteredGroupedItems
73
+ );
74
+ useNotifyProperty('selectedItems', api.selectedItems);
75
+
76
+ useImperativeApi(
77
+ {
78
+ removeItems,
79
+ removeItem,
80
+ replaceItem,
81
+ replaceItemAtIndex,
82
+ selectItem,
83
+ deselectItem,
84
+ isItemSelected,
85
+ },
86
+ [
87
+ removeItems,
88
+ removeItem,
89
+ replaceItem,
90
+ replaceItemAtIndex,
91
+ selectItem,
92
+ deselectItem,
93
+ isItemSelected,
94
+ ]
95
+ );
96
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-omnitable",
3
- "version": "12.0.2",
3
+ "version": "12.2.0",
4
4
  "description": "[![Build Status](https://travis-ci.org/Neovici/cosmoz-omnitable.svg?branch=master)](https://travis-ci.org/Neovici/cosmoz-omnitable)",
5
5
  "keywords": [
6
6
  "web-components"
@@ -1,51 +0,0 @@
1
- import { html } from 'haunted';
2
- import { ifDefined } from 'lit-html/directives/if-defined.js';
3
- import { triangle } from '../icons';
4
-
5
- export const render = ({ columns, on, descending, setOn, setDescending }) =>
6
- columns?.map(
7
- (c) =>
8
- html`<button
9
- class="sg"
10
- title=${c.title}
11
- data-on=${ifDefined(
12
- (c.name === on && (descending ? 'desc' : 'asc')) || undefined
13
- )}
14
- @click=${(e) => {
15
- const on = e.currentTarget?.dataset.on;
16
- if (!on) {
17
- setOn(c.name);
18
- setDescending(false);
19
- }
20
- if (on === 'asc') {
21
- setDescending(true);
22
- } else if (on === 'desc') {
23
- setOn();
24
- setDescending(false);
25
- }
26
- }}
27
- >
28
- <span>${c.title}</span> ${triangle}
29
- </button>`
30
- );
31
-
32
- export default () => html`
33
- <sort-and-group-consumer
34
- class="sgs"
35
- .render=${({
36
- columns,
37
- groupOn: on,
38
- setGroupOn: setOn,
39
- groupOnDescending: descending,
40
- setGroupOnDescending: setDescending,
41
- } = {}) =>
42
- render({
43
- columns: columns?.filter?.((c) => c['groupOn']),
44
- on,
45
- setOn,
46
- descending,
47
- setDescending,
48
- })}
49
- >
50
- </sort-and-group-consumer>
51
- `;
@@ -1,23 +0,0 @@
1
- import { html } from 'haunted';
2
- import { render } from './cosmoz-omnitable-group';
3
-
4
- export default () => html`
5
- <sort-and-group-consumer
6
- class="sgs"
7
- .render=${({
8
- columns,
9
- sortOn: on,
10
- setSortOn: setOn,
11
- descending,
12
- setDescending,
13
- } = {}) =>
14
- render({
15
- columns: columns?.filter?.((c) => c['sortOn']),
16
- on,
17
- setOn,
18
- descending,
19
- setDescending,
20
- })}
21
- >
22
- </sort-and-group-consumer>
23
- `;