@neatui/vue 3.0.0 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatui/vue",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -1,34 +1,64 @@
1
1
  <template>
2
2
  <div
3
+ ref="root"
3
4
  class="fekit-ipull-scroll"
4
5
  :ui-scroll="`:y ${stable ? 'stable' : ''}`"
5
- ref="root"
6
6
  >
7
7
  <div>
8
- <div class="fs-xs co-text o-mm" fekit-pullload-head="" v-if="pull">
9
- <slot name="pull-head" :state="demo.store.pull">
10
- <div v-show="demo.store.pull < 2" ui-flex="row cm">
11
- <i class="pulldown icon icon-pulldown" :class="`${demo.store.pull === 1 ? 'toup' : ''}`"></i>
12
- <span v-show="demo.store.pull === 0">下拉刷新</span>
13
- <span v-show="demo.store.pull === 1">松开加载</span>
14
- </div>
15
- <div v-show="demo.store.pull === 2" ui-flex="row cm">
16
- <span ui-load="@d"></span>
17
- </div>
18
- <div v-show="demo.store.pull === 3" ui-flex="row cm">
19
- <span>刷新完成</span>
20
- </div>
8
+ <div v-if="pull" class="co-text o-mm" fekit-pullload-head="">
9
+ <slot name="pull-head" :state="demo.store.pull" :text="pullText()" :locale="words" :pullload="demo">
10
+ <slot v-if="demo.store.pull === 0" name="pull-idle" :state="demo.store.pull" :text="pullText('idle')" :locale="words" :pullload="demo">
11
+ <div ui-flex="row cm">
12
+ <i class="pulldown icon icon-pulldown"></i>
13
+ <span>{{ pullText("idle") }}</span>
14
+ </div>
15
+ </slot>
16
+ <slot v-else-if="demo.store.pull === 1" name="pull-ready" :state="demo.store.pull" :text="pullText('ready')" :locale="words" :pullload="demo">
17
+ <div ui-flex="row cm">
18
+ <i class="pulldown icon icon-pulldown toup"></i>
19
+ <span>{{ pullText("ready") }}</span>
20
+ </div>
21
+ </slot>
22
+ <slot v-else-if="demo.store.pull === 2" name="pull-loading" :state="demo.store.pull" :text="pullText('loading')" :locale="words" :pullload="demo">
23
+ <div ui-flex="row cm">
24
+ <span ui-load="@d"></span>
25
+ <span v-if="pullText('loading')">{{ pullText("loading") }}</span>
26
+ </div>
27
+ </slot>
28
+ <slot v-else-if="demo.store.pull === 3" name="pull-finished" :state="demo.store.pull" :text="pullText('finished')" :locale="words" :pullload="demo">
29
+ <div ui-flex="row cm">
30
+ <span>{{ pullText("finished") }}</span>
31
+ </div>
32
+ </slot>
21
33
  </slot>
22
34
  </div>
23
35
  <slot></slot>
24
- <div class="fs-xs co-text o-mm my-sm" fekit-pullload-foot="" v-if="load">
25
- <slot name="load-foot" :state="demo.store.load">
36
+ <div v-if="load" class="co-text o-mm my-ms" fekit-pullload-foot="">
37
+ <slot name="load-foot" :state="demo.store.load" :text="loadText()" :locale="words" :pullload="demo">
26
38
  <transition name="fekit-pullload-fade" mode="out-in">
27
- <div v-if="demo.store.load === 2" ui-flex="row cm">
28
- <span ui-load="@d"></span>
29
- </div>
30
- <div v-else-if="demo.store.load === 3">没有更多了</div>
31
- <div v-else>&nbsp;</div>
39
+ <slot v-if="demo.store.load === 2" name="load-loading" :state="demo.store.load" :text="loadText('loading')" :locale="words" :pullload="demo">
40
+ <div ui-flex="row cm">
41
+ <span ui-load="@d"></span>
42
+ <span v-if="loadText('loading')">{{ loadText("loading") }}</span>
43
+ </div>
44
+ </slot>
45
+ <slot v-else-if="demo.store.load === 3" name="load-over" :state="demo.store.load" :text="loadText('over')" :locale="words" :pullload="demo">
46
+ <div class="fekit-pullload-over" ui-flex="row cm">
47
+ <span>{{ loadText("over") }}</span>
48
+ </div>
49
+ </slot>
50
+ <slot v-else-if="demo.store.load === 1" name="load-loaded" :state="demo.store.load" :text="loadText('loaded')" :locale="words" :pullload="demo">
51
+ <div ui-flex="row cm">
52
+ <span v-if="loadText('loaded')">{{ loadText("loaded") }}</span>
53
+ <span v-else>&nbsp;</span>
54
+ </div>
55
+ </slot>
56
+ <slot v-else name="load-idle" :state="demo.store.load" :text="loadText('idle')" :locale="words" :pullload="demo">
57
+ <div ui-flex="row cm">
58
+ <span v-if="loadText('idle')">{{ loadText("idle") }}</span>
59
+ <span v-else>&nbsp;</span>
60
+ </div>
61
+ </slot>
32
62
  </transition>
33
63
  </slot>
34
64
  </div>
@@ -36,8 +66,22 @@
36
66
  </div>
37
67
  </template>
38
68
  <script setup lang="ts">
39
- import { onMounted, onBeforeUnmount, ref } from "vue";
69
+ import { computed, nextTick, onMounted, onBeforeUnmount, ref, watch } from "vue";
40
70
  const emits = defineEmits(["onpull", "onload"]);
71
+ const defaultLocale = {
72
+ pull: {
73
+ idle: "下拉刷新",
74
+ ready: "松开加载",
75
+ loading: "",
76
+ finished: "刷新完成",
77
+ },
78
+ load: {
79
+ idle: "",
80
+ loaded: "",
81
+ loading: "",
82
+ over: "没有更多了",
83
+ },
84
+ };
41
85
  const props: any = defineProps({
42
86
  pull: {
43
87
  type: Boolean,
@@ -51,10 +95,264 @@ const props: any = defineProps({
51
95
  type: Boolean,
52
96
  default: false,
53
97
  },
98
+ loadPos: {
99
+ type: [Number, String],
100
+ default: null,
101
+ },
102
+ loadPosition: {
103
+ type: [Number, String],
104
+ default: null,
105
+ },
106
+ loadOffset: {
107
+ type: [Number, String],
108
+ default: null,
109
+ },
110
+ initialLoad: {
111
+ type: [Boolean, String],
112
+ default: false,
113
+ },
114
+ locale: {
115
+ type: Object,
116
+ default: () => ({}),
117
+ },
54
118
  });
55
119
  const root: any = ref(null);
56
120
  const demo: any = ref({ store: { pull: 0, load: 0 } });
57
121
  let alive = true;
122
+ let removeLoadOffsetListener = () => {};
123
+ let checkingLoad = false;
124
+ let loadPending = false;
125
+ let initialPreloadDone = false;
126
+ let pullLoadListening = false;
127
+ let removePullLoadListener = () => {};
128
+ let resizeObserver: any = null;
129
+ let pullLoadReady: Promise<any> | null = null;
130
+
131
+ const words = computed(() => ({
132
+ pull: {
133
+ ...defaultLocale.pull,
134
+ ...(props.locale?.pull || {}),
135
+ },
136
+ load: {
137
+ ...defaultLocale.load,
138
+ ...(props.locale?.load || {}),
139
+ },
140
+ }));
141
+
142
+ const pullStatusMap: any = {
143
+ 0: "idle",
144
+ 1: "ready",
145
+ 2: "loading",
146
+ 3: "finished",
147
+ };
148
+
149
+ const loadStatusMap: any = {
150
+ 0: "idle",
151
+ 1: "loaded",
152
+ 2: "loading",
153
+ 3: "over",
154
+ };
155
+
156
+ const pullText = (status = pullStatusMap[demo.value?.store?.pull] || "idle") => {
157
+ return words.value.pull?.[status] ?? "";
158
+ };
159
+
160
+ const loadText = (status = loadStatusMap[demo.value?.store?.load] || "idle") => {
161
+ return words.value.load?.[status] ?? "";
162
+ };
163
+
164
+ const numberValue = (value: any) => {
165
+ const num = parseFloat(String(value ?? "").trim());
166
+ return Number.isFinite(num) ? num : 0;
167
+ };
168
+
169
+ const hasValue = (value: any) => value !== null && value !== undefined && value !== "";
170
+
171
+ const boolValue = (value: any) => {
172
+ if (value === false || value === 0) return false;
173
+ const text = String(value ?? "").trim().toLowerCase();
174
+ return !["", "false", "0", "off", "no"].includes(text);
175
+ };
176
+
177
+ const clampRatio = (value: any) => {
178
+ const text = String(value ?? "").trim();
179
+ const ratio = text.endsWith("%") ? numberValue(text) / 100 : numberValue(text);
180
+ return Math.min(1, Math.max(0, ratio > 1 ? ratio / 100 : ratio));
181
+ };
182
+
183
+ const effectiveLoadPos = () => {
184
+ if (hasValue(props.loadPos)) return props.loadPos;
185
+ if (hasValue(props.loadPosition)) return props.loadPosition;
186
+ if (hasValue(props.loadOffset)) return null;
187
+ return 0.8;
188
+ };
189
+
190
+ const loadOffsetPx = (el: HTMLElement) => {
191
+ const text = String(props.loadOffset ?? "").trim();
192
+ const value = numberValue(text);
193
+ if (text.endsWith("%")) return Math.max(0, el.clientHeight * (value / 100));
194
+ return Math.max(0, value);
195
+ };
196
+
197
+ const isInitialLoadEnabled = () => {
198
+ return boolValue(props.initialLoad);
199
+ };
200
+
201
+ const shouldLoad = (el: HTMLElement) => {
202
+ const scrollable = el.scrollHeight - el.clientHeight;
203
+ if (scrollable <= 0) return false;
204
+ const loadPos = effectiveLoadPos();
205
+ if (hasValue(loadPos)) {
206
+ return el.scrollTop / scrollable >= clampRatio(loadPos);
207
+ }
208
+ return scrollable - el.scrollTop <= loadOffsetPx(el);
209
+ };
210
+
211
+ const shouldInitialPreload = (el: HTMLElement) => {
212
+ if (initialPreloadDone || !props.load || demo.value?.store?.load >= 2) return false;
213
+ if (!isInitialLoadEnabled()) return false;
214
+ return el.scrollHeight - el.clientHeight > 0;
215
+ };
216
+
217
+ const emitPull = async () => {
218
+ if (!props.pull) return;
219
+ emits("onpull", demo.value);
220
+ };
221
+
222
+ const emitLoad = async () => {
223
+ if (!props.load) {
224
+ demo.value?.loaded?.();
225
+ return;
226
+ }
227
+ try {
228
+ emits("onload", demo.value);
229
+ } catch (error) {
230
+ resetLoad();
231
+ throw error;
232
+ }
233
+ };
234
+
235
+ const syncPullLoadHandlers = () => {
236
+ if (!demo.value) return;
237
+ demo.value.onpull = props.pull ? emitPull : null;
238
+ demo.value.onload = props.load ? emitLoad : null;
239
+ };
240
+
241
+ const checkLoad = () => {
242
+ const el = root.value;
243
+ if (!props.load || !el || loadPending || demo.value?.store?.pull === 2 || demo.value?.store?.load >= 2) return;
244
+ if (shouldInitialPreload(el)) {
245
+ initialPreloadDone = true;
246
+ triggerLoad();
247
+ return;
248
+ }
249
+ if (shouldLoad(el)) {
250
+ triggerLoad();
251
+ }
252
+ };
253
+
254
+ const triggerLoad = () => {
255
+ if (loadPending || demo.value?.store?.load >= 2) return;
256
+ loadPending = true;
257
+ const run = () => {
258
+ if (!alive || !props.load || demo.value?.store?.load >= 2) {
259
+ loadPending = false;
260
+ return;
261
+ }
262
+ if (typeof demo.value?.isload !== "function") {
263
+ loadPending = false;
264
+ return;
265
+ }
266
+ demo.value.isload();
267
+ };
268
+ if (typeof window !== "undefined" && window.requestAnimationFrame) {
269
+ window.requestAnimationFrame(() => setTimeout(run, 0));
270
+ } else {
271
+ setTimeout(run, 0);
272
+ }
273
+ };
274
+
275
+ const requestCheckLoad = () => {
276
+ if (checkingLoad) return;
277
+ checkingLoad = true;
278
+ const run = () => {
279
+ checkingLoad = false;
280
+ checkLoad();
281
+ };
282
+ if (typeof window !== "undefined" && window.requestAnimationFrame) {
283
+ window.requestAnimationFrame(run);
284
+ } else {
285
+ setTimeout(run, 0);
286
+ }
287
+ };
288
+
289
+ const scheduleCheckLoad = async () => {
290
+ await nextTick();
291
+ if (!alive) return;
292
+ requestCheckLoad();
293
+ };
294
+
295
+ const removeLoadListener = () => {
296
+ removeLoadOffsetListener();
297
+ removeLoadOffsetListener = () => {};
298
+ };
299
+
300
+ const removePullListener = () => {
301
+ removePullLoadListener();
302
+ removePullLoadListener = () => {};
303
+ pullLoadListening = false;
304
+ };
305
+
306
+ const syncPullListener = () => {
307
+ const el = root.value;
308
+ if (!demo.value || !el) return;
309
+ if (!props.pull) {
310
+ removePullListener();
311
+ demo.value?.setcss?.(0);
312
+ if (demo.value?.store) {
313
+ demo.value.store.pull = 0;
314
+ }
315
+ return;
316
+ }
317
+ if (pullLoadListening) return;
318
+ demo.value?.listen?.(el);
319
+ removePullLoadListener = typeof demo.value?.remove === "function" ? () => demo.value.remove() : () => {};
320
+ pullLoadListening = true;
321
+ };
322
+
323
+ const syncLoadListener = () => {
324
+ removeLoadListener();
325
+ const el = root.value;
326
+ if (!props.load) {
327
+ loadPending = false;
328
+ initialPreloadDone = false;
329
+ if (demo.value?.store) {
330
+ demo.value.store.load = 0;
331
+ }
332
+ return;
333
+ }
334
+ if (!el) return;
335
+
336
+ const onChange = () => {
337
+ requestCheckLoad();
338
+ };
339
+ el.addEventListener("scroll", onChange, { passive: true });
340
+ el.addEventListener("touchmove", onChange, { passive: true });
341
+ removeLoadOffsetListener = () => {
342
+ el.removeEventListener("scroll", onChange);
343
+ el.removeEventListener("touchmove", onChange);
344
+ resizeObserver?.disconnect?.();
345
+ resizeObserver = null;
346
+ };
347
+ if (typeof ResizeObserver !== "undefined") {
348
+ resizeObserver = new ResizeObserver(onChange);
349
+ resizeObserver.observe(el);
350
+ if (el.firstElementChild) {
351
+ resizeObserver.observe(el.firstElementChild);
352
+ }
353
+ }
354
+ scheduleCheckLoad();
355
+ };
58
356
 
59
357
  const createPullLoad = async () => {
60
358
  const { default: PullLoad, Easing } = await import("@fekit/pullload");
@@ -63,33 +361,42 @@ const createPullLoad = async () => {
63
361
  friction: 0.3,
64
362
  triggerDistance: 60,
65
363
  loadingDistance: 50,
66
- ...(props.pull
67
- ? {
68
- onpull: async () => {
69
- emits("onpull", demo.value);
70
- },
71
- }
72
- : {}),
73
- ...(props.load
74
- ? {
75
- onload: async () => {
76
- emits("onload", demo.value);
77
- },
78
- }
79
- : {}),
364
+ onpull: props.pull ? emitPull : null,
365
+ // IScrollView owns the scroll threshold, so onload is attached after listen().
366
+ onload: null,
80
367
  });
81
368
  };
82
369
 
370
+ const ensurePullLoad = async () => {
371
+ if (demo.value?.isload || demo.value?.ispull) return demo.value;
372
+ if (!pullLoadReady) {
373
+ pullLoadReady = createPullLoad()
374
+ .then((pullLoad) => {
375
+ if (!alive) {
376
+ pullLoad?.remove?.();
377
+ return null;
378
+ }
379
+ demo.value = pullLoad;
380
+ syncPullLoadHandlers();
381
+ return pullLoad;
382
+ })
383
+ .catch((error) => {
384
+ pullLoadReady = null;
385
+ throw error;
386
+ });
387
+ }
388
+ return pullLoadReady;
389
+ };
390
+
83
391
  onMounted(async () => {
84
- const pullLoad = await createPullLoad();
85
- if (!alive) {
86
- pullLoad?.remove?.();
87
- return;
392
+ if (props.pull || props.load) {
393
+ await ensurePullLoad();
88
394
  }
89
395
 
90
- demo.value = pullLoad;
91
396
  if (root.value) {
92
- demo.value?.listen?.(root.value);
397
+ syncPullLoadHandlers();
398
+ syncPullListener();
399
+ syncLoadListener();
93
400
  if (props.pull) {
94
401
  demo.value?.ispull?.();
95
402
  }
@@ -97,14 +404,92 @@ onMounted(async () => {
97
404
  });
98
405
  onBeforeUnmount(() => {
99
406
  alive = false;
100
- demo.value?.remove?.();
407
+ removePullListener();
408
+ removeLoadListener();
409
+ pullLoadReady = null;
101
410
  });
102
411
 
103
- const pullload = () => {
412
+ const pullload = async () => {
413
+ if (!props.pull) return;
414
+ await ensurePullLoad();
104
415
  demo.value?.ispull?.();
105
416
  };
106
417
 
107
- defineExpose({ pullload, ex: () => demo.value });
418
+ const loadmore = async () => {
419
+ if (!props.load || demo.value?.store?.load >= 2) return;
420
+ await ensurePullLoad();
421
+ demo.value?.isload?.();
422
+ };
423
+
424
+ const loaded = () => {
425
+ loadPending = false;
426
+ demo.value?.loaded?.();
427
+ };
428
+
429
+ const noMore = () => {
430
+ loadPending = false;
431
+ demo.value?.isover?.();
432
+ };
433
+
434
+ const resetLoad = () => {
435
+ loadPending = false;
436
+ if (demo.value?.store) {
437
+ demo.value.store.load = 0;
438
+ }
439
+ initialPreloadDone = false;
440
+ scheduleCheckLoad();
441
+ };
442
+
443
+ const scrollToTop = () => {
444
+ if (root.value) {
445
+ root.value.scrollTop = 0;
446
+ }
447
+ };
448
+
449
+ const pulled = () => {
450
+ demo.value?.pulled?.();
451
+ };
452
+
453
+ watch(
454
+ () => props.pull,
455
+ async () => {
456
+ if (props.pull) {
457
+ await ensurePullLoad();
458
+ }
459
+ syncPullLoadHandlers();
460
+ syncPullListener();
461
+ }
462
+ );
463
+
464
+ watch(
465
+ () => props.load,
466
+ async () => {
467
+ if (props.load) {
468
+ await ensurePullLoad();
469
+ }
470
+ syncPullLoadHandlers();
471
+ syncLoadListener();
472
+ }
473
+ );
474
+
475
+ watch(
476
+ () => [props.loadPos, props.loadPosition, props.loadOffset, props.initialLoad],
477
+ () => {
478
+ initialPreloadDone = false;
479
+ scheduleCheckLoad();
480
+ }
481
+ );
482
+
483
+ watch(
484
+ () => demo.value?.store?.load,
485
+ (load) => {
486
+ if (load !== 2) {
487
+ loadPending = false;
488
+ }
489
+ }
490
+ );
491
+
492
+ defineExpose({ pullload, loadmore, loaded, noMore, resetLoad, scrollToTop, pulled, ex: () => demo.value });
108
493
  </script>
109
494
 
110
495
  <style lang="scss">
@@ -144,5 +529,16 @@ defineExpose({ pullload, ex: () => demo.value });
144
529
  transform: rotate(180deg);
145
530
  }
146
531
  }
532
+
533
+ .fekit-pullload-over {
534
+ gap: 0.75em;
535
+ &::before,
536
+ &::after {
537
+ content: "";
538
+ width: 3em;
539
+ border-top: 1px solid currentColor;
540
+ opacity: 0.35;
541
+ }
542
+ }
147
543
  }
148
544
  </style>