@ramathibodi/nuxt-commons 0.1.63 → 0.1.65

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/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.63",
7
+ "version": "0.1.65",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -45,6 +45,7 @@ watch(() => alert?.hasAlert(), (hasAlert) => {
45
45
  elevation="2"
46
46
  theme="dark"
47
47
  variant="flat"
48
+ v-bind="currentItem.alertIcon ? { icon: currentItem.alertIcon } : {}"
48
49
  @click:close="renewAlert()"
49
50
  >
50
51
  {{ currentItem.statusCode ? currentItem.statusCode + ' ' : '' }} {{ currentItem.message }} {{ !isEmpty(currentItem.data) ? currentItem.data : '' }}
@@ -1,14 +1,14 @@
1
1
  <script lang="ts" setup>
2
2
  import '@vuepic/vue-datepicker/dist/main.css'
3
- import {computed, nextTick, onMounted, ref, watch, watchEffect} from 'vue'
3
+ import {computed, onMounted, ref} from 'vue'
4
4
  import {VTextField} from "vuetify/components/VTextField";
5
- import {isArray, isString} from "lodash-es";
6
- import {useRules} from "../../composables/utils/validation";
5
+ import {isArray} from "lodash-es";
7
6
 
8
7
  interface IDobPrecision {
9
8
  yearMonthDay: string
10
9
  yearMonth: string
11
10
  year: string
11
+ estimated: string
12
12
  }
13
13
 
14
14
  interface Props {
@@ -31,10 +31,11 @@ const dobPrecisionText = computed(() => {
31
31
  yearMonthDay: 'YMD',
32
32
  yearMonth: 'YM',
33
33
  year: 'Y',
34
+ estimated: 'EST',
34
35
  }
35
36
  })
36
- const dobPrecisionChoice = ref<('yearMonthDay' | 'yearMonth' | 'year')[]>(['yearMonthDay', 'yearMonth', 'year'])
37
- const dobPrecisionSelected = defineModel<'yearMonthDay' | 'yearMonth' | 'year'>('dobPrecision')
37
+ const dobPrecisionChoice = ref<('yearMonthDay' | 'yearMonth' | 'year' | 'estimated')[]>(['yearMonthDay', 'yearMonth', 'year', 'estimated'])
38
+ const dobPrecisionSelected = defineModel<'yearMonthDay' | 'yearMonth' | 'year' | 'estimated'>('dobPrecision')
38
39
  onMounted(()=>{
39
40
  if (!dobPrecisionSelected.value) {
40
41
  dobPrecisionSelected.value = 'yearMonthDay'
@@ -70,6 +71,9 @@ const dobFormat = computed(() => {
70
71
  if (dobPrecisionSelected.value=="year") {
71
72
  displayFormat = "yyyy"
72
73
  }
74
+ if (dobPrecisionSelected.value=="estimated") {
75
+ displayFormat = "~ MMM yyyy"
76
+ }
73
77
  return displayFormat
74
78
  })
75
79
  </script>
@@ -15,7 +15,7 @@ import {
15
15
  import {watchDebounced} from '@vueuse/core'
16
16
  import {useRules} from '../../composables/utils/validation'
17
17
  import {useDocumentTemplate} from '../../composables/document/template'
18
- import { isObject, isArray, isString, isPlainObject, isEqual } from 'lodash-es'
18
+ import { isObject, isArray, isString, isPlainObject, isEqual, debounce } from 'lodash-es'
19
19
  import FormPad from './Pad.vue'
20
20
 
21
21
  defineOptions({
@@ -87,7 +87,9 @@ function isBlankString(v: unknown): v is string {
87
87
  return isString(v) && v.trim().length === 0
88
88
  }
89
89
 
90
- function sanitizeBlankStrings(val: any, original?: any): void {
90
+ const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw, 500)
91
+
92
+ function sanitizeBlankStringsRaw(val: any, original?: any): void {
91
93
  if (!original && props.originalData) {
92
94
  sanitizeBlankStrings(val, props.originalData)
93
95
  return
@@ -101,7 +103,7 @@ function sanitizeBlankStrings(val: any, original?: any): void {
101
103
  } else if (isPlainObject(item) || isArray(item)) {
102
104
  sanitizeBlankStrings(item,{})
103
105
  } else {
104
- if (item && typeof item.trimEnd == "function") val[i] = item.trimEnd()
106
+ if (item && typeof item === 'string') val[i] = item.replace(/[ \t]+$/, '')
105
107
  }
106
108
  }
107
109
  return
@@ -117,7 +119,7 @@ function sanitizeBlankStrings(val: any, original?: any): void {
117
119
  let originalChild = (original && (isPlainObject(original[key]) || isArray(original[key]))) ? original[key] : {}
118
120
  sanitizeBlankStrings(v, originalChild)
119
121
  } else {
120
- if (v && typeof v.trimEnd == "function") val[key] = v.trimEnd()
122
+ if (v && typeof v === 'string') val[key] = v.replace(/[ \t]+$/, '')
121
123
  }
122
124
  }
123
125
  }
@@ -2,39 +2,142 @@
2
2
  import { DateTime } from "luxon";
3
3
  import { computed } from "vue";
4
4
 
5
- type Locale = "th" | "en" | "en-US" | "th-TH";
5
+ type Unit = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds';
6
+ type Locale = 'th' | 'en' | 'en-US' | 'th-TH';
6
7
 
7
8
  interface Props {
8
9
  modelValue: DateTime;
9
10
  endDate?: DateTime;
10
11
  locale?: Locale;
11
- zeroBase?: boolean; // true = start at 0, false = start at 1
12
+ showSuffix?: boolean;
13
+ units?: Unit[];
14
+ zeroBase?: boolean; // true = start at 0, false = start at 1 (inclusive)
12
15
  }
13
16
 
14
17
  const props = withDefaults(defineProps<Props>(), {
15
- locale: "th",
18
+ locale: 'th',
19
+ showSuffix: true,
16
20
  zeroBase: true,
17
21
  });
18
22
 
19
- const normalizeLocale = (locale: Locale): "en" | "th" =>
20
- locale.startsWith("en") ? "en" : "th";
23
+ // Fallback map: map complex locale (e.g., en-US) to base (e.g., en)
24
+ const normalizeLocale = (locale: Locale): 'en' | 'th' => {
25
+ if (locale.startsWith('en')) return 'en';
26
+ if (locale.startsWith('th')) return 'th';
27
+ return 'th'; // default fallback
28
+ };
21
29
 
22
- const countDate = computed(() => {
23
- const base = props.endDate ?? DateTime.now();
30
+ const labelsEnPlural: Record<Unit, string> = {
31
+ years: 'years',
32
+ months: 'months',
33
+ days: 'days',
34
+ hours: 'hours',
35
+ minutes: 'minutes',
36
+ seconds: 'seconds',
37
+ };
38
+ const labelsEnSingular: Record<Unit, string> = {
39
+ years: 'year',
40
+ months: 'month',
41
+ days: 'day',
42
+ hours: 'hour',
43
+ minutes: 'minute',
44
+ seconds: 'second',
45
+ };
46
+
47
+ const localizedLabels: Record<'en' | 'th', Record<Unit, string>> = {
48
+ en: labelsEnPlural, // จะสลับเป็น singular ตามค่าจริงตอน render
49
+ th: {
50
+ years: 'ปี',
51
+ months: 'เดือน',
52
+ days: 'วัน',
53
+ hours: 'ชั่วโมง',
54
+ minutes: 'นาที',
55
+ seconds: 'วินาที',
56
+ },
57
+ };
58
+
59
+ const localizedSuffix: Record<'en' | 'th', string> = {
60
+ en: 'ago',
61
+ th: 'ที่ผ่านมา',
62
+ };
63
+
64
+ const outputText = computed(() => {
65
+ const base = props.endDate ?? DateTime.now(); // ใช้ endDate ถ้ามี ไม่งั้น now
24
66
  const baseLocale = normalizeLocale(props.locale);
25
67
 
26
- let days = Math.floor(base.diff(props.modelValue, "days").days ?? 0);
68
+ const units: Unit[] = props.units?.length
69
+ ? props.units
70
+ : ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
71
+
72
+ const diffObj = base.diff(props.modelValue, units).toObject();
73
+
74
+ // helper: คืน label ตาม singular/plural (เฉพาะ en)
75
+ const labelFor = (unit: Unit, value: number) => {
76
+ if (baseLocale === 'en') {
77
+ return value === 1 ? labelsEnSingular[unit] : labelsEnPlural[unit];
78
+ }
79
+ return localizedLabels[baseLocale][unit];
80
+ };
81
+
82
+ // ---------- โหมด single unit ----------
83
+ if (!props.units) {
84
+ const foundUnit = units.find((unit) => (diffObj[unit] ?? 0) >= 1);
85
+ if (foundUnit) {
86
+ const raw = Math.floor(diffObj[foundUnit] ?? 0); // >= 1
87
+ const value = props.zeroBase ? raw : raw + 1; // inclusive (+1)
88
+ const label = labelFor(foundUnit, value);
89
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
90
+ return `${value} ${label}${suffix ? ` ${suffix}` : ''}`;
91
+ }
92
+
93
+ // ถ้าไม่มีหน่วยใด >= 1 ให้ใช้หน่วยเล็กสุด
94
+ const lastUnit = units.at(-1)!;
95
+ const raw = Math.max(0, Math.floor(diffObj[lastUnit] ?? 0));
96
+ const value = props.zeroBase ? raw : 1; // inclusive: อย่างน้อย 1
97
+ const label = labelFor(lastUnit, value);
98
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
99
+ return `${value} ${label}${suffix ? ` ${suffix}` : ''}`;
100
+ }
101
+
102
+ // ---------- โหมด multi-unit ----------
103
+ // เก็บค่าแบบตัวเลขก่อน แล้วค่อยเรนเดอร์
104
+ const values = units.map((unit) => ({
105
+ unit,
106
+ raw: Math.max(0, Math.floor(diffObj[unit] ?? 0)),
107
+ }));
108
+
109
+ // เลือกหน่วยที่เล็กที่สุด
110
+ const smallest = values[values.length - 1];
111
+
112
+ // ถ้ามีค่าอย่างน้อยหนึ่งหน่วย > 0 และเป็น inclusive (zeroBase=false) => +1 ที่หน่วยเล็กสุด
113
+ const anyPositive = values.some((v) => v.raw > 0);
114
+ const adjusted = values.map((v, idx) => {
115
+ if (!props.zeroBase && anyPositive && idx === values.length - 1) {
116
+ return { ...v, raw: v.raw + 1 };
117
+ }
118
+ return v;
119
+ });
27
120
 
28
- if (!props.zeroBase) days += 1;
121
+ // สร้างข้อความจากหน่วยที่มีค่า > 0
122
+ const parts = adjusted
123
+ .filter((v) => v.raw > 0)
124
+ .map((v) => `${v.raw} ${labelFor(v.unit, v.raw)}`);
29
125
 
30
- if (baseLocale === "en") {
31
- return days === 1 ? "1 day" : `${days} days`;
126
+ // หากทั้งหมดเป็น 0:
127
+ if (parts.length === 0) {
128
+ const minValue = props.zeroBase ? 0 : 1;
129
+ const label = labelFor(smallest.unit, minValue);
130
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
131
+ return `${minValue} ${label}${suffix ? ` ${suffix}` : ''}`;
32
132
  }
33
133
 
34
- return `${days} วัน`;
134
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
135
+ return `${parts.join(' ')}${suffix ? ` ${suffix}` : ''}`;
35
136
  });
36
137
  </script>
37
138
 
38
139
  <template>
39
- <span>{{ countDate }}</span>
140
+ <span>
141
+ <i v-if="props.zeroBase" class="mdi mdi-clock-time-twelve-outline" aria-hidden="true" ></i>
142
+ {{ outputText }}</span>
40
143
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",