repos
/ timestrap-django master

timestrap-django

mirror archived upstream

Time tracking you can host anywhere, built on Django. Full export support in multiple formats and easily extensible.

djangodockerhandcodedpythonself-hostedtime-trackingtimetracker

5.0 KB · 209 lines · vue Raw History
  1<template>
  2  <div class="selector">
  3    <div
  4      class="focuser"
  5      v-if="edit"
  6      @click="toggleEdit()"/>
  7
  8    <div
  9      class="select border shadow rounded w-100"
 10      v-if="edit"
 11      @keydown.esc.prevent="toggleEdit()"
 12      @keydown.tab.prevent="toggleEdit()"
 13      @keydown.up.prevent="selectedUp()"
 14      @keydown.down.prevent="selectedDown()"
 15      @keydown.enter.prevent="activeUpdate()">
 16      <div class="form-control d-flex rounded-0 border-0 rounded-top bg-light">
 17      <span class="badge badge-dark d-block-inline mr-2 px-2" v-if="selectedObject">
 18        {{ selectedObject.text }}
 19        <span
 20          v-if="active"
 21          class="ml-2 clear"
 22          @click="clearActive()">
 23          <icon :icon="['fas', 'times-circle']"/>
 24        </span>
 25      </span>
 26        <input
 27          type="text"
 28          v-model="search"
 29          ref="search"
 30          class="form-control-plaintext flex-fill"
 31          placeholder="Type to filter...">
 32      </div>
 33      <div class="options list-group list-group-flush w-100 shadow">
 34        <template v-if="optionsFiltered(search).length">
 35            <a
 36              v-for="option in optionsFiltered(search)"
 37              :key="option.id"
 38              href="#"
 39              :class="['option', 'list-group-item', 'list-group-item-action', active === option.id ? 'active' : '']"
 40              @click.prevent="clickUpdate(option.id)">
 41              {{ option.text }}
 42            </a>
 43        </template>
 44        <template v-else>
 45          <div class="list-group-item text-muted">Nothing matches your filter.</div>
 46        </template>
 47      </div>
 48    </div>
 49
 50    <div
 51      :class="['selected', 'form-control', 'shadow-sm', 'overflow-hidden', small ? 'form-control-sm' : '']"
 52      tabindex="0"
 53      ref="selected"
 54      @click.prevent="toggleEdit()"
 55      @keydown.up.prevent="toggleEdit()"
 56      @keydown.down.prevent="toggleEdit()"
 57      @keydown.enter.prevent="toggleEdit()"
 58      v-if="selectedObject">
 59      {{ selectedObject.text }}
 60    </div>
 61  </div>
 62</template>
 63
 64
 65<script>
 66  import Vue from 'vue';
 67
 68
 69  export default {
 70    data() {
 71      return {
 72        search: '',
 73        edit: false,
 74        active: null,
 75      }
 76    },
 77    props: {
 78      options: {
 79        type: Array,
 80        default: [
 81          {
 82            id: '',
 83            text: 'Empty',
 84          }
 85        ],
 86      },
 87      selected: {
 88        type: String,
 89        default: '',
 90      },
 91      placeholder: {
 92        type: String,
 93        default: 'Empty',
 94      },
 95      small: {
 96        type: Boolean,
 97        default: false,
 98      }
 99    },
100    computed: {
101      selectedObject(){
102        if (this.selected) {
103          return this.options.find(option => {
104            return option.id === this.selected;
105          });
106        } else {
107          return {
108            text: this.placeholder,
109          }
110        }
111      },
112      activeObject() {
113        return this.options.find(option => {
114          return option.id === this.active;
115        });
116      },
117    },
118    watch: {
119      search() {
120        if (this.optionsFiltered(this.search).length && this.optionsFiltered(this.search).indexOf(this.activeObject) === -1)
121          this.active = this.optionsFiltered(this.search)[0].id;
122      },
123    },
124    methods: {
125      clearActive() {
126        this.active = null;
127        this.$emit('input', null);
128      },
129      optionsFiltered(search) {
130        return this.options.filter(option => {
131          return option.text.toLowerCase().includes(search);
132        });
133      },
134      clickUpdate(id) {
135        this.toggleEdit();
136        this.active = id;
137        this.$emit('input', id);
138      },
139      activeUpdate() {
140        this.toggleEdit();
141        this.$emit('input', this.active);
142      },
143      toggleEdit() {
144        if (this.edit === true) {
145          this.edit = false;
146          this.search = '';
147          Vue.nextTick(() => {
148            this.$refs.selected.focus();
149          });
150        } else {
151          this.edit = true;
152          Vue.nextTick(() => {
153            this.$refs.search.focus();
154          });
155        }
156      },
157      selectedUp() {
158        const up = this.optionsFiltered(this.search).indexOf(this.activeObject) - 1;
159        if (up > -1) this.active = this.optionsFiltered(this.search)[up].id;
160      },
161      selectedDown() {
162        const down = this.optionsFiltered(this.search).indexOf(this.activeObject) + 1;
163        if (down < this.optionsFiltered(this.search).length)
164          this.active = this.optionsFiltered(this.search)[down].id;
165      },
166    },
167    mounted() {
168      this.active = this.selected;
169    },
170  };
171</script>
172
173
174<style lang="scss" scoped>
175  .clear {
176    cursor: pointer;
177  }
178
179  .focuser {
180    position: fixed;
181    z-index: 1;
182    top: 0;
183    left: 0;
184    bottom: 0;
185    right: 0;
186  }
187
188  .select {
189    position: absolute;
190    z-index: 2;
191    min-width: 400px;
192
193    .badge {
194      align-items: center;
195      display: flex;
196    }
197  }
198
199  .options {
200    max-height: 200px;
201    overflow-y: scroll;
202    background-color: #fff;
203  }
204
205  .list-group-item {
206    padding: .25rem .5rem;
207  }
208</style>