@citruslime/ui 1.2.1-beta.0 → 2.0.0-beta.3
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/.eslintrc.js +8 -2
- package/dist/@types/appUser.d.ts +1 -0
- package/dist/@types/components/grid/column.d.ts +2 -1
- package/dist/@types/components/header/index.d.ts +0 -1
- package/dist/@types/components/{header/navigation.d.ts → navigation/index.d.ts} +7 -4
- package/dist/@types/index.d.ts +1 -0
- package/dist/components/index.d.ts +17 -14
- package/dist/main.d.ts +1 -1
- package/dist/style.css +1 -1
- package/dist/theme.js +2 -4
- package/dist/ui.es.js +1 -1
- package/dist/ui.umd.js +1 -1
- package/package.json +7 -4
- package/src/components/accordion/cl-ui-accordion.vue +89 -0
- package/src/components/app/cl-ui-app.vue +35 -0
- package/src/components/button/{button.vue → cl-ui-button.vue} +26 -6
- package/src/components/calendar/cl-ui-calendar.vue +277 -0
- package/src/components/card/{card.vue → cl-ui-card.vue} +17 -1
- package/src/components/combo-box/cl-ui-combo-box.vue +357 -0
- package/src/components/combo-box/search-container/cl-ui-combo-box-search.vue +279 -0
- package/src/components/combo-box/search-container/{header-option/header-option.vue → header/cl-ui-combo-box-header.vue} +17 -2
- package/src/components/combo-box/search-container/selectable/cl-ui-combo-box-selectable.vue +99 -0
- package/src/components/footer/{footer.vue → cl-ui-footer.vue} +10 -2
- package/src/components/grid/cell/{cell.vue → cl-ui-grid-cell.vue} +90 -1
- package/src/components/grid/cl-ui-grid.vue +477 -0
- package/src/components/grid/filter/cl-ui-grid-filter.vue +270 -0
- package/src/components/grid/footer/{footer.vue → cl-ui-grid-footer.vue} +100 -5
- package/src/components/grid/header/cl-ui-grid-header.vue +76 -0
- package/src/components/grid/view-manager/cl-ui-grid-view-manager.vue +145 -0
- package/src/components/header/cl-ui-header.vue +11 -0
- package/src/components/header-helper/cl-ui-header-helper.vue +50 -0
- package/src/components/language-switcher/{language-switcher.vue → cl-ui-language-switcher.vue} +49 -3
- package/src/components/loading-spinner/cl-ui-loading-spinner.vue +16 -0
- package/src/components/login/{login.vue → cl-ui-login.vue} +101 -19
- package/src/components/modal/{modal.vue → cl-ui-modal.vue} +74 -2
- package/src/components/navigation/cl-ui-navigation.vue +124 -0
- package/src/components/notification/{notification.vue → cl-ui-notification.vue} +21 -2
- package/src/components/slider/cl-ui-slider.vue +145 -0
- package/src/components/accordion/accordion.vue +0 -30
- package/src/components/calendar/calendar.vue +0 -35
- package/src/components/combo-box/combo-box.vue +0 -79
- package/src/components/combo-box/search-container/search-container.vue +0 -57
- package/src/components/combo-box/search-container/selectable-option/selectable-option.vue +0 -27
- package/src/components/grid/filter/filter.vue +0 -93
- package/src/components/grid/grid.vue +0 -194
- package/src/components/grid/header/header.vue +0 -39
- package/src/components/grid/view-manager/view-manager.vue +0 -73
- package/src/components/header/header-helper/header-helper.vue +0 -95
- package/src/components/header/header.vue +0 -33
- package/src/components/header/navigation/navigation.vue +0 -84
- package/src/components/loading-spinner/loading-spinner.vue +0 -8
- package/src/components/slider/slider.vue +0 -41
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { ComboBoxItem, ComboBoxCreateRequest, ComboBoxCreateResponse, ComboBoxLocalisations, ComboBoxSearchContainerLocalisations } from '../../@types';
|
|
5
|
+
import { showNotification } from '../../composables/notification';
|
|
6
|
+
|
|
7
|
+
import ClUiComboBoxSearch from './search-container/cl-ui-combo-box-search.vue';
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{
|
|
10
|
+
localisations?: ComboBoxLocalisations & ComboBoxSearchContainerLocalisations;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
objectType: string;
|
|
15
|
+
objectParentType?: string;
|
|
16
|
+
canCreateNewObject?: boolean;
|
|
17
|
+
canClearSelectedObject?: boolean;
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
objectCreatedResponse?: ComboBoxCreateResponse | null;
|
|
20
|
+
parentObjectCreatedResponse?: ComboBoxCreateResponse | null;
|
|
21
|
+
results?: ComboBoxItem[];
|
|
22
|
+
parentResults?: ComboBoxItem[];
|
|
23
|
+
currentObject?: ComboBoxItem | null;
|
|
24
|
+
}>(), {
|
|
25
|
+
localisations: () => ({
|
|
26
|
+
emptyHintText: 'Choose a {0}',
|
|
27
|
+
createTitle: 'Create {0}',
|
|
28
|
+
createProperty: 'Name',
|
|
29
|
+
createParentTitle: 'Create {0}',
|
|
30
|
+
createParentyProperty: 'Name',
|
|
31
|
+
parentName: 'Object',
|
|
32
|
+
create: 'Create',
|
|
33
|
+
cancel: 'Cancel',
|
|
34
|
+
createSuccessful: 'Successfully created the {0} \'{1}\'',
|
|
35
|
+
createFailed: 'Could not create \'{0}\' - {1}',
|
|
36
|
+
required: 'Required',
|
|
37
|
+
|
|
38
|
+
searchHintText: 'Enter the {0} name',
|
|
39
|
+
errorMessage: 'An error has occurred: {0}',
|
|
40
|
+
addPrompt: 'Add \'{0}\' as new {1}',
|
|
41
|
+
noResults: '0 matching results for \'{0}\''
|
|
42
|
+
}),
|
|
43
|
+
loading: false,
|
|
44
|
+
disabled: false,
|
|
45
|
+
required: false,
|
|
46
|
+
objectParentType: '',
|
|
47
|
+
canCreateNewObject: false,
|
|
48
|
+
canClearSelectedObject: true,
|
|
49
|
+
errorMessage: '',
|
|
50
|
+
objectCreatedResponse: () => null,
|
|
51
|
+
parentObjectCreatedResponse: () => null,
|
|
52
|
+
results: () => [],
|
|
53
|
+
parentResults: () => [],
|
|
54
|
+
currentObject: () => null
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const emit = defineEmits({
|
|
58
|
+
search: null,
|
|
59
|
+
'search-parent': null,
|
|
60
|
+
'create-object': null,
|
|
61
|
+
'create-parent-object': null,
|
|
62
|
+
'update:current-object': null
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const searchContainerVisible = ref<boolean>(false);
|
|
66
|
+
|
|
67
|
+
const selectedItem = computed<ComboBoxItem | null>({
|
|
68
|
+
get: () => props.currentObject,
|
|
69
|
+
set: (value) => emit('update:current-object', value)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const parentItem = ref<ComboBoxItem | null>(null);
|
|
73
|
+
const currentText = computed<string>(() => getDisplayName());
|
|
74
|
+
const hintText = computed<string>(() => props.localisations.emptyHintText.replacePlaceholders(props.objectType));
|
|
75
|
+
const createHeader = computed<string>(() => props.localisations.createTitle.replacePlaceholders(props.objectType));
|
|
76
|
+
|
|
77
|
+
const objectToCreateValue = ref<string>('');
|
|
78
|
+
const parentObjectToCreateValue = ref<string>('');
|
|
79
|
+
const createObjectRequest = ref<ComboBoxCreateRequest>();
|
|
80
|
+
const objectToCreateValid = ref<boolean>(true);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Toggles the visibility of the dropdown.
|
|
84
|
+
*
|
|
85
|
+
* @param {boolean} forcedState - The state to force the visibility to.
|
|
86
|
+
*/
|
|
87
|
+
function toggleDropdown (forcedState?: boolean) : void {
|
|
88
|
+
if (!props.disabled) {
|
|
89
|
+
if (typeof forcedState !== 'undefined') {
|
|
90
|
+
searchContainerVisible.value = forcedState;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
searchContainerVisible.value = !searchContainerVisible.value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Emit an event to get data filtered by the search term.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} searchTerm - The search term.
|
|
102
|
+
*/
|
|
103
|
+
function search (searchTerm: string) : void {
|
|
104
|
+
objectToCreateValue.value = searchTerm;
|
|
105
|
+
emit('search', searchTerm);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Emit an event to get data for the parent, filtered by the search term.
|
|
110
|
+
*
|
|
111
|
+
* @param searchTerm - The search term.
|
|
112
|
+
*/
|
|
113
|
+
function searchParent (searchTerm: string) : void {
|
|
114
|
+
parentObjectToCreateValue.value = searchTerm;
|
|
115
|
+
emit('search-parent', searchTerm);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear the object that is currently set.
|
|
120
|
+
*/
|
|
121
|
+
function clearObject () : void {
|
|
122
|
+
selectedItem.value = null;
|
|
123
|
+
toggleDropdown(false);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Select an object by an id.
|
|
128
|
+
*
|
|
129
|
+
* @param {number} id - The id of the object.
|
|
130
|
+
*/
|
|
131
|
+
function selectObject (id: number) : void {
|
|
132
|
+
const item = getComboBoxItemById(id);
|
|
133
|
+
|
|
134
|
+
selectedItem.value = item;
|
|
135
|
+
toggleDropdown(false);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get a combo box item by an id.
|
|
140
|
+
*
|
|
141
|
+
* @param {number} id - The id of the item.
|
|
142
|
+
* @returns {ComboBoxItem | null} - The combo box item with the id.
|
|
143
|
+
*/
|
|
144
|
+
function getComboBoxItemById (id: number) : ComboBoxItem | null {
|
|
145
|
+
let currentItem = null;
|
|
146
|
+
|
|
147
|
+
if (props.results !== null) {
|
|
148
|
+
for (let i = 0; i < props.results.length; i++) {
|
|
149
|
+
if (props.results[i].id === id) {
|
|
150
|
+
currentItem = props.results[i];
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return currentItem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Emit an event to create a new object.
|
|
161
|
+
*/
|
|
162
|
+
function createObject () : void {
|
|
163
|
+
createObjectRequest.value = {
|
|
164
|
+
name: objectToCreateValue.value,
|
|
165
|
+
parentId: parentItem.value?.id ?? 0
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
emit('create-object', createObjectRequest.value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Emit an event to create a new parent object.
|
|
173
|
+
*/
|
|
174
|
+
function createParentObject () : void {
|
|
175
|
+
createObjectRequest.value = {
|
|
176
|
+
name: parentObjectToCreateValue.value,
|
|
177
|
+
parentId: 0
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
emit('create-parent-object', createObjectRequest.value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle the response after trying to create an object.
|
|
185
|
+
*/
|
|
186
|
+
function handleObjectCreateResponse () : void {
|
|
187
|
+
if (props.objectCreatedResponse?.error) {
|
|
188
|
+
showNotification({
|
|
189
|
+
message: props.localisations.createFailed.replacePlaceholders(objectToCreateValue.value, props.objectCreatedResponse.error),
|
|
190
|
+
colour: 'danger',
|
|
191
|
+
duration: 10000
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else if (props.objectCreatedResponse?.id) {
|
|
195
|
+
handleSuccessfulObjectCreation();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handle the response after creating a parent object.
|
|
201
|
+
*/
|
|
202
|
+
function handleParentObjectCreateResponse (): void {
|
|
203
|
+
if (props.parentObjectCreatedResponse?.id) {
|
|
204
|
+
parentItem.value = {
|
|
205
|
+
id: props.parentObjectCreatedResponse.id,
|
|
206
|
+
name: props.parentObjectCreatedResponse.name ?? ''
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Handle the successful creation of an object.
|
|
213
|
+
*/
|
|
214
|
+
function handleSuccessfulObjectCreation () : void {
|
|
215
|
+
showNotification({
|
|
216
|
+
message: props.localisations.createSuccessful.replacePlaceholders(props.objectType, props.objectCreatedResponse?.name ?? ''),
|
|
217
|
+
colour: 'primary',
|
|
218
|
+
duration: 10000
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
let parentName = '';
|
|
222
|
+
|
|
223
|
+
if (parentItem.value && parentItem.value.id === props.objectCreatedResponse?.parentId) {
|
|
224
|
+
parentName = parentItem.value.name;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
selectedItem.value = {
|
|
228
|
+
id: props.objectCreatedResponse?.id ?? 0,
|
|
229
|
+
name: props.objectCreatedResponse?.name ?? '',
|
|
230
|
+
parentId: props.objectCreatedResponse?.parentId ?? 0,
|
|
231
|
+
parentName
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get the name of the selected object to display on the root input.
|
|
237
|
+
*
|
|
238
|
+
* @returns {string} - The name to display.
|
|
239
|
+
*/
|
|
240
|
+
function getDisplayName () : string {
|
|
241
|
+
let name = '';
|
|
242
|
+
|
|
243
|
+
if (selectedItem.value) {
|
|
244
|
+
if (selectedItem.value.parentName) {
|
|
245
|
+
name = selectedItem.value.parentName + '; ' + selectedItem.value.name;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
name = selectedItem.value.name;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return name;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate the object that will be created to determine whether
|
|
257
|
+
* a name has been entered and a parent object has been selected.
|
|
258
|
+
*/
|
|
259
|
+
function validateObjectToCreate () : void {
|
|
260
|
+
let valid = true;
|
|
261
|
+
|
|
262
|
+
if (objectToCreateValue.value === '') {
|
|
263
|
+
valid = false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (props.objectParentType !== '' && parentItem.value === null) {
|
|
267
|
+
valid = false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
objectToCreateValid.value = valid;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
watch(() => props.objectCreatedResponse, () => handleObjectCreateResponse());
|
|
274
|
+
watch(() => props.parentObjectCreatedResponse, () => handleParentObjectCreateResponse());
|
|
275
|
+
watch(() => objectToCreateValue.value, () => validateObjectToCreate());
|
|
276
|
+
watch(() => parentItem.value, () => validateObjectToCreate());
|
|
277
|
+
</script>
|
|
278
|
+
|
|
279
|
+
<template>
|
|
280
|
+
<div>
|
|
281
|
+
<div class="flex">
|
|
282
|
+
<input v-model="currentText"
|
|
283
|
+
class="!bg-transparent z-10"
|
|
284
|
+
:class="{ 'cursor-pointer': disabled === false }"
|
|
285
|
+
type="text"
|
|
286
|
+
:placeholder="hintText"
|
|
287
|
+
:disabled="disabled"
|
|
288
|
+
@focus="toggleDropdown(true)">
|
|
289
|
+
|
|
290
|
+
<div v-if="required && disabled === false"
|
|
291
|
+
class="relative right-20">
|
|
292
|
+
<div class="absolute flex float-right font-semibold mt-2 text-danger-default text-sm">
|
|
293
|
+
{{ localisations.required }}
|
|
294
|
+
|
|
295
|
+
<icon class="ml-1 mt-1"
|
|
296
|
+
icon="ph:warning" />
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div class="border border-grey-1 float-right hover:bg-primary-default hover:text-white px-1 py-2.5 text-grey-3 w-min"
|
|
301
|
+
:class="{
|
|
302
|
+
'bg-off-white cursor-default hover:bg-off-white hover:text-grey-3': disabled,
|
|
303
|
+
'cursor-pointer': disabled === false
|
|
304
|
+
}"
|
|
305
|
+
@click="toggleDropdown(true)">
|
|
306
|
+
<icon icon="ph:caret-down"
|
|
307
|
+
weight="fill" />
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<cl-ui-modal size="x-small"
|
|
312
|
+
:title="createHeader"
|
|
313
|
+
:confirm-button="localisations.create"
|
|
314
|
+
:cancel-button="localisations.cancel"
|
|
315
|
+
:confirm-enabled="objectToCreateValid"
|
|
316
|
+
@confirm="createObject">
|
|
317
|
+
<template #trigger="{toggleModal}">
|
|
318
|
+
<cl-ui-combo-box-search v-show="searchContainerVisible"
|
|
319
|
+
:can-create-new-object="canCreateNewObject"
|
|
320
|
+
:can-clear-selected-object="canClearSelectedObject"
|
|
321
|
+
:localisations="localisations"
|
|
322
|
+
:loading="loading"
|
|
323
|
+
:results="results"
|
|
324
|
+
:object-type="objectType"
|
|
325
|
+
:object-parent-type="objectParentType"
|
|
326
|
+
:error-message="errorMessage"
|
|
327
|
+
:current-object-name="currentText"
|
|
328
|
+
:is-visible="searchContainerVisible"
|
|
329
|
+
@create-object="toggleModal"
|
|
330
|
+
@select-object="selectObject"
|
|
331
|
+
@clear-object="clearObject"
|
|
332
|
+
@search="search"
|
|
333
|
+
@hide-dropdown="toggleDropdown(false)" />
|
|
334
|
+
</template>
|
|
335
|
+
|
|
336
|
+
<cl-ui-combo-box v-if="objectParentType !== ''"
|
|
337
|
+
v-model:current-object="parentItem"
|
|
338
|
+
class="mt-3"
|
|
339
|
+
:results="parentResults"
|
|
340
|
+
:loading="loading"
|
|
341
|
+
:object-type="objectParentType"
|
|
342
|
+
:object-created-response="parentObjectCreatedResponse"
|
|
343
|
+
:can-create-new-object="canCreateNewObject"
|
|
344
|
+
:can-clear-selected-object="canClearSelectedObject"
|
|
345
|
+
:error-message="errorMessage"
|
|
346
|
+
@search="searchParent"
|
|
347
|
+
@create-object="createParentObject" />
|
|
348
|
+
|
|
349
|
+
<label class="mt-3">
|
|
350
|
+
{{ localisations.createProperty }}
|
|
351
|
+
</label>
|
|
352
|
+
|
|
353
|
+
<input v-model="objectToCreateValue"
|
|
354
|
+
type="text">
|
|
355
|
+
</cl-ui-modal>
|
|
356
|
+
</div>
|
|
357
|
+
</template>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { ComboBoxItem, ComboBoxSearchContainerLocalisations } from '../../../@types';
|
|
5
|
+
|
|
6
|
+
import ClUiComboBoxHeader from './header/cl-ui-combo-box-header.vue';
|
|
7
|
+
import ClUiComboBoxSelectable from './selectable/cl-ui-combo-box-selectable.vue';
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{
|
|
10
|
+
localisations: ComboBoxSearchContainerLocalisations;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
objectType: string;
|
|
13
|
+
objectParentType?: string;
|
|
14
|
+
errorMessage?: string;
|
|
15
|
+
canCreateNewObject?: boolean;
|
|
16
|
+
canClearSelectedObject?: boolean;
|
|
17
|
+
currentObjectName?: string;
|
|
18
|
+
results?: ComboBoxItem[];
|
|
19
|
+
isVisible?: boolean;
|
|
20
|
+
}>(), {
|
|
21
|
+
loading: false,
|
|
22
|
+
objectParentType: '',
|
|
23
|
+
errorMessage: '',
|
|
24
|
+
canCreateNewObject: false,
|
|
25
|
+
canClearSelectedObject: true,
|
|
26
|
+
currentObjectName: '',
|
|
27
|
+
results: () => [],
|
|
28
|
+
isVisible: false
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits({
|
|
32
|
+
'clear-object': null,
|
|
33
|
+
'create-object': null,
|
|
34
|
+
'select-object': null,
|
|
35
|
+
search: null,
|
|
36
|
+
'hide-dropdown': null
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const searchText = ref<string>('');
|
|
40
|
+
const showAddNewOption = ref<boolean>(false);
|
|
41
|
+
const currentSelection = ref<number>(-1);
|
|
42
|
+
const firstIndexInResults = computed<number>(() => showAddNewOption.value ? -1 : 0);
|
|
43
|
+
|
|
44
|
+
const combinedErrorMessage = computed<string>(() => props.localisations.errorMessage.replacePlaceholders(props.errorMessage));
|
|
45
|
+
const searchHint = computed<string>(() => props.localisations.searchHintText.replacePlaceholders(props.objectType));
|
|
46
|
+
const addPrompt = computed<string>(() => props.localisations.addPrompt.replacePlaceholders(searchText.value, props.objectType));
|
|
47
|
+
const noResults = computed<string>(() => props.localisations.noResults.replacePlaceholders(searchText.value));
|
|
48
|
+
|
|
49
|
+
const container = ref<HTMLElement>();
|
|
50
|
+
const searchBox = ref<HTMLInputElement>();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Event when the text in the search box is updated that will trigger
|
|
54
|
+
* the search call.
|
|
55
|
+
*
|
|
56
|
+
* @param {KeyboardEvent} keyboardEvent - The keyboard event on the input.
|
|
57
|
+
*/
|
|
58
|
+
function search (keyboardEvent?: KeyboardEvent) : void {
|
|
59
|
+
if (keyboardEvent === undefined || (
|
|
60
|
+
keyboardEvent?.code.startsWith('Arrow') === false
|
|
61
|
+
&& keyboardEvent?.code.startsWith('Control') === false
|
|
62
|
+
&& keyboardEvent?.code.startsWith('Shift') === false
|
|
63
|
+
&& keyboardEvent?.code.startsWith('Tab') === false
|
|
64
|
+
&& keyboardEvent?.code !== 'Enter')) {
|
|
65
|
+
emit('search', searchText.value);
|
|
66
|
+
|
|
67
|
+
currentSelection.value = -2;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Emits up to the combo box to select the object with the given id.
|
|
73
|
+
*
|
|
74
|
+
* @param {number} id - The object's id.
|
|
75
|
+
*/
|
|
76
|
+
function selectObject (id: number) : void {
|
|
77
|
+
emit('select-object', id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Emits up to the combo box to create a new object.
|
|
82
|
+
*/
|
|
83
|
+
function createObject () : void {
|
|
84
|
+
emit('create-object', searchText.value);
|
|
85
|
+
hideDropdown();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Emits up to the combo box to hide the dropdown.
|
|
90
|
+
*/
|
|
91
|
+
function hideDropdown () : void {
|
|
92
|
+
emit('hide-dropdown');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Update the current selection when the Down arrow on the keyboard is pressed.
|
|
97
|
+
*
|
|
98
|
+
* @param {number} firstIndex - The index of the first item that can be selected.
|
|
99
|
+
*/
|
|
100
|
+
function onKeyboardArrowDown (firstIndex: number): void {
|
|
101
|
+
if (currentSelection.value < props.results.length - 1) {
|
|
102
|
+
|
|
103
|
+
if (currentSelection.value === -2 && searchText.value === '') {
|
|
104
|
+
currentSelection.value++;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
currentSelection.value++;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
currentSelection.value = firstIndex;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
scroll();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Update the current selection when the Up arrow on the keyboard is pressed.
|
|
118
|
+
*
|
|
119
|
+
* @param {number} firstIndex - The index of the first item that can be selected.
|
|
120
|
+
*/
|
|
121
|
+
function onKeyboardArrowUp (firstIndex: number): void {
|
|
122
|
+
if (currentSelection.value > firstIndex) {
|
|
123
|
+
currentSelection.value--;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
currentSelection.value = props.results.length - 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
scroll();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Select the currently highlighted object in the list when the Enter button on the keyboard is pressed.
|
|
134
|
+
*/
|
|
135
|
+
function onKeyboardEnter (): void {
|
|
136
|
+
if (currentSelection.value >= 0 && props.results !== undefined) {
|
|
137
|
+
selectObject(props.results[currentSelection.value].id);
|
|
138
|
+
}
|
|
139
|
+
else if (currentSelection.value === -1) {
|
|
140
|
+
createObject();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ensure the focus leaves the control when pressing Shift+Tab by forcing focus on
|
|
146
|
+
* the previously selectable input.
|
|
147
|
+
*
|
|
148
|
+
* @param {KeyboardEvent} keyboardEvent - The keyboard event.
|
|
149
|
+
*/
|
|
150
|
+
function onKeyboardShiftTab (keyboardEvent: KeyboardEvent) : void {
|
|
151
|
+
if (keyboardEvent.key === 'Tab' && keyboardEvent.shiftKey === true) {
|
|
152
|
+
const selectableElements = document.getElementsByTagName('input');
|
|
153
|
+
let indexOfPreviouslySelectableItem = -1;
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < selectableElements.length; i++) {
|
|
156
|
+
if (selectableElements[i] === keyboardEvent.target) {
|
|
157
|
+
indexOfPreviouslySelectableItem = i - 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (indexOfPreviouslySelectableItem >= 0) {
|
|
162
|
+
selectableElements[indexOfPreviouslySelectableItem].focus();
|
|
163
|
+
hideDropdown();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Scroll the results to the highlighted object.
|
|
170
|
+
*/
|
|
171
|
+
function scroll () : void {
|
|
172
|
+
if (props.results && props.results.length > 1) {
|
|
173
|
+
const heightOffset = 34;
|
|
174
|
+
container.value?.scrollTo(0, ((currentSelection.value + 1) * heightOffset) - (heightOffset * 2));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Update the add new option to determine whether it should be displayed.
|
|
180
|
+
*/
|
|
181
|
+
function updateAddNewOption () : void {
|
|
182
|
+
let finalResultMatches = false;
|
|
183
|
+
|
|
184
|
+
if (props.results !== null && props.results.length === 1) {
|
|
185
|
+
finalResultMatches = searchText.value.toUpperCase().trim() === props.results[0].name.toUpperCase().trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
showAddNewOption.value = props.canCreateNewObject === true
|
|
189
|
+
&& props.errorMessage === ''
|
|
190
|
+
&& searchText.value.trim() !== ''
|
|
191
|
+
&& finalResultMatches === false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Update the index of the selected object.
|
|
196
|
+
*
|
|
197
|
+
* @param {number} index - The index.
|
|
198
|
+
*/
|
|
199
|
+
function updateSelectedObjectIndex (index: number) : void {
|
|
200
|
+
currentSelection.value = index;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Event when visibility of the search container changes. If visible,
|
|
205
|
+
* makes a call to get the data and then sets the focus on the search box.
|
|
206
|
+
*/
|
|
207
|
+
function onVisibilityChanged () : void {
|
|
208
|
+
if (props.isVisible === true) {
|
|
209
|
+
searchText.value = '';
|
|
210
|
+
search();
|
|
211
|
+
|
|
212
|
+
nextTick(() => searchBox.value?.focus());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
watch(() => props.results, () => updateAddNewOption());
|
|
217
|
+
watch(() => props.errorMessage, () => updateAddNewOption());
|
|
218
|
+
watch(() => props.isVisible, () => onVisibilityChanged());
|
|
219
|
+
|
|
220
|
+
onMounted(() => container.value?.addEventListener('keydown', onKeyboardShiftTab));
|
|
221
|
+
|
|
222
|
+
onUnmounted(() => container.value?.removeEventListener('keydown', onKeyboardShiftTab));
|
|
223
|
+
</script>
|
|
224
|
+
|
|
225
|
+
<template>
|
|
226
|
+
<div ref="container"
|
|
227
|
+
class="bg-white border border-collapse border-grey-0 h-52 overflow-y-auto p-2 text-sm"
|
|
228
|
+
@keydown.up.prevent.stop="onKeyboardArrowUp(firstIndexInResults)"
|
|
229
|
+
@keydown.down.prevent.stop="onKeyboardArrowDown(firstIndexInResults)"
|
|
230
|
+
@keyup.enter.prevent.stop="onKeyboardEnter">
|
|
231
|
+
<div class="flex">
|
|
232
|
+
<input ref="searchBox"
|
|
233
|
+
v-model="searchText"
|
|
234
|
+
type="text"
|
|
235
|
+
:placeholder="searchHint"
|
|
236
|
+
@keyup="search"
|
|
237
|
+
@blur="hideDropdown">
|
|
238
|
+
|
|
239
|
+
<cl-ui-loading-spinner v-if="loading"
|
|
240
|
+
class="absolute ml-2 mt-11" />
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<cl-ui-combo-box-header v-if="currentObjectName !== ''"
|
|
244
|
+
:show-clear-button="canClearSelectedObject"
|
|
245
|
+
:text="currentObjectName"
|
|
246
|
+
@clear-object="$emit('clear-object')" />
|
|
247
|
+
|
|
248
|
+
<cl-ui-combo-box-header v-if="objectParentType !== '' && results !== null && results.length > 0 && errorMessage === ''"
|
|
249
|
+
:text="objectType"
|
|
250
|
+
:additional-text="objectParentType" />
|
|
251
|
+
|
|
252
|
+
<cl-ui-combo-box-header v-if="results !== null && results.length === 0 && searchText.trim() !== '' && canCreateNewObject === false && errorMessage === ''"
|
|
253
|
+
:text="noResults" />
|
|
254
|
+
|
|
255
|
+
<cl-ui-combo-box-header v-if="errorMessage !== ''"
|
|
256
|
+
class="!bg-danger-light text-danger-dark"
|
|
257
|
+
:text="combinedErrorMessage" />
|
|
258
|
+
|
|
259
|
+
<cl-ui-combo-box-selectable v-if="showAddNewOption"
|
|
260
|
+
:is-create-new-option="true"
|
|
261
|
+
:text="addPrompt"
|
|
262
|
+
:index="-1"
|
|
263
|
+
:localisations="localisations"
|
|
264
|
+
:selected-index="currentSelection"
|
|
265
|
+
@create-object="createObject"
|
|
266
|
+
@option-highlighted="updateSelectedObjectIndex" />
|
|
267
|
+
|
|
268
|
+
<div v-if="errorMessage === '' && results !== null">
|
|
269
|
+
<cl-ui-combo-box-selectable v-for="(result, index) in results"
|
|
270
|
+
:key="index"
|
|
271
|
+
:localisations="localisations"
|
|
272
|
+
:option="result"
|
|
273
|
+
:index="index"
|
|
274
|
+
:selected-index="currentSelection"
|
|
275
|
+
@select-object="selectObject"
|
|
276
|
+
@option-highlighted="updateSelectedObjectIndex" />
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</template>
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{
|
|
3
|
+
showClearButton?: boolean;
|
|
4
|
+
text?: string;
|
|
5
|
+
additionalText?: string;
|
|
6
|
+
}>(), {
|
|
7
|
+
showClearButton: false,
|
|
8
|
+
text: '',
|
|
9
|
+
additionalText: ''
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
defineEmits({
|
|
13
|
+
'clear-object': null
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
2
16
|
|
|
3
17
|
<template>
|
|
4
18
|
<div class="bg-grey-2 border-b border-grey-2 border-t flex p-1">
|
|
@@ -15,7 +29,8 @@
|
|
|
15
29
|
<div v-show="showClearButton"
|
|
16
30
|
class="cursor-pointer float-right"
|
|
17
31
|
@mousedown="$emit('clear-object')">
|
|
18
|
-
<
|
|
32
|
+
<icon class="hover:text-link-default mt-0.5 rounded-full"
|
|
33
|
+
icon="ph:x-circle" />
|
|
19
34
|
</div>
|
|
20
35
|
</div>
|
|
21
36
|
</template>
|