@rocketsearch/vue-uappi 0.0.0-stage → 0.1.1
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/README.md +29 -2
- package/index.js +189 -0
- package/package.json +23 -4
- package/runtime/banner.js +171 -0
- package/runtime/components/RocketAutocomplete.vue +309 -0
- package/runtime/components/RocketBanner.vue +128 -0
- package/runtime/components/RocketSearch.vue +513 -0
- package/runtime/components/RocketShowcase.vue +198 -0
- package/runtime/components/banner/RocketBannerItem.vue +218 -0
- package/runtime/components/search/RocketSearchFacets.vue +143 -0
- package/runtime/components/search/RocketSearchPagination.vue +62 -0
- package/runtime/components/search/RocketSearchRange.vue +76 -0
- package/runtime/css/autocomplete.css +222 -0
- package/runtime/css/banner.css +138 -0
- package/runtime/css/common.css +250 -0
- package/runtime/css/search.css +719 -0
- package/runtime/css/showcase.css +56 -0
- package/runtime/listing.js +274 -0
- package/runtime/rocket.js +661 -0
- package/runtime/uappi.js +76 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="rocket-search rocket-search--autocomplete"
|
|
4
|
+
:data-button-type-mode="searchButtonType"
|
|
5
|
+
data-search-mode="autocomplete"
|
|
6
|
+
>
|
|
7
|
+
<div class="rocket-search__content">
|
|
8
|
+
<form class="rocket-search__form" @submit.prevent="handleSubmit">
|
|
9
|
+
<input
|
|
10
|
+
ref="inputSearch"
|
|
11
|
+
v-model="term"
|
|
12
|
+
:name="searchPageParam"
|
|
13
|
+
autocomplete="off"
|
|
14
|
+
type="text"
|
|
15
|
+
:placeholder="inputPlaceholder"
|
|
16
|
+
aria-label="Buscar produtos"
|
|
17
|
+
class="rocket-search__form__input"
|
|
18
|
+
@keyup="handleKeyup"
|
|
19
|
+
@focus="openAutocomplete"
|
|
20
|
+
>
|
|
21
|
+
<button type="submit" class="rocket-search__form__button" aria-label="Buscar produtos">
|
|
22
|
+
<span v-if="searchButtonType === 'text'" class="rocket-search__form__button-text" v-text="searchButtonText" />
|
|
23
|
+
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20.35" height="20.35">
|
|
24
|
+
<path
|
|
25
|
+
d="M14.355 15.383a8.727 8.727 0 111.028-1.028l4.759 4.758a.728.728 0 11-1.029 1.029zm0-2.06a7.271 7.271 0 10-1.032 1.027 7.536 7.536 0 001.027-1.027z"
|
|
26
|
+
fill="#29282e"
|
|
27
|
+
fill-rule="evenodd"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
</button>
|
|
31
|
+
</form>
|
|
32
|
+
|
|
33
|
+
<div class="rocket-search__autocomplete-panel" :class="{ opened: suggestionTermsVisible || isOpened }">
|
|
34
|
+
<div v-if="suggestionTerms.length > 0 && term === ''" class="rocket-search__autocomplete-section">
|
|
35
|
+
<strong class="rocket-search__terms-suggestions__title" v-text="suggestionsTitle" />
|
|
36
|
+
<ul class="rocket-search__autocomplete-terms">
|
|
37
|
+
<li v-for="(t, index) in suggestionTerms" :key="'term-' + index">
|
|
38
|
+
<a :href="searchUrl(t)" @click="handleClickTerm(t, $event)">
|
|
39
|
+
<i><span v-text="index + 1" />º</i>
|
|
40
|
+
<span v-text="t" />
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
v-if="didYouMean && term.length >= minChars"
|
|
48
|
+
class="rocket-search__autocomplete-section rocket-search__autocomplete-did-you-mean"
|
|
49
|
+
>
|
|
50
|
+
Você quis dizer <a :href="didYouMeanUrl" @click="handleClickDidYouMean" v-text="didYouMean" />?
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div v-if="autocompleteProducts.length > 0" class="rocket-search__autocomplete-section">
|
|
54
|
+
<strong class="rocket-search__terms-suggestions__title" v-text="productsTitle" />
|
|
55
|
+
<ul class="rocket-search__autocomplete-products">
|
|
56
|
+
<li
|
|
57
|
+
v-for="(product, index) in autocompleteProducts"
|
|
58
|
+
:key="'product-' + product.id"
|
|
59
|
+
:data-product-id="product.id"
|
|
60
|
+
>
|
|
61
|
+
<a :href="productPath(product)" @click="handleClickProduct(product, index, $event)">
|
|
62
|
+
<span class="rocket-search__autocomplete-thumb">
|
|
63
|
+
<img v-if="productImage(product)" :src="productImage(product)" :alt="product.nome" loading="lazy">
|
|
64
|
+
<span v-else class="rocket-search__autocomplete-thumb-placeholder" />
|
|
65
|
+
</span>
|
|
66
|
+
<span class="rocket-search__autocomplete-info">
|
|
67
|
+
<strong
|
|
68
|
+
v-if="product.marca && product.marca.nome"
|
|
69
|
+
class="rocket-search__autocomplete-brand"
|
|
70
|
+
v-text="product.marca.nome"
|
|
71
|
+
/>
|
|
72
|
+
<span class="rocket-search__autocomplete-name" v-text="product.nome" />
|
|
73
|
+
<span
|
|
74
|
+
v-if="productPrice(product) !== null"
|
|
75
|
+
class="rocket-search__autocomplete-price"
|
|
76
|
+
v-text="formatCurrency(productPrice(product))"
|
|
77
|
+
/>
|
|
78
|
+
</span>
|
|
79
|
+
</a>
|
|
80
|
+
</li>
|
|
81
|
+
</ul>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div
|
|
85
|
+
v-if="term.length >= minChars && suggestionTerms.length === 0 && autocompleteProducts.length === 0 && !isLoading"
|
|
86
|
+
class="rocket-search__autocomplete-empty"
|
|
87
|
+
>
|
|
88
|
+
Nenhum resultado encontrado.
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
v-show="suggestionTermsVisible || isOpened"
|
|
94
|
+
class="rocket-search__autocomplete-overlay"
|
|
95
|
+
@mousedown.prevent="closeSearch"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<script>
|
|
102
|
+
// CSS pelo script e não por <style src>: o plugin-vue registra o dono de um src pelo caminho
|
|
103
|
+
// do arquivo, e o mesmo .css em dois SFCs (com índices diferentes) quebra o build.
|
|
104
|
+
import '../css/common.css';
|
|
105
|
+
import '../css/autocomplete.css';
|
|
106
|
+
import { productImage, productPath, productPrice, formatCurrency } from '../uappi.js';
|
|
107
|
+
import { didYouMeanQuery, dispatchDidYouMeanClick } from '../listing.js';
|
|
108
|
+
|
|
109
|
+
// Limite opcional: null = exibe tudo que o backend mandou (quem manda é o maxSuggestions
|
|
110
|
+
// do admin). Só um valor explícito na prop volta a cortar.
|
|
111
|
+
function limitList (list, limit) {
|
|
112
|
+
const arr = list || [];
|
|
113
|
+
return limit === null || limit === undefined || limit === '' ? arr : arr.slice(0, Number(limit));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeTerm (item) {
|
|
117
|
+
if (item && typeof item === 'object') return String(item.term || item.label || item.value || '');
|
|
118
|
+
return String(item || '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default {
|
|
122
|
+
name: 'RocketAutocomplete',
|
|
123
|
+
props: {
|
|
124
|
+
minChars: { type: Number, default: 2 },
|
|
125
|
+
productsLimit: { type: Number, default: null },
|
|
126
|
+
termsLimit: { type: Number, default: null },
|
|
127
|
+
mediaSize: { type: String, default: 'small' },
|
|
128
|
+
inputPlaceholder: { type: String, default: 'O que você procura?' },
|
|
129
|
+
searchButtonType: { type: String, default: 'icon' },
|
|
130
|
+
searchButtonText: { type: String, default: 'Buscar' },
|
|
131
|
+
suggestionsTitle: { type: String, default: 'Termos mais buscados:' },
|
|
132
|
+
productsTitle: { type: String, default: 'Produtos' }
|
|
133
|
+
},
|
|
134
|
+
data () {
|
|
135
|
+
return {
|
|
136
|
+
term: '',
|
|
137
|
+
isOpened: false,
|
|
138
|
+
isLoading: false,
|
|
139
|
+
suggestionTerms: [],
|
|
140
|
+
suggestionTermsVisible: false,
|
|
141
|
+
autocompleteProducts: [],
|
|
142
|
+
// "Você quis dizer?" (flag `didYouMean` da loja): com sugestão, os produtos já são do termo sugerido.
|
|
143
|
+
didYouMean: ''
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
computed: {
|
|
147
|
+
searchPagePath () {
|
|
148
|
+
return this.$rocket.config.searchPagePath;
|
|
149
|
+
},
|
|
150
|
+
searchPageParam () {
|
|
151
|
+
return this.$rocket.config.searchPageParam;
|
|
152
|
+
},
|
|
153
|
+
didYouMeanLocation () {
|
|
154
|
+
const { busca, ...origin } = didYouMeanQuery(this.didYouMean, this.term);
|
|
155
|
+
return { path: this.searchPagePath, query: { [this.searchPageParam]: busca, ...origin } };
|
|
156
|
+
},
|
|
157
|
+
didYouMeanUrl () {
|
|
158
|
+
return this.didYouMean ? this.$router.resolve(this.didYouMeanLocation).href : '';
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
watch: {
|
|
162
|
+
term () {
|
|
163
|
+
if (!this.term) {
|
|
164
|
+
this.didYouMean = '';
|
|
165
|
+
this.autocompleteProducts = [];
|
|
166
|
+
this.isOpened = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
mounted () {
|
|
171
|
+
this.onDocumentClick = (event) => {
|
|
172
|
+
if (!this.$el.contains(event.target)) {
|
|
173
|
+
this.suggestionTermsVisible = false;
|
|
174
|
+
this.isOpened = false;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
this.onKeyup = (event) => {
|
|
178
|
+
if (event.code === 'Escape') this.closeSearch();
|
|
179
|
+
};
|
|
180
|
+
document.addEventListener('click', this.onDocumentClick);
|
|
181
|
+
document.addEventListener('keyup', this.onKeyup);
|
|
182
|
+
},
|
|
183
|
+
beforeUnmount () {
|
|
184
|
+
clearTimeout(this.timeout);
|
|
185
|
+
document.removeEventListener('click', this.onDocumentClick);
|
|
186
|
+
document.removeEventListener('keyup', this.onKeyup);
|
|
187
|
+
},
|
|
188
|
+
methods: {
|
|
189
|
+
productImage (product) {
|
|
190
|
+
return productImage(product, this.mediaSize);
|
|
191
|
+
},
|
|
192
|
+
productPath,
|
|
193
|
+
productPrice,
|
|
194
|
+
formatCurrency,
|
|
195
|
+
searchUrl (term) {
|
|
196
|
+
return this.$router.resolve({ path: this.searchPagePath, query: { [this.searchPageParam]: term || '' } }).href;
|
|
197
|
+
},
|
|
198
|
+
goToSearch (term) {
|
|
199
|
+
this.closeSearch();
|
|
200
|
+
this.$router.push({ path: this.searchPagePath, query: { [this.searchPageParam]: term || '' } });
|
|
201
|
+
},
|
|
202
|
+
openAutocomplete () {
|
|
203
|
+
if (this.term && this.term.length >= this.minChars) {
|
|
204
|
+
this.fetchResult();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.fetchTerms();
|
|
208
|
+
},
|
|
209
|
+
handleKeyup (event) {
|
|
210
|
+
// Enter é tratado pelo submit do form.
|
|
211
|
+
if (event && event.key === 'Enter') return;
|
|
212
|
+
clearTimeout(this.timeout);
|
|
213
|
+
if (this.term.length < this.minChars) {
|
|
214
|
+
this.isOpened = false;
|
|
215
|
+
this.autocompleteProducts = [];
|
|
216
|
+
this.fetchTerms();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.timeout = setTimeout(() => this.fetchResult(), 250);
|
|
220
|
+
},
|
|
221
|
+
// Submit (botão ou Enter) é submissão explícita: mesmo com o campo vazio vai para a
|
|
222
|
+
// página de busca, que lista o catálogo. O dropdown abre pelo foco.
|
|
223
|
+
handleSubmit () {
|
|
224
|
+
this.goToSearch(this.term);
|
|
225
|
+
},
|
|
226
|
+
closeSearch () {
|
|
227
|
+
this.isOpened = false;
|
|
228
|
+
this.suggestionTermsVisible = false;
|
|
229
|
+
if (this.$refs.inputSearch) this.$refs.inputSearch.blur();
|
|
230
|
+
},
|
|
231
|
+
async fetchTerms () {
|
|
232
|
+
try {
|
|
233
|
+
const response = await this.$rocket.autocomplete('');
|
|
234
|
+
this.suggestionTerms = limitList((response.terms || []).map(normalizeTerm), this.termsLimit);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
this.suggestionTerms = [];
|
|
238
|
+
}
|
|
239
|
+
this.autocompleteProducts = [];
|
|
240
|
+
this.suggestionTermsVisible = this.suggestionTerms.length > 0;
|
|
241
|
+
this.isOpened = false;
|
|
242
|
+
this.isLoading = false;
|
|
243
|
+
},
|
|
244
|
+
async fetchResult () {
|
|
245
|
+
if (!this.term || this.term.length < this.minChars) {
|
|
246
|
+
this.autocompleteProducts = [];
|
|
247
|
+
this.isOpened = false;
|
|
248
|
+
this.suggestionTermsVisible = this.suggestionTerms.length > 0;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.isLoading = true;
|
|
253
|
+
const searchedTerm = this.term;
|
|
254
|
+
try {
|
|
255
|
+
const response = await this.$rocket.autocomplete(searchedTerm);
|
|
256
|
+
// Digitou de novo enquanto a resposta vinha: descarta a resposta velha.
|
|
257
|
+
if (searchedTerm !== this.term) return;
|
|
258
|
+
this.suggestionTerms = limitList((response.terms || []).map(normalizeTerm), this.termsLimit);
|
|
259
|
+
this.didYouMean = (response.didYouMean && response.didYouMean.query) || '';
|
|
260
|
+
const ids = limitList(response.products || [], this.productsLimit).map(p => p && p.id);
|
|
261
|
+
const products = await this.$rocket.fetchProducts(ids);
|
|
262
|
+
if (searchedTerm !== this.term) return;
|
|
263
|
+
this.autocompleteProducts = products;
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
if (searchedTerm !== this.term) return;
|
|
267
|
+
this.suggestionTerms = [];
|
|
268
|
+
this.autocompleteProducts = [];
|
|
269
|
+
this.didYouMean = '';
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
if (searchedTerm === this.term) this.isLoading = false;
|
|
273
|
+
}
|
|
274
|
+
this.suggestionTermsVisible = false;
|
|
275
|
+
this.isOpened = true;
|
|
276
|
+
},
|
|
277
|
+
handleClickTerm (term, event) {
|
|
278
|
+
// Ctrl/cmd/meio: o browser abre pelo href em nova aba. Não mexe no painel.
|
|
279
|
+
if (this.$rocket.isModifiedClick(event)) return;
|
|
280
|
+
if (event && event.preventDefault) event.preventDefault();
|
|
281
|
+
this.term = term;
|
|
282
|
+
this.goToSearch(term);
|
|
283
|
+
},
|
|
284
|
+
handleClickDidYouMean (event) {
|
|
285
|
+
dispatchDidYouMeanClick(this.$rocket, { from: this.term, suggestion: this.didYouMean, source: 'autocomplete' });
|
|
286
|
+
if (this.$rocket.isModifiedClick(event)) return;
|
|
287
|
+
if (event && event.preventDefault) event.preventDefault();
|
|
288
|
+
const location = this.didYouMeanLocation;
|
|
289
|
+
this.term = this.didYouMean;
|
|
290
|
+
this.closeSearch();
|
|
291
|
+
this.$router.push(location);
|
|
292
|
+
},
|
|
293
|
+
handleClickProduct (product, index, event) {
|
|
294
|
+
const href = productPath(product);
|
|
295
|
+
const send = () => this.$rocket.dispatchEvent('click', {
|
|
296
|
+
productId: String(product.id),
|
|
297
|
+
position: index,
|
|
298
|
+
meta: { source: 'autocomplete', query: this.term || '' }
|
|
299
|
+
});
|
|
300
|
+
const el = event && event.currentTarget;
|
|
301
|
+
const took = this.$rocket.trackThenNavigate(event, el, href, send, (path) => {
|
|
302
|
+
this.closeSearch();
|
|
303
|
+
this.$router.push(path);
|
|
304
|
+
});
|
|
305
|
+
if (!took && !this.$rocket.isModifiedClick(event, el)) this.closeSearch();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="['rocket-search', 'rocket-search__banner-slot', { 'rocket-search--empty': !waiting && !banners.length }]"
|
|
4
|
+
:data-rocket-banner-position="position"
|
|
5
|
+
>
|
|
6
|
+
<!-- Skeleton só com proporção declarada: sem saber a altura, ele é que causaria o pulo. -->
|
|
7
|
+
<div v-if="waiting && ratio" class="rocket-search__banner-skeleton" :style="{ aspectRatio: ratioCss }" aria-hidden="true" />
|
|
8
|
+
|
|
9
|
+
<div v-else-if="carousel && banners.length > 1" class="rocket-search__banner-swiper">
|
|
10
|
+
<ul ref="track" class="rocket-search__banner-swiper__track">
|
|
11
|
+
<li v-for="(banner, index) in banners" :key="banner.id" class="rocket-search__banner-swiper__slide">
|
|
12
|
+
<RocketBannerItem v-bind="itemProps(index)" :item="banner" />
|
|
13
|
+
</li>
|
|
14
|
+
</ul>
|
|
15
|
+
<button type="button" class="rocket-search__swiper-button-prev" aria-label="Anterior" @click="slide(-1)" />
|
|
16
|
+
<button type="button" class="rocket-search__swiper-button-next" aria-label="Próximo" @click="slide(1)" />
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<template v-else>
|
|
20
|
+
<RocketBannerItem v-for="(banner, index) in banners" :key="banner.id" v-bind="itemProps(index)" :item="banner" />
|
|
21
|
+
</template>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
// CSS pelo script e não por <style src>: o plugin-vue registra o dono de um src pelo caminho
|
|
27
|
+
// do arquivo, e o mesmo .css em dois SFCs (com índices diferentes) quebra o build.
|
|
28
|
+
import '../css/common.css';
|
|
29
|
+
import '../css/banner.css';
|
|
30
|
+
import RocketBannerItem from './banner/RocketBannerItem.vue';
|
|
31
|
+
import { exactWidth, isHeroSlot } from '../banner.js';
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
name: 'RocketBanner',
|
|
35
|
+
components: { RocketBannerItem },
|
|
36
|
+
props: {
|
|
37
|
+
// placement.position cadastrada no painel (hero, top, middle...).
|
|
38
|
+
position: { type: String, required: true },
|
|
39
|
+
// eager: busca assim que a página tem contexto (hero/LCP). ondemand: só quando o slot
|
|
40
|
+
// chega perto da viewport.
|
|
41
|
+
loading: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: 'ondemand',
|
|
44
|
+
validator: v => ['eager', 'ondemand'].includes(v)
|
|
45
|
+
},
|
|
46
|
+
// Força o pageType; sem ele vem da página atual (currentPageData do tema).
|
|
47
|
+
pageType: { type: String, default: '' },
|
|
48
|
+
limit: { type: Number, default: 0 },
|
|
49
|
+
// Proporção do slot ("16/9", "3:1") — reserva a caixa e liga o skeleton.
|
|
50
|
+
ratio: { type: String, default: '' },
|
|
51
|
+
// Mais de um banner na posição: carrossel (senão empilham na ordem do BE).
|
|
52
|
+
carousel: { type: Boolean, default: false },
|
|
53
|
+
sizes: { type: String, default: '100vw' },
|
|
54
|
+
rootMargin: { type: String, default: '200px' },
|
|
55
|
+
imageResizing: { type: Boolean, default: true }
|
|
56
|
+
},
|
|
57
|
+
data () {
|
|
58
|
+
return {
|
|
59
|
+
banners: [],
|
|
60
|
+
requested: false,
|
|
61
|
+
waiting: true,
|
|
62
|
+
width: 0
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
computed: {
|
|
66
|
+
ratioCss () {
|
|
67
|
+
return String(this.ratio).trim().replace(':', ' / ');
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
watch: {
|
|
71
|
+
// O contexto da página chega pelo currentPageData do tema (o plugin traduz para $rocket.setPage).
|
|
72
|
+
'$rocket.currentPage.value' () {
|
|
73
|
+
if (this.loading === 'eager' || this.visible) this.tryLoad();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
mounted () {
|
|
77
|
+
if (this.loading === 'eager') return this.tryLoad();
|
|
78
|
+
if (!('IntersectionObserver' in window)) {
|
|
79
|
+
this.visible = true;
|
|
80
|
+
return this.tryLoad();
|
|
81
|
+
}
|
|
82
|
+
this.observer = new IntersectionObserver((entries) => {
|
|
83
|
+
if (!entries.some(e => e.isIntersecting)) return;
|
|
84
|
+
this.observer.disconnect();
|
|
85
|
+
this.visible = true;
|
|
86
|
+
this.tryLoad();
|
|
87
|
+
}, { rootMargin: this.rootMargin });
|
|
88
|
+
this.observer.observe(this.$el);
|
|
89
|
+
},
|
|
90
|
+
beforeUnmount () {
|
|
91
|
+
if (this.observer) this.observer.disconnect();
|
|
92
|
+
},
|
|
93
|
+
methods: {
|
|
94
|
+
async tryLoad () {
|
|
95
|
+
if (this.requested) return;
|
|
96
|
+
if (!this.pageType && !this.$rocket.getPage().section) return;
|
|
97
|
+
this.requested = true;
|
|
98
|
+
// Largura exata do slot; sem medida a imagem sai com srcset.
|
|
99
|
+
if (this.$el && this.$el.clientWidth) {
|
|
100
|
+
this.width = exactWidth(this.$el.clientWidth, window.devicePixelRatio);
|
|
101
|
+
}
|
|
102
|
+
const list = await this.$rocket.getBannersByContext(this.pageType || undefined, { position: this.position });
|
|
103
|
+
const mine = list.filter(b => ((b.placement && b.placement.position) || b.position) === this.position);
|
|
104
|
+
this.banners = this.limit > 0 ? mine.slice(0, this.limit) : mine;
|
|
105
|
+
this.waiting = false;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
itemProps (index) {
|
|
109
|
+
const hero = isHeroSlot(this.position) && index === 0;
|
|
110
|
+
return {
|
|
111
|
+
position: this.position,
|
|
112
|
+
slideIndex: index,
|
|
113
|
+
loading: hero ? 'eager' : 'lazy',
|
|
114
|
+
fetchPriority: hero ? 'high' : '',
|
|
115
|
+
sizes: this.sizes,
|
|
116
|
+
width: this.width,
|
|
117
|
+
imageResizing: this.imageResizing,
|
|
118
|
+
aspectRatio: this.ratio ? this.ratioCss : ''
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
slide (direction) {
|
|
123
|
+
const track = this.$refs.track;
|
|
124
|
+
if (track) track.scrollBy({ left: direction * track.clientWidth, behavior: 'smooth' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
</script>
|