label-selector.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <template>
  2. <div class="label-selector">
  3. <input v-debounce:400ms="setQuery"
  4. class="search-field"
  5. ref="input"
  6. type="text"
  7. placeholder="Search...">
  8. <div class="filters">
  9. <span @click="resetHierarchyFilter"
  10. class="filter active">
  11. Show all
  12. </span>
  13. <span v-for="hierarchy in hierarchies"
  14. :key="hierarchy.key"
  15. @click="toggleHierarchyFilter(hierarchy.key)"
  16. :class="{active: isFilterActive(hierarchy.key)}"
  17. class="filter">
  18. {{ hierarchy.name }}
  19. </span>
  20. </div>
  21. <div class="labels">
  22. <div v-for="(label, index) in results"
  23. :key="index"
  24. @click="select(label)"
  25. class="label"
  26. :class="{selected: index === selectedIndex}"
  27. >
  28. <div class="name">
  29. <span v-html="highlight(label.name)"></span>
  30. <span v-if="label.hierarchy_level"
  31. class="hierarchy">
  32. ({{ label.hierarchy_level }})
  33. </span>
  34. </div>
  35. </div>
  36. <div v-if="results.length < 1" class="label">
  37. No results found...
  38. </div>
  39. </div>
  40. </div>
  41. </template>
  42. <script>
  43. export default {
  44. name: "label-selector",
  45. props: [
  46. "labels"
  47. ],
  48. mounted: function () {
  49. // register events to prevent global hotkeys while typing
  50. window.addEventListener('keypress', this.keypress, true);
  51. window.addEventListener('keydown', this.keypress, true);
  52. window.addEventListener('wheel', this.keypress, true);
  53. // focus text input
  54. this.$refs.input.focus();
  55. },
  56. destroyed: function () {
  57. window.removeEventListener('keypress', this.keypress, true);
  58. window.removeEventListener('keydown', this.keypress, true);
  59. window.removeEventListener('wheel', this.keypress, true);
  60. },
  61. data: function () {
  62. return {
  63. selectedIndex: 0,
  64. query: "",
  65. hierarchy_filters: [],
  66. deleteLabel: {
  67. hierarchy_level: null,
  68. identifier: null,
  69. name: 'None',
  70. },
  71. }
  72. },
  73. computed: {
  74. hierarchies: function() {
  75. let hierarchies = [...new Set(this.labels.map(l => l.hierarchy_level))]
  76. hierarchies.sort((a, b) => {
  77. if(a === null)
  78. return +1;
  79. if(b === null)
  80. return -1;
  81. if (a < b)
  82. return +1;
  83. else
  84. return -1;
  85. });
  86. return hierarchies.map(
  87. l => {
  88. return {
  89. key: l,
  90. name: (l === null ? "Art" : l)
  91. }
  92. }
  93. );
  94. },
  95. results: function() {
  96. const query = this.query;
  97. let labels = this.labels;
  98. const n_filters = this.hierarchy_filters.length;
  99. if (n_filters !== 0 && n_filters !== this.hierarchies.length)
  100. labels = labels.filter(
  101. l => this.hierarchy_filters.indexOf(l.hierarchy_level) > -1
  102. )
  103. labels = labels.filter(
  104. l => l.name.toLowerCase().indexOf(query) > -1
  105. );
  106. this.sort(labels);
  107. return [this.deleteLabel, ...labels];
  108. }
  109. },
  110. methods: {
  111. resetHierarchyFilter: function() {
  112. this.hierarchy_filters = [];
  113. },
  114. isFilterActive: function (key){
  115. return this.hierarchy_filters.length == 0 ||
  116. this.hierarchy_filters.indexOf(key) != -1;
  117. },
  118. toggleHierarchyFilter: function(key) {
  119. const idx = this.hierarchy_filters.indexOf(key);
  120. if (idx == -1)
  121. this.hierarchy_filters.push(key)
  122. else
  123. this.hierarchy_filters.splice(idx, 1)
  124. console.debug("Current hierarchy filters:",
  125. this.hierarchy_filters)
  126. },
  127. setQuery: function(query) {
  128. this.query = query.toLowerCase();
  129. },
  130. n_visible: function () {
  131. return this.results.length;
  132. },
  133. sort: function(labels) {
  134. labels.sort((a, b) => {
  135. if (a.hierarchy_level !== b.hierarchy_level) {
  136. if (a.hierarchy_level === null)
  137. return +1;
  138. if (b.hierarchy_level === null)
  139. return -1;
  140. if (a.hierarchy_level < b.hierarchy_level)
  141. return +1;
  142. else
  143. return -1;
  144. }
  145. if (a.name < b.name)
  146. return -1;
  147. else
  148. return +1;
  149. });
  150. },
  151. keypress: function (event) {
  152. event.stopPropagation();
  153. switch (event.key) {
  154. case 'ArrowDown':
  155. this.selectedIndex += 1;
  156. if (this.selectedIndex >= this.n_visible())
  157. this.selectedIndex = this.n_visible() - 1;
  158. break;
  159. case 'ArrowUp':
  160. this.selectedIndex -= 1;
  161. if (this.selectedIndex < 0)
  162. this.selectedIndex = 0;
  163. break;
  164. case 'Enter':
  165. this.select(this.results[this.selectedIndex]);
  166. break;
  167. case 'Escape':
  168. this.closeDelayed();
  169. break;
  170. default:
  171. this.selectedIndex = Math.min(1, this.n_visible() - 1);
  172. break;
  173. }
  174. },
  175. highlight: function (name) {
  176. const query = this.query
  177. if (!query)
  178. return name;
  179. const start = name.toLowerCase().indexOf(query);
  180. if (start == -1)
  181. return name
  182. const end = start + query.length;
  183. const pre = name.substring(0, start);
  184. const match = name.substring(start, end);
  185. const post = name.substring(end);
  186. return `${pre}<strong>${match}</strong>${post}`;
  187. },
  188. select: function (label) {
  189. console.debug("selected label", label);
  190. this.$emit('label', label);
  191. this.closeDelayed();
  192. },
  193. closeDelayed: function () {
  194. setTimeout(() => this.$emit('close', true), 100);
  195. },
  196. }
  197. }
  198. </script>
  199. <style scoped>
  200. .label-selector {
  201. position: absolute;
  202. top: 0;
  203. left: 50%;
  204. z-index: 101;
  205. width: 30rem;
  206. max-width: 90vw;
  207. max-height: 90%;
  208. display: flex;
  209. flex-direction: column;
  210. border-bottom-right-radius: 0.5rem;
  211. border-bottom-left-radius: 0.5rem;
  212. padding: 1rem;
  213. background-color: rgba(0, 0, 0, 0.75);
  214. color: whitesmoke;
  215. animation: label-selector-animation 0.3s ease-out forwards;
  216. }
  217. @keyframes label-selector-animation {
  218. from {
  219. transform: translateX(-50%) translateY(-100%);
  220. }
  221. to {
  222. transform: translateX(-50%) translateY(0%);
  223. }
  224. }
  225. input {
  226. margin-bottom: 0.5rem;
  227. padding: 0.5rem;
  228. border-radius: 0.5rem;
  229. border: 0px;
  230. }
  231. .labels {
  232. flex-grow: 1;
  233. overflow: auto;
  234. }
  235. .label {
  236. padding: 0.25rem 0.5rem;
  237. cursor: pointer;
  238. border-radius: 0.3rem;
  239. margin: 0.2rem;
  240. }
  241. .label:not(:last-child) {
  242. border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  243. }
  244. .label:hover, .label.selected {
  245. background-color: var(--primary);
  246. }
  247. .hierarchy {
  248. font-size: 77%;
  249. opacity: 0.8;
  250. float: right;
  251. }
  252. .filters {
  253. display: flex;
  254. flex-direction: row;
  255. justify-content: space-evenly;
  256. align-items: baseline;
  257. padding: 0.25rem;
  258. margin-bottom: 0.25rem;
  259. border-bottom: 2px solid rgba(255, 255, 255, 0.2);
  260. }
  261. .filter {
  262. flex-grow: 1;
  263. border-radius: 0.5rem;
  264. margin: 0rem 0.5rem;
  265. padding: 0.25rem;
  266. text-align: center;
  267. background-color: rgb(50%, 50%, 50%);
  268. border: 1px solid rgb(10%,10%,10%);
  269. color: rgb(20%, 20%, 20%);
  270. cursor: pointer;
  271. }
  272. .filter.active {
  273. background-color: rgb(25%, 25%, 25%);
  274. color: whitesmoke;
  275. }
  276. .filter:hover {
  277. background-color: rgb(40%, 40%, 40%);
  278. }
  279. .filter.active:hover {
  280. background-color: rgb(20%, 20%, 20%);
  281. }
  282. </style>