@bildvitta/quasar-ui-asteroid 2.22.0 → 2.23.0-beta.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@bildvitta/quasar-ui-asteroid",
3
- "version": "2.22.0",
3
+ "version": "2.23.0-beta.2",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -71,8 +71,9 @@ export default {
71
71
  pattern,
72
72
  maxFiles,
73
73
  searchable,
74
- gmt
75
- } = this.formatedField
74
+ gmt,
75
+ useLazyLoading
76
+ } = this.formattedField
76
77
 
77
78
  // Default error attributes for Quasar.
78
79
  const error = {
@@ -138,7 +139,7 @@ export default {
138
139
 
139
140
  'signature-uploader': { is: 'qas-signature-uploader', entity, uploadLabel: label, ...error },
140
141
 
141
- select: { is: 'qas-select', multiple, options, searchable, ...input }
142
+ select: { is: 'qas-select', entity, name, multiple, options, searchable, useLazyLoading, ...input }
142
143
  }
143
144
 
144
145
  return { ...(profiles[type] || profiles.default), ...this.$attrs }
@@ -154,7 +155,7 @@ export default {
154
155
  },
155
156
 
156
157
  // This computed will change the key name when the server sends different key.
157
- formatedField () {
158
+ formattedField () {
158
159
  const field = {}
159
160
 
160
161
  for (const key in this.field) {
@@ -75,7 +75,7 @@ export default {
75
75
 
76
76
  methods: {
77
77
  toggleMask (first, second) {
78
- if (!this.value.length) return
78
+ if (!this.value?.length) return
79
79
 
80
80
  const length = first.split('#').length - 2
81
81
  return this.value?.length > length ? second : first
@@ -32,6 +32,14 @@ export default {
32
32
  description: 'List height when there is no results.'
33
33
  },
34
34
 
35
+ emptyResultText: {
36
+ description: 'Text for empty result on search.'
37
+ },
38
+
39
+ entity: {
40
+ description: 'Vuex entity name to be used on lazy loading filter.'
41
+ },
42
+
35
43
  fuseOptions: {
36
44
  description: '[Fuse.js options](https://fusejs.io/api/options.html)',
37
45
  table: {
@@ -47,6 +55,21 @@ export default {
47
55
  description: 'Hides empty slot.'
48
56
  },
49
57
 
58
+ lazyLoadingProps: {
59
+ description: 'Props to be used on lazy loading.',
60
+ table: {
61
+ defaultValue: {
62
+ detail: JSON.stringify({
63
+ url: '',
64
+ params: {
65
+ limit: 48
66
+ },
67
+ decamelizeFieldName: true
68
+ })
69
+ }
70
+ }
71
+ },
72
+
50
73
  list: {
51
74
  description: 'Array of objects with `label` and `value` each, to be searched.'
52
75
  },
@@ -55,6 +78,10 @@ export default {
55
78
  description: 'Display temporary message when search field in empty.'
56
79
  },
57
80
 
81
+ useLazyLoading: {
82
+ description: 'Option to enable lazy loading.'
83
+ },
84
+
58
85
  value: {
59
86
  description: 'Search field value.'
60
87
  },
@@ -65,6 +92,26 @@ export default {
65
92
  table: noSummary
66
93
  },
67
94
 
95
+ 'fetch-options-error': {
96
+ description: 'Fires when occur an error fetching the options.',
97
+ table: {
98
+ defaultValue: {
99
+ detail: JSON.stringify({ error: 'object' }),
100
+ summary: '{}'
101
+ }
102
+ }
103
+ },
104
+
105
+ 'fetch-options-success': {
106
+ description: 'Fires when successfully fetching the options.',
107
+ table: {
108
+ defaultValue: {
109
+ detail: JSON.stringify({ results: 'array' }),
110
+ summary: '{}'
111
+ }
112
+ }
113
+ },
114
+
68
115
  input: {
69
116
  description: 'Fires when the result changes.',
70
117
  table: noSummary
@@ -85,6 +132,11 @@ export default {
85
132
  empty: {
86
133
  description: 'To be displayed when there is no results.',
87
134
  table: noSummary
135
+ },
136
+
137
+ loading: {
138
+ description: 'To be displayed when lazy loading is in progress.',
139
+ table: noSummary
88
140
  }
89
141
  }
90
142
  }
@@ -1,18 +1,26 @@
1
1
  <template>
2
2
  <qas-box v-bind="$attrs">
3
- <q-input v-model="search" clearable :disable="!list.length" outlined :placeholder="placeholder">
3
+ <q-input v-model="search" v-bind="attributes">
4
4
  <template #append>
5
5
  <q-icon color="primary" name="o_search" />
6
6
  </template>
7
7
  </q-input>
8
8
 
9
- <div class="overflow-auto q-mt-xs relative-position" :style="contentStyle">
10
- <slot v-if="hasResults" :results="results" />
9
+ <div ref="scrollContainer" :class="contentClasses" :style="contentStyle">
10
+ <component :is="component.is" v-bind="component.props" v-on="component.listeners">
11
+ <slot v-if="$_hasFilteredOptions" :results="filteredOptions" />
12
+ </component>
11
13
 
12
- <slot v-else-if="!hideEmptySlot" name="empty">
14
+ <slot v-if="isLoading" name="loading">
15
+ <div class="flex justify-center q-pb-sm">
16
+ <q-spinner-dots color="primary" size="20px" />
17
+ </div>
18
+ </slot>
19
+
20
+ <slot v-if="showEmptyResult" name="empty">
13
21
  <div class="absolute-center text-center">
14
22
  <q-icon class="q-mb-sm text-center" color="primary" name="o_search" size="38px" />
15
- <div>Não resultados disponíveis.</div>
23
+ <div>{{ emptyResultText }}</div>
16
24
  </div>
17
25
  </slot>
18
26
  </div>
@@ -20,21 +28,31 @@
20
28
  </template>
21
29
 
22
30
  <script>
31
+ import { QInfiniteScroll } from 'quasar'
23
32
  import QasBox from '../box/QasBox.vue'
24
33
 
25
34
  import Fuse from 'fuse.js'
35
+ import lazyLoadingFilterMixin from '../../mixins/lazy-loading-filter'
26
36
 
27
37
  export default {
28
38
  components: {
39
+ QInfiniteScroll,
29
40
  QasBox
30
41
  },
31
42
 
43
+ mixins: [lazyLoadingFilterMixin],
44
+
32
45
  props: {
33
46
  emptyListHeight: {
34
47
  default: '100px',
35
48
  type: String
36
49
  },
37
50
 
51
+ emptyResultText: {
52
+ default: 'Não há resultados disponíveis.',
53
+ type: String
54
+ },
55
+
38
56
  fuseOptions: {
39
57
  default: () => ({}),
40
58
  type: Object
@@ -67,15 +85,21 @@ export default {
67
85
 
68
86
  data () {
69
87
  return {
70
- fuse: null,
71
- results: this.list,
72
- search: ''
88
+ fuse: null
73
89
  }
74
90
  },
75
91
 
76
92
  computed: {
93
+ contentClasses () {
94
+ return ['overflow-auto', 'q-mt-xs', 'relative-position', this.$_virtualScrollClassName]
95
+ },
96
+
77
97
  contentStyle () {
78
- return { height: this.list.length ? this.height : this.emptyListHeight }
98
+ return { height: this.contentHeight }
99
+ },
100
+
101
+ contentHeight () {
102
+ return this.$_hasFilteredOptions ? this.height : this.showEmptyResult ? this.emptyListHeight : 'auto'
79
103
  },
80
104
 
81
105
  defaultFuseOptions () {
@@ -92,8 +116,41 @@ export default {
92
116
  }
93
117
  },
94
118
 
95
- hasResults () {
96
- return !!this.results.length
119
+ showEmptyResult () {
120
+ return !this.$_hasFilteredOptions && !this.hideEmptySlot && !this.isLoading
121
+ },
122
+
123
+ isDisabled () {
124
+ return (!this.useLazyLoading && !this.list.length) || this.isLoading
125
+ },
126
+
127
+ attributes () {
128
+ return {
129
+ clearable: true,
130
+ disable: this.isDisabled,
131
+ debounce: this.useLazyLoading ? 500 : 0,
132
+ outlined: true,
133
+ placeholder: this.placeholder,
134
+ error: this.hasFetchError
135
+ }
136
+ },
137
+
138
+ component () {
139
+ const infiniteScrollProps = {
140
+ offset: 100,
141
+ scrollTarget: this.$refs.scrollContainer,
142
+ ref: 'infiniteScrollRef'
143
+ }
144
+
145
+ return {
146
+ is: this.useLazyLoading ? 'q-infinite-scroll' : 'div',
147
+ props: {
148
+ ...(this.useLazyLoading && infiniteScrollProps)
149
+ },
150
+ listeners: {
151
+ ...(this.useLazyLoading && { load: this.onInfiniteScroll })
152
+ }
153
+ }
97
154
  }
98
155
  },
99
156
 
@@ -102,33 +159,60 @@ export default {
102
159
  this.fuse.options = { ...this.fuse.options, ...value }
103
160
  },
104
161
 
105
- hasResults (value) {
162
+ $_hasFilteredOptions (value) {
106
163
  !value && this.$emit('emptyResult')
107
164
  },
108
165
 
109
166
  list (value) {
110
- this.fuse.list = value
111
- this.setResults(this.search)
167
+ if (!this.useLazyLoading) {
168
+ this.fuse.list = value
169
+ this.filterOptionsByFuse(this.search)
170
+ }
112
171
  },
113
172
 
114
173
  search: {
115
- handler (value) {
116
- this.setResults(value)
174
+ async handler (value) {
117
175
  this.$emit('input', value)
118
- },
119
176
 
120
- immediate: true
177
+ if (this.useLazyLoading) {
178
+ await this.$_filterOptionsByStore(value)
179
+
180
+ this.$refs.infiniteScrollRef.resume()
181
+
182
+ return
183
+ }
184
+
185
+ this.filterOptionsByFuse(value)
186
+ }
121
187
  }
122
188
  },
123
189
 
124
190
  created () {
191
+ this.filteredOptions = this.list
125
192
  this.search = this.value
126
- this.fuse = new Fuse(this.list, this.defaultFuseOptions)
193
+
194
+ if (!this.useLazyLoading) {
195
+ this.fuse = new Fuse(this.list, this.defaultFuseOptions)
196
+ }
127
197
  },
128
198
 
129
199
  methods: {
130
- setResults (value) {
131
- this.results = value ? this.fuse.search(value) : this.list
200
+ filterOptionsByFuse (value) {
201
+ this.filteredOptions = value ? this.fuse.search(value) : this.list
202
+ },
203
+
204
+ async onInfiniteScroll (index, done) {
205
+ if (!this.$_hasFilteredOptions && !this.search) {
206
+ await this.$_setFetchOptions()
207
+ return done()
208
+ }
209
+
210
+ if (this.$_canFetchOptions()) {
211
+ await this.$_loadMoreOptions()
212
+ return done()
213
+ }
214
+
215
+ done(true)
132
216
  }
133
217
  }
134
218
  }
@@ -30,6 +30,10 @@ export default {
30
30
 
31
31
  argTypes: {
32
32
  // Props
33
+ entity: {
34
+ description: 'Vuex entity name to be used on lazy loading filter.'
35
+ },
36
+
33
37
  fuseOptions: {
34
38
  description: '[Fuse.js](https://fusejs.io/) options.',
35
39
  table: {
@@ -41,6 +45,25 @@ export default {
41
45
  description: 'Key to be used instead of `label`.'
42
46
  },
43
47
 
48
+ lazyLoadingProps: {
49
+ description: 'Props to be used on lazy loading.',
50
+ table: {
51
+ defaultValue: {
52
+ detail: JSON.stringify({
53
+ url: '',
54
+ params: {
55
+ limit: 48
56
+ },
57
+ decamelizeFieldName: true
58
+ })
59
+ }
60
+ }
61
+ },
62
+
63
+ name: {
64
+ description: 'Name of the field.'
65
+ },
66
+
44
67
  options: {
45
68
  description: 'Select options.'
46
69
  },
@@ -49,6 +72,14 @@ export default {
49
72
  description: 'Text for empty result on search.'
50
73
  },
51
74
 
75
+ searchable: {
76
+ description: 'option to enable the search field.'
77
+ },
78
+
79
+ useLazyLoading: {
80
+ description: 'Option to enable lazy loading.'
81
+ },
82
+
52
83
  value: {
53
84
  description: 'String to filter results.'
54
85
  },
@@ -57,16 +88,40 @@ export default {
57
88
  description: 'Key to be used instead of `value`.'
58
89
  },
59
90
 
60
- searchable: {
61
- description: 'option to enable the search field.'
91
+ // Events
92
+ 'fetch-options-error': {
93
+ description: 'Fires when occur an error fetching the options.',
94
+ table: {
95
+ defaultValue: {
96
+ detail: JSON.stringify({ error: 'object' }),
97
+ summary: '{}'
98
+ }
99
+ }
100
+ },
101
+
102
+ 'fetch-options-success': {
103
+ description: 'Fires when successfully fetching the options.',
104
+ table: {
105
+ defaultValue: {
106
+ detail: JSON.stringify({ results: 'array' }),
107
+ summary: '{}'
108
+ }
109
+ }
62
110
  },
63
111
 
64
- // Events
65
112
  input: {
66
113
  description: 'Fires when model changes. Also used by `v-model`.'
67
114
  },
68
115
 
69
116
  // Slots
117
+ 'after-options': {
118
+ description: 'Template slot for the elements that should be rendered after the list of options.',
119
+ table: {
120
+ category: 'slots',
121
+ ...noSummary
122
+ }
123
+ },
124
+
70
125
  append: {
71
126
  description: 'Attach to the inner field.',
72
127
  table: {
@@ -81,11 +136,6 @@ export default {
81
136
  category: 'slots',
82
137
  ...noSummary
83
138
  }
84
- },
85
-
86
- option: {
87
- description: 'Replace an option defaults.',
88
- table: noSummary
89
139
  }
90
140
  }
91
141
  }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <q-select v-model="model" v-bind="attributes" v-on="listeners" @filter="filterOptions">
2
+ <q-select v-model="model" v-bind="attributes" v-on="listeners">
3
3
  <slot v-for="(slot, key) in $slots" :slot="key" :name="key" />
4
4
 
5
5
  <template v-for="(slot, key) in $scopedSlots" :slot="key" slot-scope="scope">
@@ -8,7 +8,7 @@
8
8
 
9
9
  <template #append>
10
10
  <slot name="append">
11
- <q-icon v-if="searchable" name="o_search" />
11
+ <q-icon v-if="isSearchable" name="o_search" />
12
12
  </slot>
13
13
  </template>
14
14
 
@@ -16,18 +16,35 @@
16
16
  <slot name="no-option">
17
17
  <q-item>
18
18
  <q-item-section class="text-grey">
19
- {{ noOptionLabel }}
19
+ <template v-if="isLoading">
20
+ Buscando por {{ label }}...
21
+ </template>
22
+ <template v-else>
23
+ {{ noOptionLabel }}
24
+ </template>
20
25
  </q-item-section>
21
26
  </q-item>
22
27
  </slot>
23
28
  </template>
29
+
30
+ <template #after-options>
31
+ <slot v-if="isLoading" name="after-options">
32
+ <div class="flex justify-center q-pb-sm">
33
+ <q-spinner-dots color="primary" size="20px" />
34
+ </div>
35
+ </slot>
36
+ </template>
24
37
  </q-select>
25
38
  </template>
26
39
 
27
40
  <script>
41
+ import lazyLoadingFilterMixin from '../../mixins/lazy-loading-filter'
42
+
28
43
  export default {
29
44
  name: 'QasSelect',
30
45
 
46
+ mixins: [lazyLoadingFilterMixin],
47
+
31
48
  props: {
32
49
  fuseOptions: {
33
50
  default: () => ({}),
@@ -39,11 +56,20 @@ export default {
39
56
  type: String
40
57
  },
41
58
 
59
+ noOptionLabel: {
60
+ default: 'Nenhum resultado foi encontrado.',
61
+ type: String
62
+ },
63
+
42
64
  options: {
43
65
  default: () => [],
44
66
  type: Array
45
67
  },
46
68
 
69
+ searchable: {
70
+ type: Boolean
71
+ },
72
+
47
73
  value: {
48
74
  default: () => [],
49
75
  type: [Array, Object, String, Number]
@@ -52,21 +78,11 @@ export default {
52
78
  valueKey: {
53
79
  default: '',
54
80
  type: String
55
- },
56
-
57
- noOptionLabel: {
58
- default: 'Nenhum resultado foi encontrado.',
59
- type: String
60
- },
61
-
62
- searchable: {
63
- type: Boolean
64
81
  }
65
82
  },
66
83
 
67
84
  data () {
68
85
  return {
69
- filteredOptions: [],
70
86
  fuse: null
71
87
  }
72
88
  },
@@ -82,10 +98,18 @@ export default {
82
98
  }
83
99
  },
84
100
 
101
+ label () {
102
+ return this.$attrs.label || ''
103
+ },
104
+
85
105
  listeners () {
86
106
  const { input, ...events } = this.$listeners
87
107
 
88
- return events
108
+ return {
109
+ ...events,
110
+ ...(this.useLazyLoading && { 'virtual-scroll': this.$_onVirtualScroll }),
111
+ ...((this.useLazyLoading || this.searchable) && { filter: this.onFilter })
112
+ }
89
113
  },
90
114
 
91
115
  defaultFuseOptions () {
@@ -104,7 +128,7 @@ export default {
104
128
  }
105
129
  },
106
130
 
107
- formattedResult () {
131
+ defaultOptions () {
108
132
  if (!this.labelKey && !this.valueKey) {
109
133
  return this.options
110
134
  }
@@ -116,15 +140,27 @@ export default {
116
140
  return this.$attrs.multiple || this.$attrs.multiple === ''
117
141
  },
118
142
 
143
+ isSearchable () {
144
+ return this.searchable || this.useLazyLoading
145
+ },
146
+
147
+ hasError () {
148
+ return this.hasFetchError || this.$attrs.error
149
+ },
150
+
119
151
  attributes () {
120
152
  return {
121
153
  emitValue: true,
122
154
  mapOptions: true,
123
155
  outlined: true,
124
- clearable: this.searchable,
156
+ clearable: this.isSearchable,
157
+ loading: this.isLoading,
158
+ inputDebounce: this.useLazyLoading ? 500 : 0,
159
+ popupContentClass: this.$_virtualScrollClassName,
125
160
  ...this.$attrs,
126
161
  options: this.filteredOptions,
127
- useInput: this.searchable
162
+ useInput: this.isSearchable,
163
+ error: this.hasError
128
164
  }
129
165
  }
130
166
  },
@@ -136,37 +172,44 @@ export default {
136
172
 
137
173
  options: {
138
174
  handler () {
175
+ if (this.useLazyLoading && this.$_hasFilteredOptions) return
176
+
139
177
  if (this.fuse) {
140
- this.fuse.list = this.formattedResult
178
+ this.fuse.list = this.defaultOptions
141
179
  }
142
180
 
143
- this.filteredOptions = this.formattedResult
181
+ this.filteredOptions = this.defaultOptions
144
182
  },
145
183
  immediate: true
146
184
  }
147
185
  },
148
186
 
149
187
  async created () {
150
- if (this.searchable) {
188
+ if (this.searchable && !this.useLazyLoading) {
151
189
  const Fuse = (await import('fuse.js')).default
152
- this.fuse = new Fuse(this.options, this.defaultFuseOptions)
190
+ this.fuse = new Fuse(this.defaultOptions, this.defaultFuseOptions)
153
191
  }
154
192
  },
155
193
 
156
194
  methods: {
157
- filterOptions (value, update) {
195
+ onFilter (value, update) {
158
196
  update(() => {
159
- if (!this.searchable) return
197
+ if (this.useLazyLoading && value !== this.search) return this.$_filterOptionsByStore(value)
160
198
 
161
- if (value === '') {
162
- this.filteredOptions = this.formattedResult
163
- } else {
164
- const results = this.fuse.search(value)
165
- this.filteredOptions = results.map(item => item.item)
166
- }
199
+ if (!this.useLazyLoading && this.searchable) this.filterOptionsByFuse(value)
167
200
  })
168
201
  },
169
202
 
203
+ filterOptionsByFuse (value) {
204
+ if (value === '') {
205
+ this.filteredOptions = this.defaultOptions
206
+ return
207
+ }
208
+
209
+ const results = this.fuse.search(value)
210
+ this.filteredOptions = results.map(({ item }) => item)
211
+ },
212
+
170
213
  renameKey (item) {
171
214
  const mapKeys = { label: this.labelKey, value: this.valueKey }
172
215
 
@@ -1,23 +1,21 @@
1
1
  <template>
2
2
  <qas-search-box v-bind="$attrs" class="q-pa-md" :fuse-options="fuseOptions" :list="sortedOptions">
3
3
  <template #default="{ results }">
4
- <q-list separator>
5
- <q-item v-for="(result, index) in results" :key="index">
6
- <slot name="item" v-bind="self">
7
- <slot name="item-section" :result="result">
8
- <q-item-section class="items-start text-bold">
9
- <div :class="labelClass" @click="redirectRoute(result)">{{ result.label }}</div>
10
- </q-item-section>
11
- </slot>
12
-
13
- <q-item-section avatar>
14
- <slot name="item-action" v-bind="self">
15
- <qas-btn hide-mobile-label v-bind="setButtonProps(result)" size="sm" @click="handleClick(result)" />
16
- </slot>
4
+ <q-item v-for="(item, index) in results" :key="index">
5
+ <slot name="item" v-bind="self">
6
+ <slot name="item-section" :result="item">
7
+ <q-item-section class="items-start text-bold">
8
+ <div :class="labelClass" @click="redirectRoute(item)">{{ item.label }}</div>
17
9
  </q-item-section>
18
10
  </slot>
19
- </q-item>
20
- </q-list>
11
+
12
+ <q-item-section avatar>
13
+ <slot name="item-action" v-bind="self">
14
+ <qas-btn hide-mobile-label v-bind="setButtonProps(item)" size="sm" @click="handleClick(item)" />
15
+ </slot>
16
+ </q-item-section>
17
+ </slot>
18
+ </q-item>
21
19
  </template>
22
20
  </qas-search-box>
23
21
  </template>
@@ -1,6 +1,7 @@
1
1
  import contextMixin from './context.js'
2
2
  import formMixin from './form.js'
3
3
  import generatorMixin from './generator.js'
4
+ import lazyLoadingFilterMixin from './lazy-loading-filter.js'
4
5
  import mapMarkersMixin from './map-markers.js'
5
6
  import passwordMixin from './password.js'
6
7
  import screenMixin from './screen.js'
@@ -12,6 +13,7 @@ export {
12
13
  contextMixin,
13
14
  formMixin,
14
15
  generatorMixin,
16
+ lazyLoadingFilterMixin,
15
17
  mapMarkersMixin,
16
18
  passwordMixin,
17
19
  screenMixin,
@@ -0,0 +1,188 @@
1
+ import { decamelize } from 'humps'
2
+
3
+ export default {
4
+ props: {
5
+ entity: {
6
+ default: '',
7
+ type: String
8
+ },
9
+
10
+ lazyLoadingProps: {
11
+ default: () => ({}),
12
+ type: Object
13
+ },
14
+
15
+ name: {
16
+ default: '',
17
+ type: String
18
+ },
19
+
20
+ useLazyLoading: {
21
+ type: Boolean
22
+ }
23
+ },
24
+
25
+ data () {
26
+ return {
27
+ filteredOptions: [],
28
+ hasFetchError: false,
29
+ isLoading: false,
30
+ isScrolling: false,
31
+ pagination: {
32
+ page: 1,
33
+ lastPage: null
34
+ },
35
+ search: null
36
+ }
37
+ },
38
+
39
+ computed: {
40
+ $_defaultLazyLoadingProps () {
41
+ const {
42
+ url,
43
+ params,
44
+ decamelizeFieldName
45
+ } = this.lazyLoadingProps
46
+
47
+ const defaultParams = {
48
+ limit: 48
49
+ }
50
+
51
+ return {
52
+ url: url || '',
53
+ params: {
54
+ ...defaultParams,
55
+ ...params
56
+ },
57
+ decamelizeFieldName: (decamelizeFieldName ?? true) || decamelizeFieldName
58
+ }
59
+ },
60
+
61
+ $_hasFilteredOptions () {
62
+ return !!this.filteredOptions.length
63
+ },
64
+
65
+ $_virtualScrollClassName () {
66
+ return `virtual-scroll-${this.name}`
67
+ }
68
+ },
69
+
70
+ methods: {
71
+ async $_filterOptionsByStore (search) {
72
+ this.$_resetFilter(search)
73
+ await this.$_setFetchOptions()
74
+ },
75
+
76
+ $_resetFilter (search) {
77
+ this.filteredOptions = []
78
+ this.search = search
79
+ this.pagination = {
80
+ page: 1,
81
+ lastPage: null
82
+ }
83
+ },
84
+
85
+ async $_onVirtualScroll ({ index }) {
86
+ const lastIndex = this.filteredOptions.length - 1
87
+
88
+ if (index === lastIndex && this.$_canFetchOptions()) {
89
+ const { scrollContainer, top } = this.$_getScrollContainerTop()
90
+
91
+ await this.$_loadMoreOptions()
92
+
93
+ setTimeout(() => {
94
+ scrollContainer.scrollTo({ top })
95
+ }, 100)
96
+ }
97
+ },
98
+
99
+ async $_loadMoreOptions () {
100
+ this.isScrolling = true
101
+
102
+ const options = await this.$_fetchOptions()
103
+ this.filteredOptions.push(...options)
104
+
105
+ // this is to prevent the virtual-scroll event to be fired again
106
+ this.$nextTick(() => {
107
+ this.isScrolling = false
108
+ })
109
+ },
110
+
111
+ async $_fetchOptions () {
112
+ try {
113
+ if (!this.entity) throw new Error(this.$_getMissingPropsMessage('entity'))
114
+ if (!this.name) throw new Error(this.$_getMissingPropsMessage('name'))
115
+
116
+ this.hasFetchError = false
117
+ this.isLoading = true
118
+
119
+ const { url, params, decamelizeFieldName } = this.$_defaultLazyLoadingProps
120
+
121
+ const { data } = await this.$store.dispatch(`${this.entity}/fetchFieldOptions`, {
122
+ url,
123
+ field: decamelizeFieldName ? decamelize(this.name, { separator: '-' }) : this.name,
124
+ params: {
125
+ ...params,
126
+ search: this.search,
127
+ offset: (this.pagination.page - 1) * params.limit
128
+ }
129
+ })
130
+
131
+ const { results, count } = data
132
+
133
+ this.pagination = {
134
+ page: this.pagination.page + 1,
135
+ lastPage: Math.ceil(count / params.limit)
136
+ }
137
+
138
+ this.$emit('fetch-options-success', data)
139
+
140
+ return this.$_handleOptions(results)
141
+ } catch (error) {
142
+ /* eslint-disable no-console */
143
+ console.error(error)
144
+
145
+ this.hasFetchError = true
146
+ this.$emit('fetch-options-error', error)
147
+
148
+ return []
149
+ } finally {
150
+ this.isLoading = false
151
+ }
152
+ },
153
+
154
+ async $_setFetchOptions () {
155
+ this.filteredOptions = await this.$_fetchOptions()
156
+ },
157
+
158
+ $_canFetchOptions () {
159
+ const { lastPage, page } = this.pagination
160
+ const hasMorePages = lastPage && page <= lastPage
161
+
162
+ return hasMorePages && !this.isLoading && !this.isScrolling && this.useLazyLoading
163
+ },
164
+
165
+ $_getScrollContainerTop () {
166
+ const scrollContainer = document.querySelector(`.${this.$_virtualScrollClassName}`)
167
+ const scrollContainerHeight = scrollContainer.offsetHeight
168
+ const scrollContainerTop = scrollContainer.scrollTop
169
+
170
+ return {
171
+ scrollContainer,
172
+ top: scrollContainerTop + (scrollContainerHeight / 2)
173
+ }
174
+ },
175
+
176
+ $_handleOptions (options) {
177
+ if (this.labelKey && this.valueKey && this.renameKey) {
178
+ return options.map(item => this.renameKey(item))
179
+ }
180
+
181
+ return options
182
+ },
183
+
184
+ $_getMissingPropsMessage (prop) {
185
+ return `A propriedade "${prop}" é obrigatória quando a propriedade "useLazyLoading" está ativa.`
186
+ }
187
+ }
188
+ }
@@ -1,4 +1,3 @@
1
- import { camelize } from 'humps'
2
1
  import { get } from 'lodash'
3
2
  import { camelizeFieldsName } from '../helpers'
4
3