@neovici/cosmoz-omnitable 14.2.0 → 14.3.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.
@@ -307,21 +307,15 @@ export default css`
307
307
  }
308
308
 
309
309
  /* End of empty data set styling */
310
-
311
310
  .tableContent-scroller {
312
311
  flex: auto;
313
312
  position: relative;
314
313
  overflow: auto;
315
314
  overflow-x: hidden;
316
315
  will-change: transform;
317
- }
318
-
319
- #groupedList {
320
- position: absolute !important;
321
- top: 0;
322
- right: 0;
323
- bottom: 0;
324
- left: 0;
316
+ flex-basis: 0.000001px;
317
+ display: flex;
318
+ flex-direction: column;
325
319
  }
326
320
 
327
321
  .itemRow {
@@ -4,7 +4,6 @@ import '@polymer/iron-icons/iron-icons';
4
4
  import '@polymer/iron-icon/iron-icon';
5
5
  import '@polymer/paper-spinner/paper-spinner-lite';
6
6
 
7
- import '@neovici/cosmoz-grouped-list';
8
7
  import '@neovici/cosmoz-bottom-bar';
9
8
 
10
9
  import './cosmoz-omnitable-column';
@@ -18,13 +17,14 @@ import styles from './cosmoz-omnitable-styles';
18
17
  import { html as polymerHtml } from '@polymer/polymer/lib/utils/html-tag';
19
18
  import { html } from 'lit-html';
20
19
  import { guard } from 'lit-html/directives/guard.js';
20
+ import { notifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
21
21
 
22
22
  import { useOmnitable } from './lib/use-omnitable';
23
23
  import { component } from '@pionjs/pion';
24
24
  import { renderHeader } from './lib/render-header';
25
25
  import { renderFooter } from './lib/render-footer';
26
26
  import { renderList } from './lib/render-list';
27
- import { notifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
27
+ import './grouped-list/index.js';
28
28
 
29
29
  const shimCSS = (s) => window.ShadyCSS?.ApplyShim?.transformCssText?.(s) || s;
30
30
 
@@ -0,0 +1,41 @@
1
+ import { render } from 'lit-html';
2
+
3
+ customElements.define(
4
+ 'cosmoz-grouped-list-row',
5
+ class Element extends HTMLElement {
6
+ get item() {
7
+ return this._item;
8
+ }
9
+
10
+ set item(item) {
11
+ this._item = item;
12
+ this._render();
13
+ }
14
+
15
+ get index() {
16
+ return this._index;
17
+ }
18
+
19
+ set index(index) {
20
+ this._index = index;
21
+ this._render();
22
+ }
23
+
24
+ get renderFn() {
25
+ return this._renderFn;
26
+ }
27
+
28
+ set renderFn(renderFn) {
29
+ this._renderFn = renderFn;
30
+ this._render();
31
+ }
32
+
33
+ _render() {
34
+ if (this._item == null || this._index == null || this._renderFn == null) {
35
+ return;
36
+ }
37
+
38
+ render(this._renderFn(this._item, this._index), this);
39
+ }
40
+ }
41
+ );
@@ -0,0 +1,13 @@
1
+ import {
2
+ renderCosmozGroupedList,
3
+ useCosmozGroupedList,
4
+ } from './use-cosmoz-grouped-list.js';
5
+ import { component } from '@pionjs/pion';
6
+
7
+ const CosmozGroupedList = (host) =>
8
+ renderCosmozGroupedList(useCosmozGroupedList(host));
9
+
10
+ customElements.define(
11
+ 'cosmoz-grouped-list',
12
+ component(CosmozGroupedList, { useShadowDOM: false }),
13
+ );
@@ -0,0 +1 @@
1
+ import './cosmoz-grouped-list.js';
@@ -0,0 +1,32 @@
1
+ import { useCallback } from '@pionjs/pion';
2
+ import { useWeakState } from './use-weak-state';
3
+ import { isGroup } from './utils';
4
+
5
+ export const useCollapsibleItems = () => {
6
+ const { setItemState, state, signal } = useWeakState(),
7
+ toggleFold = useCallback((item, folded) => {
8
+ if (!isGroup(item)) {
9
+ return;
10
+ }
11
+
12
+ setItemState(item, (state) => ({
13
+ folded: folded !== undefined ? folded : !state.folded,
14
+ }));
15
+ }, []),
16
+ toggleCollapse = useCallback((item, collapsed) => {
17
+ if (isGroup(item)) {
18
+ return;
19
+ }
20
+
21
+ setItemState(item, (state) => ({
22
+ expanded: collapsed !== undefined ? !collapsed : !state.expanded,
23
+ }));
24
+ }, []);
25
+
26
+ return {
27
+ state,
28
+ signal,
29
+ toggleFold,
30
+ toggleCollapse,
31
+ };
32
+ };
@@ -0,0 +1,115 @@
1
+ import { useImperativeApi } from '@neovici/cosmoz-utils/hooks/use-imperative-api';
2
+ import { useNotifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
3
+ import { useCallback, useLayoutEffect, useMemo } from '@pionjs/pion';
4
+ import { html } from 'lit-html';
5
+ import './cosmoz-grouped-list-row';
6
+ import { useCollapsibleItems } from './use-collapsible-items';
7
+ import { useSelectedItems } from './use-selected-items';
8
+ import { byReference, isExpanded, isFolded, prepareData } from './utils';
9
+ import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
10
+
11
+ const styles = {
12
+ host: {
13
+ position: 'relative',
14
+ display: 'flex',
15
+ flexDirection: 'column',
16
+ },
17
+ },
18
+ // eslint-disable-next-line max-lines-per-function
19
+ useCosmozGroupedList = (host) => {
20
+ const {
21
+ data,
22
+ renderItem,
23
+ renderGroup,
24
+ displayEmptyGroups,
25
+ compareItemsFn = byReference,
26
+ } = host,
27
+ { toggleFold, toggleCollapse, state, signal } = useCollapsibleItems(),
28
+ // TODO: state changes trigger recalculation, which is slow (200ms with 10k items)
29
+ // it only makes sense to do it when a group is folded
30
+ // suggested fix: separate signal for item collapse and group fold
31
+ flatData = useMemo(
32
+ () => prepareData(data, displayEmptyGroups, state),
33
+ [data, displayEmptyGroups, signal]
34
+ ),
35
+ {
36
+ selectedItems,
37
+ isItemSelected,
38
+ isGroupSelected,
39
+ isSelected,
40
+ select,
41
+ deselect,
42
+ selectOnly,
43
+ selectAll,
44
+ deselectAll,
45
+ toggleSelect,
46
+ toggleSelectTo,
47
+ } = useSelectedItems({
48
+ initial: [],
49
+ compareItemsFn,
50
+ data,
51
+ flatData,
52
+ }),
53
+ renderRow = useCallback(
54
+ (item, index) =>
55
+ // eslint-disable-next-line no-nested-ternary
56
+ Array.isArray(item.items)
57
+ ? renderGroup(item, index, {
58
+ selected: isGroupSelected(item, selectedItems),
59
+ folded: isFolded(item, state),
60
+ toggleSelect: (selected) =>
61
+ toggleSelect(
62
+ item,
63
+ typeof selected === 'boolean' ? selected : undefined
64
+ ),
65
+ toggleFold: () => toggleFold(item),
66
+ })
67
+ : renderItem(item, index, {
68
+ selected: selectedItems.includes(item),
69
+ expanded: isExpanded(item, state),
70
+ toggleSelect: (selected) =>
71
+ toggleSelect(
72
+ item,
73
+ typeof selected === 'boolean' ? selected : undefined
74
+ ),
75
+ toggleCollapse: () => toggleCollapse(item),
76
+ }),
77
+ [renderItem, renderGroup, selectedItems, toggleSelect, signal]
78
+ );
79
+
80
+ useLayoutEffect(() => Object.assign(host.style, styles.host), []);
81
+
82
+ useNotifyProperty('selectedItems', selectedItems);
83
+ const api = {
84
+ toggleFold,
85
+ toggleCollapse,
86
+ isItemSelected,
87
+ isGroupSelected,
88
+ isSelected,
89
+ select,
90
+ deselect,
91
+ selectOnly,
92
+ selectAll,
93
+ deselectAll,
94
+ toggleSelect,
95
+ toggleSelectTo,
96
+ };
97
+
98
+ useImperativeApi(api, Object.values(api));
99
+
100
+ return {
101
+ renderRow,
102
+ flatData,
103
+ };
104
+ },
105
+ renderCosmozGroupedList = ({ renderRow, flatData }) =>
106
+ virtualize({
107
+ items: flatData,
108
+ renderItem: (item, index) => html`<cosmoz-grouped-list-row
109
+ .item=${item}
110
+ .index=${index}
111
+ .renderFn=${renderRow}
112
+ ></cosmoz-grouped-list-row>`,
113
+ });
114
+
115
+ export { renderCosmozGroupedList, useCosmozGroupedList };
@@ -0,0 +1,141 @@
1
+ import { useCallback, useEffect, useState } from '@pionjs/pion';
2
+ import { isGroup } from './utils';
3
+
4
+ // eslint-disable-next-line max-lines-per-function
5
+ export const useSelectedItems = ({
6
+ initial,
7
+ compareItemsFn,
8
+ data,
9
+ flatData
10
+ }) => {
11
+ const [selectedItems, setSelectedItems] = useState(initial),
12
+ [lastSelection, setLastSelection] = useState(),
13
+ /**
14
+ * Check if item is selected.
15
+ * @param {object} item Item.
16
+ * @returns {boolean} Whether item is selected.
17
+ */
18
+ isItemSelected = useCallback(
19
+ item => selectedItems.includes(item),
20
+ [selectedItems]
21
+ ),
22
+ /**
23
+ * Check if group is selected.
24
+ * @param {object} group Group.
25
+ * @returns {boolean} Whether group is selected.
26
+ */
27
+ isGroupSelected = useCallback(
28
+ group => group?.items?.every(isItemSelected),
29
+ [isItemSelected]
30
+ ),
31
+ /**
32
+ * Check if item.group is selected.
33
+ * @param {object} item Item.
34
+ * @returns {boolean} Whether item is selected.
35
+ */
36
+ isSelected = useCallback(
37
+ item => isItemSelected(item) || isGroupSelected(item),
38
+ [isItemSelected, isGroupSelected]
39
+ ),
40
+ /**
41
+ * Add an item/group to the list of selected items.
42
+ * @param {object} item Item to select.
43
+ * @returns {void}
44
+ */
45
+ select = useCallback(item => {
46
+ const items = item.items ?? [item];
47
+ setSelectedItems(selection => [
48
+ ...selection,
49
+ ...items.filter(i => !selection.includes(i))
50
+ ]);
51
+ setLastSelection(item);
52
+ }, []),
53
+ /**
54
+ * Removes an item/group from the list of selected items.
55
+ * @param {object} item Item to select.
56
+ * @returns {void}
57
+ */
58
+ deselect = useCallback(item => {
59
+ const items = item.items ?? [item];
60
+ setSelectedItems(selection =>
61
+ selection.filter(i => !items.includes(i))
62
+ );
63
+ setLastSelection(item);
64
+ }, []),
65
+ selectOnly = useCallback(item => {
66
+ setSelectedItems(item.items?.slice() || [item]);
67
+ setLastSelection(item);
68
+ }, []),
69
+ /**
70
+ * Select all items.
71
+ * @returns {void}
72
+ */
73
+ selectAll = useCallback(() => {
74
+ setSelectedItems(data.flatMap(item => item.items || item));
75
+ setLastSelection(undefined);
76
+ }, [data]),
77
+ /**
78
+ * Deselect all selected items.
79
+ * @returns {void}
80
+ */
81
+ deselectAll = useCallback(() => {
82
+ setSelectedItems([]);
83
+ setLastSelection(undefined);
84
+ }, []),
85
+ /**
86
+ * Toggles the selection of an item/group.
87
+ * @param {object} item Item to select.
88
+ * @param {boolean=} selected Select or deselect, fallback to toggling.
89
+ * @returns {void}
90
+ */
91
+ toggleSelect = useCallback(
92
+ (item, selected = !isSelected(item)) =>
93
+ selected ? select(item) : deselect(item),
94
+ [isSelected]
95
+ ),
96
+ toggleSelectTo = useCallback(
97
+ (item, selected) => {
98
+ const last = lastSelection
99
+ ? flatData.findIndex(i => compareItemsFn(i, lastSelection))
100
+ : -1;
101
+ if (last < 0) {
102
+ return toggleSelect(item, selected);
103
+ }
104
+ const [from, to] = [last, flatData.indexOf(item)].sort((a, b) => a - b);
105
+ flatData.slice(from, to + 1).forEach((item, i, items) => {
106
+ if (i > 0 && i < items.length - 1 && isGroup(item)) {
107
+ return;
108
+ }
109
+ toggleSelect(item, selected);
110
+ });
111
+ setLastSelection(item);
112
+ },
113
+ [flatData, compareItemsFn, toggleSelect]
114
+ );
115
+
116
+ // keep selected items across data updates
117
+ useEffect(
118
+ () =>
119
+ setSelectedItems(selectedItems =>
120
+ selectedItems.length > 0
121
+ ? flatData.filter(i =>
122
+ selectedItems.find(item => compareItemsFn(i, item))
123
+ )
124
+ : selectedItems
125
+ ),
126
+ [flatData]
127
+ );
128
+ return {
129
+ selectedItems,
130
+ isItemSelected,
131
+ isGroupSelected,
132
+ isSelected,
133
+ select,
134
+ deselect,
135
+ selectOnly,
136
+ selectAll,
137
+ deselectAll,
138
+ toggleSelect,
139
+ toggleSelectTo
140
+ };
141
+ };
@@ -0,0 +1,21 @@
1
+ import { useCallback, useState } from '@pionjs/pion';
2
+ import { callFn, getItemState } from './utils';
3
+
4
+ export const useWeakState = () => {
5
+ const [wrapper, setState] = useState(() => [new WeakMap()]),
6
+ setItemState = useCallback(
7
+ (item, newItemState) =>
8
+ setState(([state]) => {
9
+ const itemState = getItemState(item, state);
10
+ Object.assign(itemState, callFn(newItemState, itemState));
11
+ return [state];
12
+ }),
13
+ []
14
+ );
15
+
16
+ return {
17
+ setItemState,
18
+ state: wrapper[0],
19
+ signal: wrapper
20
+ };
21
+ };
@@ -0,0 +1,87 @@
1
+ const symbols = {
2
+ group: Symbol('group')
3
+ },
4
+ getItemState = (item, itemsState) => {
5
+ if (!itemsState.has(item)) {
6
+ itemsState.set(item, {});
7
+ }
8
+
9
+ return itemsState.get(item);
10
+ },
11
+ isExpanded = (item, itemsState) => Boolean(getItemState(item, itemsState).expanded),
12
+ isFolded = (group, itemsState) => Boolean(getItemState(group, itemsState).folded),
13
+ isGroup = item => item ? item.items instanceof Array : false,
14
+ /**
15
+ * Asserts that data is either all items or all groups, never mixed.
16
+ * @param {Array} data the data
17
+ * @return {void}
18
+ */
19
+ _assertDataIsHomogeneous = data => {
20
+ if (!Array.isArray(data) || data.length === 0) {
21
+ return;
22
+ }
23
+
24
+ const firstItemIsAGroup = Array.isArray(data[0].items),
25
+ isHomogeneous = data.every(
26
+ group => Array.isArray(group.items) === firstItemIsAGroup
27
+ );
28
+
29
+ if (!isHomogeneous) {
30
+ throw new Error('Data must be homogeneous.');
31
+ }
32
+ },
33
+ /**
34
+ * Prepare data.
35
+ * @param {array} data Data.
36
+ * @param {boolean} displayEmptyGroups Flag.
37
+ * @param {WeakMap} itemsState State.
38
+ * @returns {void|array} Prepared data.
39
+ */
40
+ prepareData = (data, displayEmptyGroups, itemsState) => {
41
+ if (!Array.isArray(data)) {
42
+ return;
43
+ }
44
+
45
+ // data should be either all items or all grouped items, never mixed
46
+ _assertDataIsHomogeneous(data);
47
+
48
+ const flatData = data.reduce((acc, item) => {
49
+ // simple items
50
+ if (!item.items) {
51
+ return acc.concat(item);
52
+ }
53
+
54
+ // groups with items
55
+ if (item.items.length) {
56
+ if (getItemState(item, itemsState).folded) {
57
+ return acc.concat(item);
58
+ }
59
+ return acc.concat(
60
+ item,
61
+ item.items.map(i => Object.assign(i, { [symbols.group]: item }))
62
+ );
63
+ }
64
+
65
+ // groups without items
66
+ if (displayEmptyGroups) {
67
+ return acc.concat(item);
68
+ }
69
+
70
+ return acc;
71
+ }, []);
72
+
73
+ return flatData;
74
+ },
75
+ callFn = (fn, ...args) => typeof fn === 'function' ? fn(...args) : fn,
76
+ byReference = (a, b) => a === b;
77
+
78
+ export {
79
+ symbols,
80
+ prepareData,
81
+ getItemState,
82
+ isExpanded,
83
+ isFolded,
84
+ isGroup,
85
+ callFn,
86
+ byReference
87
+ };
@@ -63,15 +63,16 @@ export const renderList = ({
63
63
  <div class="tableContent-scroller" id="scroller" part="scroller">
64
64
  <cosmoz-grouped-list
65
65
  id="groupedList"
66
- .data=${processedItems}
67
- @selected-items-changed=${(event) =>
68
- setSelectedItems(event.detail.value)}
69
- .displayEmptyGroups=${
66
+ .data="${processedItems}"
67
+ @selected-items-changed="${(event) =>
68
+ setSelectedItems(event.detail.value)}"
69
+ .displayEmptyGroups="${
70
70
  displayEmptyGroups /* TODO: check if still works */
71
- }
72
- .compareItemsFn=${compareItemsFn}
73
- .renderItem=${renderItem}
74
- .renderGroup=${renderGroup}
71
+ }"
72
+ .compareItemsFn="${compareItemsFn}"
73
+ .renderItem="${renderItem}"
74
+ .renderGroup="${renderGroup}"
75
75
  ></cosmoz-grouped-list>
76
+ <slot name="extraContent"></slot>
76
77
  </div>`;
77
78
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-omnitable",
3
- "version": "14.2.0",
3
+ "version": "14.3.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"
@@ -21,9 +21,9 @@
21
21
  },
22
22
  "files": [
23
23
  "cosmoz-*.js",
24
- "get-effective-children-legacy-mixin.js",
25
24
  "ui-helpers",
26
- "lib/**/*.js"
25
+ "lib/**/*.js",
26
+ "grouped-list/*.js"
27
27
  ],
28
28
  "scripts": {
29
29
  "lint": "eslint --cache --ext .js .",
@@ -59,12 +59,12 @@
59
59
  }
60
60
  },
61
61
  "dependencies": {
62
+ "@lit-labs/virtualizer": "^2.1.0",
62
63
  "@neovici/cosmoz-autocomplete": "^10.0.0",
63
64
  "@neovici/cosmoz-bottom-bar": "^9.0.3",
64
65
  "@neovici/cosmoz-collapse": "^1.1.0",
65
66
  "@neovici/cosmoz-datetime-input": "^4.0.1",
66
67
  "@neovici/cosmoz-dropdown": "^5.0.0",
67
- "@neovici/cosmoz-grouped-list": "^8.0.0",
68
68
  "@neovici/cosmoz-i18next": "^3.1.1",
69
69
  "@neovici/cosmoz-input": "^5.0.0",
70
70
  "@neovici/cosmoz-router": "^11.0.0",
@@ -94,6 +94,6 @@
94
94
  "@semantic-release/git": "^10.0.0",
95
95
  "husky": "^9.0.0",
96
96
  "semantic-release": "^24.0.0",
97
- "sinon": "^19.0.0"
97
+ "sinon": "^20.0.0"
98
98
  }
99
99
  }