@milaboratories/uikit 2.2.65 → 2.2.67

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/pl-uikit.js +4527 -4192
  3. package/dist/pl-uikit.js.map +1 -1
  4. package/dist/pl-uikit.umd.cjs +10 -10
  5. package/dist/pl-uikit.umd.cjs.map +1 -1
  6. package/dist/src/components/PlAutocomplete/PlAutocomplete.vue.d.ts +85 -0
  7. package/dist/src/components/PlAutocomplete/PlAutocomplete.vue.d.ts.map +1 -0
  8. package/dist/src/components/PlAutocomplete/__tests__/PlAutocomplete.spec.d.ts +2 -0
  9. package/dist/src/components/PlAutocomplete/__tests__/PlAutocomplete.spec.d.ts.map +1 -0
  10. package/dist/src/components/PlAutocomplete/index.d.ts +2 -0
  11. package/dist/src/components/PlAutocomplete/index.d.ts.map +1 -0
  12. package/dist/src/components/PlDropdown/PlDropdown.vue.d.ts.map +1 -1
  13. package/dist/src/components/PlDropdownMulti/PlDropdownMulti.vue.d.ts.map +1 -1
  14. package/dist/src/composition/useWatchFetch.d.ts +28 -0
  15. package/dist/src/composition/useWatchFetch.d.ts.map +1 -0
  16. package/dist/src/index.d.ts +2 -0
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/style.css +1 -1
  19. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  20. package/package.json +2 -2
  21. package/src/components/PlAutocomplete/PlAutocomplete.vue +413 -0
  22. package/src/components/PlAutocomplete/__tests__/PlAutocomplete.spec.ts +41 -0
  23. package/src/components/PlAutocomplete/index.ts +1 -0
  24. package/src/components/PlAutocomplete/pl-autocomplete.scss +277 -0
  25. package/src/components/PlDropdown/PlDropdown.vue +13 -6
  26. package/src/components/PlDropdown/pl-dropdown.scss +13 -3
  27. package/src/components/PlDropdownMulti/PlDropdownMulti.vue +14 -6
  28. package/src/components/PlDropdownMulti/pl-dropdown-multi.scss +39 -25
  29. package/src/composition/useWatchFetch.ts +68 -0
  30. package/src/index.ts +2 -0
@@ -0,0 +1,277 @@
1
+ @use "@/assets/mixins" as *;
2
+
3
+ .pl-autocomplete__options {
4
+ --option-hover-bg: var(--btn-sec-hover-grey);
5
+
6
+ z-index: var(--z-dropdown-options);
7
+ border: 1px solid var(--border-color-div-grey);
8
+ position: absolute;
9
+ background-color: var(--pl-dropdown-options-bg);
10
+ border-radius: 6px;
11
+ max-height: 244px;
12
+ box-shadow: 0px 4px 12px -2px rgba(15, 36, 77, 0.08), 0px 6px 24px -2px rgba(15, 36, 77, 0.08);
13
+
14
+ @include scrollbar;
15
+
16
+ .nothing-found {
17
+ padding: 0 10px;
18
+ height: var(--control-height);
19
+ line-height: var(--control-height);
20
+ background-color: #fff;
21
+ opacity: 0.5;
22
+ font-style: italic;
23
+ }
24
+
25
+ .option {
26
+ position: relative;
27
+ padding: 0 30px 0 10px;
28
+ height: var(--control-height);
29
+ line-height: var(--control-height);
30
+ cursor: pointer;
31
+ user-select: none;
32
+
33
+ .checkmark {
34
+ position: absolute;
35
+ display: none;
36
+ right: 10px;
37
+ @include abs-center-y();
38
+ }
39
+
40
+ >span {
41
+ display: block;
42
+ overflow: hidden;
43
+ white-space: nowrap;
44
+ max-width: 100%;
45
+ text-overflow: ellipsis;
46
+ }
47
+
48
+ &.selected {
49
+ background-color: var(--color-active-select);
50
+
51
+ .checkmark {
52
+ display: block;
53
+ }
54
+ }
55
+
56
+ &.active:not(.selected) {
57
+ background-color: var(--option-hover-bg);
58
+ }
59
+
60
+ &:hover {
61
+ background-color: var(--option-hover-bg);
62
+ }
63
+ }
64
+ }
65
+
66
+ .pl-autocomplete {
67
+ $root: &;
68
+
69
+ --contour-color: var(--txt-01);
70
+ --contour-border-width: 1px;
71
+
72
+ --label-offset-left-x: 8px;
73
+ --label-offset-right-x: 8px;
74
+ --label-color: var(--txt-01);
75
+
76
+ position: relative;
77
+ outline: none;
78
+ min-height: var(--control-height);
79
+ border-radius: 6px;
80
+ font-family: var(--font-family-base);
81
+ font-size: var(--font-size-base);
82
+ font-weight: var(--font-weigh-base);
83
+
84
+ &__envelope {
85
+ font-family: var(--control-font-family);
86
+ min-width: 160px;
87
+ }
88
+
89
+ label {
90
+ @include outlined-control-label();
91
+ }
92
+
93
+ &__container {
94
+ position: absolute;
95
+ top: 0;
96
+ left: 0;
97
+ right: 0;
98
+ border-radius: 6px;
99
+ min-height: var(--control-height);
100
+ color: var(--txt-01);
101
+ }
102
+
103
+ &__contour {
104
+ border-radius: var(--border-radius-control);
105
+ border: var(--contour-border-width) solid var(--contour-color);
106
+ box-shadow: var(--contour-box-shadow);
107
+ z-index: 0;
108
+ pointer-events: none;
109
+ }
110
+
111
+ &__field {
112
+ position: relative;
113
+ border-radius: 6px;
114
+ overflow: hidden;
115
+ background: transparent;
116
+ padding-left: 11px;
117
+
118
+ min-height: var(--control-height);
119
+ line-height: var(--control-height);
120
+
121
+ display: flex;
122
+ flex-direction: row;
123
+ align-items: center;
124
+ cursor: pointer;
125
+
126
+ .input-value {
127
+ position: absolute;
128
+ top: 0;
129
+ left: 0;
130
+ bottom: 0;
131
+ right: 0;
132
+ display: flex;
133
+ flex-direction: row;
134
+ align-items: center;
135
+ padding: 0 60px 0 11px; // @TODO padding-right based on controls width
136
+ pointer-events: none;
137
+ line-height: 20px;
138
+ color: var(--txt-01);
139
+ overflow: hidden;
140
+ white-space: pre;
141
+ text-overflow: ellipsis;
142
+ cursor: inherit;
143
+ }
144
+
145
+ input {
146
+ min-height: calc(var(--control-height) - 2px);
147
+ line-height: 20px;
148
+ font-family: inherit;
149
+ font-size: inherit;
150
+ background-color: transparent;
151
+ border: none;
152
+ padding: 0;
153
+ width: calc(100% - 40px);
154
+ color: var(--txt-01);
155
+ caret-color: var(--border-color-focus);
156
+
157
+ &:focus {
158
+ outline: none;
159
+ }
160
+
161
+ &:placeholder-shown {
162
+ text-overflow: ellipsis;
163
+ }
164
+
165
+ &::placeholder {
166
+ color: var(--color-placeholder);
167
+ }
168
+ }
169
+ }
170
+
171
+ &__helper {
172
+ @include field-helper();
173
+ }
174
+
175
+ &__error {
176
+ @include field-error();
177
+ }
178
+
179
+ &__controls {
180
+ display: flex;
181
+ flex-direction: row;
182
+ align-items: center;
183
+ min-height: var(--control-height);
184
+ gap: 6px;
185
+
186
+ margin-left: auto;
187
+
188
+ .icon-delete-clear {
189
+ cursor: pointer;
190
+ }
191
+
192
+ .mask-16,
193
+ .mask-24 {
194
+ background-color: var(--control-mask-fill);
195
+ cursor: pointer;
196
+ }
197
+
198
+ .mask-loading {
199
+ animation: spin 2.5s linear infinite;
200
+ background-color: #07AD3E;
201
+ }
202
+ }
203
+
204
+ &__arrow-wrapper {
205
+ display: flex;
206
+ align-items: center;
207
+ min-height: var(--control-height);
208
+ padding-right: 11px;
209
+ }
210
+ .arrow-icon {
211
+ cursor: pointer;
212
+
213
+ // Default "arrow" icon (16x16)
214
+ &.arrow-icon-default {
215
+ transition: transform .2s;
216
+ background-color: var(--control-mask-fill);
217
+ @include mask(url('@/assets/images/16_chevron-down.svg'), 16px);
218
+ }
219
+ }
220
+
221
+ &.open,
222
+ &:focus-within {
223
+ z-index: 1;
224
+ --label-color: var(--txt-focus);
225
+ }
226
+
227
+ &.open {
228
+ #{$root}__container {
229
+ z-index: 1000;
230
+ }
231
+
232
+ #{$root}__field {
233
+ border-radius: 6px 6px 0 0;
234
+ }
235
+
236
+ .arrow-icon {
237
+ &.arrow-icon-default {
238
+ background-color: var(--control-mask-fill);
239
+ transform: rotate(-180deg);
240
+ }
241
+ }
242
+ }
243
+
244
+ &:hover {
245
+ --contour-color: var(--control-hover-color);
246
+ }
247
+
248
+ &:focus-within:not(.error) {
249
+ --label-color: var(--txt-focus);
250
+ --contour-color: var(--border-color-focus);
251
+ --contour-border-width: 2px;
252
+ --contour-box-shadow: 0 0 0 4px var(--border-color-focus-shadow);
253
+ }
254
+
255
+ &:focus-within.error {
256
+ --contour-border-width: 2px;
257
+ --contour-box-shadow: 0 0 0 4px var(--color-error-shadow);
258
+ }
259
+
260
+ &.error {
261
+ --contour-color: var(--txt-error);
262
+ --label-color: var(--txt-error);
263
+ }
264
+
265
+ &.disabled {
266
+ --contour-color: var(--color-dis-01);
267
+ --control-mask-fill: var(--color-dis-01);
268
+ --label-color: var(--color-dis-01);
269
+ cursor: not-allowed;
270
+ pointer-events: none;
271
+ user-select: none;
272
+
273
+ .input-value {
274
+ color: var(--dis-01);
275
+ }
276
+ }
277
+ }
@@ -223,7 +223,12 @@ const clear = () => emit('update:modelValue', undefined);
223
223
 
224
224
  const setFocusOnInput = () => input.value?.focus();
225
225
 
226
- const toggleOpen = () => (data.open = !data.open);
226
+ const toggleOpen = () => {
227
+ data.open = !data.open;
228
+ if (!data.open) {
229
+ data.search = '';
230
+ }
231
+ };
227
232
 
228
233
  const onInputFocus = () => (data.open = true);
229
234
 
@@ -298,7 +303,7 @@ watchPostEffect(() => {
298
303
  </script>
299
304
 
300
305
  <template>
301
- <div class="pl-dropdown__envelope">
306
+ <div class="pl-dropdown__envelope" @click="setFocusOnInput">
302
307
  <div
303
308
  ref="rootRef"
304
309
  :tabindex="tabindex"
@@ -321,7 +326,7 @@ watchPostEffect(() => {
321
326
  @focus="onInputFocus"
322
327
  />
323
328
 
324
- <div v-if="!data.open" class="input-value" @click="setFocusOnInput">
329
+ <div v-if="!data.open" class="input-value">
325
330
  <LongText> {{ textValue }} </LongText>
326
331
  </div>
327
332
 
@@ -329,9 +334,11 @@ watchPostEffect(() => {
329
334
  <PlMaskIcon24 v-if="isLoadingOptions" name="loading" />
330
335
  <PlIcon16 v-if="clearable && hasValue" name="delete-clear" @click.stop="clear" />
331
336
  <slot name="append" />
332
- <div v-if="arrowIconLarge" class="arrow-icon" :class="[`icon-24 ${arrowIconLarge}`]" @click.stop="toggleOpen" />
333
- <div v-else-if="arrowIcon" class="arrow-icon" :class="[`icon-16 ${arrowIcon}`]" @click.stop="toggleOpen" />
334
- <div v-else class="arrow-icon arrow-icon-default" @click.stop="toggleOpen" />
337
+ <div class="pl-dropdown__arrow-wrapper" @click.stop="toggleOpen">
338
+ <div v-if="arrowIconLarge" class="arrow-icon" :class="[`icon-24 ${arrowIconLarge}`]" />
339
+ <div v-else-if="arrowIcon" class="arrow-icon" :class="[`icon-16 ${arrowIcon}`]" />
340
+ <div v-else class="arrow-icon arrow-icon-default" />
341
+ </div>
335
342
  </div>
336
343
  </div>
337
344
  <label v-if="label">
@@ -113,7 +113,7 @@
113
113
  border-radius: 6px;
114
114
  overflow: hidden;
115
115
  background: transparent;
116
- padding: 0 11px;
116
+ padding-left: 11px;
117
117
 
118
118
  min-height: var(--control-height);
119
119
  line-height: var(--control-height);
@@ -199,11 +199,19 @@
199
199
  }
200
200
  }
201
201
 
202
+ &__arrow-wrapper {
203
+ display: flex;
204
+ align-items: center;
205
+ min-height: var(--control-height);
206
+ padding-right: 11px;
207
+ }
208
+
202
209
  .arrow-icon {
203
210
  cursor: pointer;
204
211
 
205
212
  // Default "arrow" icon (16x16)
206
213
  &.arrow-icon-default {
214
+ transition: transform .2s;
207
215
  background-color: var(--control-mask-fill);
208
216
  @include mask(url('@/assets/images/16_chevron-down.svg'), 16px);
209
217
  }
@@ -225,8 +233,10 @@
225
233
  }
226
234
 
227
235
  .arrow-icon {
228
- background-color: var(--control-mask-fill);
229
- @include mask(url(@/assets/images/16_chevron-up.svg), 16px);
236
+ &.arrow-icon-default {
237
+ background-color: var(--control-mask-fill);
238
+ transform: rotate(-180deg);
239
+ }
230
240
  }
231
241
  }
232
242
 
@@ -164,7 +164,12 @@ const unselectOption = (d: M) => emitModel(unref(selectedValuesRef).filter((v) =
164
164
 
165
165
  const setFocusOnInput = () => input.value?.focus();
166
166
 
167
- const toggleModel = () => (data.open = !data.open);
167
+ const toggleModel = () => {
168
+ data.open = !data.open;
169
+ if (!data.open) {
170
+ data.search = '';
171
+ }
172
+ };
168
173
 
169
174
  const onFocusOut = (event: FocusEvent) => {
170
175
  const relatedTarget = event.relatedTarget as Node | null;
@@ -232,7 +237,7 @@ watchPostEffect(() => {
232
237
  </script>
233
238
 
234
239
  <template>
235
- <div class="pl-dropdown-multi__envelope">
240
+ <div class="pl-dropdown-multi__envelope" @click="setFocusOnInput">
236
241
  <div
237
242
  ref="rootRef"
238
243
  :tabindex="tabindex"
@@ -254,15 +259,18 @@ watchPostEffect(() => {
254
259
  autocomplete="chrome-off"
255
260
  @focus="data.open = true"
256
261
  />
257
- <div v-if="!data.open" class="chips-container" @click="setFocusOnInput">
262
+ <div v-if="!data.open" class="chips-container">
258
263
  <PlChip v-for="(opt, i) in selectedOptionsRef" :key="i" closeable small @click.stop="data.open = true" @close="unselectOption(opt.value)">
259
264
  {{ opt.label || opt.value }}
260
265
  </PlChip>
261
266
  </div>
262
- <PlMaskIcon24 v-if="isLoadingOptions" name="loading" />
263
- <div v-if="!isLoadingOptions" class="arrow" @click.stop="toggleModel" />
264
- <div class="pl-dropdown-multi__append">
267
+
268
+ <div class="pl-dropdown-multi__controls">
269
+ <PlMaskIcon24 v-if="isLoadingOptions" name="loading" />
265
270
  <slot name="append" />
271
+ <div class="pl-dropdown-multi__arrow-wrapper" @click.stop="toggleModel">
272
+ <div class="arrow-icon arrow-icon-default" />
273
+ </div>
266
274
  </div>
267
275
  </div>
268
276
  <label v-if="label">
@@ -129,7 +129,7 @@
129
129
  border-radius: 6px;
130
130
  overflow: hidden;
131
131
  background: transparent;
132
- padding: 0 11px;
132
+ padding-left: 11px;
133
133
 
134
134
  min-height: var(--control-height);
135
135
  line-height: 20px;
@@ -146,7 +146,7 @@
146
146
  bottom: 0;
147
147
  right: 30px;
148
148
  overflow: hidden;
149
- padding: 0 11px;
149
+ padding: 0 60px 0 11px;
150
150
  line-height: 20px;
151
151
  color: var(--contour-color);
152
152
  display: flex;
@@ -186,43 +186,57 @@
186
186
  }
187
187
  }
188
188
 
189
- &__helper {
190
- @include field-helper();
191
- }
192
-
193
- &__error {
194
- @include field-error();
195
- }
196
-
197
- &__append {
198
- padding-right: 24px;
189
+ &__controls {
199
190
  display: flex;
200
191
  flex-direction: row;
201
192
  align-items: center;
202
- gap: 4px;
193
+ gap: 6px;
194
+ margin-left: auto;
203
195
 
204
- .icon {
196
+ .mask-16,
197
+ .mask-24 {
198
+ background-color: var(--control-mask-fill);
205
199
  cursor: pointer;
206
200
  }
207
201
 
208
- .mask {
202
+ .mask-loading {
203
+ animation: spin 2.5s linear infinite;
204
+ background-color: #07AD3E;
205
+ }
206
+ }
207
+
208
+ &__arrow-wrapper {
209
+ display: flex;
210
+ align-items: center;
211
+ min-height: var(--control-height);
212
+ padding-right: 11px;
213
+ }
214
+
215
+ .arrow-icon {
216
+ cursor: pointer;
217
+
218
+ // Default "arrow" icon (16x16)
219
+ &.arrow-icon-default {
220
+ transition: transform .2s;
209
221
  background-color: var(--control-mask-fill);
210
- cursor: pointer;
222
+ @include mask(url('@/assets/images/16_chevron-down.svg'), 16px);
211
223
  }
212
224
  }
213
225
 
214
- .arrow {
215
- position: absolute;
216
- @include abs-center-y;
217
- right: 12px;
218
- background-color: var(--control-mask-fill);
219
- @include mask(url("@/assets/images/16_chevron-down.svg"), 16px);
226
+ &__helper {
227
+ @include field-helper();
228
+ }
229
+
230
+ &__error {
231
+ @include field-error();
220
232
  }
221
233
 
222
234
  &.open {
223
- .arrow {
224
- background-color: var(--control-mask-fill);
225
- @include mask(url(@/assets/images/16_up.svg), 16px);
235
+ .arrow-icon {
236
+ &.arrow-icon-default {
237
+ background-color: var(--control-mask-fill);
238
+ transform: rotate(-180deg);
239
+ }
226
240
  }
227
241
  }
228
242
 
@@ -0,0 +1,68 @@
1
+ import type { WatchSource, WatchOptions } from 'vue';
2
+ import { reactive, watch, ref, computed } from 'vue';
3
+ import { exclusiveRequest } from '@milaboratories/helpers';
4
+
5
+ export type FetchResult<V, E = unknown> = {
6
+ loading: boolean;
7
+ value: V | undefined;
8
+ error: E;
9
+ };
10
+
11
+ // TODO Should we keep the old value while fetching the new value?
12
+
13
+ /**
14
+ * Watch any reactive source and perform an asynchronous operation
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const v = useWatchFetch(
19
+ * watchSource,
20
+ * async (sourceValue) => {
21
+ * return await fetchDataFromApi(sourceValue);
22
+ * }
23
+ * );
24
+ *
25
+ * // Usage in a template
26
+ * <template>
27
+ * <div v-if="v.loading">Loading...</div>
28
+ * <div v-else-if="v.error">Error: {{ v.error.message }}</div>
29
+ * <div v-else>Data: {{ v.value }}</div>
30
+ * </template>
31
+ * ```
32
+ */
33
+ export function useWatchFetch<S, V>(watchSource: WatchSource<S>, doFetch: (s: S) => Promise<V>, watchOptions?: WatchOptions): FetchResult<V> {
34
+ const loadingRef = ref(0);
35
+
36
+ const data = reactive({
37
+ loading: computed(() => loadingRef.value > 0),
38
+ loadingRef,
39
+ value: undefined as V,
40
+ error: undefined,
41
+ }) as FetchResult<V>;
42
+
43
+ const exclusive = exclusiveRequest(doFetch);
44
+
45
+ watch(
46
+ watchSource,
47
+ async (s) => {
48
+ data.error = undefined;
49
+ loadingRef.value++;
50
+ exclusive(s)
51
+ .then((res) => {
52
+ if (res.ok) {
53
+ data.value = res.value;
54
+ }
55
+ })
56
+ .catch((err) => {
57
+ data.value = undefined;
58
+ data.error = err;
59
+ })
60
+ .finally(() => {
61
+ loadingRef.value--;
62
+ });
63
+ },
64
+ Object.assign({ immediate: true, deep: true }, watchOptions ?? {}),
65
+ );
66
+
67
+ return data;
68
+ }
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ export * from './components/PlStatusTag';
53
53
  export * from './components/PlLoaderCircular';
54
54
  export * from './components/PlSplash';
55
55
  export * from './components/PlProgressCell';
56
+ export * from './components/PlAutocomplete';
56
57
 
57
58
  export * from './components/PlFileDialog';
58
59
  export * from './components/PlFileInput';
@@ -96,6 +97,7 @@ export { useFormState } from './composition/useFormState';
96
97
  export { useQuery } from './composition/useQuery.ts';
97
98
  export { useDraggable } from './composition/useDraggable';
98
99
  export { useComponentProp } from './composition/useComponentProp';
100
+ export * from './composition/useWatchFetch';
99
101
 
100
102
  /**
101
103
  * Utils/Partials