You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

236 lines
6.6 KiB

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