tantee-nuxt-commons 0.0.169 → 0.0.171

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.
@@ -2,143 +2,39 @@
2
2
  import { DateTime } from "luxon";
3
3
  import { computed } from "vue";
4
4
 
5
- type Unit = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds';
6
- type Locale = 'th' | 'en' | 'en-US' | 'th-TH';
5
+ type Locale = "th" | "en" | "en-US" | "th-TH";
7
6
 
8
7
  interface Props {
9
8
  modelValue: DateTime;
10
9
  endDate?: DateTime;
11
10
  locale?: Locale;
12
- showSuffix?: boolean;
13
- units?: Unit[];
14
- zeroBase?: boolean; // true = start at 0, false = start at 1 (inclusive)
11
+ zeroBase?: boolean; // true = start at 0, false = start at 1
15
12
  }
16
13
 
17
14
  const props = withDefaults(defineProps<Props>(), {
18
- locale: 'th',
19
- showSuffix: true,
15
+ locale: "th",
20
16
  zeroBase: true,
21
17
  });
22
18
 
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
- };
19
+ const normalizeLocale = (locale: Locale): "en" | "th" =>
20
+ locale.startsWith("en") ? "en" : "th";
29
21
 
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
22
+ const countDate = computed(() => {
23
+ const base = props.endDate ?? DateTime.now();
66
24
  const baseLocale = normalizeLocale(props.locale);
67
25
 
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
- });
26
+ let days = Math.floor(base.diff(props.modelValue, "days").days ?? 0);
120
27
 
121
- // สร้างข้อความจากหน่วยที่มีค่า > 0
122
- const parts = adjusted
123
- .filter((v) => v.raw > 0)
124
- .map((v) => `${v.raw} ${labelFor(v.unit, v.raw)}`);
28
+ if (!props.zeroBase) days += 1;
125
29
 
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}` : ''}`;
30
+ if (baseLocale === "en") {
31
+ return days === 1 ? "1 day" : `${days} days`;
132
32
  }
133
33
 
134
- const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
135
- return `${parts.join(' ')}${suffix ? ` ${suffix}` : ''}`;
34
+ return `${days} วัน`;
136
35
  });
137
36
  </script>
138
37
 
139
38
  <template>
140
- <span>
141
- <i v-if="props.zeroBase" class="mdi mdi-clock-time-twelve-outline"></i>
142
- <span v-if="props.zeroBase">&nbsp;</span>{{ outputText }}
143
- </span>
39
+ <span>{{ countDate }}</span>
144
40
  </template>
@@ -15,12 +15,12 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof PDF['$props']> {
15
15
  }
16
16
 
17
17
  const props = withDefaults(defineProps<Props>(), {
18
- base64String: '',
19
- title: '',
20
- fileName: '',
18
+ base64String: "",
19
+ title: "",
20
+ fileName: "",
21
21
  disabled: false,
22
22
  isPrint: false,
23
- showBackToTopBtn: false,
23
+ showBackToTopBtn: false
24
24
  })
25
25
 
26
26
  const emit = defineEmits(['closeDialog'])
@@ -33,34 +33,25 @@ const generateUniqueId = (): string => {
33
33
  }
34
34
 
35
35
  const downloadPdf = (): void => {
36
- try {
37
- if (!props.base64String) alert?.addAlert({ message: 'No Base64 provided', alertType: 'error' })
36
+ const byteString = atob(props.base64String || '')
37
+ const byteArray = new Uint8Array(byteString.length)
38
38
 
39
- const byteString = atob(props.base64String || '')
40
- const byteArray = new Uint8Array(byteString.length)
41
-
42
- for (let i = 0; i < byteString.length; i++) {
43
- byteArray[i] = byteString.charCodeAt(i)
44
- }
45
-
46
- const blob = new Blob([byteArray], { type: 'application/pdf' })
47
- const link = URL.createObjectURL(blob)
48
- const anchorElement = document.createElement('a')
49
- anchorElement.style.display = 'none'
50
- anchorElement.href = link
51
- anchorElement.download = `${generateUniqueId()}.pdf`
52
-
53
- document.body.appendChild(anchorElement)
54
- anchorElement.click()
55
- URL.revokeObjectURL(link)
56
- document.body.removeChild(anchorElement)
57
- base64.value = ''
58
-
59
- alert?.addAlert({ message: 'Download success', alertType: 'success' })
60
- }
61
- catch (error) {
62
- alert?.addAlert({ message: `Download unsuccess : ${error}`, alertType: 'error' })
39
+ for (let i = 0; i < byteString.length; i++) {
40
+ byteArray[i] = byteString.charCodeAt(i)
63
41
  }
42
+
43
+ const blob = new Blob([byteArray], { type: 'application/pdf' })
44
+ const link = URL.createObjectURL(blob)
45
+ const anchorElement = document.createElement('a')
46
+ anchorElement.style.display = 'none'
47
+ anchorElement.href = link
48
+ anchorElement.download = `${generateUniqueId()}.pdf`
49
+
50
+ document.body.appendChild(anchorElement)
51
+ anchorElement.click()
52
+ URL.revokeObjectURL(link)
53
+ document.body.removeChild(anchorElement)
54
+ base64.value = ''
64
55
  }
65
56
 
66
57
  const printPdf = () => {
@@ -69,7 +60,7 @@ const printPdf = () => {
69
60
  type: 'pdf',
70
61
  base64: true,
71
62
  onPrintDialogClose: endLoadPdf,
72
- onError: (error) => {
63
+ onError: (error: any) => {
73
64
  alert?.addAlert({ message: error, alertType: 'error' })
74
65
  },
75
66
  })
@@ -82,19 +73,18 @@ const endLoadPdf = () => {
82
73
  }
83
74
 
84
75
  const isMobile = () => {
85
- return /Android|Mobi|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Macintosh/i.test(navigator.userAgent)
76
+ return /Android|Mobi|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Macintosh/i.test(navigator.userAgent);
86
77
  }
87
78
 
88
79
  const checkMobileAndPrint = computed(() => {
89
- return !isMobile() && props.isPrint
90
- })
80
+ return !isMobile() && props.isPrint;
81
+ });
91
82
 
92
83
  const setWidthPdf = computed(() => {
93
84
  if (isMobile()) {
94
- return '100%'
95
- }
96
- else {
97
- return '100dvh'
85
+ return "100%"
86
+ } else {
87
+ return "100dvh"
98
88
  }
99
89
  })
100
90
  </script>
@@ -124,10 +114,10 @@ const setWidthPdf = computed(() => {
124
114
  </v-toolbar>
125
115
  <v-card-text class="justify-center h-screen">
126
116
  <PDF
127
- v-bind="$attrs"
128
- :pdf-width="setWidthPdf"
129
- :src="base64"
130
- :show-back-to-top-btn="props.showBackToTopBtn"
117
+ v-bind="$attrs"
118
+ :pdf-width="setWidthPdf"
119
+ :src="base64"
120
+ :show-back-to-top-btn="props.showBackToTopBtn"
131
121
  />
132
122
  </v-card-text>
133
123
  </v-card>
@@ -0,0 +1,31 @@
1
+ /** Core format you wanted to standardize around */
2
+ export interface Base64String {
3
+ base64String?: string;
4
+ }
5
+ export interface Base64Asset extends Base64String {
6
+ id?: number;
7
+ }
8
+ export interface Base64File extends Base64Asset {
9
+ /** Display or server filename; when converting from a File, we use File.name */
10
+ fileName: string;
11
+ originalFileName?: string;
12
+ /** Parsed or inferred MIME type */
13
+ fileType?: string;
14
+ }
15
+ export interface Base64Image {
16
+ imageData: Base64Asset;
17
+ imageTitle?: string;
18
+ imageProps?: Record<string, unknown>;
19
+ }
20
+ /** Ensure we have a well-formed data URL; if raw base64 is provided, wrap it */
21
+ declare function ensureDataUrl(base64Data: string, contentType?: string): string;
22
+ export declare const useAssetFile: () => {
23
+ hydrateAssetFile: <T extends Base64Asset>(assetFile: T) => Promise<T>;
24
+ base64ToFile: (base64Data: string, filename: string, defaultContentType?: string) => File | undefined;
25
+ fileToBase64: (file: File, maxFileSizeMB?: number) => Promise<Base64File>;
26
+ fileToBase64String: (file: File, maxFileSizeMB?: number) => Promise<Base64String>;
27
+ downloadBase64File: (base64Data: string, filename: string) => void;
28
+ downloadAsset: (input: string | Base64File | Base64Asset, filename?: string, defaultContentType?: string) => void;
29
+ ensureDataUrl: typeof ensureDataUrl;
30
+ };
31
+ export {};
@@ -0,0 +1,134 @@
1
+ import { useGraphQlOperation } from "./graphqlOperation.js";
2
+ const DATA_URL_RE = /^data:([^;]+);base64,(.+)$/i;
3
+ function parseDataUrl(input) {
4
+ const match = input.match(DATA_URL_RE);
5
+ if (!match) return null;
6
+ const [, contentType, payload] = match;
7
+ return { contentType, payload };
8
+ }
9
+ function base64ToBytes(base64Payload) {
10
+ const normalized = base64Payload.replace(/-/g, "+").replace(/_/g, "/");
11
+ const binaryStr = atob(normalized);
12
+ const len = binaryStr.length;
13
+ const bytes = new Uint8Array(len);
14
+ const CHUNK = 32768;
15
+ for (let i = 0; i < len; i += CHUNK) {
16
+ const end = Math.min(i + CHUNK, len);
17
+ for (let j = i; j < end; j++) {
18
+ bytes[j] = binaryStr.charCodeAt(j);
19
+ }
20
+ }
21
+ return bytes;
22
+ }
23
+ function bytesToFile(bytes, filename, contentType) {
24
+ try {
25
+ return new File([bytes], filename, { type: contentType });
26
+ } catch {
27
+ return new Blob([bytes], { type: contentType });
28
+ }
29
+ }
30
+ function ensureDataUrl(base64Data, contentType = "application/octet-stream") {
31
+ if (DATA_URL_RE.test(base64Data)) return base64Data;
32
+ return `data:${contentType};base64,${base64Data}`;
33
+ }
34
+ function extensionFromMime(mime) {
35
+ const map = {
36
+ "image/png": "png",
37
+ "image/jpeg": "jpg",
38
+ "image/jpg": "jpg",
39
+ "image/gif": "gif",
40
+ "image/webp": "webp",
41
+ "application/pdf": "pdf",
42
+ "text/plain": "txt"
43
+ };
44
+ return map[mime];
45
+ }
46
+ export const useAssetFile = () => {
47
+ async function hydrateAssetFile(assetFile) {
48
+ if (!assetFile?.base64String && assetFile?.id != null) {
49
+ const result = await useGraphQlOperation(
50
+ "Query",
51
+ "assetFileById",
52
+ ["id", "base64String"],
53
+ { id: assetFile.id },
54
+ false
55
+ );
56
+ if (result?.base64String) assetFile.base64String = result.base64String;
57
+ }
58
+ return assetFile;
59
+ }
60
+ function base64ToFile(base64Data, filename, defaultContentType = "application/octet-stream") {
61
+ try {
62
+ const parsed = parseDataUrl(base64Data);
63
+ const contentType = parsed?.contentType ?? defaultContentType;
64
+ const payload = parsed?.payload ?? base64Data;
65
+ const bytes = base64ToBytes(payload);
66
+ return bytesToFile(bytes, filename, contentType);
67
+ } catch (error) {
68
+ console.error("Invalid base64 data", error);
69
+ return void 0;
70
+ }
71
+ }
72
+ function fileToBase64(file, maxFileSizeMB = 5) {
73
+ const maxSize = maxFileSizeMB * 1048576;
74
+ return new Promise((resolve, reject) => {
75
+ if (file.size > maxSize) {
76
+ reject(`File (${file.name}) size exceeds the ${maxFileSizeMB} MB limit.`);
77
+ return;
78
+ }
79
+ const reader = new FileReader();
80
+ reader.onload = (event) => {
81
+ const dataUrl = event.target?.result;
82
+ resolve({
83
+ fileName: file.name,
84
+ originalFileName: file.name,
85
+ base64String: dataUrl,
86
+ fileType: file.type || parseDataUrl(dataUrl)?.contentType
87
+ });
88
+ };
89
+ reader.onerror = reject;
90
+ reader.readAsDataURL(file);
91
+ });
92
+ }
93
+ async function fileToBase64String(file, maxFileSizeMB = 5) {
94
+ const res = await fileToBase64(file, maxFileSizeMB);
95
+ return { base64String: res.base64String };
96
+ }
97
+ function downloadBase64File(base64Data, filename) {
98
+ const file = base64ToFile(base64Data, filename);
99
+ if (!file) return;
100
+ const url = URL.createObjectURL(file);
101
+ const link = document.createElement("a");
102
+ link.href = url;
103
+ link.download = filename;
104
+ document.body.appendChild(link);
105
+ link.click();
106
+ document.body.removeChild(link);
107
+ setTimeout(() => URL.revokeObjectURL(url), 0);
108
+ }
109
+ function downloadAsset(input, filename, defaultContentType = "application/octet-stream") {
110
+ let base64 = typeof input === "string" ? input : input.base64String;
111
+ if (!base64) return;
112
+ if (!filename) {
113
+ const parsed2 = parseDataUrl(base64);
114
+ const ext = parsed2 ? extensionFromMime(parsed2.contentType) : void 0;
115
+ filename = typeof input !== "string" && input.fileName || `download${ext ? "." + ext : ""}`;
116
+ }
117
+ const parsed = parseDataUrl(base64);
118
+ if (!parsed) {
119
+ base64 = ensureDataUrl(base64, defaultContentType);
120
+ }
121
+ downloadBase64File(base64, filename);
122
+ }
123
+ return {
124
+ // core
125
+ hydrateAssetFile,
126
+ base64ToFile,
127
+ fileToBase64,
128
+ fileToBase64String,
129
+ downloadBase64File,
130
+ downloadAsset,
131
+ // helpers (exported in case you want them elsewhere)
132
+ ensureDataUrl
133
+ };
134
+ };
@@ -34,16 +34,13 @@ function guessWidth(width) {
34
34
  export function processTemplateFormTable(item, parentTemplates, dataVariable) {
35
35
  let tableOptions = Object.assign({ title: item.inputLabel || "", formTemplate: "" }, item.inputOptions);
36
36
  let tableHeader = tableOptions.headers || [];
37
+ if (!tableHeader.some((h) => h.key === "action")) tableHeader.push({ title: "Action", key: "action", width: "100px" });
37
38
  let tableItemTemplate = "";
38
39
  tableHeader.forEach((h) => {
39
40
  if (h.template) {
40
41
  tableItemTemplate += `\r
41
42
  <template #item.${h.key}="props">${h.template}</template>`;
42
- } else {
43
- tableItemTemplate += `\r
44
- <template #item.${h.key}="props">{{autoSanitizedDisplay(props.item.${h.key})}}</template>`;
45
43
  }
46
44
  });
47
- if (!tableHeader.some((h) => h.key === "action")) tableHeader.push({ title: "Action", key: "action", width: "100px" });
48
45
  return processDefaultTemplate(item, `<template #form="{data,rules}">${useDocumentTemplate(tableOptions.formTemplate)}</template>${tableItemTemplate}`, `title="${tableOptions.title}" :headers='${escapeObjectForInlineBinding(tableHeader)}'`, void 0, dataVariable);
49
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tantee-nuxt-commons",
3
- "version": "0.0.169",
3
+ "version": "0.0.171",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",