|
@@ -1,34 +1,57 @@
|
|
|
<template>
|
|
|
<div class="label-selector">
|
|
|
- <input ref="input" type="text"
|
|
|
- v-model="search" placeholder="Search...">
|
|
|
-
|
|
|
- <div v-if="results.length > 0" class="labels">
|
|
|
- <div v-for="(label, index) in results" v-bind:key="label.identifier"
|
|
|
- class="label selectable"
|
|
|
- :class="{selected: index === selectedIndex, visible: label.is_visible}"
|
|
|
- @click="select(label)">
|
|
|
- <div class="hierarchy" v-if="label.hierarchy_level">
|
|
|
- {{ label.hierarchy_level }}
|
|
|
- </div>
|
|
|
- <div class="name">
|
|
|
- {{ label.name }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <input v-debounce:400ms="setQuery"
|
|
|
+ class="search-field"
|
|
|
+ ref="input"
|
|
|
+ type="text"
|
|
|
+ placeholder="Search...">
|
|
|
+ <div class="filters">
|
|
|
+ <span @click="resetHierarchyFilter"
|
|
|
+ class="filter active">
|
|
|
+ Show all
|
|
|
+ </span>
|
|
|
+ <span v-for="hierarchy in hierarchies"
|
|
|
+ :key="hierarchy.key"
|
|
|
+ @click="toggleHierarchyFilter(hierarchy.key)"
|
|
|
+ :class="{active: isFilterActive(hierarchy.key)}"
|
|
|
+ class="filter">
|
|
|
+ {{ hierarchy.name }}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
+ <div class="labels">
|
|
|
+ <div v-for="(label, index) in results"
|
|
|
+ :key="index"
|
|
|
+ @click="select(label)"
|
|
|
+ class="label"
|
|
|
+ :class="{selected: index === selectedIndex}"
|
|
|
+ >
|
|
|
+
|
|
|
+
|
|
|
+ <div class="name">
|
|
|
+ <span v-html="highlight(label.name)"></span>
|
|
|
+ <span v-if="label.hierarchy_level"
|
|
|
+ class="hierarchy">
|
|
|
+ ({{ label.hierarchy_level }})
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
|
|
|
- <div v-else class="labels">
|
|
|
- <div class="label">
|
|
|
+ <div v-if="results.length < 1" class="label">
|
|
|
No results found...
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
+
|
|
|
+
|
|
|
<script>
|
|
|
export default {
|
|
|
name: "label-selector",
|
|
|
- props: ['labels'],
|
|
|
+ props: [
|
|
|
+ "labels"
|
|
|
+ ],
|
|
|
mounted: function () {
|
|
|
// register events to prevent global hotkeys while typing
|
|
|
window.addEventListener('keypress', this.keypress, true);
|
|
@@ -45,15 +68,94 @@ export default {
|
|
|
},
|
|
|
data: function () {
|
|
|
return {
|
|
|
- search: '',
|
|
|
selectedIndex: 0,
|
|
|
- minSearchChars: 2,
|
|
|
- deleteLabel: {identifier: null, name: 'None'}
|
|
|
+ query: "",
|
|
|
+ hierarchy_filters: [],
|
|
|
+ deleteLabel: {
|
|
|
+ hierarchy_level: null,
|
|
|
+ identifier: null,
|
|
|
+ name: 'None',
|
|
|
+ },
|
|
|
}
|
|
|
},
|
|
|
+
|
|
|
computed: {
|
|
|
- sortedLabels: function () {
|
|
|
- return [...this.labels].sort((a, b) => {
|
|
|
+ hierarchies: function() {
|
|
|
+ let hierarchies = [...new Set(this.labels.map(l => l.hierarchy_level))]
|
|
|
+ hierarchies.sort((a, b) => {
|
|
|
+ if(a === null)
|
|
|
+ return +1;
|
|
|
+ if(b === null)
|
|
|
+ return -1;
|
|
|
+ if (a < b)
|
|
|
+ return +1;
|
|
|
+ else
|
|
|
+ return -1;
|
|
|
+ });
|
|
|
+ return hierarchies.map(
|
|
|
+ l => {
|
|
|
+ return {
|
|
|
+ key: l,
|
|
|
+ name: (l === null ? "Art" : l)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ },
|
|
|
+
|
|
|
+ results: function() {
|
|
|
+ const query = this.query;
|
|
|
+ let labels = this.labels;
|
|
|
+ const n_filters = this.hierarchy_filters.length;
|
|
|
+
|
|
|
+ if (n_filters !== 0 && n_filters !== this.hierarchies.length)
|
|
|
+ labels = labels.filter(
|
|
|
+ l => this.hierarchy_filters.indexOf(l.hierarchy_level) > -1
|
|
|
+ )
|
|
|
+
|
|
|
+ labels = labels.filter(
|
|
|
+ l => l.name.toLowerCase().indexOf(query) > -1
|
|
|
+ );
|
|
|
+
|
|
|
+ this.sort(labels);
|
|
|
+ return [this.deleteLabel, ...labels];
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+
|
|
|
+ resetHierarchyFilter: function() {
|
|
|
+ this.hierarchy_filters = [];
|
|
|
+ },
|
|
|
+
|
|
|
+ isFilterActive: function (key){
|
|
|
+ return this.hierarchy_filters.length == 0 ||
|
|
|
+ this.hierarchy_filters.indexOf(key) != -1;
|
|
|
+ },
|
|
|
+
|
|
|
+ toggleHierarchyFilter: function(key) {
|
|
|
+
|
|
|
+ const idx = this.hierarchy_filters.indexOf(key);
|
|
|
+
|
|
|
+ if (idx == -1)
|
|
|
+ this.hierarchy_filters.push(key)
|
|
|
+ else
|
|
|
+ this.hierarchy_filters.splice(idx, 1)
|
|
|
+
|
|
|
+ console.debug("Current hierarchy filters:",
|
|
|
+ this.hierarchy_filters)
|
|
|
+ },
|
|
|
+
|
|
|
+ setQuery: function(query) {
|
|
|
+ this.query = query.toLowerCase();
|
|
|
+ },
|
|
|
+
|
|
|
+ n_visible: function () {
|
|
|
+ return this.results.length;
|
|
|
+ },
|
|
|
+
|
|
|
+ sort: function(labels) {
|
|
|
+ labels.sort((a, b) => {
|
|
|
+
|
|
|
if (a.hierarchy_level !== b.hierarchy_level) {
|
|
|
if (a.hierarchy_level === null)
|
|
|
return +1;
|
|
@@ -71,25 +173,15 @@ export default {
|
|
|
return +1;
|
|
|
});
|
|
|
},
|
|
|
- results: function () {
|
|
|
- let labs = [this.deleteLabel, ...this.sortedLabels];
|
|
|
- labs.forEach(l => {l.is_visible = true });
|
|
|
- return labs;
|
|
|
- },
|
|
|
|
|
|
- n_visible: function () {
|
|
|
- return this.results.filter(l => l.is_visible).length;
|
|
|
- },
|
|
|
- },
|
|
|
- methods: {
|
|
|
keypress: function (event) {
|
|
|
event.stopPropagation();
|
|
|
|
|
|
switch (event.key) {
|
|
|
case 'ArrowDown':
|
|
|
this.selectedIndex += 1;
|
|
|
- if (this.selectedIndex >= this.n_visible)
|
|
|
- this.selectedIndex = this.n_visible - 1;
|
|
|
+ if (this.selectedIndex >= this.n_visible())
|
|
|
+ this.selectedIndex = this.n_visible() - 1;
|
|
|
break;
|
|
|
|
|
|
case 'ArrowUp':
|
|
@@ -107,47 +199,75 @@ export default {
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
- this.selectedIndex = Math.min(1, this.n_visible - 1);
|
|
|
+ this.selectedIndex = Math.min(1, this.n_visible() - 1);
|
|
|
break;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ highlight: function (name) {
|
|
|
+ const query = this.query
|
|
|
+ if (!query)
|
|
|
+ return name;
|
|
|
|
|
|
- setVisibility: function (search) {
|
|
|
- console.log(search);
|
|
|
- this.results.forEach(l => {
|
|
|
- l.is_visible = !search || !l.identifier || l.name.toLowerCase().includes(search);
|
|
|
- });
|
|
|
+ const start = name.toLowerCase().indexOf(query);
|
|
|
+ if (start == -1)
|
|
|
+ return name
|
|
|
+
|
|
|
+ const end = start + query.length;
|
|
|
+
|
|
|
+ const pre = name.substring(0, start);
|
|
|
+ const match = name.substring(start, end);
|
|
|
+ const post = name.substring(end);
|
|
|
+ return `${pre}<strong>${match}</strong>${post}`;
|
|
|
},
|
|
|
+
|
|
|
select: function (label) {
|
|
|
+ console.debug("selected label", label);
|
|
|
this.$emit('label', label);
|
|
|
this.closeDelayed();
|
|
|
},
|
|
|
+
|
|
|
closeDelayed: function () {
|
|
|
setTimeout(() => this.$emit('close', true), 100);
|
|
|
- }
|
|
|
- },
|
|
|
- watch: {
|
|
|
- search: function(newVal) {
|
|
|
- let query = "";
|
|
|
-
|
|
|
- if (newVal.length >= this.minSearchChars)
|
|
|
- query = newVal.toLowerCase();
|
|
|
-
|
|
|
- this.setVisibility(query);
|
|
|
- }
|
|
|
+ },
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
+
|
|
|
<style scoped>
|
|
|
+
|
|
|
.label-selector {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 50%;
|
|
|
+ z-index: 101;
|
|
|
+
|
|
|
+ width: 30rem;
|
|
|
+ max-width: 90vw;
|
|
|
+ max-height: 90%;
|
|
|
+
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
|
|
|
+ border-bottom-right-radius: 0.5rem;
|
|
|
+ border-bottom-left-radius: 0.5rem;
|
|
|
padding: 1rem;
|
|
|
- background-color: rgba(0, 0, 0, 0.65);
|
|
|
+
|
|
|
+ background-color: rgba(0, 0, 0, 0.75);
|
|
|
color: whitesmoke;
|
|
|
+
|
|
|
+ animation: label-selector-animation 0.3s ease-out forwards;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes label-selector-animation {
|
|
|
+ from {
|
|
|
+ transform: translateX(-50%) translateY(-100%);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: translateX(-50%) translateY(0%);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
input {
|
|
@@ -161,27 +281,59 @@ input {
|
|
|
}
|
|
|
|
|
|
.label {
|
|
|
- padding: 0.25rem 0;
|
|
|
- display: none;
|
|
|
-}
|
|
|
-
|
|
|
-.label.visible{
|
|
|
- display: inherit;
|
|
|
+ padding: 0.25rem 0.5rem;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 0.3rem;
|
|
|
+ margin: 0.2rem;
|
|
|
}
|
|
|
|
|
|
.label:not(:last-child) {
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
}
|
|
|
|
|
|
-.label.selectable:hover,
|
|
|
-.label.selected {
|
|
|
+.label:hover, .label.selected {
|
|
|
background-color: var(--primary);
|
|
|
- padding: 0.25rem 0.5rem;
|
|
|
}
|
|
|
|
|
|
.hierarchy {
|
|
|
font-size: 77%;
|
|
|
opacity: 0.8;
|
|
|
+ float: right;
|
|
|
+}
|
|
|
+
|
|
|
+.filters {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: space-evenly;
|
|
|
+ align-items: baseline;
|
|
|
+ padding: 0.25rem;
|
|
|
+ border-bottom: 1px solid rgb(20%, 20%, 20%);
|
|
|
+}
|
|
|
+
|
|
|
+.filter {
|
|
|
+ flex-grow: 1;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ margin: 0rem 0.5rem;
|
|
|
+ padding: 0.25rem;
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ background-color: rgb(50%, 50%, 50%);
|
|
|
+ border: 1px solid rgb(10%,10%,10%);
|
|
|
+ color: rgb(20%, 20%, 20%);
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.filter.active {
|
|
|
+ background-color: rgb(25%, 25%, 25%);
|
|
|
+ color: whitesmoke;
|
|
|
+}
|
|
|
+
|
|
|
+.filter:hover {
|
|
|
+ background-color: rgb(40%, 40%, 40%);
|
|
|
+}
|
|
|
+
|
|
|
+.filter.active:hover {
|
|
|
+ background-color: rgb(20%, 20%, 20%);
|
|
|
}
|
|
|
|
|
|
</style>
|