@ramathibodi/nuxt-commons 0.1.13 → 0.1.14

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,5 +4,5 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.13"
7
+ "version": "0.1.14"
8
8
  }
@@ -1,8 +1,8 @@
1
1
  <script lang="ts" setup>
2
- import { ref, withDefaults } from 'vue'
2
+ import {ref, withDefaults} from 'vue'
3
3
  import * as XLSX from 'xlsx'
4
- import { VBtn } from 'vuetify/components/VBtn'
5
- import { useAlert } from '../composables/alert'
4
+ import {VBtn} from 'vuetify/components/VBtn'
5
+ import {useAlert} from '../composables/alert'
6
6
 
7
7
  interface ExportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
8
8
  fileName?: string
@@ -48,7 +48,7 @@ function exportFile() {
48
48
  >
49
49
  <slot
50
50
  :name="name"
51
- v-bind="(slotData as object)"
51
+ v-bind="((slotData || {}) as object)"
52
52
  />
53
53
  </template>
54
54
  </VBtn>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
- import { ref, watch } from 'vue'
3
- import { VBtn } from 'vuetify/components/VBtn'
2
+ import {ref, watch} from 'vue'
3
+ import {VBtn} from 'vuetify/components/VBtn'
4
4
 
5
5
  interface Props extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
6
6
  accept?: string
@@ -48,7 +48,7 @@ defineExpose({ reset })
48
48
  >
49
49
  <slot
50
50
  :name="name"
51
- v-bind="(slotData as object)"
51
+ v-bind="((slotData || {}) as object)"
52
52
  />
53
53
  </template>
54
54
  </v-btn>
@@ -57,7 +57,7 @@ function uploadedFile(files: File[] | File | undefined) {
57
57
  >
58
58
  <slot
59
59
  :name="name"
60
- v-bind="(slotData as object)"
60
+ v-bind="((slotData || {}) as object)"
61
61
  />
62
62
  </template>
63
63
  </FileBtn>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import Datepicker from '@vuepic/vue-datepicker'
3
3
  import '@vuepic/vue-datepicker/dist/main.css'
4
- import {nextTick, ref, watch, watchEffect} from 'vue'
4
+ import {nextTick, ref, watch, watchEffect,computed} from 'vue'
5
5
  import {type dateFormat, Datetime} from '../../utils/datetime'
6
6
 
7
7
  interface IDobPrecision {
@@ -101,6 +101,12 @@ function updateDate(dateString: string | null) {
101
101
  selectedDate.value = null
102
102
  }
103
103
  else {
104
+ if (dobPrecisionSelected.value=="yearMonth") {
105
+ dateTime.luxonDateTime = dateTime.luxonDateTime.startOf('month')
106
+ }
107
+ if (dobPrecisionSelected.value=="year") {
108
+ dateTime.luxonDateTime = dateTime.luxonDateTime.startOf('year')
109
+ }
104
110
  selectedDate.value = dateTime.toFormat('yyyy-MM-dd', 'EN')
105
111
  displayedDate.value = selectedDate.value
106
112
  }
@@ -111,8 +117,16 @@ function updateDate(dateString: string | null) {
111
117
  function formatDate(dateString: string | null) {
112
118
  if (!dateString) return null
113
119
 
120
+ let displayFormat = props.format
121
+ if (dobPrecisionSelected.value=="yearMonth") {
122
+ displayFormat = "MMM yyyy"
123
+ }
124
+ if (dobPrecisionSelected.value=="year") {
125
+ displayFormat = "yyyy"
126
+ }
127
+
114
128
  const dateTime = Datetime().fromString(dateString, undefined, props.locale)
115
- return dateTime.toFormat(props.format, props.locale)
129
+ return dateTime.toFormat(displayFormat, props.locale)
116
130
  }
117
131
 
118
132
  function handleTextFieldClear() {
@@ -126,8 +140,7 @@ function resetDatePicker() {
126
140
 
127
141
  watchEffect(() => {
128
142
  if (!isTextFieldFocused.value && selectedDate.value) {
129
- const dateTime = Datetime().fromString(selectedDate.value, undefined, props.locale)
130
- displayedDate.value = dateTime.toFormat(props.format, props.locale)
143
+ displayedDate.value = formatDate(selectedDate.value)
131
144
  }
132
145
  else {
133
146
  displayedDate.value = selectedDate.value
@@ -142,6 +155,10 @@ watch(() => props.modelValue, () => {
142
155
  updateDate(props.modelValue || null)
143
156
  }, { immediate: true })
144
157
 
158
+ watch(dobPrecisionSelected,()=>{
159
+ updateDate(selectedDate.value)
160
+ })
161
+
145
162
  function toggleMenuOpen(trigger: string) {
146
163
  if ((trigger === 'textField' && props.pickerOnly) || (trigger === 'icon' && !props.pickerOnly)) {
147
164
  isMenuOpen.value = true
@@ -205,7 +205,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
205
205
  >
206
206
  <slot
207
207
  :name="name"
208
- v-bind="(slotData as object)"
208
+ v-bind="((slotData || {}) as object)"
209
209
  :operation="operation"
210
210
  />
211
211
  </template>
@@ -0,0 +1,231 @@
1
+ <script lang="ts" setup>
2
+ import {computed, onBeforeUnmount, onMounted, ref, watchEffect} from "vue"
3
+ import {useDevicesList, useUserMedia} from "@vueuse/core"
4
+ import {getBase64Strings, type ImageFormat} from 'exif-rotate-js'
5
+ import {useAlert} from '../../../composables/alert'
6
+
7
+ interface Props {
8
+ imageFormat?: ImageFormat
9
+ autoStart?: boolean
10
+ fileOnly?: boolean
11
+ required?: boolean
12
+ readonly?: boolean
13
+ disabled?: boolean
14
+ requiredMessage?: string
15
+ buttonText?: string
16
+ maxHeight?: string | number
17
+ maxWidth?: string | number
18
+ aspectRatio?: number
19
+ }
20
+
21
+ const props = withDefaults(defineProps<Props>(), {
22
+ imageFormat: "image/jpeg",
23
+ autoStart: true,
24
+ fileOnly: false,
25
+ required: false,
26
+ readonly: false,
27
+ disabled: false,
28
+ requiredMessage: "This field is required",
29
+ buttonText: "Add Photo",
30
+ maxWidth: 1024
31
+ })
32
+
33
+ const alert = useAlert()
34
+
35
+ const imageData = defineModel<string>()
36
+
37
+ const isLoading = ref<boolean>(false)
38
+ const isCaptured = computed(()=>!!imageData.value)
39
+ const isEditing = ref<boolean>(false)
40
+ const showRequiredMessage = ref<boolean>(false)
41
+
42
+ const videoScreen = ref<HTMLVideoElement>()
43
+
44
+ const currentCameraId = ref<ConstrainDOMString | undefined>()
45
+ const { videoInputs: cameras } = useDevicesList({
46
+ requestPermissions: true,
47
+ constraints: { audio: false, video: true },
48
+ onUpdated() {
49
+ if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
50
+ currentCameraId.value = cameras.value[0]?.deviceId
51
+ },
52
+ })
53
+ const hasCamera = computed(()=>{ return !!currentCameraId.value })
54
+
55
+ const { stream, start: cameraStart, stop: cameraStop, enabled: cameraEnabled } = useUserMedia({
56
+ constraints: { video: { deviceId: currentCameraId.value, width: {min: 1280}, aspectRatio: props.aspectRatio}},
57
+ })
58
+
59
+ watchEffect(() => {
60
+ if (videoScreen.value) videoScreen.value.srcObject = (stream.value) ? stream.value! : null
61
+ })
62
+
63
+ function startCamera() {
64
+ imageData.value = undefined
65
+ if (!cameraEnabled.value && hasCamera) {
66
+ isLoading.value = true
67
+ cameraStart().finally(()=>{
68
+ isLoading.value = false
69
+ })
70
+ }
71
+ }
72
+
73
+ function stopCamera() {
74
+ if (cameraEnabled.value) cameraStop()
75
+ }
76
+
77
+ function captureImage() {
78
+ if (videoScreen.value) {
79
+ const canvas = document.createElement('canvas')
80
+ canvas.width = videoScreen.value.videoWidth
81
+ canvas.height = videoScreen.value.videoHeight
82
+ const context = canvas.getContext('2d')
83
+ if (context) {
84
+ context.drawImage(videoScreen.value, 0, 0, canvas.width, canvas.height)
85
+ isEditing.value = false
86
+ imageData.value = canvas.toDataURL(props.imageFormat)
87
+ stopCamera()
88
+ }
89
+ }
90
+ }
91
+
92
+ function captureImageFile(selectedFile: File | File[] | undefined) {
93
+ if (!selectedFile) {
94
+ alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
95
+ return
96
+ }
97
+
98
+ const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
99
+
100
+ getBase64Strings([scanImageSingleFile], { maxSize: computedMaxSize.value,type: props.imageFormat,quality: 1 }).then((returnData) => {
101
+ isEditing.value = false
102
+ imageData.value = returnData[0]
103
+ stopCamera()
104
+ }).catch((e) => void e)
105
+ }
106
+
107
+ onMounted(() => {
108
+ if (!isCaptured.value && props.autoStart && !props.fileOnly) startCamera()
109
+ })
110
+
111
+ onBeforeUnmount(() => {
112
+ stopCamera()
113
+ })
114
+
115
+ function validate() {
116
+ if (!props.required || isCaptured.value) {
117
+ showRequiredMessage.value = false
118
+ return true
119
+ } else {
120
+ showRequiredMessage.value = true
121
+ return false
122
+ }
123
+ }
124
+
125
+ function reset() {
126
+ imageData.value = undefined
127
+ stopCamera()
128
+ if (props.autoStart && !props.fileOnly) startCamera()
129
+ }
130
+
131
+ const computedMaxWidth = computed(() => {
132
+ if (typeof props.maxWidth === 'number') {
133
+ return `${props.maxWidth}px`
134
+ } else if (!isNaN(Number(props.maxWidth))) {
135
+ return `${props.maxWidth}px`
136
+ }
137
+ return props.maxWidth
138
+ })
139
+
140
+ const computedMaxHeight = computed(() => {
141
+ if (typeof props.maxHeight === 'number') {
142
+ return `${props.maxHeight}px`
143
+ } else if (!isNaN(Number(props.maxHeight))) {
144
+ return `${props.maxHeight}px`
145
+ }
146
+ return props.maxHeight
147
+ })
148
+
149
+ const computedMaxSize = computed(() => {
150
+ let tmpMaxHeight : number = 1024
151
+ let tmpMaxWidth : number = 1024
152
+
153
+ if (typeof props.maxWidth === 'number') {
154
+ tmpMaxWidth = <number>props.maxWidth
155
+ } else if (!isNaN(Number(props.maxWidth))) {
156
+ tmpMaxWidth = Number(props.maxWidth)
157
+ }
158
+
159
+ if (typeof props.maxHeight === 'number') {
160
+ tmpMaxHeight = <number>props.maxHeight
161
+ } else if (!isNaN(Number(props.maxHeight))) {
162
+ tmpMaxHeight = Number(props.maxHeight)
163
+ }
164
+
165
+ return (tmpMaxWidth>tmpMaxHeight) ? tmpMaxWidth : tmpMaxHeight
166
+ })
167
+
168
+ const operation = ref({startCamera,stopCamera,reset,captureImage,captureImageFile,isLoading,isCaptured,hasCamera})
169
+ </script>
170
+
171
+ <template>
172
+ <v-card>
173
+ <v-card-text class="d-flex justify-center text-center" v-if="!isLoading && !isCaptured">
174
+ <template v-if="!hasCamera || fileOnly">
175
+ <FileBtn
176
+ color="primary"
177
+ variant="flat"
178
+ accept="image/*"
179
+ @update:model-value="captureImageFile"
180
+ :disabled="disabled || readonly"
181
+ >
182
+ <v-icon>mdi mdi-image-plus</v-icon>
183
+ {{ buttonText }}
184
+ </FileBtn>
185
+ </template>
186
+ <template v-else>
187
+ <div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}">
188
+ <video autoplay ref="videoScreen" width="100%" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}"></video>
189
+ <div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
190
+ <FileBtn
191
+ accept="image/*"
192
+ icon="mdi mdi-image-plus"
193
+ icon-only
194
+ @update:model-value="captureImageFile"
195
+ :disabled="disabled || readonly"
196
+ />
197
+ </div>
198
+ </div>
199
+ </template>
200
+ </v-card-text>
201
+ <v-card-text class="d-flex justify-center" v-if="isCaptured">
202
+ <div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}" v-if="!isEditing">
203
+ <v-img :src="imageData" :max-height="maxHeight" :max-width="maxWidth" contain></v-img>
204
+ <div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
205
+ <v-btn
206
+ icon="mdi mdi-image-edit"
207
+ icon-only
208
+ @click="isEditing=true"
209
+ :disabled="disabled || readonly"
210
+ />
211
+ </div>
212
+ </div>
213
+ <form-images-edit v-model="imageData" :aspect-ratio="aspectRatio" :image-format="imageFormat" @update:model-value="isEditing=false" v-else></form-images-edit>
214
+ </v-card-text>
215
+ <v-card-text class="d-flex justify-center" v-if="isLoading">
216
+ <v-progress-circular indeterminate></v-progress-circular>
217
+ </v-card-text>
218
+ <v-card-text v-if="showRequiredMessage" class="text-center">
219
+ <span class="red--text">{{ requiredMessage }}</span>
220
+ </v-card-text>
221
+ <v-card-actions v-if="!readonly && (!fileOnly || isCaptured) && !isEditing">
222
+ <slot name="actions" :operation="operation">
223
+ <v-spacer></v-spacer>
224
+ <v-btn color="primary" variant="flat" @click="startCamera" v-if="!cameraEnabled && hasCamera && !fileOnly" :disabled="disabled">{{ (isCaptured) ? "Retake" : "Start" }}</v-btn>
225
+ <v-btn color="primary" variant="flat" @click="captureImage" v-if="cameraEnabled" :disabled="disabled">Capture</v-btn>
226
+ <v-btn color="primary" variant="flat" @click="reset" :disabled="disabled">Reset</v-btn>
227
+ <v-spacer></v-spacer>
228
+ </slot>
229
+ </v-card-actions>
230
+ </v-card>
231
+ </template>
@@ -1,143 +1,117 @@
1
- <script setup lang="ts">
2
- import { ref, onMounted, watch } from 'vue'
1
+ <script lang="ts" setup>
2
+ import {ref,computed} from 'vue'
3
3
  import Cropper from 'cropperjs'
4
4
  import 'cropperjs/dist/cropper.css'
5
+ import type {ImageFormat} from "exif-rotate-js"
5
6
 
6
7
  interface Props {
7
- modelValue?: string
8
- readonly?: boolean
9
- flat?: boolean
8
+ imageFormat?: ImageFormat
9
+ maxHeight?: string | number
10
+ maxWidth?: string | number
11
+ aspectRatio?: number
10
12
  }
11
13
 
12
14
  const props = withDefaults(defineProps<Props>(), {
13
- readonly: false,
14
- flat: false,
15
+ imageFormat: "image/jpeg",
16
+ maxWidth: 1024
15
17
  })
16
18
 
17
- const emit = defineEmits(['update:modelValue', 'closeDialog', 'openCamera'])
19
+ const imageData = defineModel<string>()
18
20
 
19
- const cropper = ref<Cropper | null>(null)
20
- const imageCanvas = ref<HTMLCanvasElement | null>(null)
21
+ const cropper = ref<Cropper>()
22
+ const imageRef = ref<HTMLImageElement>()
23
+ const dragMode = ref<string>("crop")
21
24
 
22
- const loadImageToCanvas = () => {
23
- if (imageCanvas.value && props.modelValue) {
24
- const canvas = imageCanvas.value as HTMLCanvasElement
25
- const context = canvas.getContext('2d')
26
- const image = new Image()
27
- image.src = props.modelValue
28
- image.onload = () => {
29
- canvas.width = image.width
30
- canvas.height = image.height
31
- context?.clearRect(0, 0, canvas.width, canvas.height)
32
- context?.drawImage(image, 0, 0)
33
- if (!props.readonly) {
34
- cropper.value?.destroy()
35
- cropper.value = new Cropper(canvas, {
36
- aspectRatio: 1,
37
- rotatable: true,
38
- })
39
- }
40
- }
25
+ function loadCropper() {
26
+ if (cropper.value) cropper.value?.destroy()
27
+ if (imageRef.value) {
28
+ cropper.value = new Cropper(<HTMLImageElement>imageRef.value,{
29
+ aspectRatio: props.aspectRatio
30
+ })
41
31
  }
42
32
  }
43
33
 
44
- onMounted(() => {
45
- loadImageToCanvas()
46
- })
34
+ const dragMove = () => {
35
+ cropper.value?.setDragMode('move')
36
+ }
37
+ const dragCrop = () => {
38
+ cropper.value?.setDragMode('crop')
39
+ }
40
+ const rotateLeft90 = () => cropper.value?.rotate(-90)
41
+ const rotateLeft = () => cropper.value?.rotate(-5)
42
+ const rotateRight = () => cropper.value?.rotate(5)
43
+ const rotateRight90 = () => cropper.value?.rotate(90)
44
+ const flipHorizontal = () => cropper.value?.scaleX(cropper.value.getData().scaleX * -1)
45
+ const flipVertical = () => cropper.value?.scaleY(cropper.value.getData().scaleY * -1)
46
+ const zoomIn = () => cropper.value?.zoom(0.1)
47
+ const zoomOut = () => cropper.value?.zoom(-0.1)
48
+ const moveUp = () => cropper.value?.move(0, -10)
49
+ const moveDown = () => cropper.value?.move(0, 10)
50
+ const moveLeft = () => cropper.value?.move(-10, 0)
51
+ const moveRight = () => cropper.value?.move(10, 0)
52
+ const accept = () => {
53
+ const croppedCanvas = cropper.value?.getCroppedCanvas()
54
+ console.log(props.imageFormat)
55
+ imageData.value = croppedCanvas?.toDataURL(props.imageFormat,1)
56
+ }
57
+ const reset = () => cropper.value?.reset()
47
58
 
48
- watch(() => props.modelValue, (newValue, oldValue) => {
49
- if (newValue !== oldValue) {
50
- loadImageToCanvas()
59
+ const computedMaxWidth = computed(() => {
60
+ if (typeof props.maxWidth === 'number') {
61
+ return `${props.maxWidth}px`
62
+ } else if (!isNaN(Number(props.maxWidth))) {
63
+ return `${props.maxWidth}px`
51
64
  }
65
+ return props.maxWidth
52
66
  })
53
67
 
54
- const handleCrop = () => {
55
- const croppedCanvas = cropper.value?.getCroppedCanvas()
56
- if (croppedCanvas) {
57
- const dataURL = croppedCanvas.toDataURL()
58
- emit('update:modelValue', dataURL)
59
- emit('closeDialog', false)
68
+ const computedMaxHeight = computed(() => {
69
+ if (typeof props.maxHeight === 'number') {
70
+ return `${props.maxHeight}px`
71
+ } else if (!isNaN(Number(props.maxHeight))) {
72
+ return `${props.maxHeight}px`
60
73
  }
61
- }
62
-
63
- const resetCrop = () => {
64
- emit('closeDialog', false)
65
- emit('openCamera', true)
66
- }
67
-
68
- const close = () => {
69
- emit('closeDialog', false)
70
- }
74
+ return props.maxHeight
75
+ })
71
76
  </script>
72
-
73
77
  <template>
74
- <v-card :flat="flat">
75
- <v-card-title>
76
- <v-row
77
- align="center"
78
- justify="end"
79
- >
80
- <slot
81
- name="closeButton"
82
- :close="close"
83
- >
84
- <v-btn
85
- icon="mdi mdi-close"
86
- variant="text"
87
- @click="close"
88
- />
89
- </slot>
90
- </v-row>
91
- </v-card-title>
92
- <v-card-text>
93
- <v-row
94
- v-if="props.readonly"
95
- justify="center"
96
- >
97
- <v-img
98
- :src="props.modelValue as string"
99
- height="70vh"
100
- />
101
- </v-row>
102
- <v-row
103
- v-else
104
- justify="center"
105
- >
106
- <v-col cols="12">
107
- <canvas
108
- ref="imageCanvas"
109
- class="canvas-photo"
110
- style="max-height: 70vh; max-width: 40vh"
111
- />
112
- <v-row
113
- justify="center"
114
- class="mt-2"
115
- >
116
- <slot
117
- name="actionButtons"
118
- :reset-crop="resetCrop"
119
- :handle-crop="handleCrop"
120
- >
121
- <v-btn
122
- class="ma-1"
123
- prepend-icon="mdi mdi-camera"
124
- color="primary"
125
- @click="resetCrop"
126
- >
127
- Retake
128
- </v-btn>
129
- <v-btn
130
- class="ma-1"
131
- prepend-icon="fa-solid fa-check"
132
- color="success"
133
- @click="handleCrop"
134
- >
135
- Done
136
- </v-btn>
137
- </slot>
138
- </v-row>
139
- </v-col>
140
- </v-row>
141
- </v-card-text>
142
- </v-card>
78
+ <div :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}">
79
+ <v-card>
80
+ <v-toolbar color="primary" dark>
81
+ <v-btn-toggle v-model="dragMode" class="ml-1">
82
+ <v-btn value="move" icon="mdi mdi-cursor-move" @click="dragMove"></v-btn>
83
+ <v-btn value="crop" icon="mdi mdi-crop" @click="dragCrop"></v-btn>
84
+ </v-btn-toggle>
85
+ <v-btn-group class="ml-1">
86
+ <v-btn value="rotateLeft" icon="mdi mdi-rotate-left-variant" @click="rotateLeft90"></v-btn>
87
+ <v-btn value="rotateRight" icon="mdi mdi-rotate-left" @click="rotateLeft"></v-btn>
88
+ <v-btn value="rotateRight" icon="mdi mdi-rotate-right" @click="rotateRight"></v-btn>
89
+ <v-btn value="rotateRight" icon="mdi mdi-rotate-right-variant" @click="rotateRight90"></v-btn>
90
+ </v-btn-group>
91
+ <v-btn-group class="ml-1">
92
+ <v-btn value="flipHorizontal" icon="mdi mdi-flip-horizontal" @click="flipHorizontal"></v-btn>
93
+ <v-btn value="flipVertical" icon="mdi mdi-flip-vertical" @click="flipVertical"></v-btn>
94
+ </v-btn-group>
95
+ <v-btn-group class="ml-1">
96
+ <v-btn value="flipHorizontal" icon="mdi mdi-magnify-plus-outline" @click="zoomIn"></v-btn>
97
+ <v-btn value="flipVertical" icon="mdi mdi-magnify-minus-outline" @click="zoomOut"></v-btn>
98
+ </v-btn-group>
99
+ <v-btn-group class="ml-1">
100
+ <v-btn value="moveUp" icon="mdi mdi-arrow-up" @click="moveUp"></v-btn>
101
+ <v-btn value="moveDown" icon="mdi mdi-arrow-down" @click="moveDown"></v-btn>
102
+ <v-btn value="moveLeft" icon="mdi mdi-arrow-left" @click="moveLeft"></v-btn>
103
+ <v-btn value="moveRight" icon="mdi mdi-arrow-right" @click="moveRight"></v-btn>
104
+ </v-btn-group>
105
+ <v-spacer></v-spacer>
106
+ <v-btn icon="mdi mdi-check" @click="accept"></v-btn>
107
+ <v-btn icon="mdi mdi-refresh" @click="reset"></v-btn>
108
+ </v-toolbar>
109
+ <v-card-text class="ma-0 pa-0">
110
+ <img ref="imageRef" :src="imageData" alt="" @load="loadCropper" style="display: block; max-width: 100%">
111
+ </v-card-text>
112
+ </v-card>
113
+ </div>
143
114
  </template>
115
+ <style>
116
+ .cropper-bg{background-repeat:repeat!important}
117
+ </style>
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" setup>
2
2
  interface Props {
3
3
  label: string
4
- value?: string | string | null | undefined
4
+ value?: string | null | undefined
5
+ horizontal?: boolean
5
6
  }
6
7
 
7
8
  const props = withDefaults(defineProps<Props>(), {
@@ -10,18 +11,30 @@ const props = withDefaults(defineProps<Props>(), {
10
11
  </script>
11
12
 
12
13
  <template>
13
- <v-card variant="flat">
14
- <VCardSubtitle class="ma-0 pa-0">
14
+ <div v-if="horizontal" class="d-flex align-end" :="$attrs">
15
+ <div class="text-medium-emphasis">
15
16
  <slot name="label">
16
- {{ label }}
17
+ {{ label }}:
17
18
  </slot>
18
- </VCardSubtitle>
19
- <VCardText class="text-h6 pa-0 mb-2">
19
+ </div>
20
+ <div class="ml-1">
20
21
  <slot name="value">
21
22
  {{ value }}
22
23
  </slot>
23
- </VCardText>
24
- </v-card>
24
+ </div>
25
+ </div>
26
+ <v-card v-else variant="flat" :="$attrs">
27
+ <VCardSubtitle class="ma-0 pa-0 text-black">
28
+ <slot name="label">
29
+ {{ label }}
30
+ </slot>
31
+ </VCardSubtitle>
32
+ <VCardText class="text-h6 pa-0 mb-2">
33
+ <slot name="value">
34
+ {{ value }}
35
+ </slot>
36
+ </VCardText>
37
+ </v-card>
25
38
  </template>
26
39
 
27
40
  <style lang="">
@@ -1,10 +1,10 @@
1
1
  <script lang="ts" setup>
2
- import { VAutocomplete } from 'vuetify/components/VAutocomplete'
3
- import { concat, isEmpty } from 'lodash-es'
4
- import { computed, ref, watch } from 'vue'
5
- import { watchDebounced } from '@vueuse/core'
6
- import { useFuzzy } from '../../composables/utils/fuzzy'
7
- import { useGraphQl } from '../../composables/graphql'
2
+ import {VAutocomplete} from 'vuetify/components/VAutocomplete'
3
+ import {concat, isEmpty} from 'lodash-es'
4
+ import {computed, ref, watch} from 'vue'
5
+ import {watchDebounced} from '@vueuse/core'
6
+ import {useFuzzy} from '../../composables/utils/fuzzy'
7
+ import {useGraphQl} from '../../composables/graphql'
8
8
 
9
9
  interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$props']> {
10
10
  fuzzy?: boolean
@@ -134,7 +134,7 @@ const computedNoDataText = computed(() => {
134
134
  >
135
135
  <slot
136
136
  :name="name"
137
- v-bind="(slotData as object)"
137
+ v-bind="((slotData || {}) as object)"
138
138
  :operation="operation"
139
139
  />
140
140
  </template>
@@ -65,7 +65,7 @@ query()
65
65
  >
66
66
  <slot
67
67
  :name="name"
68
- v-bind="(slotData as object)"
68
+ v-bind="((slotData || {}) as object)"
69
69
  :operation="operation"
70
70
  />
71
71
  </template>
@@ -63,7 +63,7 @@ const itemTitleField = computed(() => {
63
63
  >
64
64
  <slot
65
65
  :name="name"
66
- v-bind="(slotData as object)"
66
+ v-bind="((slotData || {}) as object)"
67
67
  :operation="operation"
68
68
  />
69
69
  </template>
@@ -63,7 +63,7 @@ query()
63
63
  >
64
64
  <slot
65
65
  :name="name"
66
- v-bind="(slotData as object)"
66
+ v-bind="((slotData || {}) as object)"
67
67
  :operation="operation"
68
68
  />
69
69
  </template>
@@ -155,7 +155,7 @@ defineExpose({ reload })
155
155
  >
156
156
  <slot
157
157
  :name="name"
158
- v-bind="(slotData as object)"
158
+ v-bind="((slotData || {}) as object)"
159
159
  :operation="operation"
160
160
  />
161
161
  </template>
@@ -196,7 +196,7 @@ defineExpose({ reload })
196
196
  >
197
197
  <slot
198
198
  :name="name"
199
- v-bind="(slotData as object)"
199
+ v-bind="((slotData || {}) as object)"
200
200
  :operation="operation"
201
201
  />
202
202
  </template>
@@ -77,7 +77,7 @@ type SortItem = { key: string, order?: boolean | 'asc' | 'desc' }
77
77
  const sortBy = ref<SortItem[]>()
78
78
 
79
79
  const pageCount = computed(() => {
80
- if (itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value == '-1' || itemsPerPageInternal.value <= 0 || !itemsPerPageInternal.value) return 1
80
+ if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value == '-1' || Number(itemsPerPageInternal.value) <= 0) return 1
81
81
  else return Math.ceil(itemsLength.value / Number(itemsPerPageInternal.value))
82
82
  })
83
83
  const currentPage = ref<number>(1)
@@ -86,7 +86,7 @@ watch([currentPage, itemsPerPageInternal, sortBy], () => {
86
86
  if (canServerPageable.value) {
87
87
  loadItems({
88
88
  page: currentPage.value,
89
- itemsPerPage: (itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value <= 0) ? '-1' : itemsPerPageInternal.value,
89
+ itemsPerPage: (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) ? '-1' : itemsPerPageInternal.value,
90
90
  sortBy: sortBy.value,
91
91
  })
92
92
  }
@@ -99,7 +99,7 @@ const computedInitialData = computed(() => {
99
99
  })
100
100
 
101
101
  const computedSkeletonPerPage = computed(() => {
102
- if (itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value <= 0) return 1
102
+ if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) return 1
103
103
  else return Number(itemsPerPageInternal.value)
104
104
  })
105
105
 
@@ -3,7 +3,10 @@ import { type GraphqlModelConfigProps } from './graphqlModelOperation';
3
3
  export interface HeaderProps {
4
4
  headers?: any[];
5
5
  }
6
- export type GraphqlModelProps = GraphqlModelConfigProps & Partial<HeaderProps>;
6
+ export interface InitialDataProps {
7
+ initialData?: Record<string, any>;
8
+ }
9
+ export type GraphqlModelProps = GraphqlModelConfigProps & Partial<HeaderProps> & Partial<InitialDataProps>;
7
10
  export declare function useGraphqlModel<T extends GraphqlModelProps>(props: T): {
8
11
  items: import("vue").Ref<Record<string, any>[]>;
9
12
  itemsLength: import("vue").Ref<number>;
@@ -63,7 +63,7 @@ export function useGraphqlModel(props) {
63
63
  function createItem(item, callback, importing = false) {
64
64
  isLoading.value = true;
65
65
  return operationCreate.value?.call(fields.value, { input: item }).then((result) => {
66
- if (canServerPageable) {
66
+ if (canServerPageable.value) {
67
67
  if (!importing)
68
68
  loadItems(currentOptions.value);
69
69
  } else
@@ -81,7 +81,7 @@ export function useGraphqlModel(props) {
81
81
  isLoading.value = true;
82
82
  const importPromise = [];
83
83
  importItems2.forEach((item) => {
84
- const eachPromise = createItem(item, void 0, true);
84
+ const eachPromise = createItem(Object.assign({}, props.initialData, item), void 0, true);
85
85
  if (eachPromise)
86
86
  importPromise.push(eachPromise);
87
87
  });
@@ -95,7 +95,7 @@ export function useGraphqlModel(props) {
95
95
  function updateItem(item, callback) {
96
96
  isLoading.value = true;
97
97
  return operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
98
- if (canServerPageable)
98
+ if (canServerPageable.value)
99
99
  loadItems(currentOptions.value);
100
100
  else {
101
101
  const index = items.value.findIndex((item2) => item2[props.modelKey || "id"] === result[props.modelKey || "id"]);
@@ -124,7 +124,7 @@ export function useGraphqlModel(props) {
124
124
  }
125
125
  function loadItems(options) {
126
126
  currentOptions.value = options;
127
- if (canServerPageable) {
127
+ if (canServerPageable.value) {
128
128
  const pageableVariable = {
129
129
  page: options.page,
130
130
  perPage: options.itemsPerPage,
@@ -7,6 +7,9 @@ export function useGraphqlModelOperation(props) {
7
7
  function lowercaseFirstLetter(string) {
8
8
  return string?.charAt(0).toLowerCase() + string?.slice(1);
9
9
  }
10
+ const computedModelName = computed(() => {
11
+ return props.modelName.split("By")[0].trim();
12
+ });
10
13
  const operationCreate = computed(() => {
11
14
  if (props.operationCreate) {
12
15
  if (typeof props.operationCreate === "string") {
@@ -16,7 +19,7 @@ export function useGraphqlModelOperation(props) {
16
19
  return props.operationCreate;
17
20
  }
18
21
  }
19
- return graphqlOperation["create" + capitalizeFirstLetter(props.modelName)];
22
+ return graphqlOperation["create" + capitalizeFirstLetter(computedModelName.value)];
20
23
  });
21
24
  const operationUpdate = computed(() => {
22
25
  if (props.operationUpdate) {
@@ -27,7 +30,7 @@ export function useGraphqlModelOperation(props) {
27
30
  return props.operationUpdate;
28
31
  }
29
32
  }
30
- return graphqlOperation["update" + capitalizeFirstLetter(props.modelName)];
33
+ return graphqlOperation["update" + capitalizeFirstLetter(computedModelName.value)];
31
34
  });
32
35
  const operationDelete = computed(() => {
33
36
  if (props.operationDelete) {
@@ -38,7 +41,7 @@ export function useGraphqlModelOperation(props) {
38
41
  return props.operationDelete;
39
42
  }
40
43
  }
41
- return graphqlOperation["delete" + capitalizeFirstLetter(props.modelName)];
44
+ return graphqlOperation["delete" + capitalizeFirstLetter(computedModelName.value)];
42
45
  });
43
46
  const operationRead = computed(() => {
44
47
  if (props.operationRead) {
@@ -71,7 +74,7 @@ export function useGraphqlModelOperation(props) {
71
74
  return props.operationSearch;
72
75
  }
73
76
  }
74
- return graphqlOperation["search" + capitalizeFirstLetter(props.modelName)];
77
+ return graphqlOperation["search" + capitalizeFirstLetter(computedModelName.value)];
75
78
  });
76
79
  return { operationCreate, operationUpdate, operationDelete, operationRead, operationReadPageable, operationSearch };
77
80
  }
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
- import { computed } from 'vue'
3
- import { VTextField } from 'vuetify/components/VTextField'
4
- import { type MaskaDetail, type MaskOptions, vMaska } from 'maska'
2
+ import {computed} from 'vue'
3
+ import {VTextField} from 'vuetify/components/VTextField'
4
+ import {type MaskaDetail, type MaskOptions, vMaska} from 'maska'
5
5
 
6
6
  export interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props']> {
7
7
  type: 'eReceipt' | 'telephone'
@@ -36,7 +36,7 @@ function onMaska(event: CustomEvent<MaskaDetail>) {
36
36
  >
37
37
  <slot
38
38
  :name="name"
39
- v-bind="(slotData as object)"
39
+ v-bind="((slotData || {}) as object)"
40
40
  />
41
41
  </template>
42
42
  </v-text-field>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -66,8 +66,8 @@
66
66
  "@fullcalendar/multimonth": "^6.1.11",
67
67
  "@fullcalendar/timegrid": "^6.1.11",
68
68
  "@fullcalendar/vue3": "^6.1.11",
69
- "@graphql-codegen/cli": "^5.0.2",
70
69
  "@graphql-codegen/add": "^5.0.2",
70
+ "@graphql-codegen/cli": "^5.0.2",
71
71
  "@graphql-codegen/plugin-helpers": "^5.0.4",
72
72
  "@graphql-codegen/typescript": "^4.0.6",
73
73
  "@mdi/font": "^7.4.47",
@@ -79,20 +79,21 @@
79
79
  "@zxing/browser": "^0.1.4",
80
80
  "cropperjs": "^1.6.2",
81
81
  "currency.js": "^2.0.4",
82
+ "exif-rotate-js": "^1.5.0",
82
83
  "fuse.js": "^7.0.0",
83
- "graphql": "^16.9.0",
84
84
  "gql-query-builder": "^3.8.0",
85
+ "graphql": "^16.9.0",
85
86
  "lodash": "^4.17.21",
86
87
  "luxon": "^3.4.4",
87
88
  "maska": "^2.1.11",
88
89
  "pdf-vue3": "^1.0.12",
90
+ "prettier": "3.3.2",
89
91
  "print-js": "^1.6.0",
90
92
  "uid": "^2.0.2",
91
93
  "vue": "^3.4.25",
92
94
  "vue-codemirror": "^6.1.1",
93
95
  "vue-signature-pad": "^3.0.2",
94
96
  "vuetify": "^3.6.8",
95
- "prettier": "3.3.2",
96
97
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz"
97
98
  },
98
99
  "devDependencies": {
@@ -1,129 +0,0 @@
1
- <script lang="ts" setup>
2
- import { ref, onMounted, watchEffect } from 'vue'
3
- import { useDevicesList, useUserMedia } from '@vueuse/core'
4
- import { useAlert } from '../composables/alert'
5
-
6
- interface Props {
7
- modelValue?: string
8
- }
9
- const props = defineProps<Props>()
10
- const emit = defineEmits(['update:modelValue', 'closeDialog'])
11
-
12
- const alert = useAlert()
13
- const videoRef = ref<HTMLVideoElement>()
14
- const capturedPhoto = ref<string | null>(null)
15
- const currentCameraId = ref<ConstrainDOMString | undefined>()
16
-
17
- const { videoInputs: cameras } = useDevicesList({
18
- requestPermissions: true,
19
- constraints: { audio: false, video: true },
20
- onUpdated() {
21
- if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
22
- currentCameraId.value = cameras.value[0]?.deviceId
23
- },
24
- })
25
-
26
- const { stream, enabled } = useUserMedia({
27
- constraints: { video: { deviceId: currentCameraId.value } },
28
- })
29
-
30
- watchEffect(() => {
31
- if (videoRef.value) (videoRef.value as HTMLVideoElement).srcObject = stream.value!
32
- })
33
-
34
- const captureImage = () => {
35
- if (videoRef.value) {
36
- const canvas = document.createElement('canvas')
37
- canvas.width = (videoRef.value as HTMLVideoElement).videoWidth
38
- canvas.height = (videoRef.value as HTMLVideoElement).videoHeight
39
- const context = canvas.getContext('2d')
40
- if (context) {
41
- context.drawImage(videoRef.value as HTMLVideoElement, 0, 0, canvas.width, canvas.height)
42
- capturedPhoto.value = canvas.toDataURL('image/jpeg')
43
-
44
- enabled.value = false
45
- emit('update:modelValue', capturedPhoto.value)
46
- }
47
- }
48
- }
49
-
50
- function loadImageFile(selectedFile: File | File[] | undefined) {
51
- if (!selectedFile) {
52
- alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
53
- return
54
- }
55
-
56
- const reader = new FileReader()
57
- reader.onload = (event) => {
58
- capturedPhoto.value = event.target?.result as string
59
-
60
- enabled.value = false
61
- emit('update:modelValue', capturedPhoto.value)
62
- }
63
- const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
64
- reader.readAsDataURL(scanImageSingleFile)
65
- }
66
-
67
- const openCamera = () => {
68
- enabled.value = true
69
- }
70
-
71
- const closeDialog = () => {
72
- enabled.value = false
73
- emit('closeDialog')
74
- }
75
-
76
- defineExpose({
77
- openCamera,
78
- })
79
-
80
- onMounted(() => {
81
- enabled.value = true
82
- })
83
- </script>
84
-
85
- <template>
86
- <v-card>
87
- <v-card-title class="d-flex justify-end">
88
- <v-btn
89
- icon="fa:fa-solid fa-xmark"
90
- variant="text"
91
- @click="closeDialog"
92
- />
93
- </v-card-title>
94
- <v-card-text class="d-flex justify-center">
95
- <video
96
- ref="videoRef"
97
- autoplay
98
- style="max-width: 1024px"
99
- />
100
- <div style="z-index: 2000; position: relative; bottom: -410px; left: -80px; height: 50px">
101
- <FileBtn
102
- accept="image/*"
103
- icon="mdi mdi-image-plus"
104
- icon-only
105
- @update:model-value="loadImageFile"
106
- />
107
- </div>
108
- </v-card-text>
109
- <v-card-actions class="d-flex justify-center">
110
- <v-btn
111
- icon
112
- size="x-large"
113
- variant="tonal"
114
- @click="captureImage()"
115
- >
116
- <v-icon
117
- icon="fa:fa-solid fa-circle"
118
- size="x-large"
119
- />
120
- <v-tooltip
121
- activator="parent"
122
- location="top"
123
- >
124
- ถ่ายภาพ
125
- </v-tooltip>
126
- </v-btn>
127
- </v-card-actions>
128
- </v-card>
129
- </template>
@@ -1,58 +0,0 @@
1
- <script lang="ts" setup>
2
- import { ref } from 'vue'
3
- import { isUndefined } from 'lodash'
4
-
5
- interface Props {
6
- modelValue?: string
7
- }
8
-
9
- const props = defineProps<Props>()
10
- const emit = defineEmits(['update:modelValue', 'closeDialog', 'openCamera'])
11
-
12
- const selectedImage = ref<string>()
13
- const isDialogOpen = ref<boolean>(false)
14
- const cameraRef = ref()
15
-
16
- const handleAddImage = (image: string) => {
17
- selectedImage.value = image
18
- isDialogOpen.value = true
19
- }
20
-
21
- const handleCloseDialog = () => {
22
- isDialogOpen.value = false
23
- emit('openCamera', true)
24
- emit('closeDialog', false)
25
- }
26
-
27
- const handleCameraClose = (shouldClose: boolean, editStatus: string) => {
28
- if (isUndefined(editStatus)) {
29
- emit('closeDialog', shouldClose)
30
- }
31
- }
32
-
33
- const handleImageEdit = (imageData: any) => {
34
- emit('update:modelValue', imageData)
35
- }
36
-
37
- const handleOpenCamera = () => {
38
- emit('openCamera', true)
39
- isDialogOpen.value = false
40
- cameraRef.value.openCamera()
41
- }
42
- </script>
43
-
44
- <template>
45
- <Camera
46
- ref="cameraRef"
47
- @update:model-value="handleAddImage"
48
- @close-dialog="handleCameraClose"
49
- />
50
- <v-dialog v-model="isDialogOpen">
51
- <FormImagesEdit
52
- v-model="selectedImage"
53
- @update:model-value="handleImageEdit"
54
- @close-dialog="handleCloseDialog"
55
- @open-camera="handleOpenCamera"
56
- />
57
- </v-dialog>
58
- </template>
@@ -1,48 +0,0 @@
1
- <script lang="ts" setup>
2
- interface Props {
3
- modelValue?: any
4
- readonly?: boolean
5
- }
6
-
7
- const props = withDefaults(defineProps<Props>(), {
8
- readonly: false,
9
- })
10
-
11
- const emit = defineEmits([
12
- 'update:modelValue',
13
- 'closeDialogPreview',
14
- 'toPaint',
15
- ])
16
-
17
- const editImage = () => {
18
- emit('toPaint')
19
- }
20
- </script>
21
-
22
- <template>
23
- <v-card>
24
- <v-toolbar>
25
- <v-toolbar-title>Preview</v-toolbar-title>
26
- <v-spacer />
27
- <v-btn
28
- v-if="!props.readonly"
29
- icon="fa:fa-solid fa-pen"
30
- variant="text"
31
- @click="editImage"
32
- />
33
- <v-btn
34
- icon="fa:fa-solid fa-xmark"
35
- variant="text"
36
- @click="$emit('closeDialogPreview', false)"
37
- />
38
- </v-toolbar>
39
- <v-card-text>
40
- <v-row>
41
- <v-img
42
- :src="modelValue.data"
43
- height="70dvh"
44
- />
45
- </v-row>
46
- </v-card-text>
47
- </v-card>
48
- </template>