@neovici/cosmoz-omnitable 8.7.3 → 8.9.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.
@@ -37,13 +37,15 @@ const /* eslint-disable-next-line max-lines-per-function */
37
37
  ></cosmoz-omnitable-resize-nub>`,
38
38
  ]
39
39
  ),
40
- HeaderRow = ({ content, columns, ...thru }) => [
40
+ HeaderRow = ({ columns, settingsConfig, ...thru }) => [
41
41
  columns &&
42
42
  renderHeaderRow({
43
43
  columns,
44
44
  ...thru,
45
45
  }),
46
- content,
46
+ html`<cosmoz-omnitable-settings
47
+ .config=${settingsConfig}
48
+ ></cosmoz-omnitable-settings>`,
47
49
  ];
48
50
 
49
51
  customElements.define(
@@ -24,7 +24,6 @@ import { translatable } from '@neovici/cosmoz-i18next';
24
24
  import { mixin, hauntedPolymer } from '@neovici/cosmoz-utils';
25
25
  import { isEmpty } from '@neovici/cosmoz-utils/lib/template.js';
26
26
  import { useOmnitable } from './lib/use-omnitable';
27
- import './lib/cosmoz-omnitable-settings';
28
27
  import { saveAsCsvAction } from './lib/save-as-csv-action';
29
28
  import { saveAsXlsxAction } from './lib/save-as-xlsx-action';
30
29
  import { defaultPlacement } from '@neovici/cosmoz-dropdown';
@@ -54,8 +53,8 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
54
53
  columns="[[ normalizedColumns ]]"
55
54
  filters="[[ filters ]]"
56
55
  group-on-column="[[ groupOnColumn ]]"
57
- content="[[ _renderSettings(normalizedSettings, collapsedColumns, settingsId, hasChangedSettings, hasHiddenFilter, filters) ]]"
58
56
  set-filter-state="[[ setFilterState ]]"
57
+ settings-config="[[ settingsConfig ]]"
59
58
  ></cosmoz-omnitable-header-row>
60
59
  </div>
61
60
  </sort-and-group-provider>
@@ -547,21 +546,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
547
546
  value && close(); /* eslint-disable-line no-unused-expressions */
548
547
  };
549
548
  }
550
-
551
- // eslint-disable-next-line max-params
552
- _renderSettings(normalizedSettings, collapsed, settingsId, hasChangedSettings, hasHiddenFilter, filters) {
553
- return litHtml`<cosmoz-omnitable-settings
554
- .settings=${ normalizedSettings }
555
- .onSettings=${ this.setSettings }
556
- .collapsed=${ collapsed?.map(c => c.name) }
557
- .settingsId=${ settingsId }
558
- .hasChanges=${ hasChangedSettings }
559
- .onSave=${ this.onSettingsSave }
560
- .onReset=${ this.onSettingsReset }
561
- .badge=${ hasHiddenFilter }
562
- .filters=${ filters }
563
- >`;
564
- }
565
549
  }
566
550
  customElements.define('cosmoz-omnitable', Omnitable);
567
551
 
@@ -1,16 +1,6 @@
1
1
  import { html } from 'haunted';
2
2
  import { ifDefined } from 'lit-html/directives/if-defined';
3
-
4
- const sortIcon = html`<svg
5
- width="8"
6
- height="6"
7
- viewBox="0 0 8 6"
8
- fill="currentColor"
9
- xmlns="http://www.w3.org/2000/svg"
10
- >
11
- <path d="M0.5,0.5h7c0.4,0,0.6,0.4,0.4,0.7L4.4,5.3c-0.2,0.2-0.5,0.2-0.7,0L0.1,1.2
12
- C-0.1,0.9,0.1,0.5,0.5,0.5z" />
13
- </svg> `;
3
+ import { triangle } from './icons';
14
4
 
15
5
  export default (column) => html`
16
6
  <sort-and-group-consumer
@@ -40,7 +30,7 @@ export default (column) => html`
40
30
  }
41
31
  }}
42
32
  >
43
- ${sortIcon}
33
+ ${triangle}
44
34
  </button>`}
45
35
  >
46
36
  </sort-and-group-consumer>
package/lib/icons.js ADDED
@@ -0,0 +1,66 @@
1
+ /* eslint-disable import/group-exports */
2
+ import { html } from 'lit-html';
3
+
4
+ export const close = html`<svg
5
+ width="10"
6
+ height="9"
7
+ viewBox="0 0 10 9"
8
+ stroke="currentColor"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ >
11
+ <line
12
+ x1="8.53033"
13
+ y1="0.53033"
14
+ x2="1.53033"
15
+ y2="7.53033"
16
+ stroke-width="1.5"
17
+ />
18
+ <line
19
+ x1="8.46967"
20
+ y1="7.53033"
21
+ x2="1.46967"
22
+ y2="0.530331"
23
+ stroke-width="1.5"
24
+ />
25
+ </svg>`;
26
+
27
+ export const pull = html`
28
+ <svg
29
+ width="16"
30
+ height="6"
31
+ viewBox="0 0 16 6"
32
+ fill="currentColor"
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ >
35
+ <rect width="16" height="2" />
36
+ <rect y="4" width="16" height="2" />
37
+ </svg>
38
+ `;
39
+
40
+ export const arrow = html` <svg
41
+ width="12"
42
+ height="7"
43
+ viewBox="0 0 12 7"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ xmlns="http://www.w3.org/2000/svg"
47
+ >
48
+ <path
49
+ d="M0.999998 0.999999L6 6L11 1"
50
+ stroke-width="1.5"
51
+ stroke-linejoin="round"
52
+ />
53
+ </svg>`;
54
+
55
+ export const triangle = html`<svg
56
+ width="8"
57
+ height="6"
58
+ viewBox="0 0 8 6"
59
+ fill="currentColor"
60
+ xmlns="http://www.w3.org/2000/svg"
61
+ >
62
+ <path
63
+ d="M0.5,0.5h7c0.4,0,0.6,0.4,0.4,0.7L4.4,5.3c-0.2,0.2-0.5,0.2-0.7,0L0.1,1.2
64
+ C-0.1,0.9,0.1,0.5,0.5,0.5z"
65
+ />
66
+ </svg> `;
@@ -0,0 +1,43 @@
1
+ import { html } from 'haunted';
2
+ import { ifDefined } from 'lit-html/directives/if-defined';
3
+ import { triangle } from '../icons';
4
+
5
+ export default () => html`
6
+ <sort-and-group-consumer
7
+ class="groups"
8
+ .render=${({
9
+ columns,
10
+ groupOnDescending: descending,
11
+ groupOn,
12
+ setGroupOn,
13
+ setGroupOnDescending,
14
+ } = {}) => {
15
+ const items = columns?.filter?.((c) => c['groupOn']);
16
+ return items?.map(
17
+ (c) =>
18
+ html`<button
19
+ class="group"
20
+ data-group=${ifDefined(
21
+ (c.name === groupOn && (descending ? 'desc' : 'asc')) || undefined
22
+ )}
23
+ @click=${(e) => {
24
+ const group = e.currentTarget?.dataset.group;
25
+ if (!group) {
26
+ setGroupOn(c.name);
27
+ setGroupOnDescending(false);
28
+ }
29
+ if (group === 'asc') {
30
+ setGroupOnDescending(true);
31
+ } else if (group === 'desc') {
32
+ setGroupOn();
33
+ setGroupOnDescending(false);
34
+ }
35
+ }}
36
+ >
37
+ ${c.title} ${triangle}
38
+ </button>`
39
+ );
40
+ }}
41
+ >
42
+ </sort-and-group-consumer>
43
+ `;
@@ -0,0 +1,135 @@
1
+ import { html, component } from 'haunted';
2
+ import { nothing } from 'lit-html';
3
+ import { _ } from '@neovici/cosmoz-i18next';
4
+ import { isEmpty } from '@neovici/cosmoz-utils/lib/template';
5
+ import { defaultPlacement } from '@neovici/cosmoz-dropdown';
6
+ import '@neovici/cosmoz-collapse';
7
+ import sort from '../cosmoz-omnitable-sort';
8
+ import group from './cosmoz-omnitable-group';
9
+ import style, { dropdown as dropdownStyle } from './style.css';
10
+ import useSettingsUi from './use-settings-ui';
11
+ import { close, pull, arrow } from '../icons';
12
+
13
+ /* eslint-disable max-lines-per-function */
14
+ const placement = ['bottom-right', ...defaultPlacement],
15
+ renderItem =
16
+ ({
17
+ onDragStart,
18
+ onDragEnter,
19
+ onDragOver,
20
+ onDragLeave,
21
+ onDrop,
22
+ onDown,
23
+ onToggle,
24
+ collapsed,
25
+ filters,
26
+ }) =>
27
+ (column, i) => {
28
+ const indeterminate = collapsed?.includes(column.name),
29
+ checked = !column.disabled && !indeterminate;
30
+ return html` <div
31
+ class="item"
32
+ data-index=${i}
33
+ @mousedown=${onDown}
34
+ draggable="true"
35
+ @dragstart=${onDragStart}
36
+ @dragenter=${onDragEnter}
37
+ @dragover=${onDragOver}
38
+ @dragleave=${onDragLeave}
39
+ @drop=${onDrop}
40
+ >
41
+ <button class="pull">${pull}</button>
42
+ <label
43
+ class="title"
44
+ ?has-filter=${!isEmpty(filters[column.name]?.filter)}
45
+ >${column.title}</label
46
+ >
47
+ ${sort(column.name)}
48
+ <input
49
+ class="checkbox"
50
+ type="checkbox"
51
+ .checked=${checked}
52
+ @click=${onToggle}
53
+ .indeterminate=${indeterminate}
54
+ .windeterminate=${indeterminate}
55
+ />
56
+ </div>`;
57
+ },
58
+ SettingsUI = (host) => {
59
+ const {
60
+ settings,
61
+ settingsId,
62
+ onSave,
63
+ onReset,
64
+ hasChanges,
65
+ opened,
66
+ setOpened,
67
+ ...thru
68
+ } = useSettingsUi(host);
69
+ return [
70
+ html`
71
+ <style>
72
+ ${style}
73
+ </style>
74
+ <div class="headline">
75
+ ${_('Sort and filter')}
76
+ <button class="close" @click=${(e) => e.currentTarget?.blur()}>
77
+ ${close}
78
+ </button>
79
+ </div>
80
+ <div class="contents">
81
+ <div
82
+ class="heading"
83
+ ?data-opened=${opened.columns}
84
+ @click=${() => setOpened((c) => ({ ...c, columns: !c.columns }))}
85
+ >
86
+ ${_('Columns')} ${arrow}
87
+ </div>
88
+ <cosmoz-collapse ?opened=${opened.columns}>
89
+ <div class="list">${settings?.map(renderItem(thru))}</div>
90
+ </cosmoz-collapse>
91
+ <div
92
+ class="heading"
93
+ ?data-opened=${opened.group}
94
+ @click=${() => setOpened((c) => ({ ...c, group: !c.group }))}
95
+ >
96
+ ${_('Group on')} ${arrow}
97
+ </div>
98
+ <cosmoz-collapse ?opened=${opened.group}>
99
+ ${group()}
100
+ </cosmoz-collapse>
101
+ </div>
102
+ `,
103
+ settingsId &&
104
+ html`<div class="buttons">
105
+ <button
106
+ class="button reset"
107
+ @click=${onReset}
108
+ ?disabled=${!hasChanges}
109
+ >
110
+ ${_('Reset')}
111
+ </button>
112
+ <button class="button" @click=${onSave} ?disabled=${!hasChanges}>
113
+ ${_('Save')}
114
+ </button>
115
+ </div>`,
116
+ ].filter(Boolean);
117
+ },
118
+ Settings = ({ config }) => html`
119
+ <style>
120
+ ${dropdownStyle}
121
+ </style>
122
+ <cosmoz-dropdown
123
+ .render=${() => html`<cosmoz-omnitable-settings-ui
124
+ .config=${config}
125
+ ></cosmoz-omnitable-settings-ui>`}
126
+ .placement=${placement}
127
+ >
128
+ <svg viewBox="0 0 24 24" width="24" slot="button" fill="currentColor">
129
+ <path d="M10 18h5V5h-5v13zm-6 0h5V5H4v13zM16 5v13h5V5h-5z"></path>
130
+ </svg>
131
+ ${config?.badge ? html`<div class="badge" slot="button"></div>` : nothing}
132
+ </cosmoz-dropdown>
133
+ `;
134
+ customElements.define('cosmoz-omnitable-settings', component(Settings));
135
+ customElements.define('cosmoz-omnitable-settings-ui', component(SettingsUI));
@@ -0,0 +1,3 @@
1
+ import './cosmoz-omnitable-settings';
2
+
3
+ export { default as useSettings } from './use-settings';
@@ -0,0 +1,40 @@
1
+ const byName = (name) => (item) => item.name === name;
2
+
3
+ export default (columns = [], settings = [], savedSettings = []) => {
4
+ const cols = columns.filter(
5
+ (column) =>
6
+ !settings.some(byName(column.name)) &&
7
+ !savedSettings.some(byName(column.name))
8
+ ),
9
+ _savedSettings = savedSettings.filter(
10
+ (column) => !settings.some(byName(column.name))
11
+ );
12
+
13
+ return [
14
+ ...settings,
15
+ ..._savedSettings.flatMap((setting) => {
16
+ const column = columns.find((c) => c.name === setting.name);
17
+
18
+ if (!column) {
19
+ return [];
20
+ }
21
+
22
+ return {
23
+ ...setting,
24
+ title: column.title,
25
+ minWidth: parseInt(column.minWidth, 10),
26
+ };
27
+ }),
28
+ ...cols.map((column) => {
29
+ const { name, title, priority, minWidth, width, flex } = column;
30
+ return {
31
+ name,
32
+ title,
33
+ priority,
34
+ minWidth: parseInt(minWidth, 10),
35
+ width: parseInt(width, 10),
36
+ flex: parseInt(flex, 10),
37
+ };
38
+ }),
39
+ ];
40
+ };
@@ -0,0 +1,211 @@
1
+ import { tagged as css } from '@neovici/cosmoz-utils';
2
+ import { checkbox, sort as sortStyle } from '../../cosmoz-omnitable-styles';
3
+
4
+ export default css`
5
+ :host {
6
+ box-sizing: border-box;
7
+ display: flex;
8
+ flex-direction: column;
9
+ max-height: var(--ot-height, 60vh);
10
+ outline: none;
11
+ min-width: 264px;
12
+ }
13
+
14
+ .headline {
15
+ box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.15),
16
+ inset 0px 1px 0px rgba(0, 0, 0, 0.15);
17
+ font-weight: 500;
18
+ font-size: 16px;
19
+ line-height: 0.95;
20
+ padding: 12px 14px 11px 14px;
21
+ display: flex;
22
+ }
23
+ .close {
24
+ border: none;
25
+ background: none;
26
+ cursor: pointer;
27
+ padding: 0;
28
+ margin-left: auto;
29
+ }
30
+
31
+ .contents {
32
+ overflow-y: auto;
33
+ scrollbar-width: 2px;
34
+ scrollbar-gutter: stable;
35
+ }
36
+ .contents::-webkit-scrollbar {
37
+ width: 2px;
38
+ }
39
+ .contents::-webkit-scrollbar-thumb {
40
+ background: rgba(0, 0, 0, 0.5);
41
+ }
42
+ .contents::-webkit-scrollbar-track-piece:start,
43
+ .contents::-webkit-scrollbar-track-piece:end {
44
+ background: transparent;
45
+ }
46
+
47
+ .heading {
48
+ box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.15);
49
+ font-weight: 500;
50
+ font-size: 14px;
51
+ line-height: 0.95;
52
+ padding: 14px;
53
+ display: flex;
54
+ cursor: pointer;
55
+ align-items: center;
56
+ }
57
+ .heading svg {
58
+ margin-left: auto;
59
+ }
60
+ .heading[data-opened] svg {
61
+ transform: scaleY(-1);
62
+ }
63
+ cosmoz-collapse[opened] + .heading {
64
+ box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.15),
65
+ inset 0px 1px 0px rgba(0, 0, 0, 0.15);
66
+ }
67
+
68
+ .list {
69
+ flex: 1;
70
+ padding: 2px 14px;
71
+ min-width: 232px;
72
+ }
73
+ .item {
74
+ display: flex;
75
+ align-items: center;
76
+ background: white;
77
+ }
78
+ .item.drag {
79
+ opacity: 0.6;
80
+ pointer-events: none;
81
+ }
82
+ .item.dragover {
83
+ box-shadow: 0 -2px 0 0 currentColor;
84
+ }
85
+ .pull {
86
+ border: none;
87
+ padding: 0;
88
+ font-size: 0;
89
+ vertical-align: bottom;
90
+ outline: none;
91
+ background: transparent;
92
+ cursor: move;
93
+ margin-right: 12px;
94
+ color: #c4c4c4;
95
+ }
96
+ .title {
97
+ flex: 1;
98
+ }
99
+ .title[has-filter] {
100
+ font-weight: bold;
101
+ }
102
+ ${checkbox}
103
+ .checkbox {
104
+ margin: 4px 0;
105
+ }
106
+
107
+ ${sortStyle}
108
+ .sort {
109
+ order: initial;
110
+ margin: 0;
111
+ width: auto;
112
+ padding: 8px;
113
+ }
114
+
115
+ .buttons {
116
+ display: flex;
117
+ gap: 8px;
118
+ padding: 12px 14px;
119
+ box-shadow: inset 0px 1px 0px rgba(0, 0, 0, 0.15);
120
+ }
121
+ .button {
122
+ border: none;
123
+ cursor: pointer;
124
+ background: var(--cosmoz-button-bg-color, #101010);
125
+ color: var(--cosmoz-button-color, #fff);
126
+ font-family: inherit;
127
+ font-size: 13px;
128
+ font-weight: 600;
129
+ line-height: 26px;
130
+ border-radius: 4px;
131
+ padding: 8px;
132
+ flex: 1;
133
+ }
134
+ .button:not(.reset):hover {
135
+ background: var(--cosmoz-button-hover-bg-color, #3a3f44);
136
+ }
137
+ .button[disabled] {
138
+ opacity: 0.2;
139
+ pointer-events: none;
140
+ }
141
+ .reset {
142
+ background: transparent;
143
+ color: inherit;
144
+ text-decoration: underline;
145
+ }
146
+ .groups {
147
+ display: grid;
148
+ column-gap: 7px;
149
+ row-gap: 8px;
150
+ grid-template-columns: 1fr 1fr;
151
+ padding: 14px;
152
+ }
153
+ .group {
154
+ border: 1px solid rgba(0, 0, 0, 0.2);
155
+ border-radius: 2px;
156
+ font-size: 13px;
157
+ line-height: 1;
158
+ text-align: center;
159
+ padding: 6px 12px;
160
+ background: transparent;
161
+ cursor: pointer;
162
+ }
163
+ .group[data-group] {
164
+ background: var(
165
+ --cosmoz-omnitable-checkbox-checked-color,
166
+ var(--primary-color)
167
+ );
168
+ }
169
+ .group svg {
170
+ vertical-align: middle;
171
+ }
172
+ .group:not([data-group]) svg {
173
+ display: none;
174
+ }
175
+ .group:not([data-group='desc']) svg {
176
+ transform: scaleY(-1);
177
+ }
178
+ `;
179
+
180
+ export const dropdown = css`
181
+ :host {
182
+ display: contents;
183
+ color: var(--cosmoz-omnitable-settings-color, #101010);
184
+ --cosmoz-dropdown-box-shadow: 0 3px 4px 0 rgb(0 0 0 / 14%),
185
+ 0 1px 8px 0 rgb(0 0 0 / 12%), 0 3px 3px -2px rgb(0 0 0 / 40%);
186
+ }
187
+ cosmoz-dropdown {
188
+ outline: none;
189
+ }
190
+ cosmoz-dropdown::part(button) {
191
+ --cosmoz-dropdown-button-size: 24px;
192
+ padding: 0;
193
+ background: transparent;
194
+ color: inherit;
195
+ }
196
+ cosmoz-dropdown::part(anchor) {
197
+ display: inline-block;
198
+ }
199
+ .badge {
200
+ position: absolute;
201
+ top: 1px;
202
+ right: -4px;
203
+ background-color: var(
204
+ --cosmoz-omnitable-checkbox-checked-color,
205
+ var(--primary-color)
206
+ );
207
+ width: 8px;
208
+ height: 8px;
209
+ border-radius: 100%;
210
+ }
211
+ `;
@@ -3,7 +3,7 @@ import { props } from '@neovici/cosmoz-utils/lib/object';
3
3
 
4
4
  const storagePrefix = 'omnitable-';
5
5
 
6
- export const useSavedSettings = (settingsId, settings, setSettings) => {
6
+ export default (settingsId, settings, setSettings) => {
7
7
  const
8
8
  [counter, setCounter] = useState(0),
9
9
  savedSettings = useMemo(() => {
@@ -19,9 +19,10 @@ export const useSavedSettings = (settingsId, settings, setSettings) => {
19
19
  }, [settingsId, counter]);
20
20
 
21
21
  return {
22
+ settingsId,
22
23
  savedSettings,
23
24
 
24
- onSettingsSave: useCallback(() => {
25
+ onSave: useCallback(() => {
25
26
  if (!settingsId) {
26
27
  return;
27
28
  }
@@ -36,7 +37,7 @@ export const useSavedSettings = (settingsId, settings, setSettings) => {
36
37
  }
37
38
  }, [settings]),
38
39
 
39
- onSettingsReset: e => {
40
+ onReset: e => {
40
41
  setSettings([]);
41
42
 
42
43
  if (e.shiftKey) {
@@ -49,6 +50,6 @@ export const useSavedSettings = (settingsId, settings, setSettings) => {
49
50
  }
50
51
  },
51
52
 
52
- hasChangedSettings: settings.length !== 0
53
+ hasChanges: settings.length !== 0
53
54
  };
54
55
  };
@@ -0,0 +1,116 @@
1
+ import { useCallback } from 'haunted';
2
+ import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
3
+
4
+ const parseIndex = (str) => {
5
+ const idx = parseInt(str, 10);
6
+ return isFinite(idx) ? idx : undefined;
7
+ };
8
+
9
+ // eslint-disable-next-line max-lines-per-function
10
+ export default (host) => {
11
+ const { config } = host,
12
+ { settings, setSettings } = config,
13
+ meta = useMeta({ settings, setSettings });
14
+
15
+ return {
16
+ ...config,
17
+ onDown: useCallback(
18
+ (e) => {
19
+ if (!e.target.closest('.pull')) {
20
+ return;
21
+ }
22
+
23
+ meta.handle = e.currentTarget;
24
+ },
25
+ [meta]
26
+ ),
27
+
28
+ onDragStart: useCallback(
29
+ (e) => {
30
+ const { target } = e,
31
+ index = parseIndex(target.dataset.index);
32
+
33
+ if (!meta.handle?.contains(target) || index == null) {
34
+ return e.preventDefault();
35
+ }
36
+
37
+ meta.handle = null;
38
+ e.dataTransfer.effectAllowed = 'move';
39
+ e.dataTransfer.setData('omnitable/sort-index', index);
40
+ setTimeout(() => target.classList.add('drag'), 0);
41
+ target.addEventListener(
42
+ 'dragend',
43
+ (e) => e.target.classList.remove('drag'),
44
+ { once: true }
45
+ );
46
+ },
47
+ [meta]
48
+ ),
49
+
50
+ onDragEnter: useCallback((e) => {
51
+ const ctg = e.currentTarget;
52
+ if (ctg !== e.target) {
53
+ return;
54
+ }
55
+
56
+ e.preventDefault();
57
+ e.dataTransfer.dropEffect = 'move';
58
+ ctg.classList.add('dragover');
59
+ }, []),
60
+
61
+ onDragOver: useCallback((e) => {
62
+ e.preventDefault();
63
+ e.currentTarget.classList.add('dragover');
64
+ }, []),
65
+
66
+ onDragLeave: useCallback((e) => {
67
+ const ctg = e.currentTarget;
68
+ if (ctg.contains(e.relatedTarget)) {
69
+ return;
70
+ }
71
+
72
+ ctg.classList.remove('dragover');
73
+ }, []),
74
+
75
+ onDrop: useCallback(
76
+ (e) => {
77
+ const from = parseIndex(e.dataTransfer.getData('omnitable/sort-index')),
78
+ to = parseIndex(e.currentTarget.dataset.index),
79
+ { settings, setSettings } = meta;
80
+
81
+ e.currentTarget.classList.remove('dragover');
82
+ e.preventDefault();
83
+
84
+ const newSettings = settings.slice();
85
+ newSettings.splice(
86
+ to + (from >= to ? 0 : -1),
87
+ 0,
88
+ newSettings.splice(from, 1)[0]
89
+ );
90
+ setSettings(newSettings);
91
+ },
92
+ [meta]
93
+ ),
94
+
95
+ onToggle: useCallback(
96
+ (e) => {
97
+ const checked = e.target.checked,
98
+ idx = parseIndex(e.target.closest('[data-index]')?.dataset.index),
99
+ { settings, setSettings } = meta,
100
+ newSettings = settings.slice(),
101
+ setting = settings[idx],
102
+ priority = e.target.windeterminate
103
+ ? settings.reduce((acc, s) => Math.max(acc, s.priority), 0) + 1
104
+ : setting.priority;
105
+
106
+ newSettings.splice(idx, 1, {
107
+ ...settings[idx],
108
+ disabled: !checked,
109
+ priority,
110
+ });
111
+ setSettings(newSettings);
112
+ },
113
+ [meta]
114
+ ),
115
+ };
116
+ };
@@ -0,0 +1,32 @@
1
+ import { useMemo, useState } from 'haunted';
2
+
3
+ import useSavedSettings from './use-saved-settings';
4
+ import normalize from './normalize';
5
+
6
+ export default ({ settingsId, columns }) => {
7
+ const [settings, setSettings] = useState([]),
8
+ [opened, setOpened] = useState({ columns: true, group: true }),
9
+ { savedSettings, ...thru } = useSavedSettings(
10
+ settingsId,
11
+ settings,
12
+ setSettings
13
+ ),
14
+ normalizedSettings = useMemo(
15
+ () => normalize(columns, settings, savedSettings),
16
+ [columns, settings, savedSettings]
17
+ ),
18
+ normalizedColumns = useMemo(
19
+ () => normalizedSettings.map(s => columns.find(c => c.name === s.name)),
20
+ [columns, ...normalizedSettings.map(s => s.name)]
21
+ );
22
+
23
+ return {
24
+ ...thru,
25
+ opened,
26
+ setOpened,
27
+ settings: normalizedSettings,
28
+ normalizedColumns,
29
+ setSettings,
30
+ };
31
+ };
32
+
@@ -1,22 +1,18 @@
1
- import { useCallback, useMemo, useState, useEffect } from 'haunted';
2
- import { normalizeSettings } from './normalize-settings';
1
+ import { useCallback, useMemo, useEffect } from 'haunted';
3
2
  import { useProcessedItems } from './use-processed-items';
4
3
  import { useFastLayout } from './use-fast-layout';
5
- import { useSavedSettings } from './use-saved-settings';
4
+ import { useSettings } from './settings';
6
5
  import { useDOMColumns } from './use-dom-columns';
7
6
  import { useSortAndGroupOptions } from './use-sort-and-group-options';
8
7
  import { onItemChange } from './utils-data';
9
8
 
10
9
  // eslint-disable-next-line max-lines-per-function
11
- export const useOmnitable = host => {
10
+ export const useOmnitable = (host) => {
12
11
  const { enabledColumns, hashParam } = host,
13
12
  columns = useDOMColumns(host, { enabledColumns }),
14
- sortAndGroupOptions = useSortAndGroupOptions(
15
- columns,
16
- hashParam,
17
- host
18
- ),
19
- { groupOnColumn, groupOnDescending, sortOnColumn, descending } = sortAndGroupOptions,
13
+ sortAndGroupOptions = useSortAndGroupOptions(columns, hashParam, host),
14
+ { groupOnColumn, groupOnDescending, sortOnColumn, descending } =
15
+ sortAndGroupOptions,
20
16
  { data, resizeSpeedFactor, settingsId } = host,
21
17
  // TODO: drop filterFunctions
22
18
  {
@@ -25,7 +21,7 @@ export const useOmnitable = host => {
25
21
  setFilterState,
26
22
  numProcessedItems,
27
23
  filterFunctions,
28
- groupsCount
24
+ groupsCount,
29
25
  } = useProcessedItems({
30
26
  data,
31
27
  columns,
@@ -33,73 +29,93 @@ export const useOmnitable = host => {
33
29
  groupOnDescending,
34
30
  sortOnColumn,
35
31
  descending,
36
- hashParam
32
+ hashParam,
37
33
  }),
38
- [settings, setSettings] = useState([]),
39
- { savedSettings, onSettingsSave, onSettingsReset, hasChangedSettings } =
40
- useSavedSettings(settingsId, settings, setSettings),
41
- normalizedSettings = useMemo(
42
- () => normalizeSettings(columns, settings, savedSettings),
43
- [columns, settings, savedSettings]
44
- ),
45
- normalizedColumns = useMemo(
46
- () => normalizedSettings.map(s => columns.find(c => c.name === s.name)),
47
- [columns, ...normalizedSettings.map(s => s.name)]
48
- ),
34
+ settingsOpts = useSettings({ settingsId, columns }),
35
+ { settings, setSettings, normalizedColumns } = settingsOpts,
49
36
  layout = useFastLayout({
50
37
  host,
51
- settings: normalizedSettings,
38
+ settings,
52
39
  setSettings,
53
40
  groupOnColumn,
54
- resizeSpeedFactor
41
+ resizeSpeedFactor,
55
42
  }),
56
43
  collapsedColumns = useMemo(
57
- () => normalizedSettings.reduce((acc, column, index) =>
58
- layout[index] != null ||
44
+ () =>
45
+ settings.reduce(
46
+ (acc, column, index) =>
47
+ layout[index] != null ||
59
48
  column.name === groupOnColumn?.name ||
60
49
  column.disabled
61
- ? acc
62
- : [...acc, columns.find(c => c.name === column.name)],
63
- []),
64
- [columns, normalizedSettings, layout]
50
+ ? acc
51
+ : [...acc, columns.find((c) => c.name === column.name)],
52
+ []
53
+ ),
54
+ [columns, settings, layout]
65
55
  ),
66
56
  hasHiddenFilter = useMemo(
67
57
  () =>
68
58
  [
69
59
  groupOnColumn,
70
60
  ...collapsedColumns,
71
- ...normalizedSettings.filter(s => s.disabled)
72
- ].some(column => column && Object.keys(filterFunctions).includes(column.name)),
73
- [filterFunctions, normalizedSettings, collapsedColumns]
61
+ ...settings.filter((s) => s.disabled),
62
+ ].some(
63
+ (column) =>
64
+ column && Object.keys(filterFunctions).includes(column.name)
65
+ ),
66
+ [filterFunctions, settings, collapsedColumns]
67
+ ),
68
+ settingsConfig = useMemo(
69
+ () => ({
70
+ ...settingsOpts,
71
+ collapsed: collapsedColumns,
72
+ badge: hasHiddenFilter,
73
+ filters,
74
+ }),
75
+ [settingsOpts, collapsedColumns, hasHiddenFilter, filters]
74
76
  );
75
77
 
76
78
  useEffect(() => {
77
- const handler = ev => setFilterState(ev.detail.name, state => ({ ...state, ...ev.detail.state }));
79
+ const handler = (ev) =>
80
+ setFilterState(ev.detail.name, (state) => ({
81
+ ...state,
82
+ ...ev.detail.state,
83
+ }));
78
84
  host.addEventListener('legacy-filter-changed', handler);
79
85
  return () => host.removeEventListener('legacy-filter-changed', handler);
80
86
  }, []);
81
87
 
82
- return {
83
- columns,
88
+ useEffect(() => {
89
+ const el = host.shadowRoot.querySelector('#tableContent'),
90
+ observer = new ResizeObserver((entries) =>
91
+ requestAnimationFrame(() => {
92
+ host.style.setProperty(
93
+ '--ot-height',
94
+ entries[0]?.contentRect.height + 'px'
95
+ );
96
+ })
97
+ );
98
+ observer.observe(el);
99
+ return () => observer.unobserve(el);
100
+ }, []);
84
101
 
102
+ return {
85
103
  ...sortAndGroupOptions,
86
104
 
87
- setSettings,
88
- normalizedSettings,
105
+ columns,
89
106
  normalizedColumns,
90
107
  collapsedColumns,
91
- hasHiddenFilter,
92
- onSettingsSave,
93
- onSettingsReset,
94
- hasChangedSettings,
108
+
109
+ settingsConfig,
110
+
95
111
  filters,
96
112
  setFilterState,
97
113
  onItemChange: useCallback(
98
- (column, item) => value => onItemChange(host, column, item, value),
114
+ (column, item) => (value) => onItemChange(host, column, item, value),
99
115
  []
100
116
  ),
101
117
  numProcessedItems,
102
118
  groupsCount,
103
- sortedFilteredGroupedItems: processedItems
119
+ sortedFilteredGroupedItems: processedItems,
104
120
  };
105
121
  };
@@ -42,6 +42,8 @@ export const useSortAndGroupOptions = (columns, hashParam, initial) => {
42
42
  setSortOn,
43
43
  descending,
44
44
  setDescending,
45
+
46
+ columns,
45
47
  },
46
48
  sortAndGroup = useMemo(() => sortAndGroup_, Object.values(sortAndGroup_));
47
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-omnitable",
3
- "version": "8.7.3",
3
+ "version": "8.9.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"
@@ -54,6 +54,7 @@
54
54
  "dependencies": {
55
55
  "@neovici/cosmoz-autocomplete": "^3.1.0",
56
56
  "@neovici/cosmoz-bottom-bar": "^5.0.0",
57
+ "@neovici/cosmoz-collapse": "^1.1.0",
57
58
  "@neovici/cosmoz-datetime-input": "^3.0.3",
58
59
  "@neovici/cosmoz-dropdown": "^1.5.0",
59
60
  "@neovici/cosmoz-grouped-list": "^3.2.0",
@@ -1,366 +0,0 @@
1
- import { html, component, useCallback } from 'haunted';
2
- import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
3
- import { checkbox, sort as sortStyle } from '../cosmoz-omnitable-styles';
4
- import { nothing } from 'lit-html';
5
- import { isEmpty } from '@neovici/cosmoz-utils/lib/template';
6
- import { defaultPlacement } from '@neovici/cosmoz-dropdown';
7
- import sort from './cosmoz-omnitable-sort';
8
-
9
- const placement = ['bottom-right', ...defaultPlacement],
10
- settingsStyles = `
11
- :host {
12
- box-sizing: border-box;
13
- display: flex;
14
- flex-direction: column;
15
- max-height: 42vh;
16
- outline: none;
17
- padding-top: 14px;
18
- }
19
- .list {
20
- overflow-y: auto;
21
- flex: 1;
22
- padding: 2px 14px;
23
- min-width: 232px;
24
- }
25
- .item {
26
- display: flex;
27
- align-items: center;
28
- background: white;
29
- }
30
- .item.drag {
31
- opacity: 0.6;
32
- pointer-events: none;
33
- }
34
- .item.dragover {
35
- box-shadow: 0 -2px 0 0 currentColor;
36
- }
37
- .pull {
38
- border: none;
39
- padding: 0;
40
- font-size: 0;
41
- vertical-align: bottom;
42
- outline: none;
43
- background: transparent;
44
- cursor: move;
45
- margin-right: 12px;
46
- color: #c4c4c4;
47
- }
48
- .title {
49
- flex: 1;
50
- }
51
- .title[has-filter] {
52
- font-weight: bold;
53
- }
54
- ${checkbox}
55
- .checkbox {
56
- margin: 4px 0;
57
- }
58
-
59
- ${sortStyle}
60
- .sort {
61
- order: initial;
62
- margin: 0;
63
- width: auto;
64
- padding: 8px;
65
- }
66
-
67
- .buttons {
68
- display: flex;
69
- gap: 8px;
70
- margin-top: 10px;
71
- padding: 12px 14px;
72
- box-shadow: inset 0px 1px 0px rgba(0, 0, 0, 0.15);
73
- }
74
- .button {
75
- border: none;
76
- cursor: pointer;
77
- background: var(--cosmoz-button-bg-color, #101010);
78
- color: var(--cosmoz-button-color, #fff);
79
- font-family: inherit;
80
- font-size: 13px;
81
- font-weight: 600;
82
- line-height: 26px;
83
- border-radius: 4px;
84
- padding: 8px;
85
- flex: 1;
86
- }
87
- .button:not(.reset):hover {
88
- background: var(--cosmoz-button-hover-bg-color, #3A3F44);
89
- }
90
- .button[disabled] {
91
- opacity: 0.2;
92
- pointer-events: none;
93
- }
94
- .reset {
95
- background: transparent;
96
- color: inherit;
97
- text-decoration: underline;
98
- }
99
- `,
100
- parseIndex = (str) => {
101
- const idx = parseInt(str, 10);
102
- return isFinite(idx) ? idx : undefined;
103
- },
104
- // eslint-disable-next-line max-lines-per-function
105
- useSettings = (host) => {
106
- const { settings, onSettings } = host,
107
- meta = useMeta({ settings, onSettings });
108
-
109
- return {
110
- settings: host.settings,
111
- collapsed: host.collapsed,
112
-
113
- settingsId: host.settingsId,
114
- hasChanges: host.hasChanges,
115
- onSave: host.onSave,
116
- onReset: host.onReset,
117
-
118
- filters: host.filters,
119
-
120
- onDown: useCallback(
121
- (e) => {
122
- if (!e.target.closest('.pull')) {
123
- return;
124
- }
125
-
126
- meta.handle = e.currentTarget;
127
- },
128
- [meta]
129
- ),
130
-
131
- onDragStart: useCallback(
132
- (e) => {
133
- const { target } = e,
134
- index = parseIndex(target.dataset.index);
135
-
136
- if (!meta.handle?.contains(target) || index == null) {
137
- return e.preventDefault();
138
- }
139
-
140
- meta.handle = null;
141
- e.dataTransfer.effectAllowed = 'move';
142
- e.dataTransfer.setData('omnitable/sort-index', index);
143
- setTimeout(() => target.classList.add('drag'), 0);
144
- target.addEventListener(
145
- 'dragend',
146
- (e) => e.target.classList.remove('drag'),
147
- { once: true }
148
- );
149
- },
150
- [meta]
151
- ),
152
-
153
- onDragEnter: useCallback((e) => {
154
- const ctg = e.currentTarget;
155
- if (ctg !== e.target) {
156
- return;
157
- }
158
-
159
- e.preventDefault();
160
- e.dataTransfer.dropEffect = 'move';
161
- ctg.classList.add('dragover');
162
- }, []),
163
-
164
- onDragOver: useCallback((e) => {
165
- e.preventDefault();
166
- e.currentTarget.classList.add('dragover');
167
- }, []),
168
-
169
- onDragLeave: useCallback((e) => {
170
- const ctg = e.currentTarget;
171
- if (ctg.contains(e.relatedTarget)) {
172
- return;
173
- }
174
-
175
- ctg.classList.remove('dragover');
176
- }, []),
177
-
178
- onDrop: useCallback(
179
- (e) => {
180
- const from = parseIndex(
181
- e.dataTransfer.getData('omnitable/sort-index')
182
- ),
183
- to = parseIndex(e.currentTarget.dataset.index),
184
- { settings, onSettings } = meta;
185
-
186
- e.currentTarget.classList.remove('dragover');
187
- e.preventDefault();
188
-
189
- const newSettings = settings.slice();
190
- newSettings.splice(
191
- to + (from >= to ? 0 : -1),
192
- 0,
193
- newSettings.splice(from, 1)[0]
194
- );
195
- onSettings(newSettings);
196
- },
197
- [meta]
198
- ),
199
-
200
- onToggle: useCallback(
201
- (e) => {
202
- const checked = e.target.checked,
203
- idx = parseIndex(e.target.closest('[data-index]')?.dataset.index),
204
- { settings, onSettings } = meta,
205
- newSettings = settings.slice(),
206
- setting = settings[idx],
207
- priority = e.target.windeterminate
208
- ? settings.reduce((acc, s) => Math.max(acc, s.priority), 0) + 1
209
- : setting.priority;
210
-
211
- newSettings.splice(idx, 1, {
212
- ...settings[idx],
213
- disabled: !checked,
214
- priority,
215
- });
216
- onSettings(newSettings);
217
- },
218
- [meta]
219
- ),
220
- };
221
- },
222
- renderItem =
223
- // eslint-disable-next-line max-lines-per-function
224
- ({
225
- onDragStart,
226
- onDragEnter,
227
- onDragOver,
228
- onDragLeave,
229
- onDrop,
230
- onDown,
231
- onToggle,
232
- collapsed,
233
- filters,
234
- }) =>
235
- (column, i) => {
236
- const indeterminate = collapsed?.includes(column.name),
237
- checked = !column.disabled && !indeterminate;
238
- return html` <div
239
- class="item"
240
- data-index=${i}
241
- @mousedown=${onDown}
242
- draggable="true"
243
- @dragstart=${onDragStart}
244
- @dragenter=${onDragEnter}
245
- @dragover=${onDragOver}
246
- @dragleave=${onDragLeave}
247
- @drop=${onDrop}
248
- >
249
- <button class="pull">
250
- <svg
251
- width="16"
252
- height="6"
253
- viewBox="0 0 16 6"
254
- fill="currentColor"
255
- xmlns="http://www.w3.org/2000/svg"
256
- >
257
- <rect width="16" height="2" />
258
- <rect y="4" width="16" height="2" />
259
- </svg>
260
- </button>
261
- <label
262
- class="title"
263
- ?has-filter=${!isEmpty(filters[column.name]?.filter)}
264
- >${column.title}</label
265
- >
266
- ${sort(column.name)}
267
- <input
268
- class="checkbox"
269
- type="checkbox"
270
- .checked=${checked}
271
- @click=${onToggle}
272
- .indeterminate=${indeterminate}
273
- .windeterminate=${indeterminate}
274
- />
275
- </div>`;
276
- },
277
- SettingsUI = (host) => {
278
- const { settings, settingsId, onSave, onReset, hasChanges, ...thru } =
279
- useSettings(host);
280
- return [
281
- html`
282
- <style>
283
- ${settingsStyles}
284
- </style>
285
- <div class="list">${settings?.map(renderItem(thru))}</div>
286
- `,
287
- settingsId
288
- ? html`<div class="buttons">
289
- <button
290
- class="button reset"
291
- @click=${onReset}
292
- ?disabled=${!hasChanges}
293
- >
294
- Reset
295
- </button>
296
- <button class="button" @click=${onSave} ?disabled=${!hasChanges}>
297
- Save
298
- </button>
299
- </div>`
300
- : nothing,
301
- ];
302
- },
303
- // eslint-disable-next-line max-lines-per-function
304
- Settings = ({
305
- anchor,
306
- settings,
307
- onSettings,
308
- collapsed,
309
- settingsId,
310
- hasChanges,
311
- onSave,
312
- onReset,
313
- filters,
314
- badge,
315
- }) => html`
316
- <style>
317
- :host {
318
- display: contents;
319
- color: var(--cosmoz-omnitable-settings-color, #101010);
320
- --cosmoz-dropdown-box-shadow: 0 3px 4px 0 rgb(0 0 0 / 14%),
321
- 0 1px 8px 0 rgb(0 0 0 / 12%), 0 3px 3px -2px rgb(0 0 0 / 40%);
322
- }
323
- cosmoz-dropdown::part(button) {
324
- --cosmoz-dropdown-button-size: 24px;
325
- padding: 0;
326
- background: transparent;
327
- color: inherit;
328
- }
329
- cosmoz-dropdown::part(anchor) {
330
- display: inline-block;
331
- }
332
- .badge {
333
- position: absolute;
334
- top: 1px;
335
- right: -4px;
336
- background-color: var(
337
- --cosmoz-omnitable-checkbox-checked-color,
338
- var(--primary-color)
339
- );
340
- width: 8px;
341
- height: 8px;
342
- border-radius: 100%;
343
- }
344
- </style>
345
- <cosmoz-dropdown
346
- .render=${() => html`<cosmoz-omnitable-settings-ui
347
- .anchor=${anchor}
348
- .settings=${settings}
349
- .onSettings=${onSettings}
350
- .collapsed=${collapsed}
351
- .settingsId=${settingsId}
352
- .hasChanges=${hasChanges}
353
- .onSave=${onSave}
354
- .onReset=${onReset}
355
- .filters=${filters}
356
- ></cosmoz-omnitable-settings-ui>`}
357
- .placement=${placement}
358
- >
359
- <svg viewBox="0 0 24 24" width="24" slot="button" fill="currentColor">
360
- <path d="M10 18h5V5h-5v13zm-6 0h5V5H4v13zM16 5v13h5V5h-5z"></path>
361
- </svg>
362
- ${badge ? html`<div class="badge" slot="button"></div>` : nothing}
363
- </cosmoz-dropdown>
364
- `;
365
- customElements.define('cosmoz-omnitable-settings', component(Settings));
366
- customElements.define('cosmoz-omnitable-settings-ui', component(SettingsUI));
@@ -1,37 +0,0 @@
1
- const byName = name => item => item.name === name;
2
-
3
- export const
4
- normalizeSettings = (columns = [], settings = [], savedSettings = []) => {
5
- const
6
- cols = columns.filter(column => !settings.some(byName(column.name)) && !savedSettings.some(byName(column.name))),
7
- _savedSettings = savedSettings.filter(column => !settings.some(byName(column.name)));
8
-
9
- return [
10
- ...settings,
11
- ..._savedSettings
12
- .flatMap(setting => {
13
- const column = columns.find(c => c.name === setting.name);
14
-
15
- if (!column) {
16
- return [];
17
- }
18
-
19
- return {
20
- ...setting,
21
- title: column.title,
22
- minWidth: parseInt(column.minWidth, 10)
23
- };
24
- }),
25
- ...cols.map(column => {
26
- const { name, title, priority, minWidth, width, flex } = column;
27
- return {
28
- name,
29
- title,
30
- priority,
31
- minWidth: parseInt(minWidth, 10),
32
- width: parseInt(width, 10),
33
- flex: parseInt(flex, 10)
34
- };
35
- })
36
- ];
37
- };