@bildvitta/quasar-ui-asteroid 3.14.0-beta.9 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bildvitta/quasar-ui-asteroid",
3
3
  "description": "Asteroid",
4
- "version": "3.14.0-beta.9",
4
+ "version": "3.15.0-beta.0",
5
5
  "author": "Bild & Vitta <systemteam@bild.com.br>",
6
6
  "license": "MIT",
7
7
  "main": "dist/asteroid.cjs.min.js",
@@ -451,6 +451,7 @@ function onNavigation (date) {
451
451
  font-size: 10px !important;
452
452
  line-height: 1;
453
453
  transition: color var(--qas-generic-transition);
454
+ width: 100%;
454
455
 
455
456
  &--pointer {
456
457
  bottom: -6px;
@@ -479,10 +480,11 @@ function onNavigation (date) {
479
480
  @include set-typography($subtitle2);
480
481
 
481
482
  color: $grey-4;
483
+ line-height: 1;
482
484
  visibility: unset;
483
485
 
484
486
  span {
485
- height: 36px;
487
+ height: auto;
486
488
  padding: var(--qas-spacing-xs);
487
489
  }
488
490
  }
@@ -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="search" :placeholder="searchPlaceholder" :use-search-on-type="useSearchOnType" @clear="clearSearch" @filter="filter()" @update:model-value="onSearch">
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="filters" v-bind="filterButtonProps" />
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="filters" v-bind="filterButtonProps" />
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: ['fetch-success', 'fetch-error', 'update:currentFilters'],
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
- search: ''
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 fieldsProps = {}
174
+ const formattedFieldsProps = {}
159
175
 
160
- for (const key in this.fieldsProps) {
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
- fieldsProps[decamelizedFieldKey] = { ...this.fieldsProps[key] }
196
+ formattedFieldsProps[decamelizedFieldKey] = Object.assign(fieldsProps, lazyLoadingProps)
164
197
  }
165
198
 
166
- return fieldsProps
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.updateCurrentFilters()
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.filters
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.filters[key]
311
+ delete this.internalFilters[key]
260
312
  }
261
313
  }
262
314
  } else {
263
- this.filters = {}
315
+ this.internalFilters = {}
264
316
  }
265
317
 
266
318
  this.hideFiltersMenu()
267
319
 
268
320
  await this.updateRouteQuery(query)
269
321
 
270
- this.updateCurrentFilters()
322
+ this.onUpdateFilters()
271
323
  },
272
324
 
273
325
  clearSearch () {
274
- this.search = ''
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.filters,
365
+ ...this.internalFilters,
312
366
  ...external,
313
367
  ...context,
314
- search: this.search || undefined
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.updateCurrentFilters()
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.filters[name]
404
+ delete this.internalFilters[name]
341
405
 
342
406
  await this.updateRouteQuery(query)
343
407
 
344
- this.updateCurrentFilters()
408
+ this.onUpdateFilters()
345
409
  },
346
410
 
347
- updateCurrentFilters () {
411
+ onUpdateFilters () {
412
+ this.setCurrentFilters()
413
+ this.setSelectFieldOptions()
414
+ },
415
+
416
+ setCurrentFilters () {
348
417
  this.currentFilters = {
349
- ...this.filters,
350
- ...(this.search && { search: this.search })
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
- this.search = search || ''
448
+
449
+ this.internalSearch = search || ''
386
450
  },
387
451
 
388
452
  setFilters () {
389
- this.filters = {}
453
+ this.internalFilters = {}
390
454
 
391
455
  const { filters } = this.mx_context
392
456
 
393
457
  for (const key in filters) {
394
- this.filters[key] = parseValue(this.normalizeValues(filters[key], this.fields[key]?.multiple))
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: ["() => ({ name: 'UsersList' })"]
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
- export default function (element) {
2
- setStyle()
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
- element.addEventListener('mousedown', onMouseDown)
10
- element.addEventListener('mouseleave', onMouseLeave)
11
- element.addEventListener('mouseup', onMouseUp)
12
- element.addEventListener('mousemove', onMouseMove)
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 onMouseDown (event) {
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 onMouseLeave () {
54
+ function onLeave () {
25
55
  isDown = false
26
56
 
27
57
  element.classList.remove('active')
28
- setStyle()
29
- }
30
58
 
31
- function onMouseUp () {
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
- setStyle('grabbing')
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
- moved = true
73
+
74
+ options.onMoveFn?.({ element, event })
48
75
  }
49
76
 
50
- function setStyle (model = 'grab') {
51
- element.style.cursor = model
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 destroyEvents () {
55
- element.removeEventListener('mousedown', onMouseDown)
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 haveMoved () {
62
- return moved
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 ? decamelize(this.name, { separator: '-' }) : this.name,
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,