forstok-ui-lib 5.12.2 → 5.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forstok-ui-lib",
3
- "version": "5.12.2",
3
+ "version": "5.13.2",
4
4
  "description": "Forstok UI Components Library",
5
5
  "path": "dist",
6
6
  "main": "dist/index.js",
@@ -3536,4 +3536,43 @@ export const NavChildItem = styled.div<{ $activated?: boolean, $mode?: string }>
3536
3536
  export const TotalLabel = styled.span`
3537
3537
  letter-spacing: 1.25px;
3538
3538
  display: inline;
3539
+ `
3540
+ export const FilterColumnContainer = styled.section`
3541
+ display: grid;
3542
+ justify-content: end;
3543
+ padding-left: 10px;
3544
+ `
3545
+ export const ScrollArrowContainer = styled.section`
3546
+ padding-left: 10px;
3547
+ > :first-child {
3548
+ margin-right: 2px;
3549
+ &::after {
3550
+ position: absolute;
3551
+ content: '';
3552
+ right: 50%;
3553
+ padding: 4px;
3554
+ border-style: solid;
3555
+ border-width: 0 2px 2px 0;
3556
+ transform: rotate(135deg) translate(-50%, -50%);
3557
+ }
3558
+ }
3559
+ > :last-child {
3560
+ &::after {
3561
+ position: absolute;
3562
+ content: '';
3563
+ left: 50%;
3564
+ padding: 4px;
3565
+ border-style: solid;
3566
+ border-width: 0 2px 2px 0;
3567
+ transform: rotate(-45deg) translate(-50%, -50%);
3568
+ }
3569
+ }
3570
+ button {
3571
+ height: 30px;
3572
+ padding: 0 15px;
3573
+ border-radius: 0;
3574
+ &:hover {
3575
+ color: var(--sec-clr-lnk__hvr);
3576
+ }
3577
+ }
3539
3578
  `
@@ -26,6 +26,7 @@ export { default as RadioComponent } from './radio';
26
26
  export { default as SwitchComponent } from './switch';
27
27
  export { default as MasterTableComponent } from './masterTable';
28
28
  export { default as ListComponent } from './list';
29
+ export { default as ListTableComponent } from './listTable';
29
30
 
30
31
  export * from './dropdown/typed';
31
32
  export * from './message/typed';
@@ -0,0 +1,409 @@
1
+ import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import ButtonComponent from '../button';
4
+ import CheckboxComponent from '../checkbox';
5
+ import DropDownComponent from '../dropdown';
6
+ import LabelComponent from '../label';
7
+ import LinkComponent from '../link';
8
+ import { getStorage, setStorage } from '../../assets/javascripts/function';
9
+ import { DropdownItem, DropdownList, DropdownTitle, FilterButton, FilterColumnContainer, ScrollArrowContainer } from '../../assets/stylesheets/shares.styles';
10
+ import { TChangeEvent } from '../../typeds';
11
+
12
+ const ListTableComponent = ({ children, hiddenColumnKey, isScroll, freezeColumns, localStorageName }: { children: ReactNode, hiddenColumnKey?: string[], isScroll?: boolean, freezeColumns?: number, localStorageName: string }) => {
13
+ const [tableEl, setTableEl] = useState<HTMLElement>();
14
+
15
+ useEffect(() => {
16
+ setTableEl(document.querySelector('._refColumnContainer') as HTMLElement);
17
+ }, [])
18
+
19
+ const CheckTableWidth = useCallback(() => {
20
+ if (!tableEl) {
21
+ return false;
22
+ }
23
+ const windowWidth = window.innerWidth;
24
+ const actionEl = tableEl.querySelectorAll('.column-actions') as NodeListOf<HTMLElement>;
25
+ const columnEl = tableEl.querySelector('._refHeaderColumn') as HTMLElement;
26
+ let _width = 0;
27
+
28
+ if (windowWidth <= 1024) {
29
+ for (const el of columnEl.children) {
30
+ if (el.role === 'columnheader' && !el.classList.contains('none')) {
31
+ _width += (parseInt(getComputedStyle(el).minWidth) ? parseInt(getComputedStyle(el).minWidth) : 90);
32
+ } else if (el.role === 'group') {
33
+ for (const groupedEl of el.children) {
34
+ if (!groupedEl.classList.contains('none')) {
35
+ _width += (parseInt(getComputedStyle(groupedEl).minWidth) ? parseInt(getComputedStyle(groupedEl).minWidth) : 90);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ } else if ( 1024 < windowWidth && windowWidth < 1700) {
41
+ let _addColumnWidth = windowWidth - 1025;
42
+ let _columnCount = 0;
43
+
44
+ for (const el of columnEl.children) {
45
+ if (el.role === 'columnheader' && !el.classList.contains('none')) {
46
+ _columnCount += 1;
47
+ _width += (parseInt(getComputedStyle(el).minWidth) ? parseInt(getComputedStyle(el).minWidth) : 90);
48
+ } else if (el.role === 'group') {
49
+ for (const groupedEl of el.children) {
50
+ if (!groupedEl.classList.contains('none')) {
51
+ _columnCount += 1;
52
+ _width += (parseInt(getComputedStyle(groupedEl).minWidth) ? parseInt(getComputedStyle(groupedEl).minWidth) : 90);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ _width += (_addColumnWidth / _columnCount);
58
+ } else if (windowWidth >= 1800) {
59
+ for (const el of columnEl.children) {
60
+ if (el.role === 'columnheader' && !el.classList.contains('none')) {
61
+ _width += (parseInt(getComputedStyle(el).maxWidth) ? parseInt(getComputedStyle(el).maxWidth) : 100);
62
+ } else if (el.role === 'group') {
63
+ for (const groupedEl of el.children) {
64
+ if (!groupedEl.classList.contains('none')) {
65
+ _width += (parseInt(getComputedStyle(groupedEl).maxWidth) ? parseInt(getComputedStyle(groupedEl).maxWidth) : (parseInt(getComputedStyle(groupedEl).minWidth) ? parseInt(getComputedStyle(groupedEl).minWidth) : 90));
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ const tableBodyEls = tableEl.querySelectorAll('._refBodyColumn') as NodeListOf<HTMLElement>;
73
+ if (tableEl.clientWidth < _width) {
74
+ columnEl.style.width = _width+'px';
75
+ for (let rowEl of tableBodyEls) {
76
+ rowEl.style.width = _width+'px';
77
+ }
78
+ if (freezeColumns) {
79
+ for (let _el of actionEl) {
80
+ _el.style.borderLeft = '1px solid rgba(0, 0, 0, 0.05)';
81
+ }
82
+ }
83
+ } else {
84
+ columnEl.style.width = '';
85
+ for (let rowEl of tableBodyEls) {
86
+ rowEl.style.width = '';
87
+ }
88
+ if (freezeColumns) {
89
+ for (let _el of actionEl) {
90
+ _el.style.borderLeft = '';
91
+ }
92
+ }
93
+ }
94
+ }, [tableEl, freezeColumns])
95
+
96
+ useEffect(() => {
97
+ window.addEventListener('resize', CheckTableWidth, true);
98
+ return () => {
99
+ window.removeEventListener('resize', CheckTableWidth, true);
100
+ }
101
+ }, [CheckTableWidth])
102
+
103
+ /////////////////////////////////////////// SCROLL FUNC /////////////////////////////////////////////////////////
104
+
105
+ const evScrollArrowClick = (direction: string) => {
106
+ const columnEl = document.querySelector('._refColumnContainer') as HTMLElement;
107
+ const currentScroll = columnEl.scrollLeft;
108
+ if (direction === 'left') {
109
+ columnEl.scrollTo(currentScroll - 500, 0);
110
+ } else {
111
+ columnEl.scrollTo(currentScroll + 500, 0);
112
+ }
113
+ }
114
+
115
+ const checkTableScrollable = useCallback(() => {
116
+ const buttonArrowEl = document.getElementById('arrow-scroll-button') as HTMLElement;
117
+ if (buttonArrowEl && tableEl) {
118
+ if (tableEl.scrollWidth <= tableEl.clientWidth) {
119
+ buttonArrowEl.classList.add('none');
120
+ } else {
121
+ buttonArrowEl.classList.remove('none');
122
+ }
123
+ }
124
+ },[tableEl])
125
+
126
+ const arrowScrollRoot = useRef<ReactDOM.Root>(null);
127
+ const initArrowScroll = useCallback(() => {
128
+ const ArrowScrollElement = () => {
129
+ return (
130
+ <ScrollArrowContainer>
131
+ <ButtonComponent title='Scroll Left' $mode='nude' onClick={e => evScrollArrowClick('left')}></ButtonComponent>
132
+ <ButtonComponent title='Scroll Right' $mode='nude' onClick={e => evScrollArrowClick('right')}></ButtonComponent>
133
+ </ScrollArrowContainer>
134
+ )
135
+ };
136
+ if (!tableEl) {
137
+ return false;
138
+ }
139
+ const filterEl = document.querySelector('._refFilterContainer') as HTMLElement;
140
+ const buttonEl = document.getElementById('arrow-scroll-button') as HTMLElement;
141
+ const actionEl = tableEl.querySelectorAll('.column-actions') as NodeListOf<HTMLElement>;
142
+ const maxScrollLeft = tableEl.scrollWidth - tableEl.clientWidth;
143
+
144
+ for (let _el of actionEl) {
145
+ _el.classList.add('freezeColumn');
146
+ _el.style.right = '0';
147
+ if (maxScrollLeft > 0) {
148
+ _el.style.borderLeft = '1px solid rgba(0, 0, 0, 0.05)';
149
+ }
150
+ }
151
+
152
+ const renderTimeout = setTimeout(() => {
153
+ if (buttonEl) {
154
+ arrowScrollRoot.current && arrowScrollRoot.current.unmount();
155
+ arrowScrollRoot.current = null;
156
+ buttonEl.remove();
157
+ }
158
+ const el = document.createElement('section');
159
+ el.id = 'arrow-scroll-button';
160
+ filterEl.appendChild(el);
161
+ if (arrowScrollRoot.current === null) {
162
+ arrowScrollRoot.current = ReactDOM.createRoot(
163
+ document.getElementById('arrow-scroll-button') as HTMLElement
164
+ );
165
+ arrowScrollRoot.current.render(<ArrowScrollElement />);
166
+ }
167
+ checkTableScrollable();
168
+ })
169
+
170
+ window.addEventListener('resize', checkTableScrollable, true);
171
+ return () => {
172
+ clearTimeout(renderTimeout);
173
+ window.removeEventListener('resize', checkTableScrollable, true);
174
+ }
175
+ }, [tableEl, checkTableScrollable])
176
+
177
+ /////////////////////////////////////////// HIDE FUNC /////////////////////////////////////////////////////////
178
+
179
+ const storageFilter = getStorage(localStorageName, 'object');
180
+ const [ selectMode, setSelectMode ] = useState<boolean>(storageFilter?.length > 0);
181
+ const hiddenKeys = useRef<string[]>(storageFilter);
182
+
183
+ const evToggleClassColumn = useCallback((columnName: string, value?: boolean) => {
184
+ if (tableEl && columnName) {
185
+ let tableColumnEls = tableEl.querySelectorAll(`.column-${columnName.replaceAll(' ','-').toLowerCase()}`);
186
+ if (value !== undefined) {
187
+ for (let tableColumnEl of tableColumnEls) {
188
+ if (value) {
189
+ tableColumnEl.classList.remove('none');
190
+ } else {
191
+ tableColumnEl.classList.add('none');
192
+ }
193
+ }
194
+ } else {
195
+ for (let tableColumnEl of tableColumnEls) {
196
+ if (tableColumnEl.classList.contains('none')) {
197
+ tableColumnEl.classList.remove('none');
198
+ } else {
199
+ tableColumnEl.classList.add('none');
200
+ }
201
+ }
202
+ }
203
+ CheckTableWidth();
204
+ checkTableScrollable();
205
+ }
206
+ }, [tableEl, checkTableScrollable, CheckTableWidth])
207
+
208
+ const evSelectAll = useCallback(() => {
209
+ if (!localStorageName) {
210
+ return false;
211
+ }
212
+ const checkboxInput = document.querySelectorAll('[name="checkbox-column"]') as NodeListOf<HTMLInputElement>;
213
+ hiddenKeys.current = [];
214
+ for (let input of checkboxInput) {
215
+ if (selectMode) {
216
+ input.checked = true;
217
+ evToggleClassColumn(input.getAttribute('data-key') || '', true);
218
+ setSelectMode(false);
219
+ } else {
220
+ input.checked = false;
221
+ hiddenKeys.current.push(input.getAttribute('data-key') || '');
222
+ evToggleClassColumn(input.getAttribute('data-key') || '', false);
223
+ setSelectMode(true);
224
+ }
225
+ setStorage(localStorageName, JSON.stringify(hiddenKeys.current));
226
+ }
227
+ },[hiddenKeys, evToggleClassColumn, localStorageName, selectMode])
228
+
229
+ const evChangeColumn: TChangeEvent = useCallback((e) => {
230
+ if (!localStorageName) {
231
+ return false;
232
+ }
233
+ const target = e.target as HTMLInputElement;
234
+ const columnName = target.getAttribute('data-key') || '';
235
+ if (target.checked) {
236
+ hiddenKeys.current = hiddenKeys.current.filter((key: string) => key !== columnName);
237
+ } else {
238
+ hiddenKeys.current.push(columnName);
239
+ }
240
+ evToggleClassColumn(columnName);
241
+ setStorage(localStorageName, JSON.stringify(hiddenKeys.current));
242
+ },[hiddenKeys, evToggleClassColumn, localStorageName])
243
+
244
+ const ButtonHideElement = useCallback(() => {
245
+ return (
246
+ <FilterColumnContainer>
247
+ <DropDownComponent $internalWidth='250px' $openPosition='right'>
248
+ <div aria-label='control'>
249
+ <ButtonComponent $mode='nude' $isIndicatorArrow={true} isIndicatorArrowColor='#494949'>
250
+ <FilterButton>
251
+ Columns
252
+ </FilterButton>
253
+ </ButtonComponent>
254
+ </div>
255
+ <div aria-label='body'>
256
+ <div className='_refFilterDropdown'>
257
+ <DropdownTitle>
258
+ <strong style={{fontSize: 'inherit'}}>Columns</strong>
259
+ <LinkComponent mode='blue' onClick={evSelectAll}>{selectMode ? 'Select All' : 'Deselect All'}</LinkComponent>
260
+ </DropdownTitle>
261
+ <DropdownList style={{paddingBottom: '14px'}}>
262
+ {
263
+ hiddenColumnKey?.length ? hiddenColumnKey.map((column: any, idx: number) => {
264
+ return (
265
+ <DropdownItem key={idx} $mode='categories'>
266
+ <CheckboxComponent data-id={idx} extendKey={column} name='checkbox-column' id={`column-${idx}`} defaultChecked={!hiddenKeys.current?.includes(column)} evChange={evChangeColumn}></CheckboxComponent>
267
+ <label htmlFor={`column-${idx}`}>{column}</label>
268
+ </DropdownItem>
269
+ )
270
+ }) : <LabelComponent $mode='indi' style={{padding: '20px 14px'}}>No filter column found.</LabelComponent>
271
+ }
272
+ </DropdownList>
273
+ </div>
274
+ </div>
275
+ </DropDownComponent>
276
+ </FilterColumnContainer>
277
+ )
278
+ }, [evChangeColumn, evSelectAll, hiddenColumnKey, hiddenKeys, selectMode])
279
+
280
+ const buttonHideRoot = useRef<ReactDOM.Root>(null)
281
+ const initHiddenColumn = useCallback(() => {
282
+ const filterEl = document.querySelector('._refFilterContainer') as HTMLElement;
283
+ const buttonEl = document.getElementById('hidden-column-button') as HTMLButtonElement;
284
+
285
+ const renderTimeout = setTimeout(() => {
286
+ if (buttonEl) {
287
+ buttonHideRoot.current && buttonHideRoot.current.unmount();
288
+ buttonEl.remove();
289
+ }
290
+ const el = document.createElement('section');
291
+ el.id = 'hidden-column-button';
292
+ filterEl.appendChild(el);
293
+ buttonHideRoot.current = ReactDOM.createRoot(
294
+ document.getElementById('hidden-column-button') as HTMLElement
295
+ );
296
+ buttonHideRoot.current.render(<ButtonHideElement />);
297
+ })
298
+
299
+ if (storageFilter?.length > 0) {
300
+ for (let _column of storageFilter) {
301
+ evToggleClassColumn(_column, false);
302
+ }
303
+ }
304
+ return () => {
305
+ clearTimeout(renderTimeout);
306
+ }
307
+ }, [evToggleClassColumn, storageFilter, ButtonHideElement])
308
+
309
+ /////////////////////////////////////////// FREEZE FUNC /////////////////////////////////////////////////////////
310
+
311
+ const CheckFreezeScroll = useCallback((e: any) => {
312
+ if (!freezeColumns) {
313
+ return false;
314
+ }
315
+ const targetEl = e.target.closest('._refColumnContainer') as HTMLDivElement | HTMLElement;
316
+ if (targetEl) {
317
+ const actionEl = targetEl.querySelectorAll('.column-actions') as NodeListOf<HTMLElement>;
318
+ const maxScrollLeft = targetEl.scrollWidth - targetEl.clientWidth;
319
+ let leftHelper = 0;
320
+ const headerColumnEl = targetEl.querySelector('._refHeaderColumn') as HTMLDivElement | HTMLElement;
321
+ const bodyColumnEl = targetEl.querySelectorAll('._refBodyColumn') as NodeListOf<HTMLDivElement | HTMLElement>;
322
+ for (let i = 0 ; i < freezeColumns ; i++) {
323
+ if (targetEl.scrollLeft > 0) {
324
+ if (i > 0) {
325
+ leftHelper += parseInt(getComputedStyle((headerColumnEl.children[i] as any).previousElementSibling).width);
326
+ (headerColumnEl.children[i] as any).style.left = leftHelper + 'px';
327
+ for (let _el of bodyColumnEl) {
328
+ (_el.children[i] as any).style.left = leftHelper + 'px';
329
+ }
330
+ } else {
331
+ (headerColumnEl.children[i] as any).style.left = '0';
332
+ for (let _el of bodyColumnEl) {
333
+ (_el.children[i] as any).style.left = '0';
334
+ }
335
+ }
336
+
337
+ if (i === freezeColumns - 1) {
338
+ (headerColumnEl.children[i] as any).style.borderRight = '1px solid rgba(0, 0, 0, 0.05)';
339
+ for (let _el of bodyColumnEl) {
340
+ (_el.children[i] as any).style.borderRight = '1px solid rgba(0, 0, 0, 0.05)';
341
+ }
342
+ }
343
+ } else {
344
+ (headerColumnEl.children[i] as any).style = '';
345
+ for (let _el of bodyColumnEl) {
346
+ (_el.children[i] as any).style = '';
347
+ }
348
+ }
349
+ }
350
+
351
+ if (actionEl.length) {
352
+ if (targetEl.scrollLeft < maxScrollLeft) {
353
+ for (let _el of actionEl) {
354
+ _el.style.borderLeft = '1px solid rgba(0, 0, 0, 0.05)';
355
+ }
356
+ } else {
357
+ for (let _el of actionEl) {
358
+ _el.style.borderLeft = '';
359
+ }
360
+ }
361
+ }
362
+ }
363
+ }, [freezeColumns])
364
+
365
+ const initFreezeColumn = useCallback(() => {
366
+ if (!tableEl || !freezeColumns) {
367
+ return false;
368
+ }
369
+ for (let i = 0 ; i < freezeColumns ; i++) {
370
+ (tableEl.querySelector('._refHeaderColumn') as HTMLElement).children[i].classList.add('freezeColumn');
371
+ for (let _el of tableEl.querySelectorAll('._refBodyColumn')) {
372
+ _el.children[i].classList.add('freezeColumn');
373
+ }
374
+ }
375
+
376
+ const scrollTableContainer = document.querySelector('._refColumnContainer') as HTMLElement;
377
+ scrollTableContainer && scrollTableContainer.addEventListener('scroll', CheckFreezeScroll, true);
378
+
379
+ return () => {
380
+ const scrollTableContainer = document.querySelector('._refColumnContainer') as HTMLElement;
381
+ scrollTableContainer && scrollTableContainer.removeEventListener('scroll', CheckFreezeScroll, true);
382
+ }
383
+ }, [CheckFreezeScroll, freezeColumns, tableEl])
384
+
385
+ /////////////////////////////////////////// INIT /////////////////////////////////////////////////////////
386
+
387
+ useEffect(() => {
388
+ if (tableEl) {
389
+ CheckTableWidth()
390
+ if (hiddenColumnKey) {
391
+ initHiddenColumn();
392
+ }
393
+ if (isScroll) {
394
+ initArrowScroll();
395
+ }
396
+ if (freezeColumns) {
397
+ initFreezeColumn();
398
+ }
399
+ }
400
+ }, [hiddenColumnKey, isScroll, freezeColumns, tableEl, initHiddenColumn, initArrowScroll, initFreezeColumn, CheckTableWidth])
401
+
402
+ return(
403
+ <>
404
+ {children}
405
+ </>
406
+ )
407
+ }
408
+
409
+ export default ListTableComponent