jrs-react 1.0.3 → 1.0.4

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,18 +1,21 @@
1
1
  {
2
2
  "name": "jrs-react",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "module": "build/index.es.js",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
- "build": "rollup -c"
9
+ "build": "rollup -c",
10
+ "storybook": "storybook dev -p 6006",
11
+ "build-storybook": "storybook build"
10
12
  },
11
13
  "author": "",
12
14
  "license": "MIT",
13
15
  "dependencies": {
14
16
  "@rollup/plugin-commonjs": "^28.0.3",
15
17
  "@rollup/plugin-json": "^6.1.0",
18
+ "antd": "^5.24.4",
16
19
  "axios": "^1.8.3",
17
20
  "react": "^19.0.0",
18
21
  "react-dom": "^19.0.0",
@@ -21,12 +24,26 @@
21
24
  "devDependencies": {
22
25
  "@babel/core": "^7.26.10",
23
26
  "@babel/preset-react": "^7.26.3",
27
+ "@chromatic-com/storybook": "^3.2.6",
24
28
  "@rollup/plugin-node-resolve": "^16.0.1",
29
+ "@storybook/addon-essentials": "^8.6.6",
30
+ "@storybook/addon-onboarding": "^8.6.6",
31
+ "@storybook/blocks": "^8.6.6",
32
+ "@storybook/experimental-addon-test": "^8.6.6",
33
+ "@storybook/react": "^8.6.6",
34
+ "@storybook/react-vite": "^8.6.6",
35
+ "@storybook/test": "^8.6.6",
36
+ "@vitest/browser": "^3.0.8",
37
+ "@vitest/coverage-v8": "^3.0.8",
25
38
  "babel-loader": "^10.0.0",
39
+ "playwright": "^1.51.0",
40
+ "prop-types": "^15.8.1",
26
41
  "rollup": "^2.79.2",
27
42
  "rollup-plugin-babel": "^4.4.0",
28
43
  "rollup-plugin-peer-deps-external": "^2.2.4",
29
44
  "rollup-plugin-postcss": "^4.0.2",
30
- "rollup-plugin-terser": "^7.0.2"
45
+ "rollup-plugin-terser": "^7.0.2",
46
+ "storybook": "^8.6.6",
47
+ "vitest": "^3.0.8"
31
48
  }
32
49
  }
@@ -0,0 +1,44 @@
1
+ import babel from 'rollup-plugin-babel';
2
+ import resolve from '@rollup/plugin-node-resolve';
3
+ import external from 'rollup-plugin-peer-deps-external';
4
+ import { terser } from 'rollup-plugin-terser';
5
+ import postcss from 'rollup-plugin-postcss';
6
+ import commonjs from '@rollup/plugin-commonjs';
7
+ import nodeResolve from '@rollup/plugin-node-resolve';
8
+ import json from '@rollup/plugin-json';
9
+
10
+ export default [
11
+ {
12
+ input: './src/index.js',
13
+ output: [
14
+ {
15
+ file: 'build/index.js',
16
+ format: 'cjs',
17
+ },
18
+ {
19
+ file: 'build/index.es.js',
20
+ format: 'es',
21
+ exports: 'named',
22
+ }
23
+ ],
24
+ plugins: [
25
+ postcss({
26
+ plugins: [],
27
+ minimize: true,
28
+ }),
29
+ nodeResolve({
30
+ extensions: ['.js', '.jsx']
31
+ }),
32
+ babel({
33
+ exclude: 'node_modules/**',
34
+ presets: ['@babel/preset-react'],
35
+ extensions: ['.js', '.jsx']
36
+ }),
37
+ commonjs(),
38
+ external(),
39
+ resolve(),
40
+ terser(),
41
+ json()
42
+ ]
43
+ }
44
+ ];
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import JRSubmit from "../JRSubmit";
3
+ import { StyledJRFrame } from "./StyledJRFrame";
4
+
5
+ const FreeType=({tag:Tag,config,me,className})=>{
6
+ if(typeof config==='function'){
7
+ let style
8
+ const setStyle=function(_style){
9
+ style=_style
10
+ }
11
+ const content=config.bind(me)({setStyle})
12
+ return <Tag className={className} style={style}>{content}</Tag>
13
+ }
14
+ }
15
+
16
+ export default class JRFrame extends JRSubmit {
17
+
18
+ renderer(){
19
+ return <StyledJRFrame style={this.props.style} className={`${this.props.className} jr-frame`} >
20
+ <FreeType tag='div' config={this.props.start} me={this} className={'start'}/>
21
+ <main>
22
+ <FreeType tag='header' config={this.props.top} me={this}/>
23
+ <main>
24
+ <FreeType tag='div' config={this.props.left} me={this} className={'left'}/>
25
+ {this.renderMe()}
26
+ <FreeType tag='div' config={this.props.right} me={this} className={'right'}/>
27
+ </main>
28
+ <FreeType tag='footer' config={this.props.bottom} me={this}/>
29
+ </main>
30
+ <FreeType tag='div' config={this.props.end} me={this} className={'end'}/>
31
+ </StyledJRFrame>
32
+ }
33
+
34
+ renderMe(){
35
+ return <div style={{flex:1}}>Render me</div>
36
+ }
37
+
38
+ }
39
+
@@ -0,0 +1,37 @@
1
+ import styled from "styled-components";
2
+
3
+ export const StyledJRFrame=styled.div`
4
+ display:flex;
5
+ flex:1;
6
+ overflow:hidden;
7
+
8
+ > *{
9
+ Xborder:1px solid gray;
10
+ }
11
+ >main{
12
+ display:flex;
13
+ flex:1;
14
+ overflow:overlay;
15
+ flex-direction: column;
16
+ > *{
17
+ XXborder:1px solid gray;
18
+ min-height:30px;
19
+ }
20
+
21
+ >header{
22
+ XXborder:1px solid red;
23
+ XXoverflow: overlay;
24
+ }
25
+ >main{
26
+ display:flex;
27
+ flex:1;
28
+ overflow:hidden;
29
+ }
30
+ >footer{
31
+ XXborder:1px solid blue;
32
+ overflow: overlay;
33
+ }
34
+
35
+ }
36
+
37
+ `
@@ -1,7 +1,236 @@
1
- import JRSubmit from "../JRSubmit";
1
+ import { po } from "../JRUtils";
2
+ import { TFoot, THead } from "./THead";
3
+ import { TBodies } from "./TBodies";
4
+ import { StyledJRTable } from "./StyledJRTable";
5
+ import JRFrame from "../JRFrame/JRFrame";
6
+ import { Button, Checkbox } from "antd";
7
+ import React from "react";
2
8
 
3
- export default class JRTable extends JRSubmit {
4
- render(){
5
- return 'JRTable'
9
+
10
+ const getMapObject=(map,names)=>{
11
+ const name=names.shift(names)
12
+ if(names.length){
13
+ return getMapObject(map?.[name],names)
14
+ }else{
15
+ return map
16
+ }
17
+ }
18
+ const setMapObject=(map,names,value)=>{
19
+ const name=names.shift(names)
20
+ if(names?.length){
21
+ if(typeof map[name] != 'object'){
22
+ map[name]={}
23
+ }
24
+ setMapObject(map[name],names,value)
25
+ }else{
26
+ map[name]=value
27
+ }
28
+ }
29
+
30
+ export default class JRTable extends JRFrame {
31
+ constructor(props) {
32
+ super(props)
33
+ this.colGroupRef = React.createRef()
34
+ }
35
+
36
+ UNSAFE_componentWillMount(){
37
+ this.setColumns(this.props.columns)
38
+ }
39
+
40
+ //------------------------------------------------------------------------------------
41
+ getChecked(){
42
+ return [1,2,3,4]
43
+ }
44
+ checkableColumn(props){
45
+ return {//方法1
46
+ render({value,onChange}){
47
+ return <Checkbox checked={value} onChange={(e)=>{onChange(e.target.checked)}}/>
48
+ }
49
+ ,align:'center'
50
+ ,name:'checked'
51
+ ,...props
52
+ }
53
+ // return {//方法2
54
+ // type:Checkbox
55
+ // ,funcProps({value}){
56
+ // po('fffffffffffff',value)
57
+ // return {
58
+ // align:'center'
59
+ // ,checked:value
60
+ // }
61
+ // }
62
+ // // ,label:'A'
63
+ // ,onChange(e,{value,onChange,me}){
64
+ // onChange(e.target.checked)
65
+ // }
66
+ // ,...props
67
+ // }
68
+ }
69
+ deletableColumn({name='deletable',sendValue,sendName,valueName,...props}){
70
+ return {
71
+ render({value,onChange}){
72
+ return <Checkbox checked={value} onChange={(e)=>{onChange(e.target.checked)}}/>
73
+ }
74
+ ,align:'center'
75
+ ,name
76
+ ,label(){
77
+ return <Button
78
+ onClick={()=>{
79
+ const value=this.props.delete.value
80
+ ?? this.getDataSource()?.filter(record=>record[name]).map(record=>sendValue?record[sendValue]:record)
81
+ const callback=this.props.delete.callback
82
+ ?? function(a,b,c){
83
+ this.reload()
84
+ }
85
+ this.delete({
86
+ value:sendName?{[sendName]:value}:value
87
+ ,callback
88
+ ,...props
89
+ })
90
+ }}
91
+ >刪除</Button>
92
+ }
93
+ ,...props
94
+ }
95
+ }
96
+
97
+ initColumn(column,level,result,leafColumns,names,lastColSpan){
98
+ const isBranch=column.type===undefined&&column?.columns&&column?.columns.length
99
+ const _names=column.name
100
+ ?[...names,column.name]
101
+ :names
102
+ if(isBranch){
103
+ if(column.label!==null)result[level].push(column)
104
+ const c=this.initColumns(column.columns,level+(column.label!==null?1:0)+(column.rowSpan!=null?column.rowSpan-1:0),result,leafColumns,_names,lastColSpan)
105
+ column.colSpan=c.colSpan
106
+ column.columnNo=lastColSpan
107
+ return {
108
+ colSpan:column.colSpan
109
+ }
110
+ }else{
111
+ column.columnNo=lastColSpan
112
+ result[level].push({
113
+ ...column
114
+ ,isLeaf:true
115
+ })
116
+ leafColumns.push(column)
117
+ column.names=_names
118
+
119
+
120
+ if(_names?.length>1){
121
+ column.setValue=function(record,value){
122
+ try{
123
+ getMapObject(record,[..._names])[column.name]=value
124
+ }catch{
125
+ setMapObject(record,[..._names],value)
126
+ }
127
+ }
128
+ column.getValue=function(record){
129
+ return _names.reduce((acc,name)=>{
130
+ return acc?.[name]
131
+ },record)
132
+ }
133
+ }else{
134
+ column.setValue=function(record,value){
135
+ record[column.name]=value
136
+ }
137
+ column.getValue=function(record){
138
+ return record[column.name]
139
+ }
140
+ }
141
+ return {
142
+ colSpan:1
143
+ }
144
+ }
145
+ }
146
+ initColumns(columns,level,result,leafColumns,names,lastColSpan){
147
+ for(let i=result.length;i<=level;i++){
148
+
149
+ if(!result[i]){
150
+ result.push([])
151
+ }
152
+ }
153
+ let _lastColSpan=lastColSpan
154
+ const childrenLength=columns?.reduce((acc,column)=>{
155
+ const c=this.initColumn(column,level,result,leafColumns,names,_lastColSpan)
156
+ acc.colSpan+=c.colSpan
157
+ _lastColSpan+=c.colSpan
158
+ return acc
159
+ },{
160
+ colSpan:0
161
+ })
162
+ return childrenLength
163
+ }
164
+
165
+ setColumns([..._columns]){
166
+ if(this.props.checkable)
167
+ _columns.unshift(this.checkableColumn(this.props.checkable))
168
+ if(this.props.deletable){
169
+ _columns.unshift(this.deletableColumn(this.props.deletable))
170
+ }
171
+ const columns=[]
172
+ const leafColumns=[]
173
+ const initColumns=this.initColumns(_columns,0,columns,leafColumns,[],0)
174
+ // po('initColumns',initColumns)
175
+ this.setState({columns,leafColumns})
176
+ }
177
+ //------------------------------------------------------------------------------------
178
+ setDataSource(dataSource){
179
+
180
+ }
181
+ getDataSource(){
182
+ return this.props.dataSourceName
183
+ ? this.getValue()?.[this.props.dataSourceName]
184
+ : this.getValue()
185
+ }
186
+
187
+ add(record,index=this.getDataSource()?.length??0,group){
188
+ //group 還沒有考慮到
189
+
190
+ if(this.getDataSource()){
191
+ if(group===undefined){
192
+ this.getDataSource().splice(index,0,record)//.push(record)
193
+ }else{
194
+ this.getDataSource()[group].splice(index,0,record)//.push(record)
195
+ }
196
+ this.setValue(this.getValue())
197
+ }else{
198
+ //未完成. 沒有資料的時候, 要考慮有或沒有dataSourceName的不同處理
199
+ this.setValue({[this.props.dataSourceName]:[record]})
200
+ }
201
+ }
202
+
203
+ noData(){
204
+ const data=this.getDataSource()
205
+ return data==null || data.length==0
206
+ }
207
+ //------------------------------------------------------------------------------------
208
+ renderMe(){
209
+
210
+ return<StyledJRTable
211
+ className={`${this.props.className??''} jr-table ${this.props.onRowClick?'row-highlightable':''}`}
212
+ >
213
+ <table className={'jr-table-table'}>
214
+ <TBodies
215
+ table={this}
216
+ leafColumns={this.state.leafColumns}
217
+ groupHeader={this.props.groupHeader}
218
+ groupFooter={this.props.groupFooter}
219
+
220
+ dataSource={this.getDataSource()}
221
+ onRowClick={this.props.onRowClick}
222
+ />
223
+ <TFoot
224
+ columns={this.props.footColumns}
225
+ table={this}
226
+ />
227
+ <THead
228
+ columns={this.state.columns}
229
+ leafColumns={this.state.leafColumns}
230
+ table={this}
231
+ />
232
+ </table>
233
+ <div className={'empty'}>{this.noData() && '無資料'}</div>
234
+ </StyledJRTable>
6
235
  }
7
236
  }
@@ -0,0 +1,78 @@
1
+ import React from "react"
2
+ import styled from "styled-components"
3
+
4
+ export const StyledSlider=styled.div`
5
+ position: absolute;
6
+ top: 0;
7
+ right: 0;
8
+ height:100%;
9
+ width:6px;
10
+ user-select: none;
11
+ &.resizing:hover,&.resizing{
12
+ border-right:1px dashed black;
13
+ }
14
+ &:hover{
15
+ cursor: col-resize;
16
+ border-right:1px dashed gray;
17
+ }
18
+ `
19
+ export default class Slider extends React.Component {
20
+ constructor(){
21
+ super();
22
+ this.sliderRef=React.createRef()
23
+ }
24
+
25
+ stop=(e)=>{
26
+ this.sliderRef.current.classList.remove('resizing');
27
+ document.body.style.cursor='default'
28
+ window.removeEventListener('mousemove',this.move)
29
+ window.removeEventListener('mouseup',this.stop)
30
+ }
31
+ move=({clientX})=>{
32
+ const {th,selectedCols,widthRates}=this.data
33
+ const {left}=th.getBoundingClientRect()
34
+
35
+ const min=left+10
36
+ const x=clientX>=min
37
+ ?clientX
38
+ :min
39
+ const width=x-left
40
+
41
+ selectedCols.forEach((col,index)=>{
42
+ const _width=Math.round((width/100)*widthRates[index])
43
+ col.style.width=_width+'px'
44
+ })
45
+ }
46
+ start(thPRef,column){
47
+ const selectedCols=[...this.props.table.colGroupRef.current.children].slice(column.columnNo,column.columnNo+(column.colSpan??1))
48
+ const totalWidth=selectedCols.reduce((aco,{offsetWidth})=>aco+offsetWidth,0)
49
+ const widthRates=selectedCols.map(({offsetWidth})=>{
50
+ return 100*(offsetWidth/totalWidth)
51
+ })
52
+
53
+ this.data={
54
+ selectedCols
55
+ ,th:thPRef.current
56
+ ,widthRates
57
+ }
58
+
59
+ this.sliderRef.current.classList.add('resizing');
60
+ document.body.style.cursor='col-resize'
61
+ window.addEventListener('mousemove',this.move)
62
+ window.addEventListener('mouseup',this.stop)
63
+
64
+ }
65
+
66
+ render(){
67
+ const column=this.props.column
68
+ return <StyledSlider
69
+ ref={this.sliderRef}
70
+ onMouseDown={(e)=>{
71
+ this.start(
72
+ this.props.thRef
73
+ ,column
74
+ )
75
+ }}
76
+ />
77
+ }
78
+ }
@@ -0,0 +1,135 @@
1
+ import styled from "styled-components";
2
+
3
+ export const StyledJRTable=styled.div`
4
+ --column-bd-color:#cccccc;
5
+ --column-b-color:#eeeeee;
6
+ --column-b-hover-color:#ffffff;
7
+
8
+ position: relative;
9
+ background: var(--column-b-color);
10
+ border:1px solid var(--column-bd-color);
11
+
12
+ display:flex;
13
+ flex-direction: column;
14
+ flex:1;
15
+ overflow: overlay;
16
+
17
+
18
+
19
+
20
+ &.row-highlightable{
21
+ tbody{
22
+ tr:not(.jr-group-header,.jr-group-footer):hover{
23
+ background:var(--column-b-hover-color);
24
+ cursor: pointer;
25
+ transition: background-color .8s;
26
+ td{
27
+ color:black;
28
+ transition:color .8s;
29
+ }
30
+
31
+ }
32
+ }
33
+ }
34
+
35
+ table{
36
+ Xheight: 100%;
37
+ min-width:100%;
38
+ width: max-content;
39
+ border-spacing: 0;
40
+
41
+ thead{
42
+ position: sticky;
43
+ top: 0;
44
+
45
+ th{
46
+ position: relative;
47
+ height:32px;
48
+ padding: 4px;
49
+ background: linear-gradient(180deg, rgba(227, 227, 226, 1) 0%, rgba(255, 255, 255, 1) 25%, rgba(210, 210, 210, 1) 100%);
50
+ box-shadow: 2px 2px 2px 0 #ffffffd6 inset, -1px -1px 2px 0px #8a847dbf inset;
51
+ color: #525252;
52
+ white-space: nowrap;
53
+ }
54
+ }
55
+
56
+ tfoot{
57
+ position: sticky;
58
+ bottom: -1px;
59
+
60
+ Xtr:nth-child(1){
61
+ th{
62
+ Xborder-top:1px solid var(--column-bd-color);
63
+ }
64
+ }
65
+ Xth:nth-child(1){
66
+ Xborder-left:1px solid var(--column-bd-color);
67
+ }
68
+
69
+ th{
70
+ height:32px;
71
+ padding: 4px;
72
+ background: #e4e4e4;
73
+ xbackground: linear-gradient(180deg, rgba(227, 227, 226, 1) 0%, rgba(255, 255, 255, 1) 25%, rgba(210, 210, 210, 1) 100%);
74
+ xbox-shadow: 2px 2px 2px 0 #ffffffd6 inset, -1px -1px 2px 0px #8a847dbf inset;
75
+ color: #525252;
76
+ white-space: nowrap;
77
+
78
+ border-left:2px solid #f4f4f4;
79
+ border-top:2px solid #f4f4f4;
80
+ border-right:2px solid var(--column-bd-color);
81
+ border-bottom:2px solid var(--column-bd-color);
82
+ }
83
+ }
84
+
85
+ tbody{
86
+ tr{
87
+ transition: background .3s linear;
88
+ background:var(--column-b-color);
89
+ a:#ededed;
90
+ th{
91
+ color:#444444;
92
+ }
93
+ td{
94
+ border-bottom: 1px solid var(--column-bd-color);
95
+ color:#2e2e2e;
96
+ padding: 4px;
97
+ }
98
+ }
99
+ tr:hover{
100
+ background:var(--column-b-hover-color);
101
+
102
+ }
103
+
104
+ tr.jr-group-header
105
+ ,tr.jr-group-footer{
106
+ text-align:left;
107
+ background:#dddbdb;
108
+
109
+ th{
110
+ border-bottom: 1px solid var(--column-bd-color);
111
+ border-right: 1px solid var(--column-bd-color);
112
+ padding: 4px 8px;
113
+ }
114
+ }
115
+ }
116
+ tbody.empty-tbody{
117
+ td{
118
+ border:10px solid red;
119
+ }
120
+ }
121
+ }
122
+
123
+
124
+
125
+
126
+ > .empty{
127
+ user-select: none;
128
+ color:#848484;
129
+ height:100%;
130
+ flex:1;
131
+ display:flex;
132
+ justify-content: center;
133
+ align-items: center;
134
+ }
135
+ `