cdui-js 1.0.18 → 1.0.20

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/css/all.css CHANGED
@@ -8,7 +8,7 @@
8
8
  @import url(textbox.css);
9
9
  @import url(popup.css);
10
10
  @import url(combobox.css);
11
- @import url(canlendar.css);
11
+ @import url(datewidget.css);
12
12
  @import url(datepicker.css);
13
13
  @import url(mobile-datepicker.css);
14
14
  @import url(form.css);
@@ -1,5 +1,4 @@
1
- .canlendar,
2
- .monthwidget {
1
+ .datewidget {
3
2
  max-width: 380px;
4
3
  height: 320px;
5
4
  border-radius: 12px;
@@ -8,17 +7,14 @@
8
7
  user-select: none;
9
8
  }
10
9
 
11
- .canlendar-header,
12
- .canlendar-weeks,
13
- .monthwidget-header {
10
+ .datewidget-header {
14
11
  display: flex;
15
12
  align-items: center;
16
13
  height: 36px;
17
14
  line-height: 36px;
18
15
  }
19
16
 
20
- .canlendar-title,
21
- .monthwidget-title {
17
+ .datewidget-title {
22
18
  flex: auto;
23
19
  padding-left: 12px;
24
20
  font-weight: bold;
@@ -26,8 +22,7 @@
26
22
  cursor: pointer;
27
23
  }
28
24
 
29
- .canlendar-header > .icon,
30
- .monthwidget-header > .icon {
25
+ .datewidget-header > .icon {
31
26
  box-sizing: border-box;
32
27
  width: 40px;
33
28
  height: 100%;
@@ -35,42 +30,24 @@
35
30
  cursor: pointer;
36
31
  }
37
32
 
38
- .canlendar-weeks > span {
39
- flex: auto;
40
- text-align: center;
41
- vertical-align: middle;
42
- }
43
-
44
- .canlendar-body {
45
- height: calc(100% - 72px);
46
- }
47
-
48
- .canlendar-date,
49
- .monthwidget-month {
33
+ .datewidget-item {
50
34
  display: inline-flex;
51
35
  justify-content: center;
52
36
  align-items: center;
53
- width: 14.28%;
54
- height: 16.66%;
55
37
  cursor: pointer;
56
38
  }
57
39
 
58
- .canlendar-date.disabled,
59
- .monthwidget-month.disabled {
40
+ .datewidget-item.disabled {
60
41
  cursor: not-allowed;
61
42
  }
62
43
 
63
- .canlendar-date.today,
64
- .canlendar-date.selected,
65
- .monthwidget-month.today,
66
- .monthwidget-month.selected {
44
+ .datewidget-item.today,
45
+ .datewidget-item.selected {
67
46
  position: relative;
68
47
  }
69
48
 
70
- .canlendar-date.today::before,
71
- .canlendar-date.selected::before,
72
- .monthwidget-month.today::before,
73
- .monthwidget-month.selected::before {
49
+ .datewidget-item.today::before,
50
+ .datewidget-item.selected::before {
74
51
  position: absolute;
75
52
  content: '';
76
53
  width: 40px;
@@ -78,3 +55,18 @@
78
55
  border-radius: 40px;
79
56
  z-index: -1;
80
57
  }
58
+
59
+ .canlendar-weeks > span {
60
+ flex: auto;
61
+ text-align: center;
62
+ vertical-align: middle;
63
+ }
64
+
65
+ .canlendar-body {
66
+ height: calc(100% - 72px);
67
+ }
68
+
69
+ .canlendar-body .datewidget-item {
70
+ width: 14.28%;
71
+ height: 16.66%;
72
+ }
package/css/form.css CHANGED
@@ -1,11 +1,11 @@
1
1
  .form-align-left,
2
2
  .form-align-right {
3
3
  display: flex;
4
- align-items: center;
5
4
  }
6
5
 
7
6
  .form-align-left > label,
8
7
  .form-align-right > label {
8
+ margin-top: 4px;
9
9
  width: 100px;
10
10
  }
11
11
 
@@ -17,3 +17,9 @@
17
17
  .form-align-right > label {
18
18
  padding-left: 8px;
19
19
  }
20
+
21
+ .form-error {
22
+ margin-top: 4px;
23
+ font-size: 12px;
24
+ color: #ff295a;
25
+ }
package/demo/css/css.md CHANGED
@@ -287,17 +287,17 @@
287
287
  .combobox:has(.combobox-input:focus) { border: 1px solid #1b212d; }
288
288
 
289
289
 
290
- # canlendar 日历
290
+ # canlendar monthwidget yearwidget 日历 年月 年
291
291
 
292
- .canlendar { border: 1px solid #e4e4e4; }
293
- .canlendar-header > .icon { stroke: #1b212d; }
294
- .canlendar-weeks > span { color: #888f97; }
295
- .canlendar-date.disabled { color: #e4e4e4; }
296
- .canlendar-date.prev-month, .canlendar-date.next-month { color: #888f97; }
297
- .canlendar-date.today::before { background: #f5f5f5; }
298
- .canlendar-date.selected { color: white; }
299
- .canlendar-date.selected::before { background: #ff4000; }
292
+ .datewidget { border: 1px solid #e4e4e4; }
293
+ .datewidget-header > .icon { stroke: #1b212d; }
294
+ .datewidget-item.disabled { color: #e4e4e4; }
295
+ .datewidget-item.prev-month, .datewidget-item.next-month { color: #888f97; }
296
+ .datewidget-item.today::before { background: #f5f5f5; }
297
+ .datewidget-item.selected { color: white; }
298
+ .datewidget-item.selected::before { background: #ff4000; }
300
299
 
300
+ .canlendar-weeks > span { color: #888f97; }
301
301
 
302
302
  # datepicker 日期选择
303
303
 
package/demo/src/App.tsx CHANGED
@@ -1,35 +1,14 @@
1
- import { batch, createEffect, omitProps, pickProps, reactive } from '../../src/reactive';
2
- import { Icon } from '../../src/components/Icon';
3
1
  import { CanlendarPage } from './pages/Canlendar';
2
+ import { CarouselPage } from './pages/Carousel';
4
3
  import { ComboBoxPage } from './pages/ComboBox';
5
4
  import { DatePickerPage } from './pages/DatePicker';
6
5
  import { FormPage } from './pages/Form';
7
6
  import { MobileDatePickerPage } from './pages/MobileDatePicker';
8
7
 
9
- const Test = (props: { text1: string; text2: string }) => {
10
- return (
11
- <div>
12
- <div>{pickProps(props, ['text1']).text1}</div>
13
- <div>{omitProps(props, ['text1']).text2}</div>
14
- </div>
15
- );
16
- };
17
-
18
8
  export const App = () => {
19
- let state = reactive({
20
- text1: '111',
21
- text2: '222',
22
- });
23
-
24
- createEffect(() => {
25
- console.log(state.text1, state.text2);
26
- });
27
-
28
9
  return (
29
10
  <div style={{ 'min-height': '100%' }}>
30
- <Icon name="dropdown" class="test" onclick={() => alert(1111)}></Icon>
31
- <Test text1={state.text1} text2={state.text2}></Test>
32
- <button onclick={() => batch(() => (state.text1 = state.text2 = '' + Math.random()))}>click</button>
11
+ <CarouselPage></CarouselPage>
33
12
  <CanlendarPage></CanlendarPage>
34
13
  <DatePickerPage></DatePickerPage>
35
14
  <MobileDatePickerPage></MobileDatePickerPage>
@@ -913,14 +913,14 @@ body { stroke: #A2A7AD; fill: #A2A7AD; }
913
913
  .combobox:has(.combobox-input:focus) { border: 1px solid #1b212d; }
914
914
 
915
915
 
916
- .canlendar { border: 1px solid #e4e4e4; }
917
- .canlendar-header > .icon { stroke: #1b212d; }
916
+ .datewidget { border: 1px solid #e4e4e4; }
917
+ .datewidget-header > .icon { stroke: #1b212d; }
918
+ .datewidget-item.disabled { color: #e4e4e4; }
919
+ .datewidget-item.prev-month, .datewidget-item.next-month { color: #888f97; }
920
+ .datewidget-item.today::before { background: #f5f5f5; }
921
+ .datewidget-item.selected { color: white; }
922
+ .datewidget-item.selected::before { background: #ff4000; }
918
923
  .canlendar-weeks > span { color: #888f97; }
919
- .canlendar-date.disabled { color: #e4e4e4; }
920
- .canlendar-date.prev-month, .canlendar-date.next-month { color: #888f97; }
921
- .canlendar-date.today::before { background: #f5f5f5; }
922
- .canlendar-date.selected { color: white; }
923
- .canlendar-date.selected::before { background: #ff4000; }
924
924
 
925
925
 
926
926
  .datepicker { border: 1px solid #e4e4e4; background: white; }
@@ -1,12 +1,16 @@
1
1
  import { Canlendar } from '../../../src/components/Canlendar';
2
+ import { MonthWidget } from '../../../src/components/MonthWidget';
2
3
 
3
4
  export const CanlendarPage = () => {
4
5
  return (
5
- <Canlendar
6
- value={new Date()}
7
- onValueChange={(date) => {
8
- console.log(date);
9
- }}
10
- ></Canlendar>
6
+ <div>
7
+ <Canlendar
8
+ value={new Date()}
9
+ onValueChange={(date) => {
10
+ console.log(date);
11
+ }}
12
+ ></Canlendar>
13
+ <MonthWidget></MonthWidget>
14
+ </div>
11
15
  );
12
16
  };
@@ -0,0 +1,40 @@
1
+ import { Carousel, CarouselApi, CarouselButtons } from '../../../src/components/Carousel';
2
+
3
+ const carousels = [
4
+ {
5
+ text: '页面1',
6
+ },
7
+ {
8
+ text: '页面2',
9
+ },
10
+ {
11
+ text: '页面3',
12
+ },
13
+ {
14
+ text: '页面4',
15
+ },
16
+ ];
17
+
18
+ export const CarouselPage = () => {
19
+ let carousel: CarouselApi;
20
+
21
+ return (
22
+ <div class="relative">
23
+ <Carousel
24
+ class="row-gap"
25
+ api={(api) => (carousel = api)}
26
+ each={carousels}
27
+ disabledScroll={true}
28
+ autoplay={false}
29
+ style={{ width: '100%', height: '200px' }}
30
+ >
31
+ {(item) => (
32
+ <div class="border round" style={{ flex: 'none', width: '100%', height: '100%', 'margin-right': '4px' }}>
33
+ {item.text}
34
+ </div>
35
+ )}
36
+ </Carousel>
37
+ <CarouselButtons carousel={carousel}></CarouselButtons>
38
+ </div>
39
+ );
40
+ };
@@ -1,8 +1,25 @@
1
+ import { reactive } from '../../../src';
1
2
  import { ComboBox } from '../../../src/components/ComboBox';
2
3
 
3
4
  export const ComboBoxPage = () => {
5
+ let combobox;
6
+
7
+ const state = reactive({
8
+ value: '111',
9
+ });
10
+
4
11
  return (
5
- <ComboBox popup={{ style: { width: '200px', padding: '8px 0' } }}>
12
+ <ComboBox
13
+ api={(api) => (combobox = api)}
14
+ value={state.value}
15
+ popup={{
16
+ style: { width: '200px', padding: '8px 0' },
17
+ onclick: (event) => {
18
+ state.value = event.target.textContent;
19
+ combobox.closePopup();
20
+ },
21
+ }}
22
+ >
6
23
  <div>111</div>
7
24
  <div>222</div>
8
25
  </ComboBox>
@@ -1,6 +1,7 @@
1
1
  import { reactive } from '../../../src/reactive';
2
2
  import { For } from '../../../src/components/For';
3
3
  import { TextBox } from '../../../src/components/TextBox';
4
+ import { ComboBox } from '../../../src/components/ComboBox';
4
5
  import { Form, FormApi, FormItem, ValidateRules } from '../../../src/components/Form';
5
6
 
6
7
  const rules: ValidateRules = {
@@ -18,27 +19,52 @@ export const FormPage = () => {
18
19
  field: 'a',
19
20
  label: '111',
20
21
  required: true,
22
+ Input: () => <TextBox></TextBox>,
21
23
  },
22
24
  {
23
25
  field: 'b',
24
26
  label: '222',
25
27
  required: true,
26
28
  labelWidth: '100px',
29
+ Input: () => {
30
+ let combobox;
31
+
32
+ return (
33
+ <ComboBox
34
+ api={(api) => (combobox = api)}
35
+ value={state.b}
36
+ popup={{
37
+ onclick: (event) => {
38
+ state.b = +event.target.textContent;
39
+ combobox.closePopup();
40
+ },
41
+ }}
42
+ >
43
+ <div>1</div>
44
+ <div>2</div>
45
+ </ComboBox>
46
+ );
47
+ },
27
48
  },
28
49
  ],
50
+ a: 1,
51
+ b: 2,
29
52
  });
30
53
 
31
54
  let form: FormApi;
32
55
 
33
56
  return (
34
- <Form data={{}} rules={{}} align={state.align} labelWidth={state.labelWidth} api={(api) => (form = api)}>
57
+ <Form data={state} rules={{}} align={state.align} labelWidth={state.labelWidth} api={(api) => (form = api)}>
35
58
  <For each={state.items}>
36
59
  {(item) => (
37
60
  <FormItem field={item.field} label={item.label} required={item.required}>
38
- <TextBox></TextBox>
61
+ <div>
62
+ <item.Input></item.Input>
63
+ </div>
39
64
  </FormItem>
40
65
  )}
41
66
  </For>
67
+ <div>{`a: ${state.a} b: ${state.b}`}</div>
42
68
  <button
43
69
  type="button"
44
70
  onclick={() =>
@@ -47,6 +73,7 @@ export const FormPage = () => {
47
73
  label: '???',
48
74
  required: false,
49
75
  labelWidth: '100px',
76
+ Input: () => <TextBox></TextBox>,
50
77
  })
51
78
  }
52
79
  >
@@ -61,6 +88,9 @@ export const FormPage = () => {
61
88
  <button type="button" onclick={() => form.validate()}>
62
89
  validate
63
90
  </button>
91
+ <button type="button" onclick={() => form.clearErrors()}>
92
+ clearErrors
93
+ </button>
64
94
  </Form>
65
95
  );
66
96
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdui-js",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  import { JSX } from '../jsx';
2
2
 
3
- export const Button = (props?: JSX.SvgSVGAttributes<never>) => {
3
+ export const Button = (props?: JSX.HTMLAttributes<never>) => {
4
4
  return <button type="button" {...props}></button>;
5
5
  };
@@ -24,7 +24,7 @@ const renderDates = (
24
24
 
25
25
  for (let i = from; i <= to; i++) {
26
26
  items.push(
27
- `<span class="canlendar-date${className}${today === i ? ' today' : ''}${selected === i ? ' selected' : ''}${
27
+ `<span class="datewidget-item${className}${today === i ? ' today' : ''}${selected === i ? ' selected' : ''}${
28
28
  disableFn && disableFn(year, month, i) ? ' disabled' : ''
29
29
  }" data-date="${year + '|' + month + '|' + i}">${i}</span>`,
30
30
  );
@@ -162,9 +162,9 @@ export const Canlendar = (
162
162
  const [currentValue, setCurrentValue] = createSignal(selectedValue() || new Date());
163
163
 
164
164
  return (
165
- <div class={combineClass('canlendar', props.class)} {...omitProps(props, OMIT_PROPS)}>
166
- <div class="canlendar-header">
167
- <div ref={domTitle as any} class="canlendar-title">
165
+ <div class={combineClass('canlendar datewidget', props.class)} {...omitProps(props, OMIT_PROPS)}>
166
+ <div class="datewidget-header canlendar-header">
167
+ <div ref={domTitle as any} class="datewidget-title">
168
168
  {replaceTemplate(i18n.Month, currentValue().getFullYear(), formatMonth(currentValue()))}
169
169
  </div>
170
170
  <svg class="icon icon-s" aria-hidden={true} onclick={() => setCurrentValue(switchMonth(currentValue(), -1))}>
@@ -174,12 +174,12 @@ export const Canlendar = (
174
174
  <use href="#icon-forward"></use>
175
175
  </svg>
176
176
  </div>
177
- <div class="canlendar-weeks">
177
+ <div class="datewidget-header canlendar-weeks">
178
178
  <For each={i18n.Weeks}>{(item) => <span>{item}</span>}</For>
179
179
  </div>
180
180
  <div
181
181
  ref={domBody as any}
182
- class="canlendar-body"
182
+ class="datewidget-body canlendar-body"
183
183
  onclick={(event) => {
184
184
  let target = event.target as HTMLElement;
185
185
  let date, onValueChange;
@@ -15,9 +15,7 @@ import {
15
15
  import { For } from './For';
16
16
  import { Icon } from './Icon';
17
17
 
18
- const CLASS_NAME = 'carousel-vertical';
19
-
20
- const EVENT_OPTIONS = { passive: true, capture: true };
18
+ const EVENT_OPTIONS = { passive: false, capture: true };
21
19
 
22
20
  const DOTS = new Array(100).join('0').split('');
23
21
 
@@ -48,7 +46,7 @@ if (isBrowser) {
48
46
  );
49
47
  }
50
48
 
51
- const OMIT_PROPS = ['class', 'each', 'children', 'autoplay', 'interval', 'vertical', 'api'] as const;
49
+ const OMIT_PROPS = ['class', 'each', 'children', 'autoplay', 'interval', 'disabledScroll', 'api'] as const;
52
50
 
53
51
  /**
54
52
  * 轮播组件外部调用接口
@@ -94,9 +92,9 @@ export const Carousel = <T, U extends JSX.Element>(
94
92
  */
95
93
  interval?: number;
96
94
  /**
97
- * 是否竖直滚动
95
+ * 是否禁止滚动(触控时不允许上下滚动)
98
96
  */
99
- vertical?: boolean;
97
+ disabledScroll?: boolean;
100
98
  /**
101
99
  * 外部调用接口
102
100
  */
@@ -113,11 +111,6 @@ export const Carousel = <T, U extends JSX.Element>(
113
111
  // 自动滚动计时器
114
112
  let autoplayTimer: any;
115
113
 
116
- // 获取滚动方向
117
- const scrollType = createMemo(() => (props.vertical ? 'scrollTop' : 'scrollLeft'));
118
- const offsetType = createMemo(() => (props.vertical ? 'offsetTop' : 'offsetLeft'));
119
- const screenType = createMemo(() => (props.vertical ? 'screenY' : 'screenX'));
120
-
121
114
  // 滚动到指定索引
122
115
  const scrollTo = (index: number) => {
123
116
  let children = ref.children;
@@ -135,7 +128,7 @@ export const Carousel = <T, U extends JSX.Element>(
135
128
  animateScrollIntoView(ref, children[index % length] as HTMLElement).then(() => {
136
129
  if (index >= count) {
137
130
  // 滚动到对应节点
138
- ref[scrollType()] = (children[index - count] as HTMLElement)[offsetType()];
131
+ ref.scrollLeft = (children[index - count] as HTMLElement).offsetLeft;
139
132
  // 调整到指定节点
140
133
  index -= count;
141
134
  }
@@ -152,7 +145,7 @@ export const Carousel = <T, U extends JSX.Element>(
152
145
  let count = props.each.length;
153
146
 
154
147
  // 先滚动到对应节点的填充节点
155
- ref[scrollType()] = (ref.children[index + count] as HTMLElement)[offsetType()];
148
+ ref.scrollLeft = (ref.children[index + count] as HTMLElement).offsetLeft;
156
149
  // 调整到指定节点
157
150
  index += count;
158
151
  }
@@ -174,21 +167,31 @@ export const Carousel = <T, U extends JSX.Element>(
174
167
  checkFirstIndex();
175
168
 
176
169
  // 记录按下时状态
177
- pressdown = (event.changedTouches[0] || event.touches[0])[screenType()];
178
- pressdownScroll = ref[scrollType()];
170
+ pressdown = (event.changedTouches[0] || event.touches[0]).screenX;
171
+ pressdownScroll = ref.scrollLeft;
179
172
 
180
173
  // 取消自动播放
181
174
  clearTimeout(autoplayTimer);
175
+
176
+ if (props.disabledScroll) {
177
+ event.preventDefault();
178
+ return false;
179
+ }
182
180
  };
183
181
 
184
182
  const ontouchmove = (event: TouchEvent) => {
185
183
  if (pressdown >= 0) {
186
- ref[scrollType()] = pressdownScroll - ((event.changedTouches[0] || event.touches[0])[screenType()] - pressdown);
184
+ ref.scrollLeft = pressdownScroll - ((event.changedTouches[0] || event.touches[0]).screenX - pressdown);
185
+ }
186
+
187
+ if (props.disabledScroll) {
188
+ event.preventDefault();
189
+ return false;
187
190
  }
188
191
  };
189
192
 
190
193
  const ontouchend = (event: TouchEvent) => {
191
- let distance = (event.changedTouches[0] || event.touches[0])[screenType()] - pressdown;
194
+ let distance = (event.changedTouches[0] || event.touches[0]).screenX - pressdown;
192
195
  let index = currentIndex();
193
196
 
194
197
  // 清除按下状态
@@ -200,13 +203,18 @@ export const Carousel = <T, U extends JSX.Element>(
200
203
  } else if (distance < -20) {
201
204
  // 如果是第一个位置,则恢复滚动位置
202
205
  if (index === 0) {
203
- ref[scrollType()] = ref.children[offsetType()];
206
+ ref.scrollLeft = (ref.children[0] as HTMLElement).offsetLeft;
204
207
  }
205
208
 
206
209
  scrollTo(index + 1);
207
210
  } else {
208
211
  animateScrollIntoView(ref, ref.children[index % ref.children.length] as HTMLElement);
209
212
  }
213
+
214
+ if (props.disabledScroll) {
215
+ event.preventDefault();
216
+ return false;
217
+ }
210
218
  };
211
219
 
212
220
  const autoplay = () => {
@@ -244,18 +252,6 @@ export const Carousel = <T, U extends JSX.Element>(
244
252
  ) as CarouselApi,
245
253
  );
246
254
 
247
- createEffect(() => {
248
- let classList = ref.classList;
249
-
250
- if (props.vertical) {
251
- if (!classList.contains(CLASS_NAME)) {
252
- classList.add();
253
- }
254
- } else {
255
- classList.remove(CLASS_NAME);
256
- }
257
- });
258
-
259
255
  createEffect(autoplay);
260
256
 
261
257
  onMount(() => {
@@ -1,7 +1,8 @@
1
1
  import { JSX } from '../jsx';
2
- import { combineClass, omitProps } from '../reactive';
2
+ import { combineClass, omitProps, useContext, watch } from '../reactive';
3
3
  import { disableAutoCloseEvent } from '../dom';
4
4
  import { Popup, PopupApi, PopupProps } from './Popup';
5
+ import { FormItemContext } from './provider';
5
6
 
6
7
  const OMIT_PROPS = ['class', 'value', 'readonly', 'popupOnFocus', 'popup', 'onPopup', 'api', 'children'] as const;
7
8
 
@@ -14,7 +15,13 @@ export const ComboBox = (
14
15
  /**
15
16
  * 值
16
17
  */
17
- value?: string;
18
+ value?: any;
19
+ /**
20
+ * 值样式
21
+ *
22
+ * @param value 当前值
23
+ */
24
+ format?(value: any): string;
18
25
  /**
19
26
  * 是否只读
20
27
  */
@@ -36,12 +43,24 @@ export const ComboBox = (
36
43
  props.api && props.api(popup);
37
44
  };
38
45
 
46
+ const formItem = useContext(FormItemContext);
47
+
48
+ const initFormItem = (dom: HTMLInputElement) => {
49
+ watch(
50
+ () => props.value,
51
+ (value) => formItem.setValue(value),
52
+ );
53
+
54
+ formItem.init(dom);
55
+ };
56
+
39
57
  return (
40
58
  <div class={combineClass('combobox', props.class)} {...omitProps(props, OMIT_PROPS)}>
41
59
  <div class="combobox-host" {...disableAutoCloseEvent}>
42
60
  <input
61
+ ref={formItem && initFormItem}
43
62
  class="combobox-input"
44
- value={props.value || ''}
63
+ value={props.format ? props.format(props.value) : '' + (props.value || '')}
45
64
  readonly={props.readonly}
46
65
  onfocus={() => props.popupOnFocus && popup.openPopup()}
47
66
  onclick={() => props.readonly && !props.popupOnFocus && popup.togglePopup()}
@@ -33,10 +33,6 @@ export interface FormItemProps {
33
33
  * 是否隐藏
34
34
  */
35
35
  hidden?: boolean;
36
- /**
37
- * 点击 label 自动获取焦点的组件(选择器)
38
- */
39
- for?: string;
40
36
  /**
41
37
  * 错误信息
42
38
  */
@@ -47,6 +43,55 @@ export interface FormItemProps {
47
43
  requiredError?: string;
48
44
  }
49
45
 
46
+ /**
47
+ * 表单外部调用接口
48
+ */
49
+ export interface FormApi {
50
+ /**
51
+ * 校验表单
52
+ *
53
+ * @param filter 过滤器(校验部分字段)
54
+ */
55
+ validate(filter?: (target: ValidateTarget) => void | boolean): Promise<boolean>;
56
+
57
+ /**
58
+ * 滚动到第一个错误位置
59
+ */
60
+ scrollToError(): void;
61
+
62
+ /**
63
+ * 清除所有错误信息
64
+ */
65
+ clearErrors(): void;
66
+ }
67
+
68
+ export interface FormProps {
69
+ /**
70
+ * 表单数据
71
+ */
72
+ data: object;
73
+
74
+ /**
75
+ * 表单校验规则
76
+ */
77
+ rules: ValidateRules;
78
+
79
+ /**
80
+ * label 对齐方式
81
+ */
82
+ align?: 'left' | 'top' | 'right';
83
+
84
+ /**
85
+ * 标签宽度
86
+ */
87
+ labelWidth?: string;
88
+
89
+ /**
90
+ * 外部调用接口
91
+ */
92
+ api?: (api: FormApi) => void;
93
+ }
94
+
50
95
  /**
51
96
  * 校验目标
52
97
  */
@@ -198,11 +243,6 @@ export interface ValidateRules {
198
243
  [key: string]: ValidateRule;
199
244
  }
200
245
 
201
- /**
202
- * 表单上下文
203
- */
204
- const FormContext = createContext<Pick<FormItemProps, 'align' | 'labelWidth'>>();
205
-
206
246
  const replaceError = (item: FormItemProps, error: string, value?: unknown) => {
207
247
  let label = item.label;
208
248
 
@@ -393,18 +433,20 @@ const validate = async (
393
433
  let error;
394
434
 
395
435
  // 有设置了字段且未隐藏
396
- if (!item.hidden && (field = child.dataset.field)) {
436
+ if (!item.hidden && (field = item.field)) {
437
+ let fields = field.split('.');
438
+
397
439
  // 必填
398
440
  if (item.required) {
399
441
  // 当前值
400
- let value = findValue(data, field);
442
+ let value = findValue(data, fields);
401
443
 
402
444
  if (value == null || value === '') {
403
445
  error = replaceError(item, item.requiredError || i18n.Required, value);
404
446
  }
405
- } else if ((rule = findRule(rules, (field = field.split('.'))))) {
447
+ } else if ((rule = findRule(rules, fields))) {
406
448
  // 当前值
407
- let value = findValue(data, field);
449
+ let value = findValue(data, fields);
408
450
  // 校验目标
409
451
  let target = {
410
452
  data,
@@ -443,6 +485,41 @@ const validate = async (
443
485
  return result;
444
486
  };
445
487
 
488
+ function scrollToError(this: HTMLFormElement) {
489
+ let error = this.querySelector('.form-error') as HTMLElement;
490
+
491
+ if (error) {
492
+ (error.parentNode as HTMLElement).scrollIntoView();
493
+ }
494
+ }
495
+
496
+ function clearErrors(this: HTMLFormElement) {
497
+ let errors = this.querySelectorAll('.form-error');
498
+
499
+ for (let i = errors.length; i--; ) {
500
+ errors[i].parentNode.removeChild(errors[i]);
501
+ }
502
+ }
503
+
504
+ const setValue = (data: object, field: string, value: any) => {
505
+ let fields = field.split('.');
506
+ let last = fields.length - 1;
507
+
508
+ for (let i = 0; i < last; i++) {
509
+ if ((data = data[fields[i]])) {
510
+ } else {
511
+ return;
512
+ }
513
+ }
514
+
515
+ data[fields[last]] = value;
516
+ };
517
+
518
+ /**
519
+ * 表单上下文
520
+ */
521
+ const FormContext = createContext<FormProps>();
522
+
446
523
  const OMIT_ITEM_PROPS = [
447
524
  'class',
448
525
  'field',
@@ -451,7 +528,6 @@ const OMIT_ITEM_PROPS = [
451
528
  'align',
452
529
  'required',
453
530
  'hidden',
454
- 'for',
455
531
  'error',
456
532
  'requiredError',
457
533
  'children',
@@ -461,17 +537,27 @@ const OMIT_ITEM_PROPS = [
461
537
  * 表单项
462
538
  */
463
539
  export const FormItem = (props?: JSX.HTMLAttributes<never> & FormItemProps) => {
464
- const form = useContext(FormContext) as any;
465
-
466
- let item: HTMLElement;
467
-
468
- onMount(() => {
469
- (item as any).FORM_ITEM = props;
470
- });
540
+ let domItem: HTMLElement;
541
+ let domInput: HTMLElement;
542
+
543
+ const form = useContext(FormContext);
544
+ const provider = {
545
+ getValue: () => findValue(form.data, props.field.split('.')),
546
+ setValue: (value: any) => setValue(form.data, props.field, value),
547
+ init: (input: HTMLElement) => {
548
+ if (!domInput) {
549
+ domInput = input;
550
+ return true;
551
+ }
552
+ },
553
+ };
471
554
 
472
555
  return (
473
556
  <div
474
- ref={item as any}
557
+ ref={(dom) => {
558
+ domItem = dom;
559
+ (domItem as any).FORM_ITEM = props;
560
+ }}
475
561
  class={combineClass(
476
562
  'form-item',
477
563
  'form-align-' + (props.align || form.align || 'left'),
@@ -481,104 +567,38 @@ export const FormItem = (props?: JSX.HTMLAttributes<never> & FormItemProps) => {
481
567
  )}
482
568
  {...omitProps(props, OMIT_ITEM_PROPS)}
483
569
  >
484
- <label for={props.for} style={{ width: props.labelWidth || form.labelWidth }}>
570
+ <label style={{ width: props.labelWidth || form.labelWidth }} onclick={() => domInput && domInput.focus()}>
485
571
  {props.label}
486
572
  </label>
487
573
  <div class="form-body">
488
- <FormItemContext.Provider value={1}>{props.children}</FormItemContext.Provider>
574
+ <FormItemContext.Provider value={provider}>{props.children}</FormItemContext.Provider>
489
575
  </div>
490
576
  </div>
491
577
  );
492
578
  };
493
579
 
494
- function scrollToError(this: HTMLFormElement) {
495
- let error = this.querySelector('.form-error') as HTMLElement;
496
-
497
- if (error) {
498
- (error.parentNode as HTMLElement).scrollIntoView();
499
- }
500
- }
501
-
502
- function clearErrors(this: HTMLFormElement) {
503
- let errors = this.querySelectorAll('.form-error');
504
-
505
- for (let i = errors.length; i--; ) {
506
- errors[i].parentNode.removeChild(errors[i]);
507
- }
508
- }
509
-
510
580
  const OMIT_FORM_PROPS = ['data', 'rules', 'align', 'labelWidth', 'api', 'children'] as const;
511
581
 
512
- /**
513
- * 表单外部调用接口
514
- */
515
- export interface FormApi {
516
- /**
517
- * 校验表单
518
- *
519
- * @param filter 过滤器(校验部分字段)
520
- */
521
- validate(filter?: (target: ValidateTarget) => void | boolean): Promise<boolean>;
522
-
523
- /**
524
- * 滚动到第一个错误位置
525
- */
526
- scrollToError(): void;
527
-
528
- /**
529
- * 清除所有错误信息
530
- */
531
- clearErrors(): void;
532
- }
533
-
534
582
  /**
535
583
  * 表单组件
536
584
  */
537
- export const Form = (
538
- props?: JSX.HTMLAttributes<never> & {
539
- /**
540
- * 表单数据
541
- */
542
- data: object;
543
-
544
- /**
545
- * 表单校验规则
546
- */
547
- rules: ValidateRules;
548
-
549
- /**
550
- * label 对齐方式
551
- */
552
- align?: 'left' | 'top' | 'right';
553
-
554
- /**
555
- * 标签宽度
556
- */
557
- labelWidth?: string;
558
-
559
- /**
560
- * 外部调用接口
561
- */
562
- api?: (api: FormApi) => void;
563
- },
564
- ) => {
565
- let form: HTMLFormElement;
566
-
567
- onMount(() => {
568
- // 初始化外部调用接口
569
- props.api &&
570
- props.api(
571
- (form.api = {
572
- validate: (filter?: (target: ValidateTarget) => void | boolean) =>
573
- validate(form, props.rules, props.data, filter),
574
- scrollToError: scrollToError.bind(form),
575
- clearErrors: clearErrors.bind(form),
576
- }),
577
- );
578
- });
579
-
585
+ export const Form = (props?: JSX.HTMLAttributes<never> & FormProps) => {
580
586
  return (
581
- <form ref={form as any} {...omitProps(props, OMIT_FORM_PROPS)}>
587
+ <form
588
+ ref={(dom) => {
589
+ // 初始化外部调用接口
590
+ props.api &&
591
+ props.api(
592
+ (dom.api = {
593
+ validate: (filter?: (target: ValidateTarget) => void | boolean) =>
594
+ validate(dom, props.rules, props.data, filter),
595
+ scrollToError: scrollToError.bind(dom),
596
+ clearErrors: clearErrors.bind(dom),
597
+ }),
598
+ );
599
+ }}
600
+ {...omitProps(props, OMIT_FORM_PROPS)}
601
+ >
582
602
  <FormContext.Provider value={props}>{props.children}</FormContext.Provider>
583
603
  </form>
584
604
  );
@@ -29,7 +29,7 @@ const switchMonth = (value: [year: number, month: number], offset: 1 | -1) => {
29
29
  return [year, month];
30
30
  };
31
31
 
32
- const MONTH_LIST = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
32
+ const MONTH_LIST = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4];
33
33
 
34
34
  export const MonthWidget = (
35
35
  props: Omit<JSX.HTMLAttributes<never>, 'children'> & {
@@ -54,9 +54,9 @@ export const MonthWidget = (
54
54
  const [currentValue, setCurrentValue] = createSignal(selectedValue() || getCurrentMonth());
55
55
 
56
56
  return (
57
- <div>
58
- <div class="monthwidget-header">
59
- <div ref={domTitle as any} class="monthwidget-title">
57
+ <div class={combineClass('monthwidget datewidget', props.class)}>
58
+ <div class="datewidget-header">
59
+ <div ref={domTitle as any} class="datewidget-title">
60
60
  {replaceTemplate(i18n.Month, currentValue()[0], formatMonth(currentValue()[1]))}
61
61
  </div>
62
62
  <svg class="icon icon-s" aria-hidden={true} onclick={() => setCurrentValue(switchMonth(selectedValue(), -1))}>
@@ -68,7 +68,7 @@ export const MonthWidget = (
68
68
  </div>
69
69
  <div
70
70
  ref={domBody as any}
71
- class="monthwidget-body"
71
+ class="datewidget-body"
72
72
  onclick={(event) => {
73
73
  let target = event.target as HTMLElement;
74
74
  let value, onValueChange;
@@ -155,6 +155,10 @@ const OMIT_PROPS = ['onPopup', 'api', 'children'] as const;
155
155
  * 弹出层外部访问接口
156
156
  */
157
157
  export interface PopupApi {
158
+ /**
159
+ * 是否已经弹出
160
+ */
161
+ popup: boolean;
158
162
  /**
159
163
  * 打开弹出框
160
164
  */
@@ -162,7 +166,7 @@ export interface PopupApi {
162
166
  /**
163
167
  * 关闭弹出框
164
168
  */
165
- closePupup(): void;
169
+ closePopup(): void;
166
170
  /**
167
171
  * 显示或关闭弹出层
168
172
  */
@@ -192,8 +196,11 @@ export const Popup = (props?: JSX.HTMLAttributes<never> & PopupProps) => {
192
196
  // 初始化外部调用接口
193
197
  props.api &&
194
198
  props.api({
199
+ get popup() {
200
+ return currentPopup.dom === popup;
201
+ },
195
202
  openPopup: () => showPopup(popup, props.onPopup),
196
- closePupup: () => currentPopup.dom === popup && hidePopup(),
203
+ closePopup: () => currentPopup.dom === popup && hidePopup(),
197
204
  togglePopup: () => togglePopup(popup, props.onPopup),
198
205
  });
199
206
 
@@ -1,8 +1,24 @@
1
1
  import { JSX } from '../jsx';
2
- import { combineClass, omitProps } from '../reactive';
2
+ import { combineClass, omitProps, useContext } from '../reactive';
3
+ import { FormItemContext } from './provider';
3
4
 
4
- const OMIT_PROPS = ['class'] as const;
5
+ const OMIT_PROPS = ['class', 'value', 'onchange'] as const;
5
6
 
6
- export const TextBox = (props?: JSX.SvgSVGAttributes<never>) => {
7
- return <input type="text" class={combineClass('textbox', props.class)} {...omitProps(props, OMIT_PROPS)}></input>;
7
+ export const TextBox = (props?: JSX.InputHTMLAttributes<never>) => {
8
+ const formItem = useContext(FormItemContext);
9
+
10
+ const initFormItem = (dom: HTMLInputElement) => {
11
+ dom.addEventListener('change', () => formItem.setValue(dom.value));
12
+ formItem.init(dom);
13
+ };
14
+
15
+ return (
16
+ <input
17
+ ref={formItem && initFormItem}
18
+ type="text"
19
+ class={combineClass('textbox', props.class)}
20
+ value={formItem ? formItem.getValue() : props.value}
21
+ {...omitProps(props, OMIT_PROPS)}
22
+ ></input>
23
+ );
8
24
  };
@@ -2,7 +2,31 @@
2
2
 
3
3
  import { createContext } from '../reactive';
4
4
 
5
+ /**
6
+ * 表单项提供者值类型
7
+ */
8
+ export interface FormItemProviderValue {
9
+ /**
10
+ * 获取绑定值
11
+ */
12
+ getValue(): any;
13
+
14
+ /**
15
+ * 设置绑定值
16
+ *
17
+ * @param value 绑定值
18
+ */
19
+ setValue(value: any): void;
20
+
21
+ /**
22
+ * 初始化表单输入组件
23
+ *
24
+ * @param input 表单输入组件
25
+ */
26
+ init(input: HTMLElement): boolean;
27
+ }
28
+
5
29
  /**
6
30
  * 表单项上下文
7
31
  */
8
- export const FormItemContext = createContext<any>();
32
+ export const FormItemContext = createContext<FormItemProviderValue>();