fix(select): 通过 Tab 键跳到下一个焦点时关闭弹出菜单

fix(select): 通过 Tab 键跳到下一个焦点时关闭弹出菜单

<template lang="pug">
.c-select(
  role="combobox"
  aria-autocomplete="list"
  aria-haspopup="true"
  :aria-expanded="isOpen"
  aria-disabled="disabled"
  :tabindex="disabled ? -1 : 0"
  :class="classNames"
  @keydown="onKeyDown"
  @keydown.tab="onTabOut"
  @click="toggleOpen"
)
  i.c-select__caret
  .c-select__selection
    .c-select__placeholder(
      v-if="showPlaceholder"
    ) {{ placeholder }}
    .c-select__value(
      v-if="!multiple && selectedOptions.length"
    ) {{ selectedOptions[0].label }}
    .c-chip(
      v-if="multiple && index <= maxChipCount"
      v-for="(option, index) in selectedOptions"
      :key="index"
      :class=`{
        'is-disabled': option.disabled,
        'is-closeable': index < maxChipCount
      }`
    )
      slot(name="selection" :option="option")
        span(v-if="index < maxChipCount") {{ option.label }}
        span(v-else) {{ maxChipText }}
      .c-chip__close(
        @click.stop="unselectOption(option)"
        v-if="index < maxChipCount"
      )
        c-icon(name="x" valign="middle")
    .c-select__input(
      v-show="showInput"
      :class="multiple ? 'is-multiple' : 'is-single'"
    )
      input(
        ref="input"
        v-model="query"
        autocomplete="off"
        @click.stop="noop"
        @blur="$el.focus()"
        @keydown.delete="onDeleteKey"
        @input="onSearchInput"
      )
  c-portal(:aria-hidden="'' + !isOpen")
    transition(name="fade-in-down")
      .c-select__menu(
        role="menu"
        aria-activedescendant
        v-show="isOpen"
        :style="menuStyle"
        :class="size ? 'is-'+size : ''"
        ref="menu"
      )
        slot(
          name="no-match"
          v-if="autocomplete && !filteredOptions.length"
        )
          .c-select__empty 无匹配选项
        c-option(
          ref="$options"
          v-for="(option, index) in filteredOptions"
          :key="index"
          :label="option.label"
          :isActive="activeOption == option"
          :isSelected="selectedOptions.indexOf(option) > -1"
          :disabled="option.disabled"
          :option="option"
        )
          slot(
            name="menu-item"
            :label="option.label"
            :isActive="activeOption == option"
            :isSelected="selectedOptions.indexOf(option) > -1"
            :disabled="option.disabled"
            :index="index"
            :option="option"
          )
</template>

<script>
import throttle from 'lodash/throttle'

import zIndex from '@util/zIndexManager'
import { getPosition } from './position'
import resettable from '@scripts/mixins/resettable'
import validatable from '@scripts/mixins/validatable'
import PortalComponent from '../portal'
import './index.css'

// ensure each option has label and value
const normalizeOptions = (options = []) => {
  return options.map(option => {
    if (typeof option === 'string') return { label: option, value: option }
    return option
  })
}

export default {
  name: 'c-select',

  components: {
    'c-portal': PortalComponent
  },

  mixins: [resettable, validatable],

  props: {
    value: [Number, String, Object, Array],
    options: Array,
    disabled: Boolean,
    placeholder: {
      type: String,
      default: '请选择...'
    },
    shouldMenuOverlap: {
      type: Boolean,
      default: true
    },
    multiple: Boolean,
    combobox: Boolean,
    autocomplete: Boolean,
    size: String,
    width: String,
    filter: {
      type: Function,
      default: (options, query) => {
        const q = query.trim().toLowerCase()
        if (!q) return options
        return options
          .filter(option => option.label.toLowerCase().indexOf(q) > -1)
      }
    },
    filterThrottle: {
      type: Number,
      default: 0
    },
    maxChipCount: {
      type: Number,
      default: Infinity
    },
    maxChipPlaceholder: {
      type: [String, Function],
      default: ommittedCount => `和其它${ommittedCount}个选项`
    }
  },

  model: {
    event: 'change'
  },

  provide () {
    return { $select: this }
  },

  inject: {
    $form: { default: null }
  },

  data () {
    return {
      isOpen: false,
      menuStyle: {
        top: 'auto',
        left: 'auto',
        minWidth: 0
      },
      activeOption: null,
      selectedOptions: [],
      filteredOptions: [],
      selectionEl: null,
      menuEl: null,
      query: '',
      promiseId: 0
    }
  },

  computed: {
    normalizedOptions () {
      return normalizeOptions(this.options)
    },
    canInput () {
      return this.combobox || this.autocomplete
    },
    showInput () {
      return this.canInput && this.isOpen
    },
    classNames () {
      const classNames = [
        {
          'is-open': this.isOpen,
          'is-disabled': this.disabled
        }
      ]
      const { size, width, $form } = this
      const actualSize = size || ($form && $form.size)
      const actualWidth = width || ($form && $form.width)
      if (actualSize) classNames.push(`is-${actualSize}`)
      if (actualWidth) classNames.push(`is-${actualWidth}`)
      return classNames
    },
    selectedValues () {
      return this.selectedOptions.map(option => option.value)
    },
    showPlaceholder () {
      const empty = !this.selectedOptions.length
      return empty && !this.showInput
    },
    exceedMaxChipCount () {
      return this.selectedOptions.length > this.maxChipCount
    },
    maxChipText () {
      if (!this.exceedMaxChipCount) return
      const ommittedCount = this.selectedOptions.length - this.maxChipCount
      const { maxChipPlaceholder } = this
      if (typeof maxChipPlaceholder === 'function') {
        return maxChipPlaceholder(ommittedCount)
      }
      return maxChipPlaceholder
    }
  },

  watch: {
    isOpen () {
      if (this.isOpen) {
        this.menuStyle.minWidth = `${this.$el.offsetWidth}px`
        // reset positon to next tick
        // this would fix cases where input element in `<c-select autocomplete />`
        // is expanding (add a new line)
        this.$nextTick(this.positionMenu)
        window.addEventListener('click', this.onBodyClick, true)
      } else {
        window.removeEventListener('click', this.onBodyClick, true)
      }
    },

    value: {
      immediate: true,
      handler: function () {
        this.updateSelectedOptions()
      }
    },

    options: {
      immediate: true,
      handler: function () {
        this.updateSelectedOptions()
      }
    },

    selectedOptions: function () {
      if (!this.multiple || this.$isServer) return
      this.$nextTick(this.positionMenu)
    }
  },

  created () {
    // filter function throttled
    this.filterFunction = throttle((options, query) => {
      const filtered = this.filter(options, query)
      if (typeof filtered.then === 'function') {
        const promiseId = Date.now()
        this.promiseId = promiseId
        filtered.then(options => {
          if (this.promiseId > promiseId) return
          this.filteredOptions = normalizeOptions(options)
        })
      } else {
        this.filteredOptions = normalizeOptions(filtered)
      }
    }, this.filterThrottle, {
      leading: true,
      trailing: true
    })
  },

  mounted () {
    this.menuEl = this.$refs.menu

    // hover the option
    this.$on('option-activated', option => {
      this.activeOption = option
    })

    // reset position on window.resize
    this.__onresize = throttle(
      () => {
        if (this.isOpen) {
          this.positionMenu()
        }
      },
      this.$clair.defaultThrottleTime
    )
    window.addEventListener('resize', this.__onresize)

    // select the option
    this.$on('option-clicked', option => this.selectOption(option))

    // watch options, query to filter options
    this.$watch(
      function () {
        return [this.normalizedOptions, this.query, this.isOpen]
      },
      function filterOptions () {
        const { autocomplete, query } = this
        if (!autocomplete) {
          this.filteredOptions = this.normalizedOptions
          return
        }
        this.filterFunction(this.normalizedOptions, query)
      }
    )
  },

  beforeDestroy () {
    window.removeEventListener('resize', this.__onresize)
  },

  methods: {
    toggleOpen () {
      if (this.disabled) return
      if (this.isOpen) {
        this.close()
      } else {
        this.open()
      }
    },

    getOption (value) {
      const fn = option => option.value === value
      return this.filteredOptions.find(fn) ||
        this.normalizedOptions.find(fn) ||
        this.selectedOptions.find(fn)
    },

    updateSelectedOptions () {
      const { value, multiple } = this
      const isEmpty = value === void 0 || value === null
      if (isEmpty) {
        this.selectedOptions = []
        return
      }
      if (multiple) {
        const isArray = Array.isArray(value)
        const isEmptyArray = isArray && value.length === 0
        if (isEmptyArray) return
        const valueArr = isArray ? value : [value]
        this.selectedOptions = valueArr
          .map(v => this.getOption(v))
          .filter(option => option)
      } else {
        const option = this.getOption(value)
        this.selectedOptions = option ? [option] : []
      }
    },

    focusOnInput () {
      this.$nextTick(() => {
        this.$refs.input.focus()
      })
    },

    open () {
      this.isOpen = true;
      [this.activeOption] = this.filteredOptions
      if (this.showInput) {
        this.query = ''
        this.focusOnInput()
      }
    },

    close () {
      this.isOpen = false
    },

    getNextOption (current) {
      const currentIndex = this.filteredOptions.indexOf(current)
      const next = this.filteredOptions.find((option, index) => {
        return index > currentIndex && !option.disabled
      })
      return next || current
    },

    getPreviousOption (current) {
      let prev = null
      const currentIndex = this.filteredOptions.indexOf(current)
      for (let i = currentIndex - 1; i >= 0; i--) {
        if (!this.filteredOptions[i].disabled) {
          prev = this.filteredOptions[i]
          break
        }
      }
      return prev || current
    },

    activateNext () {
      const next = this.getNextOption(this.activeOption)
      this.activeOption = next
    },

    activatePrevious () {
      const prev = this.getPreviousOption(this.activeOption)
      this.activeOption = prev
    },

    selectPrevious () {
      const prev = this.getPreviousOption(this.selectedOptions[0])
      this.selectOption(prev)
    },

    selectNext () {
      const next = this.getNextOption(this.selectedOptions[0])
      this.selectOption(next)
    },

    selectOption (option) {
      if (this.multiple) {
        if (this.autocomplete) this.query = ''
        const isSelected = this.selectedOptions.includes(option)
        if (isSelected) return this.unselectOption(option)
        this.selectedOptions.push(option)
      } else {
        this.selectedOptions = [option]
        this.close()
      }
      this.emitChange()
    },

    unselectOption (option) {
      const index = this.selectedOptions.indexOf(option)
      this.selectedOptions.splice(index, 1)
      this.emitChange()
      // 输入框重新获得焦点
      this.focusOnInput()
    },

    positionMenu () {
      const { shouldMenuOverlap, canInput, menuEl } = this
      const { top, left, height } = getPosition(menuEl, this.$el)
      const overlap = shouldMenuOverlap && !canInput

      const offset = 4
      const menuTop = overlap ? top : (top + height + offset)
      menuEl.style.left = `${left}px`
      menuEl.style.top = `${menuTop}px`
      menuEl.style.zIndex = zIndex.next()
    },

    onBodyClick (e) {
      const isInSelect = this.$el.contains(e.target)
      const isInMenu = this.menuEl.contains(e.target)
      if (!isInSelect && !isInMenu) {
        this.close()
        this.$el.focus()
      }
    },

    onDeleteKey (e) {
      if (!this.query) this.selectedOptions.pop()
    },

    // 通过 Tab 键跳到下一个焦点
    // 理应关闭弹出菜单
    onTabOut () {
      this.close()
    },

    onKeyDown (e) {
      const keys = {
        ENTER: 13,
        ESC: 27,
        SPACE: 32,
        LEFT: 37,
        UP: 38,
        RIGHT: 39,
        DOWN: 40
      }
      const { keyCode } = e
      const {
        isOpen,
        multiple,
        open,
        close,
        selectOption,
        selectPrevious,
        selectNext,
        activeOption,
        activateNext,
        activatePrevious
      } = this

      if (Object.values(keys).includes(keyCode)) e.preventDefault()

      // open menu on space, up, down key
      const openTrigger = [
        keys.SPACE,
        keys.ENTER,
        keys.UP,
        keys.DOWN
      ].includes(keyCode)
      if (openTrigger && !isOpen) return open()

      // close menu on escape
      if (keyCode === keys.ESC && isOpen) return close()

      // press enter to select
      if (keyCode === keys.ENTER && isOpen) return selectOption(activeOption)

      // use left, right to navigate on closed state of non-multiple select
      const canSelect = !isOpen && !multiple
      if (canSelect && keyCode === keys.LEFT) return selectPrevious()
      if (canSelect && keyCode === keys.RIGHT) return selectNext()

      // use up, down to navigate on open state
      if (isOpen && keyCode === keys.UP) return activatePrevious()
      if (isOpen && keyCode === keys.DOWN) return activateNext()
    },

    onSearchInput (e) {
      this.$emit('searchinput', e.target.value)
    },

    emitChange () {
      const value = this.multiple ? this.selectedValues : this.selectedValues[0]
      this.$emit('change', value)
    }
  }
}
</script>

<template lang="pug">
.c-select(
  role="combobox"
  aria-autocomplete="list"
  aria-haspopup="true"
  :aria-expanded="isOpen"
  aria-disabled="disabled"
  :tabindex="disabled ? -1 : 0"
  :class="classNames"
  @keydown="onKeyDown"
  @click="toggleOpen"
)
  i.c-select__caret
  .c-select__selection
    .c-select__placeholder(
      v-if="showPlaceholder"
    ) {{ placeholder }}
    .c-select__value(
      v-if="!multiple && selectedOptions.length"
    ) {{ selectedOptions[0].label }}
    .c-chip(
      v-if="multiple && index <= maxChipCount"
      v-for="(option, index) in selectedOptions"
      :key="index"
      :class=`{
        'is-disabled': option.disabled,
        'is-closeable': index < maxChipCount
      }`
    )
      slot(name="selection" :option="option")
        span(v-if="index < maxChipCount") {{ option.label }}
        span(v-else) {{ maxChipText }}
      .c-chip__close(
        @click.stop="unselectOption(option)"
        v-if="index < maxChipCount"
      )
        c-icon(name="x" valign="middle")
    .c-select__input(
      v-show="showInput"
      :class="multiple ? 'is-multiple' : 'is-single'"
    )
      input(
        ref="input"
        v-model="query"
        autocomplete="off"
        @click.stop="noop"
        @blur="$el.focus()"
        @keydown.delete="onDeleteKey"
        @input="onSearchInput"
      )
  c-portal(:aria-hidden="'' + !isOpen")
    transition(name="fade-in-down")
      .c-select__menu(
        role="menu"
        aria-activedescendant
        v-show="isOpen"
        :style="menuStyle"
        :class="size ? 'is-'+size : ''"
        ref="menu"
      )
        slot(
          name="no-match"
          v-if="autocomplete && !filteredOptions.length"
        )
          .c-select__empty 无匹配选项
        c-option(
          ref="$options"
          v-for="(option, index) in filteredOptions"
          :key="index"
          :label="option.label"
          :isActive="activeOption == option"
          :isSelected="selectedOptions.indexOf(option) > -1"
          :disabled="option.disabled"
          :option="option"
        )
          slot(
            name="menu-item"
            :label="option.label"
            :isActive="activeOption == option"
            :isSelected="selectedOptions.indexOf(option) > -1"
            :disabled="option.disabled"
            :index="index"
            :option="option"
          )
</template>

<script>
import throttle from 'lodash/throttle'

import zIndex from '@util/zIndexManager'
import { getPosition } from './position'
import resettable from '@scripts/mixins/resettable'
import validatable from '@scripts/mixins/validatable'
import PortalComponent from '../portal'
import './index.css'

// ensure each option has label and value
const normalizeOptions = (options = []) => {
  return options.map(option => {
    if (typeof option === 'string') return { label: option, value: option }
    return option
  })
}

export default {
  name: 'c-select',

  components: {
    'c-portal': PortalComponent
  },

  mixins: [resettable, validatable],

  props: {
    value: [Number, String, Object, Array],
    options: Array,
    disabled: Boolean,
    placeholder: {
      type: String,
      default: '请选择...'
    },
    shouldMenuOverlap: {
      type: Boolean,
      default: true
    },
    multiple: Boolean,
    combobox: Boolean,
    autocomplete: Boolean,
    size: String,
    width: String,
    filter: {
      type: Function,
      default: (options, query) => {
        const q = query.trim().toLowerCase()
        if (!q) return options
        return options
          .filter(option => option.label.toLowerCase().indexOf(q) > -1)
      }
    },
    filterThrottle: {
      type: Number,
      default: 0
    },
    maxChipCount: {
      type: Number,
      default: Infinity
    },
    maxChipPlaceholder: {
      type: [String, Function],
      default: ommittedCount => `和其它${ommittedCount}个选项`
    }
  },

  model: {
    event: 'change'
  },

  provide () {
    return { $select: this }
  },

  inject: {
    $form: { default: null }
  },

  data () {
    return {
      isOpen: false,
      menuStyle: {
        top: 'auto',
        left: 'auto',
        minWidth: 0
      },
      activeOption: null,
      selectedOptions: [],
      filteredOptions: [],
      selectionEl: null,
      menuEl: null,
      query: '',
      promiseId: 0
    }
  },

  computed: {
    normalizedOptions () {
      return normalizeOptions(this.options)
    },
    canInput () {
      return this.combobox || this.autocomplete
    },
    showInput () {
      return this.canInput && this.isOpen
    },
    classNames () {
      const classNames = [
        {
          'is-open': this.isOpen,
          'is-disabled': this.disabled
        }
      ]
      const { size, width, $form } = this
      const actualSize = size || ($form && $form.size)
      const actualWidth = width || ($form && $form.width)
      if (actualSize) classNames.push(`is-${actualSize}`)
      if (actualWidth) classNames.push(`is-${actualWidth}`)
      return classNames
    },
    selectedValues () {
      return this.selectedOptions.map(option => option.value)
    },
    showPlaceholder () {
      const empty = !this.selectedOptions.length
      return empty && !this.showInput
    },
    exceedMaxChipCount () {
      return this.selectedOptions.length > this.maxChipCount
    },
    maxChipText () {
      if (!this.exceedMaxChipCount) return
      const ommittedCount = this.selectedOptions.length - this.maxChipCount
      const { maxChipPlaceholder } = this
      if (typeof maxChipPlaceholder === 'function') {
        return maxChipPlaceholder(ommittedCount)
      }
      return maxChipPlaceholder
    }
  },

  watch: {
    isOpen () {
      if (this.isOpen) {
        this.menuStyle.minWidth = `${this.$el.offsetWidth}px`
        // reset positon to next tick
        // this would fix cases where input element in `<c-select autocomplete />`
        // is expanding (add a new line)
        this.$nextTick(this.positionMenu)
        window.addEventListener('click', this.onBodyClick, true)
      } else {
        window.removeEventListener('click', this.onBodyClick, true)
      }
    },

    value: {
      immediate: true,
      handler: function () {
        this.updateSelectedOptions()
      }
    },

    options: {
      immediate: true,
      handler: function () {
        this.updateSelectedOptions()
      }
    },

    selectedOptions: function () {
      if (!this.multiple || this.$isServer) return
      this.$nextTick(this.positionMenu)
    }
  },

  created () {
    // filter function throttled
    this.filterFunction = throttle((options, query) => {
      const filtered = this.filter(options, query)
      if (typeof filtered.then === 'function') {
        const promiseId = Date.now()
        this.promiseId = promiseId
        filtered.then(options => {
          if (this.promiseId > promiseId) return
          this.filteredOptions = normalizeOptions(options)
        })
      } else {
        this.filteredOptions = normalizeOptions(filtered)
      }
    }, this.filterThrottle, {
      leading: true,
      trailing: true
    })
  },

  mounted () {
    this.menuEl = this.$refs.menu

    // hover the option
    this.$on('option-activated', option => {
      this.activeOption = option
    })

    // reset position on window.resize
    this.__onresize = throttle(
      () => {
        if (this.isOpen) {
          this.positionMenu()
        }
      },
      this.$clair.defaultThrottleTime
    )
    window.addEventListener('resize', this.__onresize)

    // select the option
    this.$on('option-clicked', option => this.selectOption(option))

    // watch options, query to filter options
    this.$watch(
      function () {
        return [this.normalizedOptions, this.query, this.isOpen]
      },
      function filterOptions () {
        const { autocomplete, query } = this
        if (!autocomplete) {
          this.filteredOptions = this.normalizedOptions
          return
        }
        this.filterFunction(this.normalizedOptions, query)
      }
    )
  },

  beforeDestroy () {
    window.removeEventListener('resize', this.__onresize)
  },

  methods: {
    toggleOpen () {
      if (this.disabled) return
      if (this.isOpen) {
        this.close()
      } else {
        this.open()
      }
    },

    getOption (value) {
      const fn = option => option.value === value
      return this.filteredOptions.find(fn) ||
        this.normalizedOptions.find(fn) ||
        this.selectedOptions.find(fn)
    },

    updateSelectedOptions () {
      const { value, multiple } = this
      const isEmpty = value === void 0 || value === null
      if (isEmpty) {
        this.selectedOptions = []
        return
      }
      if (multiple) {
        const isArray = Array.isArray(value)
        const isEmptyArray = isArray && value.length === 0
        if (isEmptyArray) return
        const valueArr = isArray ? value : [value]
        this.selectedOptions = valueArr
          .map(v => this.getOption(v))
          .filter(option => option)
      } else {
        const option = this.getOption(value)
        this.selectedOptions = option ? [option] : []
      }
    },

    focusOnInput () {
      this.$nextTick(() => {
        this.$refs.input.focus()
      })
    },

    open () {
      this.isOpen = true;
      [this.activeOption] = this.filteredOptions
      if (this.showInput) {
        this.query = ''
        this.focusOnInput()
      }
    },

    close () {
      this.isOpen = false
    },

    getNextOption (current) {
      const currentIndex = this.filteredOptions.indexOf(current)
      const next = this.filteredOptions.find((option, index) => {
        return index > currentIndex && !option.disabled
      })
      return next || current
    },

    getPreviousOption (current) {
      let prev = null
      const currentIndex = this.filteredOptions.indexOf(current)
      for (let i = currentIndex - 1; i >= 0; i--) {
        if (!this.filteredOptions[i].disabled) {
          prev = this.filteredOptions[i]
          break
        }
      }
      return prev || current
    },

    activateNext () {
      const next = this.getNextOption(this.activeOption)
      this.activeOption = next
    },

    activatePrevious () {
      const prev = this.getPreviousOption(this.activeOption)
      this.activeOption = prev
    },

    selectPrevious () {
      const prev = this.getPreviousOption(this.selectedOptions[0])
      this.selectOption(prev)
    },

    selectNext () {
      const next = this.getNextOption(this.selectedOptions[0])
      this.selectOption(next)
    },

    selectOption (option) {
      if (this.multiple) {
        if (this.autocomplete) this.query = ''
        const isSelected = this.selectedOptions.includes(option)
        if (isSelected) return this.unselectOption(option)
        this.selectedOptions.push(option)
      } else {
        this.selectedOptions = [option]
        this.close()
      }
      this.emitChange()
    },

    unselectOption (option) {
      const index = this.selectedOptions.indexOf(option)
      this.selectedOptions.splice(index, 1)
      this.emitChange()
      // 输入框重新获得焦点
      this.focusOnInput()
    },

    positionMenu () {
      const { shouldMenuOverlap, canInput, menuEl } = this
      const { top, left, height } = getPosition(menuEl, this.$el)
      const overlap = shouldMenuOverlap && !canInput

      const offset = 4
      const menuTop = overlap ? top : (top + height + offset)
      menuEl.style.left = `${left}px`
      menuEl.style.top = `${menuTop}px`
      menuEl.style.zIndex = zIndex.next()
    },

    onBodyClick (e) {
      const isInSelect = this.$el.contains(e.target)
      const isInMenu = this.menuEl.contains(e.target)
      if (!isInSelect && !isInMenu) {
        this.close()
        this.$el.focus()
      }
    },

    onDeleteKey (e) {
      if (!this.query) this.selectedOptions.pop()
    },

    onKeyDown (e) {
      const keys = {
        ENTER: 13,
        ESC: 27,
        SPACE: 32,
        LEFT: 37,
        UP: 38,
        RIGHT: 39,
        DOWN: 40
      }
      const { keyCode } = e
      const {
        isOpen,
        multiple,
        open,
        close,
        selectOption,
        selectPrevious,
        selectNext,
        activeOption,
        activateNext,
        activatePrevious
      } = this

      if (Object.values(keys).includes(keyCode)) e.preventDefault()

      // open menu on space, up, down key
      const openTrigger = [
        keys.SPACE,
        keys.ENTER,
        keys.UP,
        keys.DOWN
      ].includes(keyCode)
      if (openTrigger && !isOpen) return open()

      // close menu on escape
      if (keyCode === keys.ESC && isOpen) return close()

      // press enter to select
      if (keyCode === keys.ENTER && isOpen) return selectOption(activeOption)

      // use left, right to navigate on closed state of non-multiple select
      const canSelect = !isOpen && !multiple
      if (canSelect && keyCode === keys.LEFT) return selectPrevious()
      if (canSelect && keyCode === keys.RIGHT) return selectNext()

      // use up, down to navigate on open state
      if (isOpen && keyCode === keys.UP) return activatePrevious()
      if (isOpen && keyCode === keys.DOWN) return activateNext()
    },

    onSearchInput (e) {
      this.$emit('searchinput', e.target.value)
    },

    emitChange () {
      const value = this.multiple ? this.selectedValues : this.selectedValues[0]
      this.$emit('change', value)
    }
  }
}
</script>


Check whether feature has geometry in method Tracking.updateViewport

<template>

Resource Tracking

                  {{{ annotations.icon(t) }}}

                    {{{ annotations.icon(t) }}} {{ t.label }}

                    <input class="switch-input" id="resourcesInViewport" type="checkbox" v-bind:checked="viewportOnly" @change="toggleViewportOnly" />

                      Viewport resources only

                  Restrict to viewport ({{extentFeaturesSize}}/{{featurelistSize}})

                 Toggles

                <input class="switch-input" id="toggleDBCAResourceLabels" type="checkbox" v-bind:disabled="clippedOnly" v-bind:checked="showDBCAResource" @change="showDBCAResource = !showDBCAResource" />

                  Show DBCA Resources

              Show DBCA Resources

                <input class="switch-input" id="toggleDFESResourceLabels" type="checkbox" v-bind:disabled="clippedOnly" v-bind:checked="showDFESResource" @change="showDFESResource = !showDFESResource" />

                  Show DFES Resources

              Show DFES Resources

                <input class="switch-input" id="toggleOtherExternalResourceLabels" type="checkbox" v-bind:disabled="clippedOnly" v-bind:checked="showOtherExternalResource" @change="showOtherExternalResource = !showOtherExternalResource" />

                  Show Other Aviation Resources

              Show Other Aviation Resources

                <input class="switch-input" id="toggleResourceLabels" type="checkbox" v-bind:checked="resourceLabels" @change="toggleResourceLabels"  v-bind:disabled="featureLabelDisabled"/>

                  Display resource labels

              Display resource labels

                <input class="switch-input" id="toggleResourceDirections" type="checkbox" v-bind:checked="resourceDirections" @change="toggleResourceDirections" v-bind:disabled="featureDirDisabled"/>

                  Display resource directions

              Display resource directions

                <input class="switch-input" id="toggleResourceInfo" type="checkbox" v-bind:disabled="!systemsetting.hoverInfoSwitchable" v-bind:checked="systemsetting.hoverInfo" @change="systemsetting.toggleHoverInfo" />

                  Display hovering resource info

              Display hovering resource info

                      <input class="switch-input" id="clippedResourceOnly" v-bind:disabled="clippedFeatures.length === 0" type="checkbox" v-model="clippedOnly"  @change="updateCQLFilter()"/>

                        Show saved selection

                    Show saved selection

                        Save selection ({{selectedFeaturesSize}})

                    ({{clippedFeatures.length}})

                <select name="select" v-model="groupFilter" @change="updateCQLFilter()"  v-bind:disabled="clippedOnly">
                  <option value="" selected>All resources</option>
                  <option value="symbolid LIKE '%aircraft'">Aircraft</option>
                  <option value="symbolid LIKE '%comms_bus'">Communications Bus</option>
                  <option value="symbolid LIKE '%gang_truck'">Gang Truck</option>
                  <option value="symbolid LIKE '%heavy_duty'">Heavy Duty</option>
                  <option value="(symbolid LIKE '%heavy_duty' OR symbolid LIKE '%gang_truck')">Gang Truck and Heavy Duty</option>
                  <option value="symbolid LIKE '%light_unit'">Light Unit</option>
                  <option value="(symbolid LIKE '%dozer' OR symbolid LIKE '%grader' OR symbolid LIKE '%loader')">Machinery</option>
                  <option value="(symbolid LIKE '%2_wheel_drive' OR symbolid LIKE '%4_wheel_drive_passenger' OR symbolid LIKE '%4_wheel_drive_ute')">Other Light Fleet</option>
                </select>

                <input type="search" v-model="search" placeholder="Find a resource" @keyup="updateFeatureFilter(500)">

                      <input class="switch-input" id="resourceHistory" type="checkbox" v-model="toggleHistory" @change="clearHistory" />

                    Query history

                    Query history

                  From:

                  <input type="text" id="historyFromDate" class="span2" v-model="historyFromDate" placeholder="YYYY-MM-DD HH:mm" v-bind:disabled="historyRange !== '-1'" readonly></input>

                  <select name="select" v-model="historyRange" >
                      <option value="21001">Last hour</option>
                      <option value="21003">Last 3 hours</option>
                      <option value="21024">Last 24 hours</option>
                      <option value="31007">Last 7 days</option>
                      <option value="31030">Last 30 days</option>
                      <option value="-1">User Defined</option>
                </select>

                  To:

                  <input type="text" id="historyToDate" class="span2" v-model="historyToDate" placeholder="YYYY-MM-DD HH:mm"  v-bind:disabled="historyRange !== '-1'"  readonly></input>

                  <button v-bind:disabled="queryHistoryDisabled" class="button" style="float: right" @click="historyCQLFilter">Go</button>

              <template v-for="f in featurelist" track-by="get('id')">

                  <a v-if="whoami.editVehicle && ['tracplus','dfes'].indexOf(f.get('source_device_type')) < 0" @click.stop.prevent="utils.editResource($event)" title="Edit resource" href="{{env.resourceTrackingService}}/sss_admin/tracking/device/{{ f.get('id') }}/change/" target="{{env.resourceTrackingService}}" class="button tiny secondary float-right">

 {{ f.get('label') }} ({{ ago(f.get('seen')) }})

              </template>

</template>


tab-item: Remove duplicate class. #715

tab-item: Remove duplicate class. #715

<template>
  <div class="vux-tab-item" :class="[currentSelected ? activeClass : '', {'vux-tab-selected': currentSelected, 'vux-tab-disabled': disabled}]" :style="style" @click="onItemClick">
    <slot></slot>
  </div>
</template>

<script>
import { childMixin } from '../../mixins/multi-items'

export default {
  mixins: [childMixin],
  props: {
    activeClass: String,
    disabled: Boolean
  },
  computed: {
    style () {
      return {
        borderWidth: this.$parent.lineWidth + 'px',
        borderColor: this.$parent.activeColor,
        color: this.currentSelected ? this.$parent.activeColor : this.disabled ? this.$parent.disabledColor : this.$parent.defaultColor,
        border: this.$parent.animate ? 'none' : 'auto'
      }
    }
  }
}
</script>

<template>
  <div class="vux-tab-item" :class="[currentSelected ? activeClass : '', {'vux-tab-selected': currentSelected, 'vux-tab-disabled': disabled, 'vux-tab-selected': currentSelected}]" :style="style" @click="onItemClick">
    <slot></slot>
  </div>
</template>

<script>
import { childMixin } from '../../mixins/multi-items'

export default {
  mixins: [childMixin],
  props: {
    activeClass: String,
    disabled: Boolean
  },
  computed: {
    style () {
      return {
        borderWidth: this.$parent.lineWidth + 'px',
        borderColor: this.$parent.activeColor,
        color: this.currentSelected ? this.$parent.activeColor : this.disabled ? this.$parent.disabledColor : this.$parent.defaultColor,
        border: this.$parent.animate ? 'none' : 'auto'
      }
    }
  }
}
</script>


encode link when saving design

encode link when saving design

<template>

		<menu isdesigner="true" v-bind:logtitle="title"></menu>

{{err}} {{err.stack ? err.stack : ""}}

{{info}}

				Visual

				Schema

	        <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>

Modal title

	      		Type
	      		<input type="text" class="form-control" id="inputType" disabled />

	        	Title
	        	<input type="text" class="form-control" id="inputTitle" />

		        Property Name
		        <input type="text" class="form-control" id="inputPropertyName" required />

	        	Description
	        	<input type="text" class="form-control" id="inputDescription" />

	        	Format
	        	<input type="text" class="form-control" id="inputFormat" />

	          Enum

	            <input type="text" class="form-control" placeholder="Enum value" id="inputEnum" />

	              <button class="btn btn-default" type="button" v-on:click="clickAddEnum();">Add</button>

	          Default
	          <input type="text" class="form-control" id="inputDefault" />

	        <button type="button" class="btn btn-primary gr-hotkey" id="editAccept" accesskey="a">Accept</button>
	        <button type="button" class="btn btn-default gr-hotkey" data-dismiss="modal" accesskey="c">Cancel</button>

String

				<input type="text" class="form-control" disabled />

Integer

				<input type="number" class="form-control" disabled />

Number

				<input type="number" class="form-control" disabled />

Boolean

				<select class="form-control" disabled>
					<option value="undefined"></option>
					<option value="1">true</option>
					<option value="0">false</option>
				</select>

Array

Object

			<button class="btn btn-primary" v-on:click="saveSchema">Save</button>
			<button class="btn btn-default" v-on:click="cancelSchemaChange">Cancel</button>

		<textarea rows="10" cols="80" id="schemaText" v-model="schemaText"></textarea>

</template>


age update

age update

<template>

Emmanuel Béziat

{{ age }} ({{ nextBirthday }})

Si vous voulez m’offrir un petit cadeau,
vous pouvez consulter ma liste de souhaits Amazon !

</template>


style perseus

style perseus

<template>

Hint:

                <button @click="checkAnswer" v-if="!complete" class="question-btn" id="check-answer-button">{{ checkText }}</button>
                <button @click="nextQuestion" v-if="complete && passNum >= 1" class="question-btn" id="next-question-button">{{ $tr("correct") }}</button>
                <button @click="nextContent" v-if="complete && passNum < 1" class="question-btn" id="next-content-button">{{ $tr("nextContent") }}</button>
                <attemptprogress id="attemptprogress" :recent-attempts="recentAttempts"></attemptprogress>
                <button @click="takeHint" id="take-hint">
                  {{ $tr("hint") }}
                </button>

        <button v-if="scratchpad" id="scratchpad-show">{{ $tr("showScratch") }}</button>
        <button v-else disabled id="scratchpad-not-available">{{ $tr("notAvailable") }}</button>

</template>

<template>

 {{attemptProgress}}

                  <button @click="checkAnswer" v-if="!complete" id="check-answer-button">{{ checkText }}</button>
                  <button @click="nextQuestion" v-if="complete && passNum >= 1" id="next-question-button">{{ $tr("correct") }}</button>
                  <button @click="nextContent" v-if="complete && passNum < 1" id="next-content-button">{{ $tr("nextContent") }}</button>
                  <button @click="takeHint">
                    {{ $tr("hint") }}
                  </button>

        <button v-if="scratchpad" id="scratchpad-show">{{ $tr("showScratch") }}</button>
        <button v-else disabled id="scratchpad-not-available">{{ $tr("notAvailable") }}</button>

</template>


docs(Index): fix api nav

docs(Index): fix api nav

<template>
  <v-app>
    <default-bar />

    <default-drawer />

    <default-fab-to-top />

    <default-view />

    <default-toc />

    <default-snackbar />
  </v-app>
</template>

<script>
  // Utilities
  import { get, sync } from 'vuex-pathify'

  // Data
  import nav from '@/data/nav'

  export default {
    name: 'DefaultLayout',

    beforeRouteEnter (to, from, next) {
      next(vm => vm.init())
    },

    components: {
      DefaultBar: () => import(
        /* webpackChunkName: "default-app-bar" */
        './AppBar'
      ),
      DefaultDrawer: () => import(
        /* webpackChunkName: "default-drawer" */
        './Drawer'
      ),
      DefaultFabToTop: () => import(
        /* webpackChunkName: "default-app-fab-to-top" */
        './FabToTop'
      ),
      DefaultSnackbar: () => import(
        /* webpackChunkName: "default-snackbar" */
        './Snackbar'
      ),
      DefaultToc: () => import(
        /* webpackChunkName: "default-toc" */
        './Toc'
      ),
      DefaultView: () => import(
        /* webpackChunkName: "default-view" */
        './View'
      ),
    },

    data: () => ({ unassigned: [] }),

    computed: {
      ...sync('app', [
        'modified',
        'nav',
      ]),
      pages: sync('pages/pages'),
      locale: get('route/params@locale'),
    },

    methods: {
      async init () {
        this.getModified()
        this.getPages()
      },
      async getModified () {
        const { default: modified } = await import(
          /* webpackChunkName: "modified-[request]" */
          `@docs/${this.locale}/modified`
        )

        this.modified = modified
      },
      async getPages () {
        const { default: api } = await import(
          /* webpackChunkName: "api-pages-[request]" */
          `@/api/${this.locale}/pages.json`
        )
        const { default: pages } = await import(
          /* webpackChunkName: "local-pages-[request]" */
          `@docs/${this.locale}/pages`
        )

        this.pages = { ...pages, ...api }
        this.genNav()
      },
      findItems (group) {
        const path = `/${this.locale}/${group}/`

        return Object.keys(this.pages).reduce((acc, cur) => {
          if (cur.startsWith(path)) {
            acc.push({
              title: this.pages[cur],
              to: cur,
            })
          }

          return acc
        }, [])
      },
      genItem (item, group) {
        let to = item.to
        const parent = !!item.items || item.group
        const items = parent && this.genItems(item.items, group)

        if (!item.href) {
          const page = to || (!items && item.title)
          const url = [
            this.locale,
            group,
            page,
          ].filter(v => v)

          to = `/${url.join('/')}/`
        }

        const title = this.$i18n.te(item.title)
          ? this.$i18n.t(item.title)
          : this.pages[to] || item.title

        const created = {
          ...item,
          group: parent && group,
          items,
          title,
          to,
        }

        for (const key in created) {
          if (created[key]) continue

          delete created[key]
        }

        return created
      },
      genItems (items, group) {
        if (!items) return this.findItems(group)

        return items.map(item => {
          return this.genItem(
            Object(item) === item
              ? item
              : { title: item },
            group,
          )
        })
      },
      genNav () {
        const items = []

        for (const item of nav) {
          items.push(this.genItem(item, item.group || (item.items && item.title)))
        }

        this.nav = items
      },
    },
  }
</script>

<template>
  <v-app>
    <default-bar />

    <default-drawer />

    <default-fab-to-top />

    <default-view />

    <default-toc />

    <default-snackbar />
  </v-app>
</template>

<script>
  // Utilities
  import { get, sync } from 'vuex-pathify'

  // Data
  import nav from '@/data/nav'

  export default {
    name: 'DefaultLayout',

    beforeRouteEnter (to, from, next) {
      next(vm => vm.init())
    },

    components: {
      DefaultBar: () => import(
        /* webpackChunkName: "default-app-bar" */
        './AppBar'
      ),
      DefaultDrawer: () => import(
        /* webpackChunkName: "default-drawer" */
        './Drawer'
      ),
      DefaultFabToTop: () => import(
        /* webpackChunkName: "default-app-fab-to-top" */
        './FabToTop'
      ),
      DefaultSnackbar: () => import(
        /* webpackChunkName: "default-snackbar" */
        './Snackbar'
      ),
      DefaultToc: () => import(
        /* webpackChunkName: "default-toc" */
        './Toc'
      ),
      DefaultView: () => import(
        /* webpackChunkName: "default-view" */
        './View'
      ),
    },

    data: () => ({ unassigned: [] }),

    computed: {
      ...sync('app', [
        'modified',
        'nav',
      ]),
      pages: sync('pages/pages'),
      locale: get('route/params@locale'),
    },

    methods: {
      async init () {
        this.getModified()
        this.getPages()
      },
      async getModified () {
        const { default: modified } = await import(
          /* webpackChunkName: "modified-[request]" */
          `@docs/${this.locale}/modified`
        )

        this.modified = modified
      },
      async getPages () {
        const { default: api } = await import(
          /* webpackChunkName: "api-pages-[request]" */
          `@/api/${this.locale}/pages.json`
        )
        const { default: pages } = await import(
          /* webpackChunkName: "local-pages-[request]" */
          `@docs/${this.locale}/pages`
        )

        this.pages = { ...pages, ...api }
        this.genNav()
      },
      findItems (group) {
        const path = `/${this.locale}/${group}/`

        return Object.keys(this.pages).reduce((acc, cur) => {
          if (cur.startsWith(path)) {
            acc.push({
              title: this.pages[cur],
              to: cur,
            })
          }

          return acc
        }, [])
      },
      genItem (item, group) {
        let to = item.to
        const parent = !!item.items
        const items = parent && this.genItems(item.items, group)

        if (!item.href) {
          const page = to || (!items && item.title)
          const url = [
            this.locale,
            group,
            page,
          ].filter(v => v)

          to = `/${url.join('/')}/`
        }

        const title = this.$i18n.te(item.title)
          ? this.$i18n.t(item.title)
          : this.pages[to] || item.title

        const created = {
          ...item,
          group: parent && group,
          items,
          title,
          to,
        }

        for (const key in created) {
          if (created[key]) continue

          delete created[key]
        }

        return created
      },
      genItems (items, group) {
        if (!items) return this.findItems(group)

        return items.map(item => {
          return this.genItem(
            Object(item) === item
              ? item
              : { title: item },
            group,
          )
        })
      },
      genNav () {
        const items = []

        for (const item of nav) {
          items.push(this.genItem(item, item.group || (item.items && item.title)))
        }

        this.nav = items
      },
    },
  }
</script>


eslint

eslint

<template>
  <uw-card
    v-if="showCard"
    :loaded="isReadyAdvisers && isReadyProfile"
    :errored="isErroredAdvisers || isErroredProfile"
    :errored-show="showError"
  >
    <template #card-heading>

        Your Advisers

    </template>
    <template #card-body>

{{ adviser.program }}

{{ adviser.full_name }}
              ({{ adviser.pronouns }})

{{ adviser.email_address }}

{{ formatPhoneNumberDisaply(adviser.phone_number) }}

              Make an appointment online

      <uw-card-property title="Major" class="mt-3" :no-margin-bottom="true">

            <template v-for="(termMajor, index) in termMajors">

                {{ degreeListString(termMajor.majors) }}

                Beginning {{ titilizeTerm(termMajor.quarter) }} {{ termMajor.year }}:

                 0">
                  {{ degreeListString(termMajor.majors) }}

                  None

            </template>

      </uw-card-property>
    </template>
    <template #card-error>
      An error occurred and MyUW cannot load your adviser information right now.
      In the meantime, to contact a general adviser, try the

        Undergraduate Advising
       page and to contact a departmental or major adviser, try the

        Departmental and Major Advising Offices
       page.
    </template>
  </uw-card>
</template>

<template>
  <uw-card
    v-if="showCard"
    :loaded="isReadyAdvisers && isReadyProfile"
    :errored="isErroredAdvisers || isErroredProfile"
    :errored-show="showError"
  >
    <template #card-heading>

        Your Advisers

    </template>
    <template #card-body>

{{ adviser.program }}

{{ adviser.full_name }} ({{ adviser.pronouns }})

{{ adviser.email_address }}

{{ formatPhoneNumberDisaply(adviser.phone_number) }}

              Make an appointment online

      <uw-card-property title="Major" class="mt-3" :noMarginBottom="true">

            <template v-for="(termMajor, index) in termMajors">

                {{ degreeListString(termMajor.majors) }}

                Beginning {{ titilizeTerm(termMajor.quarter) }} {{ termMajor.year }}:

                 0">
                  {{ degreeListString(termMajor.majors) }}

                  None

            </template>

      </uw-card-property>
    </template>
    <template #card-error>
      An error occurred and MyUW cannot load your adviser information right now. In the meantime, to contact a general adviser, try the
      Undergraduate Advising page and to contact a departmental or major adviser, try the
      Departmental and Major Advising Offices page.
    </template>
  </uw-card>
</template>


Уменьшение отступа

Уменьшение отступа

<template>

    <slot></slot>

</template>

<template>

    <slot></slot>

</template>


Pagination

Pagination

<template>
  <div id="characters">
    <vue-headful :title="title" />

    <v-breadcrumbs :items="breadcrumbs">
      <v-icon slot="divider">chevron_right</v-icon>
    </v-breadcrumbs>

    <v-progress-linear :indeterminate="true" v-if="!loaded"></v-progress-linear>

    <v-pagination v-model="current_page" :length="total_pages" v-if="loaded"></v-pagination>

    <v-card v-if="loaded">
      <v-container fluid grid-list-lg>
        <v-layout row wrap>
          <character-item v-for="character in characters"
                          v-bind:key="character.id"
                          v-bind="character">
          </character-item>
        </v-layout>
      </v-container>
    </v-card>

    <v-pagination v-model="current_page" :length="total_pages" v-if="loaded"></v-pagination>
  </div>
</template>

<script>
  import { mapActions } from 'vuex';

  import CharacterItem from './character-item.vue';

  export default {
    data () {
      return {
        title: 'EveMonk: EveOnline management suite',
        loaded: false,
        characters: [],
        current_page: 1,
        total_count: null,
        total_pages: null,
        corporation_id: null,
        breadcrumbs: [
          {
            text: 'Home',
            to: { name: 'welcome' },
            exact: true
          },
        ]
      }
    },

    components: {
      'character-item': CharacterItem,
    },

    watch: {
      current_page: function (page) {
        this.fetchUniverseCorporationCharacters({ id: this.corporation_id, page: page }).then(response => {
          if (response.status === 200) {
            this.total_count = response.data.total_count;
            this.total_pages = response.data.total_pages;
            this.characters = response.data.characters;

            this.$router.push({
              name: 'universe_corporation_characters',
              params: {
                id: this.corporation_id
              },
              query: {
                page: page
              }
            });
          }
        });
      },

      // '$route.query.page': function (page) {
      //   if (page !== undefined && parseInt(page) !== this.current_page) {
      //     this.current_page = parseInt(page);
      //   }
      // }
    },

    created () {
      this.corporation_id = this.$route.params.id;

      let id = this.$route.params.id;

      this.fetchUniverseCorporation(id).then(response => {
        if (response.status === 200) {
          let corporation = response.data.corporation;

          this.corporation = corporation;

          this.breadcrumbs.push({
            text: corporation.name,
            to: {
              name: 'universe_corporation',
              params: {
                id: corporation.id
              }
            },
            exact: true
          });

          this.breadcrumbs.push({
            text: 'Characters',
            to: {
              name: 'universe_corporation_characters',
              params: {
                id: corporation.id
              }
            },
            exact: true,
            disabled: true
          });

          this.title = `Characters in corporation "${corporation.name}" (${corporation.ticker}) | EveMonk: EveOnline management suite`;
        }
      });

      let page = this.$route.query.page;

      if (page !== undefined) {
        this.current_page = parseInt(page);
      }

      this.fetchUniverseCorporationCharacters({ id: id, page: this.current_page }).then(response => {
        if (response.status === 200) {
          this.total_count = response.data.total_count;
          this.total_pages = response.data.total_pages;
          this.characters = response.data.characters;
          this.loaded = true;
        }
      });
    },

    methods: {
      ...mapActions([
        'fetchUniverseCorporation',
        'fetchUniverseCorporationCharacters'
      ])
    }
  }
</script>

<template>
  <div id="characters">
    <vue-headful :title="title" />

    <v-breadcrumbs :items="breadcrumbs">
      <v-icon slot="divider">chevron_right</v-icon>
    </v-breadcrumbs>

    <v-progress-linear :indeterminate="true" v-if="!loaded"></v-progress-linear>

    <v-pagination v-model="current_page" :length="total_pages" v-if="loaded"></v-pagination>

    <v-card v-if="loaded">
      <v-container fluid grid-list-lg>
        <v-layout row wrap>
          <character-item v-for="character in characters"
                          v-bind:key="character.id"
                          v-bind="character">
          </character-item>
        </v-layout>
      </v-container>
    </v-card>

    <v-pagination v-model="current_page" :length="total_pages" v-if="loaded"></v-pagination>
  </div>
</template>

<script>
  import { mapActions } from 'vuex';

  import CharacterItem from './character-item.vue';

  export default {
    data () {
      return {
        title: 'EveMonk: EveOnline management suite',
        loaded: false,
        characters: [],
        current_page: 1,
        total_count: null,
        total_pages: null,
        corporation_id: null,
        breadcrumbs: [
          {
            text: 'Home',
            to: { name: 'welcome' },
            exact: true
          },
        ]
      }
    },

    components: {
      'character-item': CharacterItem,
    },

    created () {
      this.corporation_id = this.$route.params.id;

      let id = this.$route.params.id;

      this.fetchUniverseCorporation(id).then(response => {
        if (response.status === 200) {
          let corporation = response.data.corporation;

          this.corporation = corporation;

          this.breadcrumbs.push({
            text: corporation.name,
            to: {
              name: 'universe_corporation',
              params: {
                id: corporation.id
              }
            },
            exact: true
          });

          this.breadcrumbs.push({
            text: 'Characters',
            to: {
              name: 'universe_corporation_characters',
              params: {
                id: corporation.id
              }
            },
            exact: true,
            disabled: true
          });

          this.title = `Characters in corporation "${corporation.name}" (${corporation.ticker}) | EveMonk: EveOnline management suite`;
        }
      });

      let page = this.$route.query.page;

      if (page !== undefined) {
        this.current_page = parseInt(page);
      }

      this.fetchUniverseCorporationCharacters({ id: id, page: this.current_page }).then(response => {
        if (response.status === 200) {
          this.total_count = response.data.total_count;
          this.total_pages = response.data.total_pages;
          this.characters = response.data.characters;
          this.loaded = true;
        }
      });
    },

    methods: {
      ...mapActions([
        'fetchUniverseCorporation',
        'fetchUniverseCorporationCharacters'
      ])
    }
  }
</script>


Error options handled.

Error options handled.

<template lang="pug">
	select.form-control(v-model="value", :disabled="disabled", :name="schema.inputName", :id="getFieldID(schema)")
		option(v-if="!selectOptions.hideNoneSelectedText", :disabled="schema.required", :value="null", :selected="value == undefined") {{ selectOptions.noneSelectedText || "<Nothing selected>" }}

		template(v-for="item in items")
			optgroup(v-if="item.group", :label="getGroupName(item)")
				option(v-if="item.ops", v-for="i in item.ops", :value="getItemValue(i)") {{ getItemName(i) }}

			option(v-if="!item.group", :value="getItemValue(item)") {{ getItemName(item) }}
</template>

<script>
	import {isObject, find} from "lodash";
	import abstractField from "../abstractField";

	export default {
		mixins: [ abstractField ],

		computed: {
			selectOptions() {
				return this.schema.selectOptions || {};
			},

			items() {
				let values = this.schema.values;
				if (typeof(values) == "function") {
					return this.groupValues(values.apply(this, [this.model, this.schema]));
				} else
					return this.groupValues(values);
			}
		},

		methods: {

			groupValues(values){
				let array = [];
				let arrayElement = {};

				values.forEach((item) => {

					arrayElement = null;

					if(item.group && isObject(item)){
					// There is in a group.

						// Find element with this group.
						arrayElement = find(array, i => i.group == item.group);

						if(arrayElement){
							// There is such a group.

							arrayElement.ops.push({
								id: item.id,
								name: item.name
							});
						}else{
							// There is not such a group.

							// Initialising.
							arrayElement = {
								group:"",
								ops:[]
							};

							// Set group.
							arrayElement.group = item.group;

							// Set Group element.
							arrayElement.ops.push({
								id: item.id,
								name: item.name
							});

							// Add array.
							array.push(arrayElement);
						}
					}else{
						// There is not in a group.
						array.push(item);
					}
				});

				// With Groups.
				return array;
			},

			getGroupName(item){
				if(item && item.group){
					return item.group;
				}

				throw "Group name is missing! https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items";
			},

			getItemValue(item) {
				if (isObject(item)){
					if (typeof this.schema["selectOptions"] !== "undefined" && typeof this.schema["selectOptions"]["value"] !== "undefined") {
						return item[this.schema.selectOptions.value];
					} else {
						// Use 'id' instead of 'value' cause of backward compatibility
						if (typeof item["id"] !== "undefined") {
							return item.id;
						} else {
							throw "`id` is not defined. If you want to use another key name, add a `value` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items";
						}
					}
				} else {
					return item;
				}
			},

			getItemName(item) {
				if (isObject(item)){
					if (typeof this.schema["selectOptions"] !== "undefined" && typeof this.schema["selectOptions"]["name"] !== "undefined") {
						return item[this.schema.selectOptions.name];
					} else {
						if (typeof item["name"] !== "undefined") {
							return item.name;
						} else {
							throw "`name` is not defined. If you want to use another key name, add a `name` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items";
						}
					}
				} else {
					return item;
				}
			}
		}
	};
</script>

<style lang="sass">
</style>

<template lang="pug">
	select.form-control(v-model="value", :disabled="disabled", :name="schema.inputName", :id="getFieldID(schema)")
		option(v-if="!selectOptions.hideNoneSelectedText", :disabled="schema.required", :value="null", :selected="value == undefined") {{ selectOptions.noneSelectedText || "<Nothing selected>" }}

		template(v-for="item in items")
			optgroup(v-if="item.group", :label="getGroupName(item)")
				option(v-if="item.ops", v-for="i in item.ops", :value="getItemValue(i)") {{ getItemName(i) }}

			option(v-if="!item.group", :value="getItemValue(item)") {{ getItemName(item) }}
</template>

<script>
	import {isObject, find} from "lodash";
	import abstractField from "../abstractField";

	export default {
		mixins: [ abstractField ],

		computed: {
			selectOptions() {
				return this.schema.selectOptions || {};
			},

			items() {
				let values = this.schema.values;
				if (typeof(values) == "function") {
					return this.dataSorting(values.apply(this, [this.model, this.schema]));
				} else
					return this.dataSorting(values);
			}
		},

		methods: {

			dataSorting(values){
				let array = [];
				let arrayElement = {};

				values.forEach((item) => {

					arrayElement = null;

					if(item.group){
					// There is in a group.

						// Find element with this group.
						arrayElement = find(array, i => {return i.group == item.group});

						if(arrayElement){
							// There is such a group.

							arrayElement.ops.push({
								id: item.id,
								name: item.name
							});
						}else{
							// There is not such a group.

							// Initialising.
							arrayElement = {
								group:"",
								ops:[]
							};

							// Set group.
							arrayElement.group = item.group;

							// Set Group element.
							arrayElement.ops.push({
								id: item.id,
								name: item.name
							});

							// Add array.
							array.push(arrayElement);
						}
					}else{
						// There is not in a group.
						array.push(item);
					}
				});

				// With Groups.
				return array;
			},

			getGroupName(item){
				if(item && item.group){
					return item.group;
				}

				throw "Group name is missing! https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items";
			},

			getItemValue(item) {
				if (isObject(item)){
					if (typeof this.schema["selectOptions"] !== "undefined" && typeof this.schema["selectOptions"]["value"] !== "undefined") {
						return item[this.schema.selectOptions.value];
					} else {
						// Use 'id' instead of 'value' cause of backward compatibility
						if (typeof item["id"] !== "undefined") {
							return item.id;
						} else {
							throw "`id` is not defined. If you want to use another key name, add a `value` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items";
						}
					}
				} else {
					return item;
				}
			},

			getItemName(item) {
				if (isObject(item)){
					if (typeof this.schema["selectOptions"] !== "undefined" && typeof this.schema["selectOptions"]["name"] !== "undefined") {
						return item[this.schema.selectOptions.name];
					} else {
						if (typeof item["name"] !== "undefined") {
							return item.name;
						} else {
							throw "`name` is not defined. If you want to use another key name, add a `name` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items";
						}
					}
				} else {
					return item;
				}
			}
		}
	};
</script>

<style lang="sass">
</style>


Fixed picker for the other fields & Time

/**
* Created by Christoffer Isenberg on 19-May-16.
*/

<template>

                {{ trans('schedule.start_date') }}

                <input v-model="event.start_date" type="text" name="start_date" id="inputStart_date" class="form-control" required="required" title="">

                {{ trans('schedule.end_date') }}

                <input v-model="event.end_date" type="text" name="start_date" id="inputEnd_date" class="form-control">

                {{ trans('schedule.start_time') }}

                <input v-model="event.start_time" type="text" name="start_time" id="inputStart_time" required="required" class="form-control">

                {{ trans('schedule.end_time') }}

                <input v-model="event.end_time" type="text" name="end_time" id="inputEnd_time" required="required" class="form-control">

</template>

/**
* Created by Christoffer Isenberg on 19-May-16.
*/

<template>

                {{ trans('schedule.start_date') }}

                <input v-model="event.start_date" type="text" name="start_date" id="inputStart_date" class="form-control" required="required" title="">

                {{ trans('schedule.end_date') }}

                <input type="date" v-model="event.end_date" name="end_date" id="inputEnd_date" class="form-control">

                {{ trans('schedule.start_time') }}

                <input type="time" v-model="event.start_time" name="start_time" id="inputStart_time" class="form-control">

                {{ trans('schedule.end_time') }}

                <input type="time" v-model="event.end_time" name="end_time" id="inputEnd_time" class="form-control">

</template>


Search: track-by => key, fixed #380

Search: track-by => key, fixed #380

<template>

        <input
        ref="input"
        @click="visible = true"
        type="search"
        v-model="currentValue"
        :placeholder="visible ? placeholder : ''"
        class="mint-searchbar-core">

        <slot>
          <x-cell v-for="(item, index) in result" :key="index" :title="item"></x-cell>
        </slot>

</template>

<template>

        <input
        ref="input"
        @click="visible = true"
        type="search"
        v-model="currentValue"
        :placeholder="visible ? placeholder : ''"
        class="mint-searchbar-core">

        <slot>
          <x-cell v-for="item in result" track-by="$index" :title="item"></x-cell>
        </slot>

</template>


fix the tab bar dynamic bug

fix the tab bar dynamic bug

<script>
  module.exports = {
    name: 'el-tab-pane',

    props: {
      label: String,
      labelContent: Function,
      name: String,
      closable: Boolean,
      disabled: Boolean
    },

    data() {
      return {
        counter: 0,
        transition: '',
        paneStyle: {
          position: 'relative'
        },
        isClosable: null,
        index: ''
      };
    },

    created() {
      const propsData = this.$options.propsData;
      if (propsData && typeof propsData.closable !== 'undefined') {
        this.isClosable = propsData.closable === '' || propsData.closable;
      } else {
        this.isClosable = this.$parent.closable;
      }
      if (!this.index) {
        this.index = this.$parent.$children.indexOf(this) + 1 + '';
      }
      if (this.$parent.panes) {
        this.$parent.panes.push(this);
      }
    },

    computed: {
      show() {
        return this.$parent.currentName === this.index;
      }
    },

    destroyed() {
      if (this.$el && this.$el.parentNode) {
        this.$el.parentNode.removeChild(this.$el);
      }
      const panes = this.$parent.panes;
      if (panes) {
        panes.splice(this, panes.indexOf(this));
      }
    },

    watch: {
      name: {
        immediate: true,
        handler(val) {
          this.index = val;
        }
      },
      closable(val) {
        this.isClosable = val;
      },
      '$parent.currentName'(newValue, oldValue) {
        if (this.index === newValue) {
          this.transition = newValue > oldValue ? 'slideInRight' : 'slideInLeft';
        }
        if (this.index === oldValue) {
          this.transition = oldValue > newValue ? 'slideInRight' : 'slideInLeft';
        }
      },
      label() {
        this.$parent.$forceUpdate();
      }
    }
  };
</script>
<template>
  <div class="el-tab-pane" v-show="show && $slots.default">
    <slot></slot>
  </div>
</template>

<script>
  module.exports = {
    name: 'el-tab-pane',

    props: {
      label: String,
      labelContent: Function,
      name: String,
      closable: Boolean,
      disabled: Boolean
    },

    data() {
      return {
        counter: 0,
        transition: '',
        paneStyle: {
          position: 'relative'
        },
        isClosable: null,
        index: ''
      };
    },

    created() {
      const propsData = this.$options.propsData;
      if (propsData && typeof propsData.closable !== 'undefined') {
        this.isClosable = propsData.closable === '' || propsData.closable;
      } else {
        this.isClosable = this.$parent.closable;
      }
      if (!this.index) {
        this.index = this.$parent.$children.indexOf(this) + 1 + '';
      }
      if (this.$parent.panes) {
        this.$parent.panes.push(this);
      }
    },

    computed: {
      show() {
        return this.$parent.currentName === this.index;
      }
    },

    destroyed() {
      if (this.$el && this.$el.parentNode) {
        this.$el.parentNode.removeChild(this.$el);
      }
      const panes = this.$parent.panes;
      if (panes) {
        panes.splice(this, panes.indexOf(this));
      }
    },

    watch: {
      name: {
        immediate: true,
        handler(val) {
          this.index = val;
        }
      },
      closable(val) {
        this.isClosable = val;
      },
      '$parent.currentName'(newValue, oldValue) {
        if (this.index === newValue) {
          this.transition = newValue > oldValue ? 'slideInRight' : 'slideInLeft';
        }
        if (this.index === oldValue) {
          this.transition = oldValue > newValue ? 'slideInRight' : 'slideInLeft';
        }
      }
    }
  };
</script>
<template>
  <div class="el-tab-pane" v-show="show && $slots.default">
    <slot></slot>
  </div>
</template>


fix issue #261

fix issue #261

<template>

        <template v-if="frozenCols.length > 0">

                            <template v-if="frozenTitleCols.length > 0">

                                    <td v-for="col in row"
                                        :class="[col.titleCellClassName]"
                                        :colspan="col.colspan" :rowspan="col.rowspan"

                                        @mousemove.stop="handleTitleMouseMove($event,col.fields)"
                                        @mousedown.stop="handleTitleMouseDown($event)"
                                        @mouseout.stop="handleTitleMouseOut()"
                                        @click.stop="titleCellClick(col.fields,col.title);"
                                        @dblclick.stop="titleCellDblClick(col.fields,col.title)">

                                                     <v-checkbox
                                                             @change="handleCheckAll"
                                                             :indeterminate="indeterminate"
                                                             v-model="isAllChecked"
                                                             :show-slot="false"
                                                             label="check-all"
                                                     ></v-checkbox>

                                            <v-dropdown class="v-table-dropdown" v-if="enableFilters(col.filters,col.fields)"
                                                        v-model="col.filters"
                                                        :show-operation="col.filterMultiple"
                                                        :is-multiple="col.filterMultiple"
                                                        @on-filter-method="filterEvent"
                                                        @change="filterConditionChange(col.filterMultiple)"
                                            >

                                            </v-dropdown>

                            </template>

                            <template v-else>

                                    <td v-for="col in frozenCols"
                                        :class="[col.titleCellClassName]"
                                        @mousemove.stop="handleTitleMouseMove($event,col.field)"
                                        @mousedown.stop="handleTitleMouseDown($event)"
                                        @mouseout.stop="handleTitleMouseOut()"
                                        @click.stop="titleCellClick(col.field,col.title);"
                                        @dblclick.stop="titleCellDblClick(col.field,col.title)">

                                                         <v-checkbox
                                                                 @change="handleCheckAll"
                                                                 :indeterminate="indeterminate"
                                                                 v-model="isAllChecked"
                                                                 :show-slot="false"
                                                                 label="check-all"
                                                         ></v-checkbox>

                                                <v-dropdown class="v-table-dropdown" v-if="enableFilters(col.filters)"
                                                            v-model="col.filters"
                                                            :show-operation="col.filterMultiple"
                                                            :is-multiple="col.filterMultiple"
                                                            @on-filter-method="filterEvent"
                                                            @change="filterConditionChange(col.filterMultiple)"
                                                >

                                                </v-dropdown>

                            </template>

                        <v-checkbox-group v-model="checkboxGroupModel" @change="handleCheckGroupChange">

                                        <div v-if="isCellMergeRender(rowIndex,col.field,item)"
                                             :class="['v-table-body-cell',showVerticalBorder ? 'vertical-border':'',showHorizontalBorder?'horizontal-border':'']"
                                             :style="{'width':getRowWidthByColSpan(rowIndex,col.field,item)+'px','height': getRowHeightByRowSpan(rowIndex,col.field,item)+'px','line-height':getRowHeightByRowSpan(rowIndex,col.field,item)+'px','text-align':col.columnAlign}"
                                             :title="col.overflowTitle ?  overflowTitle(item,rowIndex,col) :''"
                                             @click.stop="rowCellClick(rowIndex,item,col);cellEditClick($event,col.isEdit,item,col.field,rowIndex)"
                                             @dblclick.stop="rowCellDbClick(rowIndex,item,col)"
                                        >

                                            <component :rowData="item" :field="col.field ? col.field : ''"
                                                       :index="rowIndex"
                                                       :is="cellMerge(rowIndex,item,col.field).componentName"
                                                       @on-custom-comp="customCompFunc"></component>

                                        <div v-else
                                             :class="['v-table-body-cell',showVerticalBorder ? 'vertical-border':'',showHorizontalBorder?'horizontal-border':'']"
                                             :style="{'width':col.width+'px','height': rowHeight+'px','line-height':rowHeight+'px','text-align':col.columnAlign}"
                                             :title="col.overflowTitle ?  overflowTitle(item,rowIndex,col) :''"
                                             @click.stop="rowCellClick(rowIndex,item,col);cellEditClick($event,col.isEdit,item,col.field,rowIndex)"
                                             @dblclick.stop="rowCellDbClick(rowIndex,item,col)"
                                        >
                                         0">
                                            <component :rowData="item" :field="col.field ? col.field : ''"
                                                       :index="rowIndex" :is="col.componentName"
                                                       @on-custom-comp="customCompFunc"></component>

                                            <v-checkbox @change="handleCheckChange(item)" :show-slot="false"
                                                        :disabled="item._disabled" :label="rowIndex"></v-checkbox>

                                                {{item[col.field]}}

                        </v-checkbox-group>

 0"
                     :class="['v-table-footer','v-table-footer-class']"
                     :style="{'width': leftViewWidth+'px','height':footerTotalHeight}">

        </template>

                        <template v-if="noFrozenTitleCols.length > 0">

                                <td v-for="col in row"
                                    :class="[col.titleCellClassName]"
                                    :colspan="col.colspan" :rowspan="col.rowspan"
                                    @mousemove.stop="handleTitleMouseMove($event,col.fields)"
                                    @mousedown.stop="handleTitleMouseDown($event)"
                                    @mouseout.stop="handleTitleMouseOut()"
                                    @click.stop="titleCellClick(col.fields,col.title);"
                                    @dblclick.stop="titleCellDblClick(col.fields,col.title)">

                                                 <v-checkbox
                                                         @change="handleCheckAll"
                                                         :indeterminate="indeterminate"
                                                         v-model="isAllChecked"
                                                         :show-slot="false"
                                                         label="check-all"
                                                 ></v-checkbox>

                                        <v-dropdown class="v-table-dropdown" v-if="enableFilters(col.filters,col.fields)"
                                                    v-model="col.filters"
                                                    :show-operation="col.filterMultiple"
                                                    :is-multiple="col.filterMultiple"
                                                    @on-filter-method="filterEvent"
                                                    @change="filterConditionChange(col.filterMultiple)"
                                        >

                                        </v-dropdown>

                        </template>

                        <template v-else>

                                <td v-for="(col,colIndex) in noFrozenCols"
                                    :class="[col.titleCellClassName]"
                                    @mousemove.stop="handleTitleMouseMove($event,col.field)"
                                    @mousedown.stop="handleTitleMouseDown($event)"
                                    @mouseout.stop="handleTitleMouseOut()"
                                    @click.stop="titleCellClick(col.field,col.title);"
                                    @dblclick.stop="titleCellDblClick(col.field,col.title)">

                                                 <v-checkbox
                                                         @change="handleCheckAll"
                                                         :indeterminate="indeterminate"
                                                         v-model="isAllChecked"
                                                         :show-slot="false"
                                                         label="check-all"
                                                 ></v-checkbox>

                                            <v-dropdown class="v-table-dropdown" v-if="enableFilters(col.filters)"
                                                        v-model="col.filters"
                                                        :show-operation="col.filterMultiple"
                                                        :is-multiple="col.filterMultiple"
                                                        @on-filter-method="filterEvent"
                                                        @change="filterConditionChange(col.filterMultiple)"
                                            >

                                            </v-dropdown>

                        </template>

                <v-checkbox-group v-model="checkboxGroupModel" @change="handleCheckGroupChange">

                                <div v-if="isCellMergeRender(rowIndex,col.field,item)"
                                     :class="['v-table-body-cell',showVerticalBorder ? 'vertical-border':'',showHorizontalBorder?'horizontal-border':'']"
                                     :style="{'width':getRowWidthByColSpan(rowIndex,col.field,item)+'px','height': getRowHeightByRowSpan(rowIndex,col.field,item)+'px','line-height':getRowHeightByRowSpan(rowIndex,col.field,item)+'px','text-align':col.columnAlign}"
                                     :title="col.overflowTitle ?  overflowTitle(item,rowIndex,col) :''"
                                     @click.stop="rowCellClick(rowIndex,item,col);cellEditClick($event,col.isEdit,item,col.field,rowIndex)"
                                     @dblclick.stop="rowCellDbClick(rowIndex,item,col)"
                                >

                                    <component :rowData="item" :field="col.field ? col.field : ''" :index="rowIndex"
                                               :is="cellMerge(rowIndex,item,col.field).componentName"
                                               @on-custom-comp="customCompFunc"></component>

                                <div v-else
                                     :class="['v-table-body-cell',showVerticalBorder ? 'vertical-border':'',showHorizontalBorder?'horizontal-border':'']"
                                     :style="{'width':col.width+'px','height': rowHeight+'px','line-height':rowHeight+'px','text-align':col.columnAlign}"
                                     :title="col.overflowTitle ?  overflowTitle(item,rowIndex,col) :''"
                                     @click.stop="rowCellClick(rowIndex,item,col);cellEditClick($event,col.isEdit,item,col.field,rowIndex)"
                                     @dblclick.stop="rowCellDbClick(rowIndex,item,col)"
                                >
                                 0">
                                    <component :rowData="item" :field="col.field ? col.field : ''" :index="rowIndex"
                                               :is="col.componentName" @on-custom-comp="customCompFunc"></component>

                                        <v-checkbox @change="handleCheckChange(item)" :show-slot="false"
                                                    :disabled="item._disabled" :label="rowIndex"></v-checkbox>

                                     {{item[col.field]}}

                </v-checkbox-group>

 0"
                 :class="['v-table-footer','v-table-footer-class',vTableFooter]"
                 :style="{'width': rightViewWidth+'px','height':footerTotalHeight}">

        <loading
                v-if="isLoading"
                :loading-content="loadingContent"
                :title-rows="internalTitleRows"
                :title-row-height="titleRowHeight"
                :columns="internalColumns"
                :loading-opacity="loadingOpacity"
        ></loading>

</template>


Fix #1156

Fix #1156

<template>

		<template v-if="kind != 'image'">%fa:file%</template>

				{{ file.properties.width }}
				×
				{{ file.properties.height }}
				px

				{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}
				:
				{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}

			<mk-file-type-icon :type="file.type"/>{{ file.type }}

			{{ file.datasize | bytes }}

			%fa:R clock%<mk-time :time="file.created_at"/>

			<a :href="`${file.url}?download`" :download="file.name">
				%fa:download%%i18n:mobile.tags.mk-drive-file-viewer.download%

			<button @click="rename">
				%fa:pencil-alt%%i18n:mobile.tags.mk-drive-file-viewer.rename%
			</button>
			<button @click="move">
				%fa:R folder-open%%i18n:mobile.tags.mk-drive-file-viewer.move%
			</button>

				%fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif%

{{ exif ? JSON.stringify(exif, null, 2) : '' }}

				%fa:hashtag%%i18n:mobile.tags.mk-drive-file-viewer.hash%

			{{ file.md5 }}

</template>

<template>

		<img v-if="kind == 'image'" ref="img"
			:src="file.url"
			:alt="file.name"
			:title="file.name"
			@load="onImageLoaded"
			:style="`background-color:rgb(${ file.properties.average_color.join(',') })`">
		<template v-if="kind != 'image'">%fa:file%</template>

				{{ file.properties.width }}
				×
				{{ file.properties.height }}
				px

				{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}
				:
				{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}

			<mk-file-type-icon :type="file.type"/>{{ file.type }}

			{{ file.datasize | bytes }}

			%fa:R clock%<mk-time :time="file.created_at"/>

			<a :href="`${file.url}?download`" :download="file.name">
				%fa:download%%i18n:mobile.tags.mk-drive-file-viewer.download%

			<button @click="rename">
				%fa:pencil-alt%%i18n:mobile.tags.mk-drive-file-viewer.rename%
			</button>
			<button @click="move">
				%fa:R folder-open%%i18n:mobile.tags.mk-drive-file-viewer.move%
			</button>

				%fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif%

{{ exif ? JSON.stringify(exif, null, 2) : '' }}

				%fa:hashtag%%i18n:mobile.tags.mk-drive-file-viewer.hash%

			{{ file.md5 }}

</template>