@ecomplus/widget-search-engine 0.3.7 → 1.0.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +862 -7
  2. package/README.md +2 -1
  3. package/cms.config.js +39 -0
  4. package/dist/public/widget-search-engine.1.min.js +2 -0
  5. package/dist/public/widget-search-engine.1.min.js.map +1 -0
  6. package/dist/public/widget-search-engine.2.min.js +2 -0
  7. package/dist/public/widget-search-engine.2.min.js.map +1 -0
  8. package/dist/public/widget-search-engine.3.min.js +5 -0
  9. package/dist/public/widget-search-engine.3.min.js.map +1 -0
  10. package/dist/public/widget-search-engine.4.min.js +5 -0
  11. package/dist/public/widget-search-engine.4.min.js.map +1 -0
  12. package/dist/public/widget-search-engine.5.min.js +2 -0
  13. package/dist/public/widget-search-engine.5.min.js.map +1 -0
  14. package/dist/public/widget-search-engine.var.min.js +1 -1
  15. package/dist/public/widget-search-engine.var.min.js.map +1 -1
  16. package/dist/widget-search-engine.1.min.js +2 -0
  17. package/dist/widget-search-engine.1.min.js.map +1 -0
  18. package/dist/widget-search-engine.2.min.js +2 -0
  19. package/dist/widget-search-engine.2.min.js.map +1 -0
  20. package/dist/widget-search-engine.3.min.js +5 -0
  21. package/dist/widget-search-engine.3.min.js.map +1 -0
  22. package/dist/widget-search-engine.4.min.js +5 -0
  23. package/dist/widget-search-engine.4.min.js.map +1 -0
  24. package/dist/widget-search-engine.5.min.js +2 -0
  25. package/dist/widget-search-engine.5.min.js.map +1 -0
  26. package/dist/widget-search-engine.min.js +1 -1
  27. package/dist/widget-search-engine.min.js.map +1 -1
  28. package/package.json +7 -6
  29. package/src/index.js +180 -30
  30. package/webpack.config.js +1 -0
  31. package/__tests__/index.js +0 -6
  32. package/src/components/EcSearchEngine.vue +0 -3
  33. package/src/components/html/EcSearchEngine.html +0 -272
  34. package/src/components/js/EcSearchEngine.js +0 -290
  35. package/src/components/scss/EcSearchEngine.scss +0 -132
  36. package/src/lib/dictionary.js +0 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomplus/widget-search-engine",
3
- "version": "0.3.7",
3
+ "version": "1.0.0-beta.100",
4
4
  "description": "Storefront plugin & Vue component for dynamic search page",
5
5
  "module": "src/index.js",
6
6
  "browser": "dist/widget-search-engine.min.js",
@@ -8,12 +8,13 @@
8
8
  "jsdelivr": "dist/public/widget-search-engine.var.min.js",
9
9
  "unpkg": "dist/public/widget-search-engine.var.min.js",
10
10
  "scripts": {
11
- "build": "NODE_ENV=production webpack",
11
+ "build": "cross-env NODE_ENV=production webpack",
12
12
  "serve": "webpack-dev-server"
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "git+https://github.com/ecomplus/storefront.git"
16
+ "url": "git+https://github.com/ecomplus/storefront.git",
17
+ "directory": "@ecomplus/widget-search-engine"
17
18
  },
18
19
  "keywords": [
19
20
  "ecomplus",
@@ -28,13 +29,13 @@
28
29
  },
29
30
  "homepage": "https://github.com/ecomplus/storefront/tree/master/@ecomplus/widget-search-engine#readme",
30
31
  "peerDependencies": {
31
- "@ecomplus/search-engine": ">=1 <3",
32
- "@ecomplus/utils": "^1.3.4",
32
+ "@ecomplus/utils": "^1.4.1",
33
33
  "core-js": "3.x",
34
34
  "vue": ">=2 <4"
35
35
  },
36
36
  "dependencies": {
37
- "@ecomplus/storefront-twbs": "^4.0.8"
37
+ "@ecomplus/storefront-components": "^1.0.0-beta.101",
38
+ "@ecomplus/storefront-twbs": "^5.11.1"
38
39
  },
39
40
  "webpackOutput": {
40
41
  "library": "widgetSearchEngine",
package/src/index.js CHANGED
@@ -4,56 +4,206 @@
4
4
  * Released under the MIT License.
5
5
  */
6
6
 
7
+ import { $ } from '@ecomplus/storefront-twbs'
7
8
  import Vue from 'vue'
8
- import '@ecomplus/storefront-twbs'
9
- import EcSearchEngine from './components/EcSearchEngine.vue'
9
+ import SearchEngine from '#components/SearchEngine.vue'
10
10
 
11
- export default (options = {}, elId = 'search-engine') => {
11
+ export default (options = {}, elId = 'search-engine', paginationElId = 'search-pagination') => {
12
12
  const $searchEngine = document.getElementById(elId)
13
-
14
13
  if ($searchEngine) {
15
- const { $overlay } = window.storefront
14
+ const $dock = document.getElementById(`${elId}-dock`)
15
+ let $productItems
16
+ const getScopedSlots = window.storefront && window.storefront.getScopedSlots
17
+ const { dataset } = $searchEngine
18
+
16
19
  const urlParams = new URLSearchParams(window.location.search)
20
+ const props = {
21
+ ...options.props,
22
+ term: urlParams.get('term'),
23
+ brands: urlParams.getAll('brands[]'),
24
+ categories: urlParams.getAll('categories[]'),
25
+ defaultFilters: urlParams.getAll('filters[]').reduce((filters, gridAndOption) => {
26
+ const [gridId, option] = gridAndOption.split(':')
27
+ if (!filters[gridId]) {
28
+ filters[gridId] = []
29
+ }
30
+ filters[gridId].push(option)
31
+ return filters
32
+ }, {}),
33
+ defaultSort: dataset.sort || urlParams.get('sort')
34
+ }
35
+
36
+ ;['brands', 'categories'].forEach(resource => {
37
+ if (dataset[resource]) {
38
+ try {
39
+ props[resource] = JSON.parse(dataset[resource])
40
+ } catch (err) {
41
+ console.error(err)
42
+ return
43
+ }
44
+ if (props[resource] && props[resource].length < 2) {
45
+ props[`isFixed${resource.charAt(0).toUpperCase()}${resource.slice(1)}`] = true
46
+ }
47
+ props.hasPopularItems = false
48
+ }
49
+ })
50
+
51
+ const { resource } = window.document.body.dataset
52
+ switch (resource) {
53
+ case 'brands':
54
+ case 'categories':
55
+ if (!props[resource] || !props[resource].length) {
56
+ console.error(new Error(`Skipping SearchEngine with empty '${resource}' filter`))
57
+ return
58
+ }
59
+ }
17
60
 
18
- new Vue({
61
+ const pageTitle = document.title
62
+ const updatePageUrl = () => {
63
+ const term = urlParams.get('term')
64
+ let title = term ? `${term} ~ ${pageTitle}` : pageTitle
65
+ const page = urlParams.get('page')
66
+ if (page > 1) {
67
+ title += ` (${page}) `
68
+ }
69
+ if (window.history) {
70
+ const query = urlParams.toString()
71
+ const { pathname } = window.location
72
+ window.history.pushState({
73
+ pathname,
74
+ query
75
+ }, title, pathname + (query ? `?${query}` : ''))
76
+ }
77
+ document.title = title
78
+ }
79
+ updatePageUrl()
80
+
81
+ const vueApp = new Vue({
19
82
  data: {
20
- showFilters: false
83
+ countRequests: 0,
84
+ canShowItems: !$dock,
85
+ term: props.term,
86
+ page: parseInt(urlParams.get('page'), 10) || 1,
87
+ totalItems: 0
21
88
  },
22
89
 
23
90
  render (createElement) {
24
91
  const vm = this
25
- return createElement(EcSearchEngine, {
92
+ if (options.pagination) {
93
+ import('#components/APagination.vue')
94
+ .then(pagination => {
95
+ new Vue({
96
+ render: h => h(pagination.default, {
97
+ props: {
98
+ totalItems: vm.totalItems,
99
+ page: vm.page
100
+ },
101
+ on: {
102
+ 'update:page' (page) {
103
+ vm.page = page
104
+ urlParams.set('page', page)
105
+ updatePageUrl()
106
+ window.scroll({
107
+ top: 0,
108
+ behavior: 'smooth'
109
+ })
110
+ }
111
+ }
112
+ })
113
+ }).$mount(document.getElementById(paginationElId))
114
+ })
115
+ }
116
+
117
+ return createElement(SearchEngine, {
26
118
  attrs: {
27
- id: elId
119
+ id: $dock ? null : elId
28
120
  },
29
121
  props: {
30
- ...options.props,
31
- term: urlParams.get('term'),
32
- page: parseInt(urlParams.get('page'), 10),
33
- brands: urlParams.getAll('brands'),
34
- categories: urlParams.getAll('categories'),
35
- navbarId: 'header',
36
- showFilters: vm.showFilters,
37
- prerenderedHTML: $searchEngine.outerHTML
122
+ ...props,
123
+ term: vm.term,
124
+ page: vm.page,
125
+ canLoadMore: !options.pagination && !dataset.disableLoadMore,
126
+ canShowItems: vm.canShowItems,
127
+ loadMoreSelector: $dock ? '#search-engine-load' : null,
128
+ isFilterable: !dataset.disableFilters
38
129
  },
39
130
 
40
131
  on: {
41
- 'update:showFilters' (canShow) {
42
- vm.showFilters = canShow
43
- if ($overlay) {
44
- if (canShow) {
45
- $overlay.show()
46
- $overlay.once('hide', () => {
47
- vm.showFilters = false
48
- })
49
- } else {
50
- $overlay.hide()
132
+ 'update:term' (term) {
133
+ vm.term = term
134
+ urlParams.set('term', term)
135
+ updatePageUrl()
136
+ },
137
+
138
+ fetch ({ ecomSearch, fetching, isPopularItems }) {
139
+ fetching.then(result => {
140
+ if (!isPopularItems) {
141
+ vm.totalItems = ecomSearch.getTotalCount()
142
+ }
143
+ if ($dock) {
144
+ vm.countRequests++
145
+ const renderNewItems = () => {
146
+ vm.canShowItems = true
147
+ $('#search-engine-snap').remove()
148
+ }
149
+ if (!vm.canShowItems) {
150
+ if (vm.countRequests > 1) {
151
+ renderNewItems()
152
+ } else if (result && result.hits) {
153
+ if (!$productItems || $productItems.length !== result.hits.hits.length) {
154
+ renderNewItems()
155
+ } else {
156
+ let isSameItems = true
157
+ const { hits } = result.hits
158
+ for (let i = 0; i < hits.length; i++) {
159
+ if (!$productItems.find(`[data-product-id="${hits[i]._id}"]`).length) {
160
+ isSameItems = false
161
+ break
162
+ }
163
+ }
164
+ if (!isSameItems) {
165
+ renderNewItems()
166
+ }
167
+ }
168
+ }
169
+ }
51
170
  }
52
- }
171
+ })
53
172
  }
54
- }
173
+ },
174
+
175
+ scopedSlots: typeof getScopedSlots === 'function'
176
+ ? getScopedSlots($searchEngine, createElement, !$dock)
177
+ : undefined
178
+ })
179
+ }
180
+ })
181
+
182
+ if ($dock) {
183
+ $($searchEngine).append($('<div>', {
184
+ id: 'search-engine-load'
185
+ }))
186
+
187
+ const mount = () => vueApp.$mount($dock)
188
+ $productItems = $('#search-engine-snap .product-item')
189
+ if ($productItems.length) {
190
+ const observer = new window.MutationObserver(() => {
191
+ clearTimeout(fallbackTimer)
192
+ observer.disconnect()
193
+ setTimeout(mount, 150)
194
+ })
195
+ observer.observe($productItems[0], {
196
+ childList: true
55
197
  })
198
+ const fallbackTimer = setTimeout(() => {
199
+ observer.disconnect()
200
+ mount()
201
+ }, 3000)
202
+ } else {
203
+ mount()
56
204
  }
57
- }).$mount($searchEngine)
205
+ } else {
206
+ vueApp.$mount($searchEngine)
207
+ }
58
208
  }
59
209
  }
@@ -0,0 +1 @@
1
+ module.exports = require('../../webpack.config')
@@ -1,6 +0,0 @@
1
- import widget from './../src/'
2
-
3
- window._widgets = false
4
- widget()
5
-
6
- export default widget
@@ -1,3 +0,0 @@
1
- <script src="./js/EcSearchEngine.js"></script>
2
- <template lang="html" src="./html/EcSearchEngine.html"></template>
3
- <style lang="scss" src="./scss/EcSearchEngine.scss"></style>
@@ -1,272 +0,0 @@
1
- <section class="ec-search-engine">
2
- <nav ref="nav">
3
- <portal-target name="search-nav"/>
4
- </nav>
5
-
6
- <portal to="search-nav">
7
- <transition enter-active-class="animated fadeInDown fast">
8
- <div
9
- class="ec-search-engine__nav"
10
- v-if="searched && (searching || totalSearchResults > 8 || hasSelectedOptions)"
11
- >
12
- <slot name="nav" v-bind="{ totalSearchResults, toggleFilters }">
13
- <div class="container">
14
- <div class="row">
15
- <div class="col-auto">
16
- <div class="ec-search-engine__count">
17
- <strong>{{ totalSearchResults }}</strong>
18
- {{ dictionary('items') }}
19
- <div
20
- class="spinner-grow ec-search-engine__spinner"
21
- role="status"
22
- v-if="searching"
23
- >
24
- <span class="sr-only">Loading...</span>
25
- </div>
26
- </div>
27
- </div>
28
-
29
- <div class="text-right col">
30
- <button
31
- class="btn ec-search-engine__toggle"
32
- @click="toggleFilters(true)"
33
- type="button"
34
- v-if="hasSelectedOptions || filters.find(({ filterObj }) => filterObj.options.length)"
35
- >
36
- <i class="fas fa-filter"></i>
37
- {{ dictionary('filter') }}
38
- <span class="d-none d-md-inline">
39
- {{ dictionary('results') }}
40
- </span>
41
- </button>
42
-
43
- <b-dropdown
44
- variant="link"
45
- toggle-class="ec-search-engine__toggle"
46
- right
47
- no-caret
48
- >
49
- <template #button-content>
50
- <i class="fas fa-sort"></i>
51
- <span class="d-none d-md-inline">
52
- {{ dictionary('sort') }}
53
- </span>
54
- </template>
55
- <b-dropdown-item
56
- href="#"
57
- v-for="(sortOption, index) in sortOptions"
58
- :key="`sort-${index}`"
59
- @click.prevent="setSortOrder(sortOption)"
60
- :active="selectedSortOption === sortOption"
61
- >
62
- {{ dictionary(sortOption || 'relevance') }}
63
- </b-dropdown-item>
64
- </b-dropdown>
65
- </div>
66
- </div>
67
- </div>
68
- </slot>
69
- </div>
70
- </transition>
71
- </portal>
72
-
73
- <transition
74
- enter-active-class="animated slideInRight"
75
- leave-active-class="animated slideOutRight"
76
- >
77
- <aside
78
- class="ec-search-engine__sidebar card shadow"
79
- v-show="showFilters"
80
- >
81
- <slot name="filters">
82
- <header class="card-header">
83
- {{ dictionary('refine_search') }}
84
- <button
85
- type="button"
86
- class="close"
87
- :aria-label="dictionary('close_filters')"
88
- @click="toggleFilters(false)"
89
- >
90
- <span aria-hidden="true">&times;</span>
91
- </button>
92
- </header>
93
-
94
- <div class="card-body">
95
- <div
96
- class="ec-search-engine__filter"
97
- v-for="({ filter, filterObj, isSpec }) in filters"
98
- v-if="filterObj.options.length"
99
- :class="`ec-search-engine__filter--${filter}`"
100
- :key="`filters-${filter}`"
101
- >
102
- <button
103
- class="btn ec-search-engine__filter__btn"
104
- type="button"
105
- :aria-expanded="filterObj.show ? 'true' : 'false'"
106
- :aria-controls="`collapse-${filter}`"
107
- @click="filterObj.show = !filterObj.show"
108
- >
109
- <i class="fas fa-chevron-down"></i>
110
- {{ filterLabel(filter) }}
111
- </button>
112
-
113
- <b-collapse :id="`collapse-${filter}`" v-model="filterObj.show">
114
- <div
115
- class="custom-control custom-checkbox ec-search-engine__option"
116
- v-for="(opt, index) in filterObj.options"
117
- :key="`${filter}-${index}`"
118
- >
119
- <input
120
- type="checkbox"
121
- class="custom-control-input"
122
- :id="`${filter}-${index}`"
123
- @change="ev => setFilterOption(filter, opt.key, ev.target.checked)"
124
- :checked="selectedOptions[filter].indexOf(opt.key) > -1"
125
- >
126
- <label class="custom-control-label" :for="`${filter}-${index}`">
127
- {{ opt.key }}
128
- <small v-if="!isSpec">
129
- ({{ opt.doc_count }})
130
- </small>
131
- </label>
132
- </div>
133
- </b-collapse>
134
- </div>
135
- </div>
136
-
137
- <footer class="card-footer">
138
- <button
139
- class="btn btn-sm btn-block btn-outline-secondary"
140
- type="button"
141
- @click="clearFilters"
142
- >
143
- <span class="mr-1">
144
- <i class="fas fa-trash-alt"></i>
145
- </span>
146
- {{ dictionary('clear_filters') }}
147
- </button>
148
- </footer>
149
- </slot>
150
- </aside>
151
- </transition>
152
-
153
- <transition enter-active-class="animated fadeIn slower">
154
- <div
155
- class="ec-search-engine__results"
156
- :style="{ opacity: searching && !loadingMore ? 0.4 : 1 }"
157
- v-if="searched"
158
- >
159
- <div class="ec-search-engine__info">
160
- <template v-if="term">
161
- <div class="ec-search-engine__no-results" v-if="emptyResult">
162
- <div class="lead">
163
- {{ dictionary('no_results_for') }}
164
- <em>{{ term }}</em>
165
- </div>
166
- <h1>{{ dictionary('popular_products') }}</h1>
167
- </div>
168
-
169
- <div class="ec-search-engine__terms" v-else>
170
- <h1>
171
- <small class="d-none d-md-block">
172
- {{ dictionary('searching_for') }}:
173
- </small>
174
- {{ fixedTerm || term }}
175
- </h1>
176
- <em v-if="fixedTerm" class="d-none d-lg-block">
177
- {{ dictionary('no_results_for') }}
178
- <s>{{ term }}</s>
179
- </em>
180
- </div>
181
- </template>
182
-
183
- <transition
184
- enter-active-class="animated fadeInDown"
185
- leave-active-class="animated position-absolute fadeOutUp"
186
- >
187
- <div v-if="hasSelectedOptions">
188
- <button
189
- class="btn btn-sm btn-outline-secondary"
190
- type="button"
191
- @click="clearFilters"
192
- >
193
- <span class="mr-1">
194
- <i class="fas fa-trash-alt"></i>
195
- </span>
196
- {{ dictionary('clear_filters') }}
197
- </button>
198
-
199
- <template v-for="(options, filter) in selectedOptions">
200
- <button
201
- class="btn m-1 btn-sm btn-light"
202
- type="button"
203
- v-for="option in options"
204
- @click="setFilterOption(filter, option, false)"
205
- >
206
- <span class="mr-1">
207
- <i class="fas fa-times"></i>
208
- </span>
209
- {{ option }}
210
- <small>\ {{ filterLabel(filter) }}</small>
211
- </button>
212
- </template>
213
- </div>
214
- </transition>
215
- </div>
216
-
217
- <article class="ec-search-engine__retail">
218
- <div class="row">
219
- <div
220
- class="col-6 col-md-4 col-lg-3"
221
- v-for="product in resultItems"
222
- :key="product._id"
223
- >
224
- <slot name="product" v-bind="{ product }">
225
- <ec-product-card
226
- class="ec-search-engine__item"
227
- :lang="lang"
228
- :storeId="storeId"
229
- :product="product"
230
- />
231
- </slot>
232
- </div>
233
- </div>
234
- </article>
235
-
236
- <transition
237
- enter-active-class="animated fadeInDown"
238
- leave-active-class="animated position-absolute fadeOutUp"
239
- >
240
- <div class="alert alert-warning" role="alert" v-if="networkError">
241
- <h4 class="alert-heading">Offline</h4>
242
- <template v-if="lang === 'pt_br'">
243
- Não foi possível buscar os produtos, por favor verifique sua
244
- conexão com a internet.
245
- </template>
246
- <template v-else>
247
- Unable to fetch the products, please check your internet connection.
248
- </template>
249
-
250
- <hr>
251
- <button
252
- type="button"
253
- class="btn btn-primary"
254
- @click="fetchItems(currentPage)"
255
- >
256
- <i class="fas fa-search mr-1"></i>
257
- {{ dictionary('search_again') }}
258
- </button>
259
- </div>
260
- </transition>
261
- </div>
262
- </transition>
263
-
264
- <transition
265
- enter-active-class="animated fadeInDown"
266
- leave-active-class="animated fadeOutUp"
267
- >
268
- <slot v-if="!searched || loadingMore">
269
- <div v-html="prerenderedHTML"></div>
270
- </slot>
271
- </transition>
272
- </section>