@bildvitta/quasar-ui-asteroid 3.14.0 → 3.15.0-beta.0
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/package.json +1 -1
- package/src/components/filters/QasFilters.vue +100 -36
- package/src/components/filters/QasFilters.yml +7 -0
- package/src/components/filters/private/PvFiltersButton.vue +6 -1
- package/src/components/grabbable/QasGrabbable.vue +187 -0
- package/src/components/grabbable/QasGrabbable.yml +22 -0
- package/src/components/search-box/QasSearchBox.yml +8 -0
- package/src/components/select/QasSelect.vue +9 -1
- package/src/components/select/QasSelect.yml +13 -0
- package/src/components/table-generator/QasTableGenerator.vue +0 -11
- package/src/components/table-generator/QasTableGenerator.yml +6 -6
- package/src/helpers/set-scroll-on-grab.js +71 -29
- package/src/mixins/search-filter.js +28 -1
- package/src/vue-plugin.js +3 -0
package/package.json
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<div v-if="showSearch" class="col-12 col-md-6">
|
|
5
5
|
<slot :filter="filter" name="search">
|
|
6
6
|
<q-form v-if="useSearch" @submit.prevent="filter()">
|
|
7
|
-
<qas-search-input v-model="
|
|
7
|
+
<qas-search-input v-model="internalSearch" :placeholder="searchPlaceholder" :use-search-on-type="useSearchOnType" @clear="clearSearch" @filter="filter()" @update:model-value="onSearch">
|
|
8
8
|
<template v-if="showFilterButton" #after-clear>
|
|
9
9
|
<slot :context="mx_context" :filter="filter" :filters="activeFilters" name="filter-button" :remove-filter="removeFilter">
|
|
10
|
-
<pv-filters-button v-if="useFilterButton" ref="filtersButton" v-model="
|
|
10
|
+
<pv-filters-button v-if="useFilterButton" ref="filtersButton" v-model="internalFilters" v-bind="filterButtonProps" />
|
|
11
11
|
</slot>
|
|
12
12
|
</template>
|
|
13
13
|
</qas-search-input>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<div v-else-if="showFilterButton" class="col-12 col-md-6">
|
|
19
19
|
<slot :context="mx_context" :filter="filter" :filters="activeFilters" name="filter-button" :remove-filter="removeFilter">
|
|
20
|
-
<pv-filters-button v-if="useFilterButton" ref="filtersButton" v-model="
|
|
20
|
+
<pv-filters-button v-if="useFilterButton" ref="filtersButton" v-model="internalFilters" v-bind="filterButtonProps" />
|
|
21
21
|
</slot>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
@@ -73,6 +73,11 @@ export default {
|
|
|
73
73
|
type: Object
|
|
74
74
|
},
|
|
75
75
|
|
|
76
|
+
filters: {
|
|
77
|
+
default: () => ({}),
|
|
78
|
+
type: Object
|
|
79
|
+
},
|
|
80
|
+
|
|
76
81
|
useFilterButton: {
|
|
77
82
|
default: true,
|
|
78
83
|
type: Boolean
|
|
@@ -113,15 +118,26 @@ export default {
|
|
|
113
118
|
}
|
|
114
119
|
},
|
|
115
120
|
|
|
116
|
-
emits: [
|
|
121
|
+
emits: [
|
|
122
|
+
'fetch-success',
|
|
123
|
+
'fetch-error',
|
|
124
|
+
'update:currentFilters',
|
|
125
|
+
'update:filters'
|
|
126
|
+
],
|
|
117
127
|
|
|
118
128
|
data () {
|
|
119
129
|
return {
|
|
120
130
|
currentFilters: {},
|
|
121
|
-
filters: {},
|
|
122
131
|
hasFetchError: false,
|
|
132
|
+
internalFilters: {},
|
|
133
|
+
internalSearch: '',
|
|
123
134
|
isFetching: false,
|
|
124
|
-
|
|
135
|
+
/**
|
|
136
|
+
* O objeto funciona como um auxiliar para armazenar opções selecionadas do lazy loading.
|
|
137
|
+
* Isso é necessário porque, por padrão, não há opções no campo. As opções selecionadas servem
|
|
138
|
+
* para exibir as "tags" dos filtros ativos na tela.
|
|
139
|
+
*/
|
|
140
|
+
lazyLoadingSelectedOptions: {}
|
|
125
141
|
}
|
|
126
142
|
},
|
|
127
143
|
|
|
@@ -155,15 +171,32 @@ export default {
|
|
|
155
171
|
},
|
|
156
172
|
|
|
157
173
|
formattedFieldsProps () {
|
|
158
|
-
const
|
|
174
|
+
const formattedFieldsProps = {}
|
|
159
175
|
|
|
160
|
-
for (const key in this.
|
|
176
|
+
for (const key in this.fields) {
|
|
161
177
|
const decamelizedFieldKey = decamelize(key)
|
|
178
|
+
const fieldsProps = this.fieldsProps[key] || {}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Aqui é onde se encontra a tratativa para campos lazy loading. É atribuída a prop useFetchOptionsOnFocus
|
|
182
|
+
* por padrão. Ela é necessária pois toda vez que o menu do filtro é aberto é criado uma nova instância
|
|
183
|
+
* e quando é fechado é destruída essa instância. O que causa que toda vez que abrir novamente o menu,
|
|
184
|
+
* irá bater todos os endpoints de lazy loading.
|
|
185
|
+
* Aqui também é escutado pelo evento onUpdate:selectedOptions, esse evento irá trazer as opções selecionadas
|
|
186
|
+
* e elas serão salvas na chave lazyLoadingSelectedOptions para serem utilizadas posteriormente nas tags.
|
|
187
|
+
*/
|
|
188
|
+
const hasLazyLoading = this.fields[key].useLazyLoading || fieldsProps.useLazyLoading
|
|
189
|
+
const lazyLoadingProps = hasLazyLoading ? {
|
|
190
|
+
useFetchOptionsOnFocus: true,
|
|
191
|
+
'onUpdate:selectedOptions': options => {
|
|
192
|
+
this.lazyLoadingSelectedOptions[key] = options
|
|
193
|
+
}
|
|
194
|
+
} : {}
|
|
162
195
|
|
|
163
|
-
|
|
196
|
+
formattedFieldsProps[decamelizedFieldKey] = Object.assign(fieldsProps, lazyLoadingProps)
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
return
|
|
199
|
+
return formattedFieldsProps
|
|
167
200
|
},
|
|
168
201
|
|
|
169
202
|
fields () {
|
|
@@ -187,6 +220,16 @@ export default {
|
|
|
187
220
|
fields: this.fields,
|
|
188
221
|
fieldsProps: this.formattedFieldsProps,
|
|
189
222
|
loading: this.isFetching,
|
|
223
|
+
menuProps: {
|
|
224
|
+
/**
|
|
225
|
+
* O tratamento no onHide do menu é que como o menu é recriado toda vez que o filtro é aberto, ocorre que as
|
|
226
|
+
* opções selecionadas anteriormente (e que não foram filtradas) não ficam salvas na memória, ocasionando em
|
|
227
|
+
* campos lazy loading um problema de exibir o uuid da opção por não achar essa opção no array de options do field.
|
|
228
|
+
* Para solucionar esse problema, sempre ao fechar os filtros as opções não filtradas são removidas,
|
|
229
|
+
* voltando o filtro para o seu estado anterior.
|
|
230
|
+
*/
|
|
231
|
+
onHide: this.setInternalFilters
|
|
232
|
+
},
|
|
190
233
|
|
|
191
234
|
onClear: this.clearFilters,
|
|
192
235
|
onFilter: () => this.filter()
|
|
@@ -224,6 +267,17 @@ export default {
|
|
|
224
267
|
this.fetchFilters()
|
|
225
268
|
this.useUpdateRoute && this.updateValues()
|
|
226
269
|
}
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
internalFilters: {
|
|
273
|
+
handler (value) {
|
|
274
|
+
this.$emit('update:filters', value)
|
|
275
|
+
},
|
|
276
|
+
deep: true
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
filters (value) {
|
|
280
|
+
this.internalFilters = value
|
|
227
281
|
}
|
|
228
282
|
},
|
|
229
283
|
|
|
@@ -232,10 +286,8 @@ export default {
|
|
|
232
286
|
|
|
233
287
|
if (this.useUpdateRoute) {
|
|
234
288
|
this.updateValues()
|
|
235
|
-
this.
|
|
289
|
+
this.onUpdateFilters()
|
|
236
290
|
}
|
|
237
|
-
|
|
238
|
-
this.handleSearchModelOnCreate()
|
|
239
291
|
},
|
|
240
292
|
|
|
241
293
|
methods: {
|
|
@@ -244,7 +296,7 @@ export default {
|
|
|
244
296
|
const query = { ...this.$route.query }
|
|
245
297
|
const activeFilters = {
|
|
246
298
|
...filters,
|
|
247
|
-
...this.
|
|
299
|
+
...this.internalFilters
|
|
248
300
|
}
|
|
249
301
|
|
|
250
302
|
if (this.hasFields) {
|
|
@@ -256,22 +308,22 @@ export default {
|
|
|
256
308
|
|
|
257
309
|
if (hasField) {
|
|
258
310
|
delete query[key]
|
|
259
|
-
delete this.
|
|
311
|
+
delete this.internalFilters[key]
|
|
260
312
|
}
|
|
261
313
|
}
|
|
262
314
|
} else {
|
|
263
|
-
this.
|
|
315
|
+
this.internalFilters = {}
|
|
264
316
|
}
|
|
265
317
|
|
|
266
318
|
this.hideFiltersMenu()
|
|
267
319
|
|
|
268
320
|
await this.updateRouteQuery(query)
|
|
269
321
|
|
|
270
|
-
this.
|
|
322
|
+
this.onUpdateFilters()
|
|
271
323
|
},
|
|
272
324
|
|
|
273
325
|
clearSearch () {
|
|
274
|
-
this.
|
|
326
|
+
this.internalSearch = ''
|
|
275
327
|
this.filter()
|
|
276
328
|
},
|
|
277
329
|
|
|
@@ -283,11 +335,13 @@ export default {
|
|
|
283
335
|
this.hasFetchError = false
|
|
284
336
|
this.isFetching = true
|
|
285
337
|
|
|
338
|
+
const { filters } = this.mx_context
|
|
339
|
+
|
|
286
340
|
try {
|
|
287
341
|
const response = await getAction.call(this, {
|
|
288
342
|
entity: this.entity,
|
|
289
343
|
key: 'fetchFilters',
|
|
290
|
-
payload: { url: this.url }
|
|
344
|
+
payload: { url: this.url, params: filters }
|
|
291
345
|
})
|
|
292
346
|
|
|
293
347
|
this.$emit('fetch-success', response)
|
|
@@ -308,10 +362,10 @@ export default {
|
|
|
308
362
|
|
|
309
363
|
const query = {
|
|
310
364
|
...filters,
|
|
311
|
-
...this.
|
|
365
|
+
...this.internalFilters,
|
|
312
366
|
...external,
|
|
313
367
|
...context,
|
|
314
|
-
search: this.
|
|
368
|
+
search: this.internalSearch || undefined
|
|
315
369
|
}
|
|
316
370
|
|
|
317
371
|
for (const key in query) {
|
|
@@ -322,7 +376,7 @@ export default {
|
|
|
322
376
|
|
|
323
377
|
await this.updateRouteQuery(query)
|
|
324
378
|
|
|
325
|
-
this.
|
|
379
|
+
this.onUpdateFilters()
|
|
326
380
|
},
|
|
327
381
|
|
|
328
382
|
getChipValue (value) {
|
|
@@ -333,21 +387,36 @@ export default {
|
|
|
333
387
|
this.$refs.filtersButton?.hideMenu()
|
|
334
388
|
},
|
|
335
389
|
|
|
390
|
+
setInternalFilters () {
|
|
391
|
+
this.internalFilters = { ...this.currentFilters }
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
setSelectFieldOptions () {
|
|
395
|
+
for (const key in this.lazyLoadingSelectedOptions) {
|
|
396
|
+
this.fields[key].options = this.lazyLoadingSelectedOptions[key]
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
336
400
|
async removeFilter ({ name }) {
|
|
337
401
|
const query = { ...this.$route.query }
|
|
338
402
|
|
|
339
403
|
delete query[name]
|
|
340
|
-
delete this.
|
|
404
|
+
delete this.internalFilters[name]
|
|
341
405
|
|
|
342
406
|
await this.updateRouteQuery(query)
|
|
343
407
|
|
|
344
|
-
this.
|
|
408
|
+
this.onUpdateFilters()
|
|
345
409
|
},
|
|
346
410
|
|
|
347
|
-
|
|
411
|
+
onUpdateFilters () {
|
|
412
|
+
this.setCurrentFilters()
|
|
413
|
+
this.setSelectFieldOptions()
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
setCurrentFilters () {
|
|
348
417
|
this.currentFilters = {
|
|
349
|
-
...this.
|
|
350
|
-
...(this.
|
|
418
|
+
...this.internalFilters,
|
|
419
|
+
...(this.internalSearch && { search: this.internalSearch })
|
|
351
420
|
}
|
|
352
421
|
|
|
353
422
|
this.$emit('update:currentFilters', this.currentFilters)
|
|
@@ -368,12 +437,6 @@ export default {
|
|
|
368
437
|
return isMultiple ? [value] : value
|
|
369
438
|
},
|
|
370
439
|
|
|
371
|
-
handleSearchModelOnCreate () {
|
|
372
|
-
if (this.useUpdateRoute && !this.useFilterButton) {
|
|
373
|
-
this.setSearch()
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
|
|
377
440
|
onSearch () {
|
|
378
441
|
if (this.useSearchOnType) {
|
|
379
442
|
this.filter()
|
|
@@ -382,16 +445,17 @@ export default {
|
|
|
382
445
|
|
|
383
446
|
setSearch () {
|
|
384
447
|
const { search } = this.mx_context
|
|
385
|
-
|
|
448
|
+
|
|
449
|
+
this.internalSearch = search || ''
|
|
386
450
|
},
|
|
387
451
|
|
|
388
452
|
setFilters () {
|
|
389
|
-
this.
|
|
453
|
+
this.internalFilters = {}
|
|
390
454
|
|
|
391
455
|
const { filters } = this.mx_context
|
|
392
456
|
|
|
393
457
|
for (const key in filters) {
|
|
394
|
-
this.
|
|
458
|
+
this.internalFilters[key] = parseValue(this.normalizeValues(filters[key], this.fields[key]?.multiple))
|
|
395
459
|
}
|
|
396
460
|
}
|
|
397
461
|
}
|
|
@@ -22,6 +22,13 @@ props:
|
|
|
22
22
|
type: Object
|
|
23
23
|
examples: ["{ name: { dense: true, onClick: () => alert('Estou sendo clicado') } }"]
|
|
24
24
|
|
|
25
|
+
filters:
|
|
26
|
+
model: true
|
|
27
|
+
desc: Diferente do model "currentFilters" que controla os filtros realizado, este model controla os filtros que ainda não foram realizados.
|
|
28
|
+
default: {}
|
|
29
|
+
type: Object
|
|
30
|
+
examples: [v-model:filters="filters"]
|
|
31
|
+
|
|
25
32
|
search-placeholder:
|
|
26
33
|
desc: Placeholder do campo de busca.
|
|
27
34
|
default: Pesquisar...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<qas-btn :color="color" data-cy="filters-btn" icon="sym_r_tune" variant="tertiary">
|
|
3
|
-
<q-menu ref="menu" anchor="center right" class="full-width" max-width="270px" self="top right">
|
|
3
|
+
<q-menu ref="menu" anchor="center right" class="full-width" max-width="270px" self="top right" v-bind="menuProps">
|
|
4
4
|
<div v-if="loading" class="q-pa-xl text-center">
|
|
5
5
|
<q-spinner color="grey" size="2em" />
|
|
6
6
|
</div>
|
|
@@ -66,6 +66,11 @@ export default {
|
|
|
66
66
|
type: Boolean
|
|
67
67
|
},
|
|
68
68
|
|
|
69
|
+
menuProps: {
|
|
70
|
+
default: () => ({}),
|
|
71
|
+
type: Object
|
|
72
|
+
},
|
|
73
|
+
|
|
69
74
|
modelValue: {
|
|
70
75
|
default: () => ({}),
|
|
71
76
|
type: Object
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="qas-grabbable relative-position">
|
|
3
|
+
<div
|
|
4
|
+
ref="grabContainer"
|
|
5
|
+
class="flex no-wrap qas-grabbable__container"
|
|
6
|
+
:class="classes"
|
|
7
|
+
>
|
|
8
|
+
<slot />
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
|
15
|
+
import { setScrollOnGrab } from '../../helpers'
|
|
16
|
+
|
|
17
|
+
defineOptions({ name: 'QasGrabbable' })
|
|
18
|
+
|
|
19
|
+
const props = defineProps({
|
|
20
|
+
useScrollBar: {
|
|
21
|
+
type: Boolean
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits(['grabbing'])
|
|
26
|
+
|
|
27
|
+
const grabContainer = ref(null)
|
|
28
|
+
const grabPosition = ref(null)
|
|
29
|
+
const isGrabbing = ref(false)
|
|
30
|
+
const scrollOnGrab = ref({})
|
|
31
|
+
|
|
32
|
+
const classes = computed(() => {
|
|
33
|
+
const baseClass = 'qas-grabbable__container'
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
[`${baseClass}--grabbing`]: isGrabbing.value,
|
|
38
|
+
[`${baseClass}--no-grab`]: !hasScrollOnGrab.value,
|
|
39
|
+
[`${baseClass}--no-scroll`]: !props.useScrollBar
|
|
40
|
+
},
|
|
41
|
+
`${baseClass}--grab-${grabPosition.value}`
|
|
42
|
+
]
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const hasScrollOnGrab = computed(() => !!Object.keys(scrollOnGrab.value).length)
|
|
46
|
+
|
|
47
|
+
function handleEnableScrollOnGrab () {
|
|
48
|
+
const { scrollWidth, offsetWidth } = grabContainer.value
|
|
49
|
+
|
|
50
|
+
if (scrollWidth > offsetWidth) {
|
|
51
|
+
initScrollOnGrab()
|
|
52
|
+
setGrabPosition()
|
|
53
|
+
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
disableScrollOnGrab()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function initScrollOnGrab () {
|
|
61
|
+
if (hasScrollOnGrab.value) return
|
|
62
|
+
|
|
63
|
+
scrollOnGrab.value = setScrollOnGrab(grabContainer.value, {
|
|
64
|
+
onGrabFn: onGrab,
|
|
65
|
+
onMoveFn: setGrabPosition,
|
|
66
|
+
onScrollFn: setGrabPosition
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function disableScrollOnGrab () {
|
|
71
|
+
if (!hasScrollOnGrab.value) return
|
|
72
|
+
|
|
73
|
+
scrollOnGrab.value.destroyEvents()
|
|
74
|
+
scrollOnGrab.value.element.style.cursor = 'auto'
|
|
75
|
+
scrollOnGrab.value = {}
|
|
76
|
+
|
|
77
|
+
grabPosition.value = null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onGrab ({ isGrabbing: grabbing }) {
|
|
81
|
+
isGrabbing.value = grabbing
|
|
82
|
+
|
|
83
|
+
emit('grabbing', grabbing)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setGrabPosition () {
|
|
87
|
+
const { offsetWidth, scrollLeft, scrollWidth } = grabContainer.value
|
|
88
|
+
const offset = 16
|
|
89
|
+
|
|
90
|
+
if (scrollLeft <= offset) {
|
|
91
|
+
/**
|
|
92
|
+
* Se o scroll estiver no início, o valor de `scrollLeft` será 0. No entanto,
|
|
93
|
+
* é necessário verificar se o valor de `scrollLeft` é menor ou igual a `offset`
|
|
94
|
+
* para garantir um espaço de segurança.
|
|
95
|
+
*/
|
|
96
|
+
grabPosition.value = 'start'
|
|
97
|
+
} else if (scrollLeft + offsetWidth + offset >= scrollWidth) {
|
|
98
|
+
/**
|
|
99
|
+
* Se o scroll estiver no final, o valor de `scrollLeft` + `offsetWidth` será
|
|
100
|
+
* igual a `scrollWidth`. Porém, estamos levamos em consideração um espaço de
|
|
101
|
+
* segurança contido na variável `offset`.
|
|
102
|
+
*/
|
|
103
|
+
grabPosition.value = 'end'
|
|
104
|
+
} else {
|
|
105
|
+
/**
|
|
106
|
+
* Se o scroll não estiver no início e nem no final, ele estará no meio.
|
|
107
|
+
*/
|
|
108
|
+
grabPosition.value = 'middle'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
handleEnableScrollOnGrab()
|
|
114
|
+
|
|
115
|
+
window.addEventListener('resize', handleEnableScrollOnGrab)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
onBeforeUnmount(() => {
|
|
119
|
+
window.removeEventListener('resize', handleEnableScrollOnGrab)
|
|
120
|
+
|
|
121
|
+
disableScrollOnGrab()
|
|
122
|
+
})
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<style lang="scss">
|
|
126
|
+
.qas-grabbable {
|
|
127
|
+
&__container {
|
|
128
|
+
-webkit-overflow-scrolling: touch;
|
|
129
|
+
-ms-overflow-style: none;
|
|
130
|
+
overflow-x: auto;
|
|
131
|
+
scrollbar-color: $blue-grey-3 transparent;
|
|
132
|
+
|
|
133
|
+
&::-webkit-scrollbar {
|
|
134
|
+
height: 12px;
|
|
135
|
+
background-color: transparent;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
&::-webkit-scrollbar-track {
|
|
139
|
+
background-color: transparent;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
&::-webkit-scrollbar-thumb {
|
|
143
|
+
background-color: $blue-grey-3;
|
|
144
|
+
border-radius: var(--qas-generic-border-radius);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
&::-webkit-scrollbar-thumb:hover {
|
|
148
|
+
background-color: $blue-grey-4;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&::before,
|
|
152
|
+
&::after {
|
|
153
|
+
content: '';
|
|
154
|
+
width: 24px;
|
|
155
|
+
height: 100%;
|
|
156
|
+
position: absolute;
|
|
157
|
+
top: 0;
|
|
158
|
+
pointer-events: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&::before {
|
|
162
|
+
background: linear-gradient(-270deg, rgba($grey-1, 0.7) 0%, rgba(251, 251, 251, 0) 100%);
|
|
163
|
+
left: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&::after {
|
|
167
|
+
background: linear-gradient(270deg, rgba($grey-1, 0.7) 0%, rgba(251, 251, 251, 0) 100%);
|
|
168
|
+
right: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&--no-scroll {
|
|
172
|
+
scrollbar-width: none;
|
|
173
|
+
|
|
174
|
+
&::-webkit-scrollbar {
|
|
175
|
+
width: 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&--no-grab::before,
|
|
180
|
+
&--no-grab::after,
|
|
181
|
+
&--grab-start::before,
|
|
182
|
+
&--grab-end::after {
|
|
183
|
+
content: none !important;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type: component
|
|
2
|
+
|
|
3
|
+
meta:
|
|
4
|
+
desc: Componente de scroll em uma determinada área (elemento) ao realizar evento de grab (puxar/agarrar) com o mouse/touch.
|
|
5
|
+
|
|
6
|
+
props:
|
|
7
|
+
use-scroll-bar:
|
|
8
|
+
desc: Se deve ou não usar a barra de rolagem.
|
|
9
|
+
default: false
|
|
10
|
+
type: Boolean
|
|
11
|
+
|
|
12
|
+
slots:
|
|
13
|
+
default:
|
|
14
|
+
desc: Slot para ter o conteúdo que terá o scroll na horizontal.
|
|
15
|
+
|
|
16
|
+
events:
|
|
17
|
+
'@grabbable -> function(grabbing)':
|
|
18
|
+
desc: Dispara toda vez que o "grab" é realizado.
|
|
19
|
+
params:
|
|
20
|
+
grabbing:
|
|
21
|
+
desc: Retorna se está ou não fazendo "grabbing".
|
|
22
|
+
type: Boolean
|
|
@@ -129,6 +129,14 @@ events:
|
|
|
129
129
|
default: false
|
|
130
130
|
type: Boolean
|
|
131
131
|
|
|
132
|
+
'@update:selectedOptions -> function (value)':
|
|
133
|
+
desc: Dispara toda vez que a lista de opções selecionadas é atualizada.
|
|
134
|
+
params:
|
|
135
|
+
value:
|
|
136
|
+
desc: Array com as opções selecionadas (label e value)
|
|
137
|
+
default: []
|
|
138
|
+
type: Array
|
|
139
|
+
|
|
132
140
|
'@fetch-options-success -> function (value)':
|
|
133
141
|
desc: Dispara toda vez que o campo de busca faz o fetch do lazy loading com sucesso.
|
|
134
142
|
params:
|
|
@@ -75,6 +75,10 @@ export default {
|
|
|
75
75
|
type: Boolean
|
|
76
76
|
},
|
|
77
77
|
|
|
78
|
+
useFetchOptionsOnFocus: {
|
|
79
|
+
type: Boolean
|
|
80
|
+
},
|
|
81
|
+
|
|
78
82
|
useSearch: {
|
|
79
83
|
type: Boolean,
|
|
80
84
|
default: undefined
|
|
@@ -238,6 +242,10 @@ export default {
|
|
|
238
242
|
this.isPopupContentOpen = true
|
|
239
243
|
this.$emit('popup-show')
|
|
240
244
|
|
|
245
|
+
if (this.useFetchOptionsOnFocus && this.mx_fetchCount === 0) {
|
|
246
|
+
this.mx_setFetchOptions('')
|
|
247
|
+
}
|
|
248
|
+
|
|
241
249
|
if (this.mx_isFetching) {
|
|
242
250
|
this.togglePopupContentClass(true)
|
|
243
251
|
}
|
|
@@ -246,7 +254,7 @@ export default {
|
|
|
246
254
|
setLazyLoading () {
|
|
247
255
|
this.mx_setCachedOptions('options')
|
|
248
256
|
|
|
249
|
-
if (this.useFetchOptionsOnCreate) this.mx_setFetchOptions('')
|
|
257
|
+
if (this.useFetchOptionsOnCreate && !this.useFetchOptionsOnFocus) this.mx_setFetchOptions('')
|
|
250
258
|
},
|
|
251
259
|
|
|
252
260
|
setSearchMethod () {
|
|
@@ -86,6 +86,11 @@ props:
|
|
|
86
86
|
default: true
|
|
87
87
|
type: Boolean
|
|
88
88
|
|
|
89
|
+
use-fetch-options-on-focus:
|
|
90
|
+
desc: Controla se o componente vai fazer um fetch das opções assim que o mesmo é focado (caso tenha lazy loading ativado). Se essa opção estiver habilitada, o componente não vai fazer o fetch das opções assim que é criado, anulando a propriedade "useFetchOptionsOnCreate".
|
|
91
|
+
default: false
|
|
92
|
+
type: Boolean
|
|
93
|
+
|
|
89
94
|
use-search:
|
|
90
95
|
desc: Controla se vai ou não ter campo de busca no select.
|
|
91
96
|
default: undefined
|
|
@@ -112,6 +117,14 @@ events:
|
|
|
112
117
|
default: false
|
|
113
118
|
type: Boolean
|
|
114
119
|
|
|
120
|
+
'@update:selectedOptions -> function (value)':
|
|
121
|
+
desc: Dispara toda vez que a lista de opções selecionadas é atualizada.
|
|
122
|
+
params:
|
|
123
|
+
value:
|
|
124
|
+
desc: Array com as opções selecionadas (label e value)
|
|
125
|
+
default: []
|
|
126
|
+
type: Array
|
|
127
|
+
|
|
115
128
|
'@fetch-options-success -> function (value)':
|
|
116
129
|
desc: Dispara toda vez que o campo de busca faz o fetch do lazy loading com sucesso.
|
|
117
130
|
params:
|
|
@@ -217,10 +217,6 @@ export default {
|
|
|
217
217
|
|
|
218
218
|
hasScrollOnGrab () {
|
|
219
219
|
return !!Object.keys(this.scrollOnGrab).length
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
isScrolling () {
|
|
223
|
-
return this.hasScrollOnGrab && this.scrollOnGrab.haveMoved()
|
|
224
220
|
}
|
|
225
221
|
},
|
|
226
222
|
|
|
@@ -299,18 +295,11 @@ export default {
|
|
|
299
295
|
return {
|
|
300
296
|
class: 'text-no-decoration text-grey-8 flex full-width items-center full-height',
|
|
301
297
|
[this.useExternalLink ? 'href' : 'to']: this.rowRouteFn(row),
|
|
302
|
-
onClick: this.onRowClickHandler,
|
|
303
298
|
...(this.useExternalLink && { target: '_blank' })
|
|
304
299
|
}
|
|
305
300
|
},
|
|
306
301
|
|
|
307
|
-
onRowClickHandler (event) {
|
|
308
|
-
if (this.isScrolling) return event.preventDefault()
|
|
309
|
-
},
|
|
310
|
-
|
|
311
302
|
onRowClick () {
|
|
312
|
-
if (this.isScrolling) return
|
|
313
|
-
|
|
314
303
|
this.$attrs.onRowClick(...arguments)
|
|
315
304
|
}
|
|
316
305
|
}
|
|
@@ -31,21 +31,21 @@ props:
|
|
|
31
31
|
default: name
|
|
32
32
|
type: String
|
|
33
33
|
|
|
34
|
-
use-scroll-on-grab:
|
|
35
|
-
desc: Adiciona scroll pelo mouse ao arrastar tabela em todas as telas (Celular, Desktop).
|
|
36
|
-
default: true
|
|
37
|
-
type: Boolean
|
|
38
|
-
|
|
39
34
|
row-route-fn:
|
|
40
35
|
desc: Usado quando há a necessidade de alteração de rota ao clicar em um item da tabela(a linha passa ser um <a> habilitando a opção de abrir em uma nova aba).
|
|
41
36
|
type: Function
|
|
42
|
-
examples: ["() => ({
|
|
37
|
+
examples: ["(row) => ({ path: 'table-generator', params: { id: row.uuid } })"]
|
|
43
38
|
|
|
44
39
|
use-external-link:
|
|
45
40
|
desc: Usado em conjunto com a prop "row-route-fn" quando há a necessidade da rota ser um link externo.
|
|
46
41
|
default: false
|
|
47
42
|
type: Boolean
|
|
48
43
|
|
|
44
|
+
use-scroll-on-grab:
|
|
45
|
+
desc: Adiciona scroll pelo mouse ao arrastar tabela em todas as telas (Celular, Desktop).
|
|
46
|
+
default: true
|
|
47
|
+
type: Boolean
|
|
48
|
+
|
|
49
49
|
use-sticky-header:
|
|
50
50
|
desc: Usado para manter o header da tabela (thead) fixo.
|
|
51
51
|
default: false
|
|
@@ -1,70 +1,112 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Helper de scroll em uma determinada área (elemento) ao realizar evento de grab (puxar/agarrar) com o mouse/touch.
|
|
3
|
+
*
|
|
4
|
+
* @param {HTMLElement} element
|
|
5
|
+
* @param {{
|
|
6
|
+
* onGrabFn: function({ element: HTMLElement, isGrabbing: boolean }),
|
|
7
|
+
* onMoveFn: function({ element: HTMLElement, event: MouseEvent | TouchEvent }),
|
|
8
|
+
* onScrollFn: function({ element: HTMLElement, event: Event })
|
|
9
|
+
* }} options
|
|
10
|
+
*
|
|
11
|
+
* @returns {{ element: HTMLElement, destroyEvents: function }}
|
|
12
|
+
*/
|
|
13
|
+
export default function (element, options = {}) {
|
|
14
|
+
setModel()
|
|
3
15
|
|
|
4
16
|
let isDown = false
|
|
5
|
-
let moved = false
|
|
6
17
|
let startX
|
|
7
18
|
let scrollLeft
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
const events = {
|
|
21
|
+
mousedown: onMouseEnter,
|
|
22
|
+
mouseleave: onLeave,
|
|
23
|
+
mousemove: onMouseMove,
|
|
24
|
+
mouseup: onLeave,
|
|
25
|
+
scroll: onScroll,
|
|
26
|
+
touchend: onLeave,
|
|
27
|
+
touchmove: onTouchMove,
|
|
28
|
+
touchstart: onEnter
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
addEvents()
|
|
32
|
+
|
|
33
|
+
function addEvents () {
|
|
34
|
+
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function destroyEvents () {
|
|
38
|
+
Object.entries(events).forEach(([event, fn]) => element.removeEventListener(event, fn))
|
|
39
|
+
}
|
|
13
40
|
|
|
14
|
-
function
|
|
41
|
+
function onEnter () {
|
|
15
42
|
isDown = true
|
|
16
|
-
moved = false
|
|
17
43
|
|
|
18
44
|
element.classList.add('active')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function onMouseEnter (event) {
|
|
48
|
+
onEnter()
|
|
19
49
|
|
|
20
50
|
startX = event.pageX - element.offsetLeft
|
|
21
51
|
scrollLeft = element.scrollLeft
|
|
22
52
|
}
|
|
23
53
|
|
|
24
|
-
function
|
|
54
|
+
function onLeave () {
|
|
25
55
|
isDown = false
|
|
26
56
|
|
|
27
57
|
element.classList.remove('active')
|
|
28
|
-
setStyle()
|
|
29
|
-
}
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
isDown = false
|
|
33
|
-
|
|
34
|
-
element.classList.remove('active')
|
|
35
|
-
setStyle()
|
|
59
|
+
setModel()
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
function onMouseMove (event) {
|
|
39
63
|
if (event) event.preventDefault()
|
|
64
|
+
|
|
40
65
|
if (!isDown) return
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
setModel('grabbing')
|
|
43
68
|
|
|
44
69
|
const x = event.pageX - element.offsetLeft
|
|
45
70
|
const walk = (x - startX) * 3 // scroll-fast
|
|
71
|
+
|
|
46
72
|
element.scrollLeft = scrollLeft - walk
|
|
47
|
-
|
|
73
|
+
|
|
74
|
+
options.onMoveFn?.({ element, event })
|
|
48
75
|
}
|
|
49
76
|
|
|
50
|
-
function
|
|
51
|
-
|
|
77
|
+
function onTouchMove (event) {
|
|
78
|
+
event = event.touches[0]
|
|
79
|
+
|
|
80
|
+
if (!isDown) return
|
|
81
|
+
|
|
82
|
+
setModel('grabbing')
|
|
83
|
+
|
|
84
|
+
options.onMoveFn?.({ element, event })
|
|
52
85
|
}
|
|
53
86
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
element.removeEventListener('mouseleave', onMouseLeave)
|
|
57
|
-
element.removeEventListener('mouseup', onMouseUp)
|
|
58
|
-
element.removeEventListener('mousemove', onMouseMove)
|
|
87
|
+
function onScroll (event) {
|
|
88
|
+
options.onScrollFn?.({ element, event })
|
|
59
89
|
}
|
|
60
90
|
|
|
61
|
-
function
|
|
62
|
-
|
|
91
|
+
function setModel (model = 'grab') {
|
|
92
|
+
const isGrabbing = model === 'grabbing'
|
|
93
|
+
|
|
94
|
+
setStyles({ model, isGrabbing })
|
|
95
|
+
|
|
96
|
+
options.onGrabFn?.({ element, isGrabbing })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setStyles ({ model, isGrabbing }) {
|
|
100
|
+
element.style.cursor = model
|
|
101
|
+
|
|
102
|
+
Array.from(element.children).forEach(child => {
|
|
103
|
+
child.classList.toggle('no-pointer-events', isGrabbing)
|
|
104
|
+
child.classList.toggle('non-selectable', isGrabbing)
|
|
105
|
+
})
|
|
63
106
|
}
|
|
64
107
|
|
|
65
108
|
return {
|
|
66
109
|
element,
|
|
67
|
-
haveMoved,
|
|
68
110
|
destroyEvents
|
|
69
111
|
}
|
|
70
112
|
}
|
|
@@ -36,6 +36,7 @@ export default {
|
|
|
36
36
|
emits: [
|
|
37
37
|
'update:modelValue',
|
|
38
38
|
'update:fetching',
|
|
39
|
+
'update:selectedOptions',
|
|
39
40
|
'fetch-options-success',
|
|
40
41
|
'fetch-options-error'
|
|
41
42
|
],
|
|
@@ -103,6 +104,10 @@ export default {
|
|
|
103
104
|
}
|
|
104
105
|
},
|
|
105
106
|
|
|
107
|
+
created () {
|
|
108
|
+
this.mx_registerSelectedOptionsEvent()
|
|
109
|
+
},
|
|
110
|
+
|
|
106
111
|
methods: {
|
|
107
112
|
async mx_filterOptionsByStore (search) {
|
|
108
113
|
this.mx_resetFilter(search)
|
|
@@ -153,6 +158,7 @@ export default {
|
|
|
153
158
|
this.mx_isScrolling = true
|
|
154
159
|
|
|
155
160
|
const options = await this.mx_fetchOptions()
|
|
161
|
+
|
|
156
162
|
this.mx_filteredOptions.push(...options)
|
|
157
163
|
|
|
158
164
|
// this is to prevent the virtual-scroll event to be fired again
|
|
@@ -161,6 +167,12 @@ export default {
|
|
|
161
167
|
})
|
|
162
168
|
},
|
|
163
169
|
|
|
170
|
+
mx_decamelizeFieldName (fieldName) {
|
|
171
|
+
if (fieldName.includes('_')) return fieldName.replaceAll('_', '-')
|
|
172
|
+
|
|
173
|
+
return decamelize(fieldName, { separator: '-' })
|
|
174
|
+
},
|
|
175
|
+
|
|
164
176
|
async mx_fetchOptions () {
|
|
165
177
|
this.mx_fetchCount++
|
|
166
178
|
|
|
@@ -180,7 +192,7 @@ export default {
|
|
|
180
192
|
key: 'fetchFieldOptions',
|
|
181
193
|
payload: {
|
|
182
194
|
url,
|
|
183
|
-
field: decamelizeFieldName ?
|
|
195
|
+
field: decamelizeFieldName ? this.mx_decamelizeFieldName(this.name) : this.name,
|
|
184
196
|
params: {
|
|
185
197
|
...params,
|
|
186
198
|
search: this.mx_search,
|
|
@@ -322,6 +334,21 @@ export default {
|
|
|
322
334
|
}
|
|
323
335
|
|
|
324
336
|
return options
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
mx_registerSelectedOptionsEvent () {
|
|
340
|
+
if (!this.useLazyLoading) return
|
|
341
|
+
|
|
342
|
+
this.$watch('modelValue', values => {
|
|
343
|
+
if (!values) return this.$emit('update:selectedOptions', [])
|
|
344
|
+
|
|
345
|
+
const findOption = value => this.mx_filteredOptions.find(option => option.value === value)
|
|
346
|
+
const isArray = Array.isArray(values)
|
|
347
|
+
|
|
348
|
+
const selectedOptions = isArray ? values.map(findOption) : [findOption(values)]
|
|
349
|
+
|
|
350
|
+
this.$emit('update:selectedOptions', selectedOptions)
|
|
351
|
+
})
|
|
325
352
|
}
|
|
326
353
|
}
|
|
327
354
|
}
|
package/src/vue-plugin.js
CHANGED
|
@@ -27,6 +27,7 @@ import QasFormGenerator from './components/form-generator/QasFormGenerator.vue'
|
|
|
27
27
|
import QasFormView from './components/form-view/QasFormView.vue'
|
|
28
28
|
import QasGallery from './components/gallery/QasGallery.vue'
|
|
29
29
|
import QasGalleryCard from './components/gallery-card/QasGalleryCard.vue'
|
|
30
|
+
import QasGrabbable from './components/grabbable/QasGrabbable.vue'
|
|
30
31
|
import QasGridGenerator from './components/grid-generator/QasGridGenerator.vue'
|
|
31
32
|
import QasHeaderActions from './components/header-actions/QasHeaderActions.vue'
|
|
32
33
|
import QasInfiniteScroll from './components/infinite-scroll/QasInfiniteScroll.vue'
|
|
@@ -114,6 +115,7 @@ async function install (app) {
|
|
|
114
115
|
app.component('QasFormView', QasFormView)
|
|
115
116
|
app.component('QasGallery', QasGallery)
|
|
116
117
|
app.component('QasGalleryCard', QasGalleryCard)
|
|
118
|
+
app.component('QasGrabbable', QasGrabbable)
|
|
117
119
|
app.component('QasGridGenerator', QasGridGenerator)
|
|
118
120
|
app.component('QasHeaderActions', QasHeaderActions)
|
|
119
121
|
app.component('QasInfiniteScroll', QasInfiniteScroll)
|
|
@@ -203,6 +205,7 @@ export {
|
|
|
203
205
|
QasFormView,
|
|
204
206
|
QasGallery,
|
|
205
207
|
QasGalleryCard,
|
|
208
|
+
QasGrabbable,
|
|
206
209
|
QasGridGenerator,
|
|
207
210
|
QasHeaderActions,
|
|
208
211
|
QasInfiniteScroll,
|