@iyulab/data-components 0.1.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.
- package/CHANGELOG.md +4 -0
- package/README.md +89 -0
- package/dist/components/data-grid/UDataGrid.d.ts +2 -0
- package/dist/components/data-grid/UDataGrid.js +172 -0
- package/dist/components/data-grid/UDataGrid.types.d.ts +39 -0
- package/dist/components/data-view/UDataView.component.d.ts +58 -0
- package/dist/components/data-view/UDataView.component.js +301 -0
- package/dist/components/data-view/UDataView.d.ts +8 -0
- package/dist/components/data-view/UDataView.js +5 -0
- package/dist/components/data-view/UDataView.styles.d.ts +1 -0
- package/dist/components/data-view/UDataView.styles.js +283 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/init.d.ts +0 -0
- package/dist/styles/devextreme-overrides.css.js +3 -0
- package/dist/utils/devExtremeCssInjection.d.ts +23 -0
- package/dist/utils/devExtremeCssInjection.js +164 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/shadowDomProtection.d.ts +23 -0
- package/dist/utils/shadowDomProtection.js +82 -0
- package/package.json +75 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @iyulab/data-components
|
|
2
|
+
|
|
3
|
+
데이터 표시 및 입력을 위한 웹 컴포넌트 라이브러리.
|
|
4
|
+
|
|
5
|
+
- **USimpleSheet** — 엑셀 호환 스프레드시트 입력 컴포넌트 (Lit)
|
|
6
|
+
- **UDataView** — Grid / List / Table 뷰 전환 컴포넌트 (Lit)
|
|
7
|
+
- **UDataGrid** — OData 기반 서버 사이드 데이터 그리드 (React + DevExtreme)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @iyulab/data-components
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### USimpleSheet
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<u-simple-sheet
|
|
21
|
+
style="height: 300px;"
|
|
22
|
+
.rows=${20}
|
|
23
|
+
.cols=${8}
|
|
24
|
+
@change=${(e) => console.log(e.detail.data)}
|
|
25
|
+
></u-simple-sheet>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
컬럼 정의 + 초기 데이터:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<u-simple-sheet
|
|
32
|
+
style="height: 400px;"
|
|
33
|
+
.columns=${[
|
|
34
|
+
{ key: 'name', label: '이름', width: 150 },
|
|
35
|
+
{ key: 'email', label: '이메일', width: 220 },
|
|
36
|
+
]}
|
|
37
|
+
.data=${[
|
|
38
|
+
['홍길동', 'hong@example.com'],
|
|
39
|
+
['김철수', 'kim@example.com'],
|
|
40
|
+
]}
|
|
41
|
+
@change=${(e) => console.log(e.target.getDataAsObjects())}
|
|
42
|
+
></u-simple-sheet>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### UDataView
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<u-data-view
|
|
49
|
+
.items=${myData}
|
|
50
|
+
mode="grid"
|
|
51
|
+
@select=${(e) => console.log(e.detail)}
|
|
52
|
+
></u-data-view>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### UDataGrid (React)
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { UDataGrid } from '@iyulab/data-components';
|
|
59
|
+
|
|
60
|
+
<UDataGrid
|
|
61
|
+
dataSourceUrl="https://api.example.com/odata/products"
|
|
62
|
+
keyField="id"
|
|
63
|
+
columns={[
|
|
64
|
+
{ dataField: 'name', caption: '상품명' },
|
|
65
|
+
{ dataField: 'price', caption: '가격', dataType: 'number' },
|
|
66
|
+
]}
|
|
67
|
+
/>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
- [USimpleSheet](./docs/USimpleSheet.md)
|
|
73
|
+
- [UDataView](./docs/UDataView.md)
|
|
74
|
+
- [UDataGrid](./docs/UDataGrid.md)
|
|
75
|
+
|
|
76
|
+
## Theming
|
|
77
|
+
|
|
78
|
+
`@iyulab/components`의 테마 시스템을 사용합니다. 라이트/다크 모드 자동 지원.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { Theme } from '@iyulab/components';
|
|
82
|
+
|
|
83
|
+
Theme.init({ default: 'system' });
|
|
84
|
+
Theme.set('dark'); // 'light' | 'dark' | 'system'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { UDataGridProps } from './UDataGrid.types.js';
|
|
2
|
+
export declare function UDataGrid({ dataSourceUrl, keyField, selectFields, columns, pageSize, showBorders, allowColumnReordering, allowColumnResizing, showGroupPanel, showSearchPanel, showFilterRow, showHeaderFilter, allowExport, exportFileName, isDevelopment, onDataLoaded, onDataLoadError, onExporting, beforeSend }: UDataGridProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import 'devextreme/data/odata/store';
|
|
4
|
+
import DataGrid, { LoadPanel, Selection, FilterRow, HeaderFilter, SearchPanel, Grouping, GroupPanel, ColumnChooser, Paging, Export, Toolbar, Item, Column } from 'devextreme-react/data-grid';
|
|
5
|
+
|
|
6
|
+
function UDataGrid({
|
|
7
|
+
dataSourceUrl,
|
|
8
|
+
keyField,
|
|
9
|
+
selectFields,
|
|
10
|
+
columns,
|
|
11
|
+
pageSize = 25,
|
|
12
|
+
showBorders = true,
|
|
13
|
+
allowColumnReordering = true,
|
|
14
|
+
allowColumnResizing = true,
|
|
15
|
+
showGroupPanel = true,
|
|
16
|
+
showSearchPanel = true,
|
|
17
|
+
showFilterRow = true,
|
|
18
|
+
showHeaderFilter = true,
|
|
19
|
+
allowExport = true,
|
|
20
|
+
exportFileName,
|
|
21
|
+
isDevelopment = false,
|
|
22
|
+
onDataLoaded,
|
|
23
|
+
onDataLoadError,
|
|
24
|
+
onExporting,
|
|
25
|
+
beforeSend
|
|
26
|
+
}) {
|
|
27
|
+
const defaultBeforeSend = (operation, ajaxOptions) => {
|
|
28
|
+
try {
|
|
29
|
+
if (!ajaxOptions) {
|
|
30
|
+
console.warn("ajaxOptions가 undefined입니다");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!ajaxOptions.headers || typeof ajaxOptions.headers !== "object") {
|
|
34
|
+
ajaxOptions.headers = {};
|
|
35
|
+
}
|
|
36
|
+
ajaxOptions.headers["Accept"] = "application/json";
|
|
37
|
+
ajaxOptions.headers["Content-Type"] = "application/json";
|
|
38
|
+
if (isDevelopment) {
|
|
39
|
+
ajaxOptions.crossDomain = true;
|
|
40
|
+
ajaxOptions.headers["Access-Control-Allow-Origin"] = "*";
|
|
41
|
+
}
|
|
42
|
+
console.log(`U DataGrid ${operation} 요청:`, {
|
|
43
|
+
url: ajaxOptions.url,
|
|
44
|
+
headers: ajaxOptions.headers,
|
|
45
|
+
method: ajaxOptions.type || "GET"
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("beforeSend 처리 중 오류:", error);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const defaultOnLoadError = (error) => {
|
|
52
|
+
console.error("U DataGrid 데이터 로딩 오류:", error);
|
|
53
|
+
let errorMessage = "데이터를 불러오는 중 오류가 발생했습니다.";
|
|
54
|
+
try {
|
|
55
|
+
if (error && typeof error === "object") {
|
|
56
|
+
if (error.status === 404) {
|
|
57
|
+
errorMessage = "API 엔드포인트를 찾을 수 없습니다. 서버가 실행중인지 확인해주세요.";
|
|
58
|
+
} else if (error.status === 0) {
|
|
59
|
+
errorMessage = "CORS 오류 또는 네트워크 연결 문제입니다. 서버 설정을 확인해주세요.";
|
|
60
|
+
} else if (error.status >= 500) {
|
|
61
|
+
errorMessage = "서버 내부 오류입니다. 서버 로그를 확인해주세요.";
|
|
62
|
+
} else if (error.message) {
|
|
63
|
+
errorMessage = error.message;
|
|
64
|
+
} else if (typeof error === "string") {
|
|
65
|
+
errorMessage = error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (parseError) {
|
|
69
|
+
console.error("오류 메시지 파싱 중 문제:", parseError);
|
|
70
|
+
}
|
|
71
|
+
alert(`${errorMessage}
|
|
72
|
+
|
|
73
|
+
요청 URL: ${dataSourceUrl}`);
|
|
74
|
+
};
|
|
75
|
+
const dataSource = useMemo(() => {
|
|
76
|
+
return {
|
|
77
|
+
store: {
|
|
78
|
+
type: "odata",
|
|
79
|
+
version: 4,
|
|
80
|
+
url: dataSourceUrl,
|
|
81
|
+
key: keyField,
|
|
82
|
+
beforeSend: beforeSend || defaultBeforeSend,
|
|
83
|
+
onLoading: () => {
|
|
84
|
+
console.log("U DataGrid 데이터 로딩 시작...");
|
|
85
|
+
},
|
|
86
|
+
onLoaded: (data) => {
|
|
87
|
+
console.log("U DataGrid 데이터 로딩 완료:", data);
|
|
88
|
+
onDataLoaded?.(data);
|
|
89
|
+
},
|
|
90
|
+
onLoadError: onDataLoadError || defaultOnLoadError
|
|
91
|
+
},
|
|
92
|
+
select: selectFields
|
|
93
|
+
};
|
|
94
|
+
}, [dataSourceUrl, keyField, selectFields, beforeSend, onDataLoaded, onDataLoadError]);
|
|
95
|
+
const handleExporting = (e) => {
|
|
96
|
+
if (exportFileName) {
|
|
97
|
+
e.fileName = exportFileName;
|
|
98
|
+
} else {
|
|
99
|
+
e.fileName = `DataGrid_Export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
100
|
+
}
|
|
101
|
+
onExporting?.(e);
|
|
102
|
+
};
|
|
103
|
+
return /* @__PURE__ */ jsx("div", { style: {
|
|
104
|
+
width: "100%",
|
|
105
|
+
height: "100%",
|
|
106
|
+
position: "relative",
|
|
107
|
+
contain: "layout style",
|
|
108
|
+
isolation: "isolate",
|
|
109
|
+
overflow: "hidden"
|
|
110
|
+
}, children: /* @__PURE__ */ jsxs(
|
|
111
|
+
DataGrid,
|
|
112
|
+
{
|
|
113
|
+
dataSource,
|
|
114
|
+
showBorders,
|
|
115
|
+
allowColumnReordering,
|
|
116
|
+
allowColumnResizing,
|
|
117
|
+
columnAutoWidth: false,
|
|
118
|
+
showColumnLines: true,
|
|
119
|
+
showRowLines: true,
|
|
120
|
+
rowAlternationEnabled: true,
|
|
121
|
+
hoverStateEnabled: true,
|
|
122
|
+
height: "100%",
|
|
123
|
+
width: "100%",
|
|
124
|
+
wordWrapEnabled: true,
|
|
125
|
+
columnResizingMode: "widget",
|
|
126
|
+
remoteOperations: true,
|
|
127
|
+
onExporting: handleExporting,
|
|
128
|
+
children: [
|
|
129
|
+
/* @__PURE__ */ jsx(LoadPanel, { enabled: true }),
|
|
130
|
+
/* @__PURE__ */ jsx(Selection, { mode: "multiple" }),
|
|
131
|
+
showFilterRow && /* @__PURE__ */ jsx(FilterRow, { visible: true }),
|
|
132
|
+
showHeaderFilter && /* @__PURE__ */ jsx(HeaderFilter, { visible: true }),
|
|
133
|
+
showSearchPanel && /* @__PURE__ */ jsx(SearchPanel, { visible: true, placeholder: "검색..." }),
|
|
134
|
+
/* @__PURE__ */ jsx(Grouping, { autoExpandAll: false }),
|
|
135
|
+
showGroupPanel && /* @__PURE__ */ jsx(GroupPanel, { visible: true }),
|
|
136
|
+
/* @__PURE__ */ jsx(ColumnChooser, { enabled: true }),
|
|
137
|
+
/* @__PURE__ */ jsx(Paging, { defaultPageSize: pageSize }),
|
|
138
|
+
allowExport && /* @__PURE__ */ jsx(Export, { enabled: true }),
|
|
139
|
+
/* @__PURE__ */ jsxs(Toolbar, { children: [
|
|
140
|
+
showGroupPanel && /* @__PURE__ */ jsx(Item, { name: "groupPanel" }),
|
|
141
|
+
allowExport && /* @__PURE__ */ jsx(Item, { name: "exportButton" }),
|
|
142
|
+
/* @__PURE__ */ jsx(Item, { name: "columnChooserButton" }),
|
|
143
|
+
showSearchPanel && /* @__PURE__ */ jsx(Item, { name: "searchPanel" })
|
|
144
|
+
] }),
|
|
145
|
+
columns.map((col, index) => /* @__PURE__ */ jsx(
|
|
146
|
+
Column,
|
|
147
|
+
{
|
|
148
|
+
dataField: col.dataField,
|
|
149
|
+
caption: col.caption,
|
|
150
|
+
width: col.width,
|
|
151
|
+
minWidth: col.minWidth,
|
|
152
|
+
dataType: col.dataType,
|
|
153
|
+
format: col.format,
|
|
154
|
+
alignment: col.alignment,
|
|
155
|
+
fixed: col.fixed,
|
|
156
|
+
fixedPosition: col.fixedPosition,
|
|
157
|
+
allowHiding: col.allowHiding,
|
|
158
|
+
allowSorting: col.allowSorting,
|
|
159
|
+
allowFiltering: col.allowFiltering,
|
|
160
|
+
allowGrouping: col.allowGrouping,
|
|
161
|
+
sortOrder: col.sortOrder,
|
|
162
|
+
cellRender: col.cellRender,
|
|
163
|
+
visible: col.visible
|
|
164
|
+
},
|
|
165
|
+
col.dataField || `column-${index}`
|
|
166
|
+
))
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { UDataGrid };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface UDataGridColumn {
|
|
2
|
+
dataField?: string;
|
|
3
|
+
caption: string;
|
|
4
|
+
width?: number;
|
|
5
|
+
minWidth?: number;
|
|
6
|
+
dataType?: 'string' | 'number' | 'date' | 'datetime' | 'boolean';
|
|
7
|
+
format?: string;
|
|
8
|
+
alignment?: 'left' | 'center' | 'right';
|
|
9
|
+
fixed?: boolean;
|
|
10
|
+
fixedPosition?: 'left' | 'right';
|
|
11
|
+
allowHiding?: boolean;
|
|
12
|
+
allowSorting?: boolean;
|
|
13
|
+
allowFiltering?: boolean;
|
|
14
|
+
allowGrouping?: boolean;
|
|
15
|
+
sortOrder?: 'asc' | 'desc';
|
|
16
|
+
cellRender?: (cellData: any) => React.ReactNode;
|
|
17
|
+
visible?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface UDataGridProps {
|
|
20
|
+
dataSourceUrl: string;
|
|
21
|
+
keyField: string;
|
|
22
|
+
selectFields?: string[];
|
|
23
|
+
columns: UDataGridColumn[];
|
|
24
|
+
pageSize?: number;
|
|
25
|
+
showBorders?: boolean;
|
|
26
|
+
allowColumnReordering?: boolean;
|
|
27
|
+
allowColumnResizing?: boolean;
|
|
28
|
+
showGroupPanel?: boolean;
|
|
29
|
+
showSearchPanel?: boolean;
|
|
30
|
+
showFilterRow?: boolean;
|
|
31
|
+
showHeaderFilter?: boolean;
|
|
32
|
+
allowExport?: boolean;
|
|
33
|
+
exportFileName?: string;
|
|
34
|
+
isDevelopment?: boolean;
|
|
35
|
+
onDataLoaded?: (data: any) => void;
|
|
36
|
+
onDataLoadError?: (error: any) => void;
|
|
37
|
+
onExporting?: (e: any) => void;
|
|
38
|
+
beforeSend?: (operation: string, ajaxOptions: any) => void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseElement } from '@iyulab/components/dist/components/BaseElement.js';
|
|
2
|
+
export interface DataViewColumnDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
display?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Data View Component
|
|
8
|
+
*/
|
|
9
|
+
export declare class UDataView extends BaseElement {
|
|
10
|
+
static styles: import('lit').CSSResultGroup[];
|
|
11
|
+
static dependencies: Record<string, typeof BaseElement>;
|
|
12
|
+
/** 표시할 데이터 배열을 지정합니다. */
|
|
13
|
+
data: any[];
|
|
14
|
+
/** 데이터 뷰의 레이아웃을 지정합니다. ('list', 'grid' 또는 'table') */
|
|
15
|
+
layout: "list" | "grid" | "table";
|
|
16
|
+
/** 각 아이템을 렌더링하는 함수를 지정합니다. */
|
|
17
|
+
renderItem?: (item: any) => HTMLElement;
|
|
18
|
+
/** 각 아이템의 이미지를 렌더링하는 함수를 지정합니다. */
|
|
19
|
+
renderImage?: (item: any) => HTMLElement;
|
|
20
|
+
/** 각 아이템의 필드를 렌더링하는 함수를 지정합니다. */
|
|
21
|
+
renderFields?: (item: any) => HTMLElement;
|
|
22
|
+
/** 페이지당 표시할 아이템 수를 지정합니다. */
|
|
23
|
+
itemsPerPage?: number;
|
|
24
|
+
/** 아이템의 이미지 필드 이름을 지정합니다. */
|
|
25
|
+
imageField: string;
|
|
26
|
+
/** 아이템 간 마진을 지정합니다. */
|
|
27
|
+
itemMargin: string;
|
|
28
|
+
/** 그리드 레이아웃에서 아이템의 최소 너비를 지정합니다. */
|
|
29
|
+
minItemWidth: string;
|
|
30
|
+
/** 표시할 열 정보를 지정합니다. 지정하지 않으면 이미지를 제외한 모든 필드가 표시됩니다. */
|
|
31
|
+
columns?: DataViewColumnDefinition[];
|
|
32
|
+
/** 아이템 선택 시 호출되는 콜백 함수를 지정합니다. */
|
|
33
|
+
onItemSelect?: (item: any) => void;
|
|
34
|
+
/** 아이템 더블 클릭 시 호출되는 콜백 함수를 지정합니다. */
|
|
35
|
+
onItemDoubleClick?: (item: any) => void;
|
|
36
|
+
private selectedItem;
|
|
37
|
+
private currentLayout;
|
|
38
|
+
private loading;
|
|
39
|
+
private imageLoadErrors;
|
|
40
|
+
private randomColors;
|
|
41
|
+
connectedCallback(): void;
|
|
42
|
+
private handleImageError;
|
|
43
|
+
private renderTable;
|
|
44
|
+
private getDefaultColumns;
|
|
45
|
+
private getDisplayName;
|
|
46
|
+
private toPascalCase;
|
|
47
|
+
private formatValue;
|
|
48
|
+
private defaultRenderFields;
|
|
49
|
+
private getRandomColor;
|
|
50
|
+
private defaultRenderImage;
|
|
51
|
+
private getContrastColor;
|
|
52
|
+
private hexToRgb;
|
|
53
|
+
protected render(): import('lit').TemplateResult<1>;
|
|
54
|
+
private renderItems;
|
|
55
|
+
protected setLayout(newLayout: "list" | "grid" | "table"): void;
|
|
56
|
+
private handleItemClick;
|
|
57
|
+
private handleItemDoubleClick;
|
|
58
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { property, state } from 'lit/decorators.js';
|
|
3
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
|
4
|
+
import { BaseElement } from '@iyulab/components/dist/components/BaseElement.js';
|
|
5
|
+
import { UIconButton } from '@iyulab/components/dist/components/icon-button/UIconButton.component.js';
|
|
6
|
+
import { USkeleton } from '@iyulab/components/dist/components/skeleton/USkeleton.component.js';
|
|
7
|
+
import { styles } from './UDataView.styles.js';
|
|
8
|
+
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
11
|
+
var result = void 0 ;
|
|
12
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
13
|
+
if (decorator = decorators[i])
|
|
14
|
+
result = (decorator(target, key, result) ) || result;
|
|
15
|
+
if (result) __defProp(target, key, result);
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
const IMAGE_SIZE_LIST = 128;
|
|
19
|
+
const IMAGE_SIZE_TABLE = 64;
|
|
20
|
+
const IMAGE_SIZE_GRID = 128;
|
|
21
|
+
class UDataView extends BaseElement {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
this.data = [];
|
|
25
|
+
this.layout = "grid";
|
|
26
|
+
this.imageField = "image";
|
|
27
|
+
this.itemMargin = "1rem";
|
|
28
|
+
this.minItemWidth = "250px";
|
|
29
|
+
this.selectedItem = null;
|
|
30
|
+
this.currentLayout = "grid";
|
|
31
|
+
this.loading = true;
|
|
32
|
+
this.imageLoadErrors = /* @__PURE__ */ new Set();
|
|
33
|
+
this.randomColors = /* @__PURE__ */ new Map();
|
|
34
|
+
}
|
|
35
|
+
static {
|
|
36
|
+
this.styles = [super.styles, styles];
|
|
37
|
+
}
|
|
38
|
+
static {
|
|
39
|
+
this.dependencies = {
|
|
40
|
+
"u-icon-button": UIconButton,
|
|
41
|
+
"u-skeleton": USkeleton
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
connectedCallback() {
|
|
45
|
+
super.connectedCallback();
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
this.loading = false;
|
|
48
|
+
this.requestUpdate();
|
|
49
|
+
}, 2e3);
|
|
50
|
+
}
|
|
51
|
+
handleImageError(item) {
|
|
52
|
+
this.imageLoadErrors.add(item[this.imageField]);
|
|
53
|
+
this.requestUpdate();
|
|
54
|
+
}
|
|
55
|
+
renderTable() {
|
|
56
|
+
const columns = this.columns || this.getDefaultColumns();
|
|
57
|
+
return html`
|
|
58
|
+
<table class="default-table">
|
|
59
|
+
<thead>
|
|
60
|
+
<tr>
|
|
61
|
+
<th>Image</th>
|
|
62
|
+
${columns.map((column) => html`<th>${this.getDisplayName(column)}</th>`)}
|
|
63
|
+
</tr>
|
|
64
|
+
</thead>
|
|
65
|
+
<tbody>
|
|
66
|
+
${this.loading ? Array(5).fill(0).map(() => html`
|
|
67
|
+
<tr>
|
|
68
|
+
<td><u-skeleton effect="shimmer" width="64px" height="64px"></u-skeleton></td>
|
|
69
|
+
${columns.map(() => html`
|
|
70
|
+
<td><u-skeleton effect="shimmer" width="80%" height="1em"></u-skeleton></td>
|
|
71
|
+
`)}
|
|
72
|
+
</tr>
|
|
73
|
+
`) : this.data?.map((item) => html`
|
|
74
|
+
<tr class="default-item ${this.selectedItem === item ? "selected" : ""}"
|
|
75
|
+
@click=${() => this.handleItemClick(item)}
|
|
76
|
+
@dblclick=${() => this.handleItemDoubleClick(item)}>
|
|
77
|
+
<td class="image-cell">
|
|
78
|
+
${this.defaultRenderImage(item, "table")}
|
|
79
|
+
</td>
|
|
80
|
+
${columns.map((column) => html`<td>${this.formatValue(item[column.name])}</td>`)}
|
|
81
|
+
</tr>
|
|
82
|
+
`)}
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
getDefaultColumns() {
|
|
88
|
+
if (this.data && this.data.length > 0) {
|
|
89
|
+
return Object.keys(this.data[0]).filter((key) => key !== this.imageField).map((key) => ({ name: key }));
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
getDisplayName(column) {
|
|
94
|
+
return column.display || this.toPascalCase(column.name);
|
|
95
|
+
}
|
|
96
|
+
toPascalCase(str) {
|
|
97
|
+
const words = str.split(/(?=[A-Z])|\s+|[-_]+/);
|
|
98
|
+
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
99
|
+
}
|
|
100
|
+
formatValue(value) {
|
|
101
|
+
if (value instanceof Date) {
|
|
102
|
+
const pad = (num) => num.toString().padStart(2, "0");
|
|
103
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}`;
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
defaultRenderFields(item) {
|
|
108
|
+
const columns = this.columns || this.getDefaultColumns();
|
|
109
|
+
return html`
|
|
110
|
+
<div class="default-fields">
|
|
111
|
+
${columns.map((column) => html`
|
|
112
|
+
<div class="field">
|
|
113
|
+
${this.loading ? html`
|
|
114
|
+
<u-skeleton effect="shimmer" width="30%" height="1em" class="field-title"></u-skeleton>
|
|
115
|
+
<u-skeleton effect="shimmer" width="60%" height="1em" class="field-value"></u-skeleton>
|
|
116
|
+
` : html`
|
|
117
|
+
<span class="field-title">${this.getDisplayName(column)}:</span>
|
|
118
|
+
<span class="field-value">${this.formatValue(item[column.name])}</span>
|
|
119
|
+
`}
|
|
120
|
+
</div>
|
|
121
|
+
`)}
|
|
122
|
+
</div>
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
getRandomColor(key) {
|
|
126
|
+
if (!this.randomColors.has(key)) {
|
|
127
|
+
const hue = Math.floor(Math.random() * 360);
|
|
128
|
+
const saturation = 70 + Math.floor(Math.random() * 30);
|
|
129
|
+
const lightness = 60 + Math.floor(Math.random() * 20);
|
|
130
|
+
this.randomColors.set(key, `hsl(${hue}, ${saturation}%, ${lightness}%)`);
|
|
131
|
+
}
|
|
132
|
+
return this.randomColors.get(key);
|
|
133
|
+
}
|
|
134
|
+
defaultRenderImage(item, viewType = "grid") {
|
|
135
|
+
if (this.loading) {
|
|
136
|
+
const skeletonSize = viewType === "table" ? `${IMAGE_SIZE_TABLE}px` : viewType === "list" ? `${IMAGE_SIZE_LIST}px` : `${IMAGE_SIZE_GRID}px`;
|
|
137
|
+
return html`<u-skeleton effect="shimmer" width=${skeletonSize} height=${skeletonSize} style="margin-bottom: 1em;"></u-skeleton>`;
|
|
138
|
+
}
|
|
139
|
+
const imageSrc = item[this.imageField];
|
|
140
|
+
const imageClass = `default-image ${viewType}-image`;
|
|
141
|
+
if (imageSrc && !this.imageLoadErrors.has(imageSrc)) {
|
|
142
|
+
return html`
|
|
143
|
+
<img
|
|
144
|
+
src=${imageSrc}
|
|
145
|
+
alt="Item image"
|
|
146
|
+
class=${imageClass}
|
|
147
|
+
@error=${() => this.handleImageError(item)}
|
|
148
|
+
style=${viewType !== "grid" ? "" : "margin-bottom: 1em;"}
|
|
149
|
+
/>
|
|
150
|
+
`;
|
|
151
|
+
} else {
|
|
152
|
+
const backgroundColor = this.getRandomColor(item.id || JSON.stringify(item));
|
|
153
|
+
const textColor = this.getContrastColor(backgroundColor);
|
|
154
|
+
const styles2 = {
|
|
155
|
+
backgroundColor,
|
|
156
|
+
color: textColor,
|
|
157
|
+
display: "flex",
|
|
158
|
+
alignItems: "center",
|
|
159
|
+
justifyContent: "center",
|
|
160
|
+
fontSize: viewType === "table" ? "1.5rem" : "2rem",
|
|
161
|
+
fontWeight: "bold",
|
|
162
|
+
width: viewType === "table" ? `${IMAGE_SIZE_TABLE}px` : viewType === "list" ? `${IMAGE_SIZE_LIST}px` : "100%",
|
|
163
|
+
height: viewType === "table" ? `${IMAGE_SIZE_TABLE}px` : viewType === "list" ? `${IMAGE_SIZE_LIST}px` : `${IMAGE_SIZE_GRID}px`
|
|
164
|
+
};
|
|
165
|
+
return html`
|
|
166
|
+
<div class=${`${imageClass} placeholder-image`} style=${styleMap(styles2)}>
|
|
167
|
+
${item.name ? item.name.charAt(0).toUpperCase() : "N/A"}
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
getContrastColor(backgroundColor) {
|
|
173
|
+
const rgb = this.hexToRgb(backgroundColor);
|
|
174
|
+
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1e3;
|
|
175
|
+
return brightness > 128 ? "#000000" : "#FFFFFF";
|
|
176
|
+
}
|
|
177
|
+
hexToRgb(hex) {
|
|
178
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
179
|
+
return result ? {
|
|
180
|
+
r: parseInt(result[1], 16),
|
|
181
|
+
g: parseInt(result[2], 16),
|
|
182
|
+
b: parseInt(result[3], 16)
|
|
183
|
+
} : { r: 0, g: 0, b: 0 };
|
|
184
|
+
}
|
|
185
|
+
render() {
|
|
186
|
+
return html`
|
|
187
|
+
<div class="u-data-view-container">
|
|
188
|
+
<div class="layout-selector">
|
|
189
|
+
<u-icon-button lib="internal" name="grid"
|
|
190
|
+
@click=${() => this.setLayout("grid")}
|
|
191
|
+
></u-icon-button>
|
|
192
|
+
<u-icon-button lib="internal" name="list-ul"
|
|
193
|
+
@click=${() => this.setLayout("list")}
|
|
194
|
+
></u-icon-button>
|
|
195
|
+
<u-icon-button lib="internal" name="table"
|
|
196
|
+
@click=${() => this.setLayout("table")}
|
|
197
|
+
></u-icon-button>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="u-data-view ${this.currentLayout}" style="--item-margin: ${this.itemMargin}; --min-item-width: ${this.minItemWidth};">
|
|
200
|
+
${this.currentLayout === "table" ? this.renderTable() : this.renderItems()}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
renderItems() {
|
|
206
|
+
if (this.loading) {
|
|
207
|
+
return Array(5).fill(0).map(() => html`
|
|
208
|
+
<div class="default-item" style="padding: 1em; border: 1px solid #eee; border-radius: 4px;">
|
|
209
|
+
<u-skeleton effect="shimmer" width="100%" height="${IMAGE_SIZE_GRID}px" style="margin-bottom: 1em;"></u-skeleton>
|
|
210
|
+
<div class="default-fields">
|
|
211
|
+
${Array(3).fill(0).map(() => html`
|
|
212
|
+
<div class="field" style="margin-bottom: 0.5em;">
|
|
213
|
+
<u-skeleton effect="shimmer" width="30%" height="1em" style="margin-right: 0.5em;"></u-skeleton>
|
|
214
|
+
<u-skeleton effect="shimmer" width="60%" height="1em"></u-skeleton>
|
|
215
|
+
</div>
|
|
216
|
+
`)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
return this.data?.map(
|
|
222
|
+
(item) => this.renderItem ? this.renderItem(item) : html`
|
|
223
|
+
<div class="default-item ${this.selectedItem === item ? "selected" : ""}"
|
|
224
|
+
@click=${() => this.handleItemClick(item)}
|
|
225
|
+
@dblclick=${() => this.handleItemDoubleClick(item)}>
|
|
226
|
+
${this.renderImage ? this.renderImage(item) : this.defaultRenderImage(item, this.currentLayout)}
|
|
227
|
+
${this.renderFields ? this.renderFields(item) : this.defaultRenderFields(item)}
|
|
228
|
+
</div>
|
|
229
|
+
`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
setLayout(newLayout) {
|
|
233
|
+
this.currentLayout = newLayout;
|
|
234
|
+
this.requestUpdate();
|
|
235
|
+
}
|
|
236
|
+
handleItemClick(item) {
|
|
237
|
+
this.selectedItem = item;
|
|
238
|
+
if (this.onItemSelect) {
|
|
239
|
+
this.onItemSelect(item);
|
|
240
|
+
}
|
|
241
|
+
this.requestUpdate();
|
|
242
|
+
}
|
|
243
|
+
handleItemDoubleClick(item) {
|
|
244
|
+
if (this.onItemDoubleClick) {
|
|
245
|
+
this.onItemDoubleClick(item);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
__decorateClass([
|
|
250
|
+
property({ type: Array })
|
|
251
|
+
], UDataView.prototype, "data");
|
|
252
|
+
__decorateClass([
|
|
253
|
+
property({ type: String })
|
|
254
|
+
], UDataView.prototype, "layout");
|
|
255
|
+
__decorateClass([
|
|
256
|
+
property({ type: Object })
|
|
257
|
+
], UDataView.prototype, "renderItem");
|
|
258
|
+
__decorateClass([
|
|
259
|
+
property({ type: Object })
|
|
260
|
+
], UDataView.prototype, "renderImage");
|
|
261
|
+
__decorateClass([
|
|
262
|
+
property({ type: Object })
|
|
263
|
+
], UDataView.prototype, "renderFields");
|
|
264
|
+
__decorateClass([
|
|
265
|
+
property({ type: Number })
|
|
266
|
+
], UDataView.prototype, "itemsPerPage");
|
|
267
|
+
__decorateClass([
|
|
268
|
+
property({ type: String })
|
|
269
|
+
], UDataView.prototype, "imageField");
|
|
270
|
+
__decorateClass([
|
|
271
|
+
property({ type: String })
|
|
272
|
+
], UDataView.prototype, "itemMargin");
|
|
273
|
+
__decorateClass([
|
|
274
|
+
property({ type: String })
|
|
275
|
+
], UDataView.prototype, "minItemWidth");
|
|
276
|
+
__decorateClass([
|
|
277
|
+
property({ type: Array })
|
|
278
|
+
], UDataView.prototype, "columns");
|
|
279
|
+
__decorateClass([
|
|
280
|
+
property({ type: Object })
|
|
281
|
+
], UDataView.prototype, "onItemSelect");
|
|
282
|
+
__decorateClass([
|
|
283
|
+
property({ type: Object })
|
|
284
|
+
], UDataView.prototype, "onItemDoubleClick");
|
|
285
|
+
__decorateClass([
|
|
286
|
+
state()
|
|
287
|
+
], UDataView.prototype, "selectedItem");
|
|
288
|
+
__decorateClass([
|
|
289
|
+
state()
|
|
290
|
+
], UDataView.prototype, "currentLayout");
|
|
291
|
+
__decorateClass([
|
|
292
|
+
state()
|
|
293
|
+
], UDataView.prototype, "loading");
|
|
294
|
+
__decorateClass([
|
|
295
|
+
state()
|
|
296
|
+
], UDataView.prototype, "imageLoadErrors");
|
|
297
|
+
__decorateClass([
|
|
298
|
+
state()
|
|
299
|
+
], UDataView.prototype, "randomColors");
|
|
300
|
+
|
|
301
|
+
export { UDataView };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import('lit').CSSResult;
|