adminforth 2.4.0-next.26 → 2.4.0-next.261
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/commands/callTsProxy.js +14 -4
- package/commands/cli.js +10 -3
- package/commands/createApp/templates/api.ts.hbs +10 -0
- package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
- package/commands/createApp/templates/index.ts.hbs +12 -1
- package/commands/createApp/utils.js +25 -8
- package/commands/createCustomComponent/configLoader.js +17 -4
- package/commands/createCustomComponent/main.js +1 -0
- package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
- package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
- package/commands/createPlugin/templates/package.json.hbs +1 -1
- package/commands/generateModels.js +30 -22
- package/dist/auth.d.ts +9 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +21 -2
- package/dist/auth.js.map +1 -1
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +46 -15
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +15 -0
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +50 -15
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts.map +1 -1
- package/dist/dataConnectors/mysql.js +11 -0
- package/dist/dataConnectors/mysql.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +43 -14
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +11 -0
- package/dist/dataConnectors/sqlite.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -9
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts +2 -0
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +50 -6
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts +6 -0
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +184 -19
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +164 -26
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +499 -13
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +555 -31
- package/dist/modules/styles.js.map +1 -1
- package/dist/modules/utils.d.ts +7 -15
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/modules/utils.js +45 -68
- package/dist/modules/utils.js.map +1 -1
- package/dist/servers/express.d.ts +5 -0
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +40 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/index.html +1 -1
- package/dist/spa/package-lock.json +5 -4
- package/dist/spa/package.json +1 -1
- package/dist/spa/src/App.vue +58 -173
- package/dist/spa/src/adminforth.ts +42 -18
- package/dist/spa/src/afcl/BarChart.vue +2 -2
- package/dist/spa/src/afcl/Button.vue +6 -6
- package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
- package/dist/spa/src/afcl/Card.vue +25 -0
- package/dist/spa/src/afcl/Checkbox.vue +21 -13
- package/dist/spa/src/afcl/CountryFlag.vue +4 -1
- package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
- package/dist/spa/src/afcl/Dialog.vue +47 -27
- package/dist/spa/src/afcl/Dropzone.vue +12 -12
- package/dist/spa/src/afcl/Input.vue +5 -5
- package/dist/spa/src/afcl/JsonViewer.vue +25 -0
- package/dist/spa/src/afcl/LinkButton.vue +3 -3
- package/dist/spa/src/afcl/PieChart.vue +5 -5
- package/dist/spa/src/afcl/ProgressBar.vue +7 -7
- package/dist/spa/src/afcl/Select.vue +68 -34
- package/dist/spa/src/afcl/Skeleton.vue +6 -6
- package/dist/spa/src/afcl/Table.vue +213 -74
- package/dist/spa/src/afcl/Textarea.vue +31 -0
- package/dist/spa/src/afcl/Toggle.vue +32 -0
- package/dist/spa/src/afcl/Tooltip.vue +26 -18
- package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
- package/dist/spa/src/afcl/index.ts +6 -3
- package/dist/spa/src/components/AcceptModal.vue +48 -14
- package/dist/spa/src/components/Breadcrumbs.vue +5 -5
- package/dist/spa/src/components/ColumnValueInput.vue +38 -18
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
- package/dist/spa/src/components/CustomRangePicker.vue +37 -8
- package/dist/spa/src/components/ErrorMessage.vue +21 -0
- package/dist/spa/src/components/Filters.vue +85 -39
- package/dist/spa/src/components/GroupsTable.vue +9 -8
- package/dist/spa/src/components/MenuLink.vue +90 -23
- package/dist/spa/src/components/ResourceForm.vue +94 -51
- package/dist/spa/src/components/ResourceListTable.vue +90 -80
- package/dist/spa/src/components/ResourceListTableVirtual.vue +86 -76
- package/dist/spa/src/components/ShowTable.vue +21 -15
- package/dist/spa/src/components/Sidebar.vue +470 -0
- package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
- package/dist/spa/src/components/SkeleteLoader.vue +3 -3
- package/dist/spa/src/components/ThreeDotsMenu.vue +73 -14
- package/dist/spa/src/components/Toast.vue +27 -9
- package/dist/spa/src/components/UserMenuSettingsButton.vue +69 -0
- package/dist/spa/src/components/ValueRenderer.vue +43 -16
- package/dist/spa/src/controls/BoolToggle.vue +34 -0
- package/dist/spa/src/i18n.ts +1 -1
- package/dist/spa/src/renderers/CompactField.vue +1 -1
- package/dist/spa/src/renderers/CompactUUID.vue +1 -1
- package/dist/spa/src/router/index.ts +8 -0
- package/dist/spa/src/shims-vue.d.ts +5 -0
- package/dist/spa/src/spa_types/core.ts +13 -1
- package/dist/spa/src/stores/core.ts +13 -1
- package/dist/spa/src/stores/filters.ts +29 -2
- package/dist/spa/src/stores/modal.ts +6 -1
- package/dist/spa/src/stores/toast.ts +22 -3
- package/dist/spa/src/types/Back.ts +158 -22
- package/dist/spa/src/types/Common.ts +81 -32
- package/dist/spa/src/types/FrontendAPI.ts +31 -5
- package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
- package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -2
- package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
- package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
- package/dist/spa/src/types/adapters/index.ts +8 -0
- package/dist/spa/src/utils.ts +279 -9
- package/dist/spa/src/views/CreateView.vue +18 -19
- package/dist/spa/src/views/EditView.vue +25 -19
- package/dist/spa/src/views/ListView.vue +144 -87
- package/dist/spa/src/views/LoginView.vue +26 -35
- package/dist/spa/src/views/ResourceParent.vue +2 -2
- package/dist/spa/src/views/SettingsView.vue +121 -0
- package/dist/spa/src/views/ShowView.vue +59 -39
- package/dist/spa/src/websocket.ts +6 -1
- package/dist/spa/tsconfig.app.json +1 -1
- package/dist/spa/vite.config.ts +45 -2
- package/dist/types/Back.d.ts +134 -14
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js +15 -0
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +96 -29
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +31 -3
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
- package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
- package/dist/types/adapters/CaptchaAdapter.js +5 -0
- package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
- package/dist/types/adapters/EmailAdapter.d.ts +1 -1
- package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
- package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
- package/dist/types/adapters/ImageVisionAdapter.js +2 -0
- package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.js +2 -0
- package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
- package/dist/types/adapters/index.d.ts +9 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/index.js +2 -0
- package/dist/types/adapters/index.js.map +1 -0
- package/package.json +4 -2
- package/dist/spa/src/types/adapters/index.js +0 -5
|
@@ -1,55 +1,58 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="afcl-select-wrapper relative inline-block" ref="internalSelect"
|
|
2
|
+
<div class="afcl-select afcl-select-wrapper relative inline-block" ref="internalSelect"
|
|
3
3
|
:class="{'opacity-50': readonly}"
|
|
4
4
|
>
|
|
5
5
|
<div class="relative">
|
|
6
6
|
<input
|
|
7
7
|
ref="inputEl"
|
|
8
8
|
type="text"
|
|
9
|
-
:readonly="readonly"
|
|
9
|
+
:readonly="readonly || searchDisabled"
|
|
10
10
|
v-model="search"
|
|
11
11
|
@click="inputClick"
|
|
12
12
|
@input="inputInput"
|
|
13
|
-
class="
|
|
13
|
+
class="block w-full pl-3 pr-10 py-2.5 border border-lightDropownButtonsBorder rounded-md leading-5 bg-lightDropdownButtonsBackground
|
|
14
|
+
placeholder-lightDropdownButtonsPlaceholderText text-lightDropdownButtonsText sm:text-sm transition duration-150 ease-in-out dark:bg-darkDropdownButtonsBackground dark:border-darkDropdownButtonsBorder dark:placeholder-darkDropdownButtonsPlaceholderText
|
|
15
|
+
dark:text-darkDropdownButtonsText focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
14
16
|
autocomplete="off" data-custom="no-autofill"
|
|
15
17
|
:placeholder="
|
|
16
18
|
selectedItems.length && !multiple ? '' : (showDropdown ? $t('Search') : placeholder || $t('Select...'))
|
|
17
19
|
"
|
|
18
20
|
/>
|
|
19
21
|
|
|
20
|
-
<div v-if="!multiple && selectedItems.length" class="absolute pointer-events-none inset-y-0 left-2 flex items-center pr-2 px-1">
|
|
22
|
+
<div v-if="!multiple && selectedItems.length" class="text-lightDropdownButtonsText dark:text-darkDropdownButtonsText absolute pointer-events-none inset-y-0 left-2 flex items-center pr-2 px-1">
|
|
21
23
|
<slot
|
|
22
24
|
name="selected-item"
|
|
23
25
|
:option="selectedItems[0]"
|
|
24
26
|
></slot>
|
|
25
|
-
<span v-if="!$slots['selected-item']" class="text-
|
|
27
|
+
<span v-if="!$slots['selected-item']" class="text-lightDropdownButtonsText dark:text-darkDropdownButtonsText font-medium ">
|
|
26
28
|
{{ selectedItems[0]?.label }}
|
|
27
29
|
</span>
|
|
28
30
|
</div>
|
|
29
31
|
|
|
30
32
|
<div class="absolute inset-y-0 right-2 flex items-center pointer-events-none">
|
|
31
33
|
<!-- triangle icon -->
|
|
32
|
-
<IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-
|
|
34
|
+
<IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-darkPrimary opacity-50 transition duration-150 ease-in"
|
|
33
35
|
:class="{ 'transform rotate-180': showDropdown }"
|
|
34
36
|
/>
|
|
35
37
|
</div>
|
|
36
38
|
</div>
|
|
37
|
-
<teleport to="body" v-if="teleportToBody && showDropdown">
|
|
38
|
-
<div ref="dropdownEl" :style="getDropdownPosition" :class="{'shadow-none': isTop}"
|
|
39
|
-
class="fixed
|
|
40
|
-
dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48"
|
|
39
|
+
<teleport to="body" v-if="(teleportToBody || teleportToTop) && showDropdown">
|
|
40
|
+
<div ref="dropdownEl" :style="getDropdownPosition" :class="{'shadow-none': isTop, 'z-[5]': teleportToBody, 'z-[1000]': teleportToTop}"
|
|
41
|
+
class="fixed w-full bg-lightDropdownOptionsBackground shadow-lg dark:shadow-black dark:bg-darkDropdownOptionsBackground
|
|
42
|
+
dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48"
|
|
43
|
+
@scroll="handleDropdownScroll">
|
|
41
44
|
<div
|
|
42
45
|
v-for="item in filteredItems"
|
|
43
46
|
:key="item.value"
|
|
44
|
-
class="px-4 py-2 cursor-pointer hover:bg-
|
|
45
|
-
:class="{ 'bg-
|
|
47
|
+
class="px-4 py-2 cursor-pointer hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground text-lightDropdownOptionsText dark:text-darkDropdownOptionsText"
|
|
48
|
+
:class="{ 'bg-lightDropdownPicked dark:bg-darkDropdownPicked': selectedItems.includes(item) }"
|
|
46
49
|
@click="toogleItem(item)"
|
|
47
50
|
>
|
|
48
51
|
<slot name="item" :option="item"></slot>
|
|
49
52
|
<label v-if="!$slots.item" :for="item.value">{{ item.label }}</label>
|
|
50
53
|
</div>
|
|
51
|
-
<div v-if="!filteredItems.length" class="px-4 py-2 cursor-pointer text-
|
|
52
|
-
{{ options
|
|
54
|
+
<div v-if="!filteredItems.length" class="px-4 py-2 cursor-pointer text-lightDropdownOptionsText dark:text-darkDropdownOptionsText">
|
|
55
|
+
{{ options?.length ? $t('No results found') : $t('No items here') }}
|
|
53
56
|
</div>
|
|
54
57
|
|
|
55
58
|
<div v-if="$slots['extra-item']" class="px-4 py-2 dark:text-gray-400">
|
|
@@ -58,24 +61,24 @@
|
|
|
58
61
|
</div>
|
|
59
62
|
</teleport>
|
|
60
63
|
|
|
61
|
-
<div v-if="!teleportToBody && showDropdown" ref="dropdownEl" :style="dropdownStyle" :class="{'shadow-none': isTop}"
|
|
62
|
-
class="absolute z-10 mt-1 w-full bg-
|
|
63
|
-
dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48"
|
|
64
|
+
<div v-if="!teleportToBody && !teleportToTop && showDropdown" ref="dropdownEl" :style="dropdownStyle" :class="{'shadow-none': isTop}"
|
|
65
|
+
class="absolute z-10 mt-1 w-full bg-lightDropdownOptionsBackground shadow-lg text-lightDropdownButtonsText dark:shadow-black dark:bg-darkDropdownOptionsBackground
|
|
66
|
+
dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48"
|
|
67
|
+
@scroll="handleDropdownScroll">
|
|
64
68
|
<div
|
|
65
69
|
v-for="item in filteredItems"
|
|
66
70
|
:key="item.value"
|
|
67
|
-
class="px-4 py-2 cursor-pointer hover:bg-
|
|
68
|
-
:class="{ 'bg-
|
|
71
|
+
class="px-4 py-2 cursor-pointer text-lightDropdownOptionsText hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground dark:text-darkDropdownOptionsText"
|
|
72
|
+
:class="{ 'bg-lightDropdownPicked dark:bg-darkDropdownPicked': selectedItems.includes(item) }"
|
|
69
73
|
@click="toogleItem(item)"
|
|
70
74
|
>
|
|
71
75
|
<slot name="item" :option="item"></slot>
|
|
72
76
|
<label v-if="!$slots.item" :for="item.value">{{ item.label }}</label>
|
|
73
77
|
</div>
|
|
74
|
-
<div v-if="!filteredItems.length" class="px-4 py-2 cursor-pointer text-
|
|
75
|
-
{{ options
|
|
78
|
+
<div v-if="!filteredItems.length" class="px-4 py-2 cursor-pointer text-lightDropdownOptionsText dark:text-darkDropdownOptionsText">
|
|
79
|
+
{{ options?.length ? $t('No results found') : $t('No items here') }}
|
|
76
80
|
</div>
|
|
77
|
-
|
|
78
|
-
<div v-if="$slots['extra-item']" class="px-4 py-2 dark:text-gray-400">
|
|
81
|
+
<div v-if="$slots['extra-item']" class="px-4 py-2 dark:text-darkDropdownOptionsText">
|
|
79
82
|
<slot name="extra-item"></slot>
|
|
80
83
|
</div>
|
|
81
84
|
|
|
@@ -87,12 +90,12 @@
|
|
|
87
90
|
<template v-for="item in selectedItems" :key="`afcl-select-${item.value}`">
|
|
88
91
|
<slot name="selected-item" :item="item"></slot>
|
|
89
92
|
<div v-if="!$slots['selected-item']"
|
|
90
|
-
class="bg-
|
|
93
|
+
class="bg-lightDropdownMultipleSelectBackground text-lightDropdownMultipleSelectText text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-darkDropdownMultipleSelectBackground dark:text-darkDropdownMultipleSelectText">
|
|
91
94
|
<span>{{ item.label }}</span>
|
|
92
95
|
<button
|
|
93
96
|
type="button"
|
|
94
97
|
@click="toogleItem(item)"
|
|
95
|
-
class="z-index-100 flex-shrink-0 ml-1 h-4 w-4 -mr-1 rounded-full inline-flex items-center justify-center text-
|
|
98
|
+
class="z-index-100 flex-shrink-0 ml-1 h-4 w-4 -mr-1 rounded-full inline-flex items-center justify-center text-lightDropdownMultipleSelectIcon hover:text-lightDropdownMultipleSelectIconHover dark:text-darkDropdownMultipleSelectIcon dark:hover:text-darkDropdownMultipleSelectIconHover focus:outline-none focus:text-lightDropdownMultipleSelectIconFocus focus:bg-lightDropdownMultipleSelectIconFocusBackground dark:focus:text-darkDropdownMultipleSelectIconFocus dark:focus:bg-darkDropdownMultipleSelectIconFocusBackground"
|
|
96
99
|
>
|
|
97
100
|
<span class="sr-only">{{ $t('Remove item') }}</span>
|
|
98
101
|
<svg class="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8">
|
|
@@ -111,14 +114,15 @@
|
|
|
111
114
|
</template>
|
|
112
115
|
|
|
113
116
|
<script setup lang="ts">
|
|
114
|
-
import { ref, computed, onMounted, onUnmounted, watch, type Ref } from 'vue';
|
|
117
|
+
import { ref, computed, onMounted, onUnmounted, watch, nextTick,type PropType, type Ref } from 'vue';
|
|
115
118
|
import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
|
|
116
119
|
import { useElementSize } from '@vueuse/core'
|
|
117
120
|
|
|
118
121
|
const props = defineProps({
|
|
119
122
|
options: Array,
|
|
120
123
|
modelValue: {
|
|
121
|
-
|
|
124
|
+
type: Array as PropType<(string | number)[]>,
|
|
125
|
+
default: () => [],
|
|
122
126
|
},
|
|
123
127
|
multiple: {
|
|
124
128
|
type: Boolean,
|
|
@@ -132,13 +136,21 @@ const props = defineProps({
|
|
|
132
136
|
type: Boolean,
|
|
133
137
|
default: false,
|
|
134
138
|
},
|
|
139
|
+
searchDisabled: {
|
|
140
|
+
type: Boolean,
|
|
141
|
+
default: false,
|
|
142
|
+
},
|
|
135
143
|
teleportToBody: {
|
|
136
144
|
type: Boolean,
|
|
137
145
|
default: false,
|
|
138
146
|
},
|
|
147
|
+
teleportToTop: {
|
|
148
|
+
type: Boolean,
|
|
149
|
+
default: false,
|
|
150
|
+
},
|
|
139
151
|
});
|
|
140
152
|
|
|
141
|
-
const emit = defineEmits(['update:modelValue']);
|
|
153
|
+
const emit = defineEmits(['update:modelValue', 'scroll-near-end', 'search']);
|
|
142
154
|
|
|
143
155
|
const search = ref('');
|
|
144
156
|
const showDropdown = ref(false);
|
|
@@ -159,24 +171,27 @@ function inputInput() {
|
|
|
159
171
|
selectedItems.value = [];
|
|
160
172
|
emit('update:modelValue', null);
|
|
161
173
|
}
|
|
174
|
+
if (!props.searchDisabled) {
|
|
175
|
+
emit('search', search.value);
|
|
176
|
+
}
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
function updateFromProps() {
|
|
165
180
|
if (props.modelValue !== undefined) {
|
|
166
181
|
if (!props.multiple) {
|
|
167
|
-
const el = props.options
|
|
182
|
+
const el = props.options?.find((item: any) => item.value === props.modelValue);
|
|
168
183
|
if (el) {
|
|
169
184
|
selectedItems.value = [el];
|
|
170
185
|
} else {
|
|
171
186
|
selectedItems.value = [];
|
|
172
187
|
}
|
|
173
188
|
} else {
|
|
174
|
-
selectedItems.value = props.options
|
|
189
|
+
selectedItems.value = props.options?.filter((item: any) => props.modelValue?.includes(item.value)) || [];
|
|
175
190
|
}
|
|
176
191
|
}
|
|
177
192
|
}
|
|
178
193
|
|
|
179
|
-
function inputClick() {
|
|
194
|
+
async function inputClick() {
|
|
180
195
|
if (props.readonly) return;
|
|
181
196
|
// Toggle local dropdown
|
|
182
197
|
showDropdown.value = !showDropdown.value;
|
|
@@ -184,6 +199,11 @@ function inputClick() {
|
|
|
184
199
|
if (!showDropdown.value && !search.value) {
|
|
185
200
|
search.value = '';
|
|
186
201
|
}
|
|
202
|
+
|
|
203
|
+
if(props.teleportToBody){
|
|
204
|
+
await nextTick();
|
|
205
|
+
handleScroll();
|
|
206
|
+
}
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
watch(
|
|
@@ -221,6 +241,15 @@ const handleScroll = () => {
|
|
|
221
241
|
}
|
|
222
242
|
};
|
|
223
243
|
|
|
244
|
+
const handleDropdownScroll = (event: Event) => {
|
|
245
|
+
const target = event.target as HTMLElement;
|
|
246
|
+
const threshold = 10; // pixels from bottom
|
|
247
|
+
|
|
248
|
+
if (target.scrollTop + target.clientHeight >= target.scrollHeight - threshold) {
|
|
249
|
+
emit('scroll-near-end');
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
224
253
|
onMounted(() => {
|
|
225
254
|
updateFromProps();
|
|
226
255
|
|
|
@@ -240,8 +269,13 @@ onMounted(() => {
|
|
|
240
269
|
}
|
|
241
270
|
});
|
|
242
271
|
|
|
243
|
-
const filteredItems = computed(() => {
|
|
244
|
-
|
|
272
|
+
const filteredItems: Ref<any[]> = computed(() => {
|
|
273
|
+
|
|
274
|
+
if (props.searchDisabled) {
|
|
275
|
+
return props.options || [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return (props.options || []).filter((item: any) =>
|
|
245
279
|
item.label.toLowerCase().includes(search.value.toLowerCase())
|
|
246
280
|
);
|
|
247
281
|
});
|
|
@@ -262,7 +296,7 @@ const removeClickListener = () => {
|
|
|
262
296
|
document.removeEventListener('click', handleClickOutside);
|
|
263
297
|
};
|
|
264
298
|
|
|
265
|
-
const toogleItem = (item) => {
|
|
299
|
+
const toogleItem = (item: any) => {
|
|
266
300
|
if (selectedItems.value.includes(item)) {
|
|
267
301
|
selectedItems.value = selectedItems.value.filter(i => i.value !== item.value);
|
|
268
302
|
} else {
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="type === 'image'" role="status" class="flex animate-pulse items-center justify-center bg-
|
|
3
|
-
<svg class="w-10 h-10 text-
|
|
2
|
+
<div v-if="type === 'image'" role="status" class="flex animate-pulse items-center justify-center bg-lightSkeletonBackgroundColor rounded dark:bg-darkSkeletonBackgroundColor">
|
|
3
|
+
<svg class="w-10 h-10 text-lightSkeletonIconColor dark:text-darkSkeletonIconColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
|
|
4
4
|
<path d="M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z"/>
|
|
5
5
|
</svg>
|
|
6
6
|
</div>
|
|
7
|
-
<div v-else-if="type === 'video'" role="status" class="flex items-center justify-center max-w-sm bg-
|
|
8
|
-
<svg class="w-10 h-10 text-
|
|
7
|
+
<div v-else-if="type === 'video'" role="status" class="flex items-center justify-center max-w-sm bg-lightSkeletonBackgroundColor rounded-lg animate-pulse dark:bg-darkSkeletonBackgroundColor">
|
|
8
|
+
<svg class="w-10 h-10 text-lightSkeletonIconColor dark:text-darkSkeletonIconColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 20">
|
|
9
9
|
<path d="M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z"/>
|
|
10
10
|
<path d="M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM9 13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2Zm4 .382a1 1 0 0 1-1.447.894L10 13v-2l1.553-1.276a1 1 0 0 1 1.447.894v2.764Z"/>
|
|
11
11
|
</svg>
|
|
12
12
|
<span class="sr-only">Loading...</span>
|
|
13
13
|
</div>
|
|
14
|
-
<svg v-else-if="type === 'avatar'" class="me-3 animate-pulse text-
|
|
14
|
+
<svg v-else-if="type === 'avatar'" class="me-3 animate-pulse text-lightSkeletonIconColor dark:text-darkSkeletonBackgroundColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
15
15
|
<path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm0 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm0 13a8.949 8.949 0 0 1-4.951-1.488A3.987 3.987 0 0 1 9 13h2a3.987 3.987 0 0 1 3.951 3.512A8.949 8.949 0 0 1 10 18Z"/>
|
|
16
16
|
</svg>
|
|
17
|
-
<div v-else role="status" class="flex items-center justify-center animate-pulse bg-
|
|
17
|
+
<div v-else role="status" class="flex items-center justify-center animate-pulse bg-lightSkeletonIconColor rounded-full dark:bg-darkSkeletonBackgroundColor">
|
|
18
18
|
<span class="sr-only">Loading...</span>
|
|
19
19
|
</div>
|
|
20
20
|
</template>
|
|
@@ -1,79 +1,140 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
<div class="
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
<div class="afcl-table-container relative overflow-x-auto shadow-md rounded-lg">
|
|
3
|
+
<div class="overflow-x-auto w-full">
|
|
4
|
+
<table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
|
|
5
|
+
<thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
6
|
+
<tr>
|
|
7
|
+
<th scope="col" class="px-6 py-3" ref="headerRefs" :key="`header-${column.fieldName}`"
|
|
8
|
+
v-for="column in columns"
|
|
9
|
+
>
|
|
10
|
+
<slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
|
|
11
|
+
|
|
12
|
+
<span v-else>
|
|
13
|
+
{{ column.label }}
|
|
14
|
+
</span>
|
|
15
|
+
</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<SkeleteLoader
|
|
20
|
+
v-if="isLoading || props.isLoading"
|
|
21
|
+
:rows="pageSize"
|
|
22
|
+
:columns="columns.length"
|
|
23
|
+
:row-heights="rowHeights"
|
|
24
|
+
:column-widths="columnWidths"
|
|
25
|
+
/>
|
|
26
|
+
<tr v-else-if="!isLoading && !props.isLoading && dataPage.length === 0" class="afcl-table-empty-body">
|
|
27
|
+
<td :colspan="columns.length" class="px-6 py-12 text-center">
|
|
28
|
+
<div class="flex flex-col items-center justify-center text-lightTableText dark:text-darkTableText">
|
|
29
|
+
<IconTableRowOutline class="w-10 h-10 mb-4 text-gray-400 dark:text-gray-500" />
|
|
30
|
+
<p class="text-md">{{ $t('No data available') }}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr
|
|
35
|
+
v-else="!isLoading && !props.isLoading"
|
|
36
|
+
v-for="(item, index) in dataPage"
|
|
37
|
+
:key="`row-${index}`"
|
|
38
|
+
ref="rowRefs"
|
|
39
|
+
:class="{
|
|
40
|
+
'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
|
|
41
|
+
'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
|
|
42
|
+
}"
|
|
32
43
|
>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
<td class="px-6 py-4" :key="`cell-${index}-${column.fieldName}`"
|
|
45
|
+
v-for="column in props.columns"
|
|
46
|
+
>
|
|
47
|
+
<slot v-if="$slots[`cell:${column.fieldName}`]"
|
|
48
|
+
:name="`cell:${column.fieldName}`"
|
|
49
|
+
:item="item" :column="column"
|
|
50
|
+
>
|
|
51
|
+
</slot>
|
|
52
|
+
<span v-else-if="!isLoading || props.isLoading" >
|
|
53
|
+
{{ item[column.fieldName] }}
|
|
54
|
+
</span>
|
|
55
|
+
<div v-else>
|
|
56
|
+
<div class=" w-full">
|
|
57
|
+
<Skeleton class="h-4" />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
</div>
|
|
65
|
+
<nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground mt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
|
|
66
|
+
v-if="totalPages > 1"
|
|
67
|
+
:aria-label="$t('Table navigation')">
|
|
44
68
|
<i18n-t
|
|
45
|
-
keypath="Showing {from} to {to} of {total}" tag="span" class="text-sm font-normal text-
|
|
69
|
+
keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto"
|
|
46
70
|
>
|
|
47
|
-
<template #from><span class="font-semibold text-
|
|
48
|
-
<template #to><span class="font-semibold text-
|
|
49
|
-
<template #total><span class="font-semibold text-
|
|
71
|
+
<template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, dataResult.total) }}</span></template>
|
|
72
|
+
<template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, dataResult.total) }}</span></template>
|
|
73
|
+
<template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
|
|
50
74
|
</i18n-t>
|
|
75
|
+
<div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
76
|
+
<div class="inline-flex" :class="isLoading || props.isLoading ? 'pointer-events-none select-none opacity-50' : ''">
|
|
77
|
+
<!-- Buttons -->
|
|
78
|
+
<button
|
|
79
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
80
|
+
@click="currentPage--; pageInput = currentPage.toString();"
|
|
81
|
+
:disabled="currentPage <= 1 || isLoading || props.isLoading">
|
|
82
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
83
|
+
viewBox="0 0 14 10">
|
|
84
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
85
|
+
d="M13 5H1m0 0 4 4M1 5l4-4"/>
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
90
|
+
@click="switchPage(1); pageInput = currentPage.toString();"
|
|
91
|
+
:disabled="currentPage <= 1 || isLoading || props.isLoading">
|
|
92
|
+
<!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
|
|
93
|
+
1
|
|
94
|
+
</button>
|
|
95
|
+
<div
|
|
96
|
+
:contenteditable="!isLoading && !props.isLoading"
|
|
97
|
+
class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
|
|
98
|
+
@keydown="onPageKeydown($event)"
|
|
99
|
+
@input="onPageInput($event)"
|
|
100
|
+
@blur="validatePageInput()"
|
|
101
|
+
>
|
|
102
|
+
{{ pageInput }}
|
|
103
|
+
</div>
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"text-blue-600 bg-lightPrimary text-lightPrimaryContrast dark:bg-darkPrimary dark:text-darkPrimaryContrast hover:opacity-90": page === currentPage,
|
|
59
|
-
"text-gray-500 border bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white": page !== currentPage,
|
|
60
|
-
"rounded-s-lg ms-0": page === 1,
|
|
61
|
-
"rounded-e-lg": page === totalPages,
|
|
62
|
-
}'
|
|
63
|
-
class="flex items-center justify-center px-3 h-8 leading-tight ">
|
|
64
|
-
{{ page }}
|
|
65
|
-
</a>
|
|
66
|
-
</li>
|
|
67
|
-
</ul>
|
|
68
|
-
</nav>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
105
|
+
<button
|
|
106
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
107
|
+
@click="currentPage = totalPages; pageInput = currentPage.toString();"
|
|
108
|
+
:disabled="currentPage >= totalPages || isLoading || props.isLoading"
|
|
109
|
+
>
|
|
110
|
+
{{ totalPages }}
|
|
71
111
|
|
|
72
|
-
|
|
112
|
+
</button>
|
|
113
|
+
<button
|
|
114
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
115
|
+
@click="currentPage++; pageInput = currentPage.toString();"
|
|
116
|
+
:disabled="currentPage >= totalPages || isLoading || props.isLoading"
|
|
117
|
+
>
|
|
118
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
119
|
+
viewBox="0 0 14 10">
|
|
120
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
121
|
+
d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
|
122
|
+
</svg>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</nav>
|
|
127
|
+
</div>
|
|
73
128
|
</template>
|
|
74
129
|
|
|
75
130
|
<script setup lang="ts">
|
|
76
|
-
import { ref,
|
|
131
|
+
import { ref, computed, useTemplateRef, watch, onMounted } from 'vue';
|
|
132
|
+
import SkeleteLoader from '@/components/SkeleteLoader.vue';
|
|
133
|
+
import { IconTableRowOutline } from '@iconify-prerendered/vue-flowbite';
|
|
134
|
+
|
|
135
|
+
defineExpose({
|
|
136
|
+
refreshTable
|
|
137
|
+
})
|
|
77
138
|
|
|
78
139
|
const props = withDefaults(
|
|
79
140
|
defineProps<{
|
|
@@ -83,34 +144,112 @@
|
|
|
83
144
|
}[],
|
|
84
145
|
data: {
|
|
85
146
|
[key: string]: any,
|
|
86
|
-
}[],
|
|
147
|
+
}[] | ((params: { offset: number, limit: number }) => Promise<{data: {[key: string]: any}[], total: number}>),
|
|
87
148
|
evenHighlights?: boolean,
|
|
88
149
|
pageSize?: number,
|
|
150
|
+
isLoading?: boolean,
|
|
89
151
|
}>(), {
|
|
90
152
|
evenHighlights: true,
|
|
91
|
-
pageSize:
|
|
153
|
+
pageSize: 5,
|
|
92
154
|
}
|
|
93
155
|
);
|
|
94
156
|
|
|
157
|
+
const pageInput = ref('1');
|
|
158
|
+
const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
|
|
159
|
+
const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
|
|
160
|
+
const rowHeights = ref<number[]>([]);
|
|
161
|
+
const columnWidths = ref<number[]>([]);
|
|
95
162
|
const currentPage = ref(1);
|
|
163
|
+
const isLoading = ref(false);
|
|
164
|
+
const dataResult = ref<{data: {[key: string]: any}[], total: number}>({data: [], total: 0});
|
|
165
|
+
const isAtLeastOneLoading = ref<boolean[]>([false]);
|
|
166
|
+
|
|
167
|
+
onMounted(() => {
|
|
168
|
+
refresh();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
watch([currentPage, () => props.data], async () => {
|
|
172
|
+
refresh();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
watch(() => currentPage.value, () => {
|
|
176
|
+
rowHeights.value = !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
|
|
177
|
+
columnWidths.value = !headerRefs.value ? [] : headerRefs.value.map((el: HTMLElement) => el.offsetWidth);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
watch([isLoading, () => props.isLoading], () => {
|
|
181
|
+
emit('update:tableLoading', isLoading.value || props.isLoading);
|
|
182
|
+
});
|
|
96
183
|
|
|
97
184
|
const totalPages = computed(() => {
|
|
98
|
-
return Math.ceil(
|
|
185
|
+
return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
|
|
99
186
|
});
|
|
100
187
|
|
|
101
188
|
const dataPage = computed(() => {
|
|
102
|
-
|
|
103
|
-
const end = start + props.pageSize;
|
|
104
|
-
return props.data.slice(start, end);
|
|
189
|
+
return dataResult.value?.data;
|
|
105
190
|
});
|
|
106
191
|
|
|
107
192
|
function switchPage(p: number) {
|
|
108
193
|
currentPage.value = p;
|
|
194
|
+
pageInput.value = p.toString();
|
|
109
195
|
}
|
|
110
196
|
|
|
111
|
-
const
|
|
112
|
-
'update:
|
|
197
|
+
const emit = defineEmits([
|
|
198
|
+
'update:tableLoading',
|
|
113
199
|
]);
|
|
114
200
|
|
|
201
|
+
function onPageInput(event: any) {
|
|
202
|
+
pageInput.value = event.target.innerText;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function validatePageInput() {
|
|
206
|
+
const newPage = parseInt(pageInput.value) || 1;
|
|
207
|
+
const validPage = Math.max(1, Math.min(newPage, totalPages.value));
|
|
208
|
+
currentPage.value = validPage;
|
|
209
|
+
pageInput.value = validPage.toString();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
watch(() => currentPage.value, (newPage) => {
|
|
213
|
+
pageInput.value = newPage.toString();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
async function onPageKeydown(event: any) {
|
|
217
|
+
// page input should accept only numbers, arrow keys and backspace
|
|
218
|
+
if (['Enter', 'Space'].includes(event.code) ||
|
|
219
|
+
(!['Backspace', 'ArrowRight', 'ArrowLeft'].includes(event.code)
|
|
220
|
+
&& isNaN(Number(String.fromCharCode(event.keyCode || 0))))) {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
if (event.code === 'Enter') {
|
|
223
|
+
validatePageInput();
|
|
224
|
+
event.target.blur();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function refresh() {
|
|
230
|
+
if (typeof props.data === 'function') {
|
|
231
|
+
isLoading.value = true;
|
|
232
|
+
const currentLoadingIndex = currentPage.value;
|
|
233
|
+
isAtLeastOneLoading.value[currentLoadingIndex] = true;
|
|
234
|
+
const result = await props.data({ offset: (currentLoadingIndex - 1) * props.pageSize, limit: props.pageSize });
|
|
235
|
+
isAtLeastOneLoading.value[currentLoadingIndex] = false;
|
|
236
|
+
if (isAtLeastOneLoading.value.every(v => v === false)) {
|
|
237
|
+
isLoading.value = false;
|
|
238
|
+
}
|
|
239
|
+
dataResult.value = result;
|
|
240
|
+
} else if (typeof props.data === 'object' && Array.isArray(props.data)) {
|
|
241
|
+
const start = (currentPage.value - 1) * props.pageSize;
|
|
242
|
+
const end = start + props.pageSize;
|
|
243
|
+
dataResult.value = { data: props.data.slice(start, end), total: props.data.length };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function refreshTable() {
|
|
248
|
+
if ( currentPage.value !== 1 ) {
|
|
249
|
+
currentPage.value = 1;
|
|
250
|
+
} else {
|
|
251
|
+
refresh();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
115
254
|
|
|
116
255
|
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<textarea
|
|
4
|
+
ref="input"
|
|
5
|
+
class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-lg block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
|
|
6
|
+
:placeholder="placeholder"
|
|
7
|
+
:value="modelValue"
|
|
8
|
+
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
9
|
+
:readonly="readonly"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
|
|
16
|
+
import { ref } from 'vue';
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
modelValue: string,
|
|
20
|
+
readonly?: boolean,
|
|
21
|
+
placeholder?: string,
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const input = ref<HTMLInputElement | null>(null)
|
|
25
|
+
|
|
26
|
+
defineExpose({
|
|
27
|
+
focus: () => input.value?.focus(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
</script>
|
|
31
|
+
|