search-results.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <template lang="pug">
  2. .search-results(v-if='searchIsFocused || (search && search.length > 1)')
  3. .search-results-container
  4. .search-results-help(v-if='!search || (search && search.length < 2)')
  5. img(src='/_assets/svg/icon-search-alt.svg')
  6. .mt-4 {{$t('common:header.searchHint')}}
  7. .search-results-loader(v-else-if='searchIsLoading && (!results || results.length < 1)')
  8. orbit-spinner(
  9. :animation-duration='1000'
  10. :size='100'
  11. color='#FFF'
  12. )
  13. .headline.mt-5 {{$t('common:header.searchLoading')}}
  14. .search-results-none(v-else-if='!searchIsLoading && (!results || results.length < 1)')
  15. img(src='/_assets/svg/icon-no-results.svg', alt='No Results')
  16. .subheading {{$t('common:header.searchNoResult')}}
  17. template(v-if='search && search.length >= 2 && results && results.length > 0')
  18. v-subheader.white--text {{$t('common:header.searchResultsCount', { total: response.totalHits })}}
  19. v-list.search-results-items.radius-7.py-0(two-line, dense)
  20. template(v-for='(item, idx) of results')
  21. v-list-item(@click='goToPage(item)', @click.middle="goToPageInNewTab(item)", :key='item.id', :class='idx === cursor ? `highlighted` : ``')
  22. v-list-item-avatar(tile)
  23. img(src='/_assets/svg/icon-selective-highlighting.svg')
  24. v-list-item-content
  25. v-list-item-title(v-text='item.title')
  26. v-list-item-subtitle.caption(v-text='item.description')
  27. .caption.grey--text(v-text='item.path')
  28. v-list-item-action
  29. v-chip(label, outlined) {{item.locale.toUpperCase()}}
  30. v-divider(v-if='idx < results.length - 1')
  31. v-pagination.mt-3(
  32. v-if='paginationLength > 1'
  33. dark
  34. v-model='pagination'
  35. :length='paginationLength'
  36. circle
  37. )
  38. template(v-if='suggestions && suggestions.length > 0')
  39. v-subheader.white--text.mt-3 {{$t('common:header.searchDidYouMean')}}
  40. v-list.search-results-suggestions.radius-7(dense, dark)
  41. template(v-for='(term, idx) of suggestions')
  42. v-list-item(:key='term', @click='setSearchTerm(term)', :class='idx + results.length === cursor ? `highlighted` : ``')
  43. v-list-item-avatar
  44. v-icon mdi-magnify
  45. v-list-item-content
  46. v-list-item-title(v-text='term')
  47. v-divider(v-if='idx < suggestions.length - 1')
  48. .text-xs-center.pt-5(v-if='search && search.length > 1')
  49. //- v-btn.mx-2(outlined, color='orange', @click='search = ``', v-if='results.length > 0')
  50. //- v-icon(left) mdi-content-save
  51. //- span {{$t('common:header.searchCopyLink')}}
  52. v-btn.mx-2(outlined, color='pink', @click='search = ``')
  53. v-icon(left) mdi-close
  54. span {{$t('common:header.searchClose')}}
  55. </template>
  56. <script>
  57. import _ from 'lodash'
  58. import { sync } from 'vuex-pathify'
  59. import { OrbitSpinner } from 'epic-spinners'
  60. import searchPagesQuery from 'gql/common/common-pages-query-search.gql'
  61. export default {
  62. components: {
  63. OrbitSpinner
  64. },
  65. data() {
  66. return {
  67. cursor: 0,
  68. pagination: 1,
  69. perPage: 10,
  70. response: {
  71. results: [],
  72. suggestions: [],
  73. totalHits: 0
  74. }
  75. }
  76. },
  77. computed: {
  78. search: sync('site/search'),
  79. searchIsFocused: sync('site/searchIsFocused'),
  80. searchIsLoading: sync('site/searchIsLoading'),
  81. searchRestrictLocale: sync('site/searchRestrictLocale'),
  82. searchRestrictPath: sync('site/searchRestrictPath'),
  83. results() {
  84. const currentIndex = (this.pagination - 1) * this.perPage
  85. return this.response.results ? _.slice(this.response.results, currentIndex, currentIndex + this.perPage) : []
  86. },
  87. hits() {
  88. return this.response.totalHits ? this.response.totalHits : 0
  89. },
  90. suggestions() {
  91. return this.response.suggestions ? this.response.suggestions : []
  92. },
  93. paginationLength() {
  94. return (this.response.totalHits > 0) ? Math.ceil(this.response.totalHits / this.perPage) : 0
  95. }
  96. },
  97. watch: {
  98. search(newValue, oldValue) {
  99. this.cursor = 0
  100. if (!newValue || (newValue && newValue.length < 2)) {
  101. this.searchIsLoading = false
  102. } else {
  103. this.searchIsLoading = true
  104. }
  105. }
  106. },
  107. mounted() {
  108. this.$root.$on('searchMove', (dir) => {
  109. this.cursor += ((dir === 'up') ? -1 : 1)
  110. if (this.cursor < -1) {
  111. this.cursor = -1
  112. } else if (this.cursor > this.results.length + this.suggestions.length - 1) {
  113. this.cursor = this.results.length + this.suggestions.length - 1
  114. }
  115. })
  116. this.$root.$on('searchEnter', () => {
  117. if (!this.results) {
  118. return
  119. }
  120. if (this.cursor >= 0 && this.cursor < this.results.length) {
  121. this.goToPage(_.nth(this.results, this.cursor))
  122. } else if (this.cursor >= 0) {
  123. this.setSearchTerm(_.nth(this.suggestions, this.cursor - this.results.length))
  124. }
  125. })
  126. },
  127. methods: {
  128. setSearchTerm(term) {
  129. this.search = term
  130. },
  131. goToPage(item) {
  132. window.location.assign(`/${item.locale}/${item.path}`)
  133. },
  134. goToPageInNewTab(item) {
  135. window.open(`/${item.locale}/${item.path}`, '_blank')
  136. }
  137. },
  138. apollo: {
  139. response: {
  140. query: searchPagesQuery,
  141. variables() {
  142. return {
  143. query: this.search
  144. }
  145. },
  146. fetchPolicy: 'network-only',
  147. debounce: 300,
  148. throttle: 1000,
  149. skip() {
  150. return !this.search || this.search.length < 2
  151. },
  152. update: (data) => _.get(data, 'pages.search', {}),
  153. watchLoading (isLoading) {
  154. this.searchIsLoading = isLoading
  155. }
  156. }
  157. }
  158. }
  159. </script>
  160. <style lang="scss">
  161. .search-results {
  162. position: fixed;
  163. top: 64px;
  164. left: 0;
  165. overflow-y: auto;
  166. width: 100%;
  167. height: calc(100% - 64px);
  168. background-color: rgba(0,0,0,.9);
  169. z-index: 100;
  170. text-align: center;
  171. animation: searchResultsReveal .6s ease;
  172. @media #{map-get($display-breakpoints, 'sm-and-down')} {
  173. top: 112px;
  174. }
  175. &-container {
  176. margin: 12px auto;
  177. width: 90vw;
  178. max-width: 1024px;
  179. }
  180. &-help {
  181. text-align: center;
  182. padding: 32px 0;
  183. font-size: 18px;
  184. font-weight: 300;
  185. color: #FFF;
  186. img {
  187. width: 104px;
  188. }
  189. }
  190. &-loader {
  191. display: flex;
  192. justify-content: center;
  193. align-items: center;
  194. flex-direction: column;
  195. padding: 32px 0;
  196. color: #FFF;
  197. }
  198. &-none {
  199. color: #FFF;
  200. img {
  201. width: 200px;
  202. }
  203. }
  204. &-items {
  205. text-align: left;
  206. .highlighted {
  207. background: #FFF linear-gradient(to bottom, #FFF, mc('orange', '100'));
  208. @at-root .theme--dark & {
  209. background: mc('grey', '900') linear-gradient(to bottom, mc('orange', '900'), darken(mc('orange', '900'), 15%));
  210. }
  211. }
  212. }
  213. &-suggestions {
  214. .highlighted {
  215. background: transparent linear-gradient(to bottom, mc('blue', '500'), mc('blue', '700'));
  216. }
  217. }
  218. }
  219. @keyframes searchResultsReveal {
  220. 0% {
  221. background-color: rgba(0,0,0,0);
  222. padding-top: 32px;
  223. }
  224. 100% {
  225. background-color: rgba(0,0,0,.9);
  226. padding-top: 0;
  227. }
  228. }
  229. </style>