label-selector.vue 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. <template>
  2. <div class="label-selector">
  3. <input ref="input" type="text"
  4. v-model="search" placeholder="Search...">
  5. <div v-if="results.length > 0" class="labels">
  6. <div v-for="(label, index) in results" v-bind:key="label.identifier"
  7. class="label selectable"
  8. :class="{selected: index === selectedIndex}"
  9. @click="select(label)">
  10. <div class="hierarchy" v-if="label.hierarchy_level">
  11. {{ label.hierarchy_level }}
  12. </div>
  13. <div class="name">
  14. {{ label.name }}
  15. </div>
  16. </div>
  17. </div>
  18. <div v-else class="labels">
  19. <div class="label">
  20. No results found...
  21. </div>
  22. </div>
  23. </div>
  24. </template>
  25. <script>
  26. export default {
  27. name: "label-selector",
  28. props: ['labels'],
  29. mounted: function () {
  30. // register events to prevent global hotkeys while typing
  31. window.addEventListener('keypress', this.keypress, true);
  32. window.addEventListener('keydown', this.keypress, true);
  33. window.addEventListener('wheel', this.keypress, true);
  34. // focus text input
  35. this.$refs.input.focus();
  36. },
  37. destroyed: function () {
  38. window.removeEventListener('keypress', this.keypress, true);
  39. window.removeEventListener('keydown', this.keypress, true);
  40. window.removeEventListener('wheel', this.keypress, true);
  41. },
  42. data: function () {
  43. return {
  44. search: '',
  45. selectedIndex: 0
  46. }
  47. },
  48. computed: {
  49. sortedLabels: function () {
  50. return [...this.labels].sort((a, b) => {
  51. if (a.hierarchy_level !== b.hierarchy_level) {
  52. if (a.hierarchy_level === null)
  53. return -1;
  54. if (b.hierarchy_level === null)
  55. return +1;
  56. if (a.hierarchy_level < b.hierarchy_level)
  57. return -1;
  58. else
  59. return +1;
  60. }
  61. if (a.name < b.name)
  62. return -1;
  63. else
  64. return +1;
  65. });
  66. },
  67. results: function () {
  68. const search = this.search.toLowerCase();
  69. return [{
  70. identifier: null,
  71. name: 'None'
  72. }, ...this.sortedLabels]
  73. .filter(l => !this.search || !l.identifier || l.name.toLowerCase().includes(search));
  74. }
  75. },
  76. methods: {
  77. keypress: function (event) {
  78. event.stopPropagation();
  79. switch (event.key) {
  80. case 'ArrowDown':
  81. this.selectedIndex += 1;
  82. if (this.selectedIndex >= this.results.length)
  83. this.selectedIndex = this.results.length - 1;
  84. break;
  85. case 'ArrowUp':
  86. this.selectedIndex -= 1;
  87. if (this.selectedIndex < 0)
  88. this.selectedIndex = 0;
  89. break;
  90. case 'Enter':
  91. this.select(this.results[this.selectedIndex]);
  92. break;
  93. case 'Escape':
  94. this.closeDelayed();
  95. break;
  96. default:
  97. this.selectedIndex = Math.min(1, this.results.length - 1);
  98. break;
  99. }
  100. },
  101. select: function (label) {
  102. this.$emit('label', label);
  103. this.closeDelayed();
  104. },
  105. closeDelayed: function () {
  106. setTimeout(() => this.$emit('close', true), 100);
  107. }
  108. }
  109. }
  110. </script>
  111. <style scoped>
  112. .label-selector {
  113. display: flex;
  114. flex-direction: column;
  115. padding: 1rem;
  116. background-color: rgba(0, 0, 0, 0.65);
  117. color: whitesmoke;
  118. }
  119. input {
  120. margin-bottom: 0.5rem;
  121. padding: 0.3rem;
  122. }
  123. .labels {
  124. flex-grow: 1;
  125. overflow: auto;
  126. }
  127. .label {
  128. padding: 0.25rem 0;
  129. }
  130. .label:not(:last-child) {
  131. border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  132. }
  133. .label.selectable:hover,
  134. .label.selected {
  135. background-color: var(--primary);
  136. padding: 0.25rem 0.5rem;
  137. }
  138. .hierarchy {
  139. font-size: 77%;
  140. opacity: 0.8;
  141. }
  142. </style>