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.

SearchableList.vue 3.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <!--
  2. - @copyright 2023 Marco Ambrosini <marcoambrosini@proton.me>
  3. -
  4. - @author Marco Ambrosini <marcoambrosini@proton.me>
  5. -
  6. - @license AGPL-3.0-or-later
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <NcPopover :shown="opened"
  24. @show="opened = true"
  25. @hide="opened = false">
  26. <template #trigger>
  27. <slot ref="popoverTrigger" name="trigger" />
  28. </template>
  29. <div class="searchable-list__wrapper">
  30. <NcTextField :value.sync="searchTerm"
  31. :label="labelText"
  32. trailing-button-icon="close"
  33. :show-trailing-button="searchTerm !== ''"
  34. @update:value="searchTermChanged"
  35. @trailing-button-click="clearSearch">
  36. <Magnify :size="20" />
  37. </NcTextField>
  38. <ul v-if="filteredList.length > 0" class="searchable-list__list">
  39. <li v-for="element in filteredList"
  40. :key="element.id"
  41. :title="element.displayName"
  42. role="button">
  43. <NcButton alignment="start"
  44. type="tertiary"
  45. :wide="true"
  46. @click="itemSelected(element)">
  47. <template #icon>
  48. <NcAvatar v-if="element.isUser" :user="element.user" :show-user-status="false" />
  49. <NcAvatar v-else
  50. :is-no-user="true"
  51. :display-name="element.displayName"
  52. :show-user-status="false" />
  53. </template>
  54. {{ element.displayName }}
  55. </NcButton>
  56. </li>
  57. </ul>
  58. <div v-else class="searchable-list__empty-content">
  59. <NcEmptyContent :name="emptyContentText">
  60. <template #icon>
  61. <AlertCircleOutline />
  62. </template>
  63. </NcEmptyContent>
  64. </div>
  65. </div>
  66. </NcPopover>
  67. </template>
  68. <script>
  69. import { NcPopover, NcTextField, NcAvatar, NcEmptyContent, NcButton } from '@nextcloud/vue'
  70. import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
  71. import Magnify from 'vue-material-design-icons/Magnify.vue'
  72. export default {
  73. name: 'SearchableList',
  74. components: {
  75. NcPopover,
  76. NcTextField,
  77. Magnify,
  78. AlertCircleOutline,
  79. NcAvatar,
  80. NcEmptyContent,
  81. NcButton,
  82. },
  83. props: {
  84. labelText: {
  85. type: String,
  86. default: 'this is a label',
  87. },
  88. searchList: {
  89. type: Array,
  90. required: true,
  91. },
  92. emptyContentText: {
  93. type: String,
  94. required: true,
  95. },
  96. },
  97. data() {
  98. return {
  99. opened: false,
  100. error: false,
  101. searchTerm: '',
  102. }
  103. },
  104. computed: {
  105. filteredList() {
  106. return this.searchList.filter((element) => {
  107. if (!this.searchTerm.toLowerCase().length) {
  108. return true
  109. }
  110. return ['displayName'].some(prop => element[prop].toLowerCase().includes(this.searchTerm.toLowerCase()))
  111. })
  112. },
  113. },
  114. methods: {
  115. clearSearch() {
  116. this.searchTerm = ''
  117. },
  118. itemSelected(element) {
  119. this.$emit('item-selected', element)
  120. this.clearSearch()
  121. this.opened = false
  122. },
  123. searchTermChanged(term) {
  124. this.$emit('search-term-change', term)
  125. },
  126. },
  127. }
  128. </script>
  129. <style lang="scss" scoped>
  130. .searchable-list {
  131. &__wrapper {
  132. padding: calc(var(--default-grid-baseline) * 3);
  133. display: flex;
  134. flex-direction: column;
  135. align-items: center;
  136. width: 250px;
  137. }
  138. &__list {
  139. width: 100%;
  140. max-height: 284px;
  141. overflow-y: auto;
  142. margin-top: var(--default-grid-baseline);
  143. padding: var(--default-grid-baseline);
  144. :deep(.button-vue) {
  145. border-radius: var(--border-radius-large) !important;
  146. span {
  147. font-weight: initial;
  148. }
  149. }
  150. }
  151. &__empty-content {
  152. margin-top: calc(var(--default-grid-baseline) * 3);
  153. }
  154. }
  155. </style>