---
nav: Avatars
meta:
  title: Avatar component
  description: The avatar component is used to control the size and border radius of an image. It can be used with numerous components to provide better visual context.
  keywords: avatars, vuetify avatar component, vue avatar component
related:
  - /components/badges/
  - /components/icons/
  - /components/lists/
---

# Avatars

The `v-avatar` component is typically used to display circular user profile pictures. This component will allow you to dynamically size and add a border radius of responsive images, icons, and text. A **tile** variation is available for displaying an avatar without border radius.

![Avatar Entry](https://cdn.vuetifyjs.com/docs/images/components-temp/v-avatar/v-avatar-entry.png)

---

## Usage

Avatars in their simplest form display content within a circular container.

<usage name="v-avatar" />

<entry />

## API

| Component | Description |
| - | - |
| [v-avatar](/api/v-avatar/) | Primary Component |

## Anatomy

The recommended placement of elements inside of `v-avatar` is:

* Place a [v-img](/components/images/) or [v-icon](/components/images/) component within the default *slot*
* Place textual content within the default *slot*

![Avatar Anatomy](https://cdn.vuetifyjs.com/docs/images/components-temp/v-avatar/v-avatar-anatomy.png)

| Element / Area | Description |
| - | - |
| 1. Container | The Avatar container that typically holds a [v-icon](/components/icons/) or [v-img](/components/images/) component |

<api-inline hide-links />

## Examples

### Props

#### Size

The `size` prop allows you to change the height and width of the avatar.

<example file="v-avatar/prop-size" />

#### Tile

The `rounded` prop can be used to remove the border radius from v-avatar leaving you with a simple square avatar.

<example file="v-avatar/prop-tile" />

### Slots

#### Default

The `v-avatar` default slot allows you to render content such as `v-icon` components, images, or text. Mix and match these with other props to create something unique.

<example file="v-avatar/slot-default" />

<discovery />

### Misc

#### Advanced usage

Combining an avatar with other components allows you to build beautiful user interfaces right out of the box.

<example file="v-avatar/misc-advanced" />

Another example combining avatar with menu.

<example file="v-avatar/misc-avatar-menu" />

#### Profile Card

Using the **tile** prop, we can create a sleek hard-lined profile card.

<example file="v-avatar/misc-profile-card" />


onClick:row={ (event, value) => emit('click:row', event, value) }

Displays linear progress bar. Can either be a String which specifies which color is applied to the progress bar (any material color or theme color - **primary**, **secondary**, **success**, **info**, **warning**, **error**) or a Boolean which uses the component **color** (set by color prop - if it's supported by the component) or the primary color


---
nav: Resize
meta:
  title: Resize directive
  description: The resize directive provides the ability to conditionally invoke functions when the screen is resized.
  keywords: resize, vuetify resize directive, vue resize directive, window resize directive
related:
  - /features/display-and-platform/
  - /components/grids/
  - /styles/flex/
---

# Resize directive

The `v-resize` directive can be used for calling specific functions when the window resizes.

<entry />

## Usage

Resize your window and observe the values change..

<example file="v-resize/usage" />

## API

| Directive | Description |
| - | - |
| [v-resize](/api/v-resize/) | The resize directive |

<api-inline hide-links />


keep keep keep replace keep keep keep keep

import { test } from '~util/testing'
import VTextField from '~components/VTextField'

test('VTextField.js', ({ mount }) => {
  it('should render component and match snapshot', () => {
    const wrapper = mount(VTextField)

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should pass events to internal input field', () => {
    const keyup = jest.fn()
    const component = {
      render (h) {
        return h(VTextField, { on: { keyUp: keyup }, props: { download: '' }, attrs: {} })
      }
    }
    const wrapper = mount(component)

    const input = wrapper.find('input')[0]
    input.trigger('keyUp', { keyCode: 65 })

    expect(keyup).toBeCalled()
  })

  it('should render aria-label attribute on text field element with label value and no id', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        label: 'Test'
      },
      attrs: {}
    })

    const inputGroup = wrapper.find('input')[0]
    expect(inputGroup.hasAttribute('aria-label', 'Test')).toBe(true)
    expect(`$attrs is readonly`).toHaveBeenWarned()
  })

  it('should not render aria-label attribute on text field element with no label value or id', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        label: null
      },
      attrs: {}
    })

    const inputGroup = wrapper.find('input')[0]
    expect(inputGroup.element.getAttribute('aria-label')).toBeFalsy()
    expect(`$attrs is readonly`).toHaveBeenWarned()
  })

  it('should not render aria-label attribute on text field element with id', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        label: 'Test'
      },
      attrs: {
        id: 'Test'
      }
    })

    const inputGroup = wrapper.find('input')[0]
    expect(inputGroup.element.getAttribute('aria-label')).toBeFalsy()
    expect(`$attrs is readonly`).toHaveBeenWarned()
  })

  it('should start out as invalid', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        rules: [(v) => !!v || 'Required']
      }
    })

    expect(wrapper.data().valid).toEqual(false)
  })

  it('should start validating on input', async () => {
    const wrapper = mount(VTextField, {})

    expect(wrapper.data().shouldValidate).toEqual(false)
    wrapper.setProps({ value: 'asd' })
    await wrapper.vm.$nextTick()
    expect(wrapper.data().shouldValidate).toEqual(true)
  })

  // This test breaks everything
  // it('should start validating on blur', async () => {
  //   const wrapper = mount(VTextField, {})

  //   const input = wrapper.find('input')[0]
  //   expect(wrapper.data().shouldValidate).toEqual(false)
  //   input.trigger('focus')
  //   await wrapper.vm.$nextTick()
  //   input.trigger('blur')
  //   await wrapper.vm.$nextTick()
  //   expect(wrapper.data().shouldValidate).toEqual(true)
  // })

  it('should not start validating on input if validate-on-blur prop is set', async () => {
    const wrapper = mount(VTextField, {
      propsData: {
        validateOnBlur: true
      }
    })

    expect(wrapper.data().shouldValidate).toEqual(false)
    wrapper.setProps({ value: 'asd' })
    await wrapper.vm.$nextTick()
    expect(wrapper.data().shouldValidate).toEqual(false)
  })

  it('should clear input value', async () => {
    const wrapper = mount(VTextField, {
      propsData: {
        clearable: true,
        value: 'foo'
      }
    })

    const clear = wrapper.find('.input-group__append-icon')[0]
    const input = jest.fn()
    wrapper.vm.$on('input', input)

    expect(wrapper.vm.inputValue).toBe('foo')

    clear.trigger('click')

    await wrapper.vm.$nextTick()

    expect(input).toHaveBeenCalledWith(null)
  })

  it('should not display counter when set to false', async () => {
    const wrapper = mount(VTextField, {
      propsData: {
        counter: true,
        max: 50
      }
    })

    expect(wrapper.find('.input-group__counter')[0]).not.toBe(undefined)
    expect(wrapper.html()).toMatchSnapshot()

    wrapper.setProps({ counter: false })
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.find('.input-group__counter')[0]).toBe(undefined)
  })
})


@import './settings/_theme.styl'

app($component)
  .application
    &--light
      $component($material-light)
    &--dark
      $component($material-dark)

theme($component, $name)
  light($component, $name)
  dark($component, $name)

light($component, $name)
  .application--light .{$name}
    $component($material-light)
  .application .theme--light.{$name}
    $component($material-light)

dark($component, $name)
  .application--dark .{$name}
    $component($material-dark)
  .application .theme--dark.{$name}
    $component($material-dark)


<script>
  import VBtn from '../VBtn'
  import VIcon from '../VIcon'
  import VProgressLinear from '../VProgressLinear'
  import VSelect from '../VSelect'

  import Filterable from '../../mixins/filterable'
  import Themeable from '../../mixins/themeable'
  import Head from './mixins/head'
  import Body from './mixins/body'
  import Foot from './mixins/foot'
  import Progress from './mixins/progress'

  import {
    createSimpleFunctional,
    getObjectValueByPath
  } from '../../util/helpers'

  export default {
    name: 'v-data-table',

    components: {
      VBtn,
      VIcon,
      VProgressLinear,
      VSelect,
      // Importing does not work properly
      'v-table-overflow': createSimpleFunctional('table__overflow')
    },

    data () {
      return {
        all: false,
        searchLength: 0,
        defaultPagination: {
          page: 1,
          rowsPerPage: 5,
          descending: false,
          totalItems: 0
        }
      }
    },

    mixins: [Head, Body, Filterable, Foot, Progress, Themeable],

    props: {
      headers: {
        type: Array,
        default: () => []
      },
      headerText: {
        type: String,
        default: 'text'
      },
      hideActions: Boolean,
      noResultsText: {
        type: String,
        default: 'No matching records found'
      },
      rowsPerPageItems: {
        type: Array,
        default () {
          return [
            5,
            10,
            25,
            { text: 'All', value: -1 }
          ]
        }
      },
      rowsPerPageText: {
        type: String,
        default: 'Rows per page:'
      },
      selectAll: [Boolean, String],
      search: {
        required: false
      },
      filter: {
        type: Function,
        default: (val, search) => {
          return val !== null &&
            ['undefined', 'boolean'].indexOf(typeof val) === -1 &&
            val.toString().toLowerCase().indexOf(search) !== -1
        }
      },
      customFilter: {
        type: Function,
        default: (items, search, filter) => {
          search = search.toString().toLowerCase()
          return items.filter(i => (
            Object.keys(i).some(j => filter(i[j], search))
          ))
        }
      },
      customSort: {
        type: Function,
        default: (items, index, isDescending) => {
          if (index === null) return items

          return items.sort((a, b) => {
            let sortA = getObjectValueByPath(a, index)
            let sortB = getObjectValueByPath(b, index)

            if (isDescending) {
              [sortA, sortB] = [sortB, sortA]
            }

            // Check if both are numbers
            if (!isNaN(sortA) && !isNaN(sortB)) {
              return sortA - sortB
            }

            // Check if both cannot be evaluated
            if (sortA === null && sortB === null) {
              return 0
            }

            [sortA, sortB] = [sortA, sortB]
              .map(s => (
                (s || '').toString().toLocaleLowerCase()
              ))

            if (sortA > sortB) return 1
            if (sortA < sortB) return -1

            return 0
          })
        }
      },
      value: {
        type: Array,
        default: () => []
      },
      items: {
        type: Array,
        required: true,
        default: () => []
      },
      totalItems: {
        type: Number,
        default: null
      },
      loading: {
        type: [Boolean, String],
        default: false
      },
      selectedKey: {
        type: String,
        default: 'id'
      },
      pagination: {
        type: Object,
        default: null
      }
    },

    computed: {
      classes () {
        return {
          'datatable table': true,
          'datatable--select-all': this.selectAll !== false,
          'theme--dark': this.dark,
          'theme--light': this.light
        }
      },
      computedPagination () {
        return this.pagination || this.defaultPagination
      },
      hasSelectAll () {
        return this.selectAll !== undefined && this.selectAll !== false
      },
      itemsLength () {
        if (this.search) return this.searchLength
        return this.totalItems || this.items.length
      },
      indeterminate () {
        return this.hasSelectAll && this.someItems && !this.everyItem
      },
      everyItem () {
        return this.filteredItems.length &&
          this.filteredItems.every(i => this.isSelected(i))
      },
      someItems () {
        return this.filteredItems.some(i => this.isSelected(i))
      },
      getPage () {
        const { rowsPerPage } = this.computedPagination

        return rowsPerPage === Object(rowsPerPage)
          ? rowsPerPage.value
          : rowsPerPage
      },
      pageStart () {
        return this.getPage === -1
          ? 0
          : (this.computedPagination.page - 1) * this.getPage
      },
      pageStop () {
        return this.getPage === -1
          ? this.itemsLength
          : this.computedPagination.page * this.getPage
      },
      filteredItems () {
        if (this.totalItems) return this.items

        let items = this.items.slice()
        const hasSearch = typeof this.search !== 'undefined' &&
          this.search !== null

        if (hasSearch) {
          items = this.customFilter(items, this.search, this.filter)
          this.searchLength = items.length
        }

        items = this.customSort(
          items,
          this.computedPagination.sortBy,
          this.computedPagination.descending
        )

        return this.hideActions &&
          !this.pagination
            ? items
            : items.slice(this.pageStart, this.pageStop)
      },
      selected () {
        const selected = {}
        this.value.forEach(i => (selected[i[this.selectedKey]] = true))
        return selected
      }
    },

    watch: {
      indeterminate (val) {
        if (val) this.all = true
      },
      someItems (val) {
        if (!val) this.all = false
      },
      search () {
        this.updatePagination({ page: 1, totalItems: this.itemsLength })
      },
      everyItem (val) {
        if (val) this.all = true
      }
    },

    methods: {
      updatePagination (val) {
        const pagination = this.pagination || this.defaultPagination
        const updatedPagination = Object.assign({}, pagination, val)

        if (this.pagination) {
          this.$emit('update:pagination', updatedPagination)
        } else {
          this.defaultPagination = updatedPagination
        }
      },
      isSelected (item) {
        return this.selected[item[this.selectedKey]]
      },
      sort (index) {
        const { sortBy, descending } = this.computedPagination
        if (sortBy === null) {
          this.updatePagination({ sortBy: index, descending: false })
        } else if (sortBy === index && !descending) {
          this.updatePagination({ descending: true })
        } else if (sortBy !== index) {
          this.updatePagination({ sortBy: index, descending: false })
        } else {
          this.updatePagination({ sortBy: null, descending: null })
        }
      },
      needsTR (row) {
        return row.length && row.find(c => c.tag === 'td')
      },
      genTR (children, data = {}) {
        return this.$createElement('tr', data, children)
      },
      toggle (value) {
        const selected = Object.assign({}, this.selected)
        this.filteredItems.forEach(i => (
          selected[i[this.selectedKey]] = value)
        )

        this.$emit('input', this.items.filter(i => (
          selected[i[this.selectedKey]]))
        )
      }
    },

    created () {
      const firstSortable = this.headers.find(h => (
        !('sortable' in h) || h.sortable)
      )

      this.defaultPagination.sortBy = firstSortable
        ? firstSortable.value
        : null

      if (!this.rowsPerPageItems.length) {
        console.warn('The prop \'rows-per-page-items\' in v-data-table can not be empty.')
      } else {
        this.defaultPagination.rowsPerPage = this.rowsPerPageItems[0]
      }

      this.defaultPagination.totalItems = this.itemsLength

      this.updatePagination(
        Object.assign({}, this.defaultPagination, this.pagination)
      )
    },

    render (h) {
      return h('v-table-overflow', {}, [
        h('table', {
          'class': this.classes
        }, [
          this.genTHead(),
          this.genTProgress(),
          this.genTBody(),
          this.genTFoot()
        ])
      ])
    }
  }
</script>

<style lang="stylus" src="../../stylus/components/_tables.styl"></style>
<style lang="stylus" src="../../stylus/components/_data-table.styl"></style>


import VAvatar from './VAvatar'

VAvatar.install = function install (Vue) {
  Vue.component(VAvatar.name, VAvatar)
}

export default VAvatar


<script>
  import { getObjectValueByPath } from '../../util/helpers'

  import VCard from '../VCard'
  import VCheckbox from '../VCheckbox'
  import {
    VList,
    VListTile,
    VListTileAction,
    VListTileContent,
    VListTileTitle
  } from '../VList'
  import VMenu from '../VMenu'

  import Filterable from '../../mixins/filterable'
  import Input from '../../mixins/input'
  import Autocomplete from './mixins/autocomplete'
  import Generators from './mixins/generators'

  import ClickOutside from '../../directives/click-outside'

  export default {
    name: 'v-select',

    inheritAttrs: false,

    components: {
      VCard,
      VCheckbox,
      VList,
      VListTile,
      VListTileAction,
      VListTileContent,
      VListTileTitle,
      VMenu
    },

    directives: {
      ClickOutside
    },

    mixins: [Autocomplete, Input, Filterable, Generators],

    data () {
      return {
        cachedItems: [],
        content: {},
        inputValue: (this.multiple || this.tags) && !this.value ? [] : this.value,
        isBooted: false,
        lastItem: 20,
        lazySearch: null,
        isActive: false,
        menuIsActive: false,
        searchTimeout: null,
        selectedIndex: -1,
        selectedItems: [],
        shouldBreak: false
      }
    },

    props: {
      appendIcon: {
        type: String,
        default: 'arrow_drop_down'
      },
      auto: Boolean,
      autocomplete: Boolean,
      bottom: Boolean,
      cacheItems: Boolean,
      chips: Boolean,
      clearable: Boolean,
      close: Boolean,
      debounceSearch: {
        type: [Number, String],
        default: 200
      },
      items: {
        type: Array,
        default: () => []
      },
      itemText: {
        type: String,
        default: 'text'
      },
      itemValue: {
        type: String,
        default: 'value'
      },
      itemDisabled: {
        type: String,
        default: 'disabled'
      },
      maxHeight: {
        type: [Number, String],
        default: 300
      },
      minWidth: {
        type: [Boolean, Number, String],
        default: false
      },
      multiple: Boolean,
      multiLine: Boolean,
      offset: Boolean,
      solo: Boolean,
      searchInput: {
        default: null
      },
      singleLine: Boolean,
      tags: Boolean,
      top: Boolean,
      returnObject: Boolean,
      overflow: Boolean,
      segmented: Boolean,
      editable: Boolean
    },

    computed: {
      classes () {
        return {
          'input-group--text-field input-group--select': true,
          'input-group--auto': this.auto,
          'input-group--overflow': this.overflow,
          'input-group--segmented': this.segmented,
          'input-group--editable': this.editable,
          'input-group--autocomplete': this.isAutocomplete,
          'input-group--single-line': this.singleLine || this.isDropdown,
          'input-group--multi-line': this.multiLine,
          'input-group--chips': this.chips,
          'input-group--solo': this.solo,
          'input-group--multiple': this.multiple
        }
      },
      computedContentClass () {
        const children = [
          'menu__content--select',
          this.auto ? 'menu__content--auto' : '',
          this.isDropdown ? 'menu__content--dropdown' : '',
          this.isAutocomplete ? 'menu__content--autocomplete' : ''
        ]

        return children.join(' ')
      },
      computedItems () {
        return this.filterDuplicates(this.items.concat(this.cachedItems))
      },
      filteredItems () {
        // If we are not actively filtering
        // Show all available items
        const items = (this.isAutocomplete &&
          this.isDirty &&
          this.searchValue === this.selectedItem)
          ? this.computedItems
          : this.filterSearch()

        return !this.auto ? items.slice(0, this.lastItem) : items
      },
      hideSelections () {
        return this.isAutocomplete &&
          !this.isMultiple &&
          this.isFocused &&
          this.isDirty &&
          !this.chips
      },
      isAutocomplete () {
        return this.autocomplete || this.editable || this.tags
      },
      isDirty () {
        return this.selectedItems.length > 0
      },
      isDropdown () {
        return this.segmented || this.overflow || this.editable || this.solo
      },
      isMultiple () {
        return this.multiple || this.tags
      },
      searchValue: {
        get () { return this.lazySearch },
        set (val) {
          if (!this.isAutocomplete ||
            this.selectedIndex > -1
          ) return

          this.lazySearch = val

          clearTimeout(this.searchTimeout)

          this.searchTimeout = setTimeout(() => {
            this.$emit('update:searchInput', val)
          }, this.debounceSearch)
        }
      },
      selectedItem () {
        if (this.isMultiple) return null

        return this.selectedItems.find(i => (
          this.getValue(i) === this.getValue(this.inputValue)
        ))
      }
    },

    watch: {
      inputValue (val) {
        // Populate selected items
        this.genSelectedItems(val)

        this.$emit('input', val)
      },
      isActive (val) {
        if (!val) {
          this.searchValue = null
          this.menuIsActive = false
          this.isFocused = false
          this.selectedIndex = -1
        } else {
          this.searchValue = this.getText(this.selectedItem)
        }

        this.lastItem += !val ? 20 : 0
      },
      isBooted () {
        this.$nextTick(() => {
          if (this.content) {
            this.content.addEventListener('scroll', this.onScroll, false)
          }
        })
      },
      items (val) {
        if (this.cacheItems) {
          this.cachedItems = this.filterDuplicates(this.cachedItems.concat(val))
        }

        this.$refs.menu.listIndex = -1

        this.searchValue && this.$nextTick(() => {
          this.$refs.menu.listIndex = 0
        })
      },
      menuIsActive (val) {
        if (!val) return

        this.isBooted = true
        this.isActive = true
      },
      isMultiple (val) {
        this.inputValue = val ? [] : null
      },
      searchValue (val) {
        // Wrap input to next line if overflowing
        if (this.$refs.input.scrollWidth > this.$refs.input.clientWidth) {
          this.shouldBreak = true
          this.$nextTick(this.$refs.menu.updateDimensions)
        } else if (val === null) {
          this.shouldBreak = false
        }

        // Activate menu if inactive and searching
        if (this.isActive && !this.menuIsActive) {
          this.menuIsActive = true
        }

        this.$refs.menu.listIndex = null

        this.$nextTick(() => {
          this.$refs.menu.listIndex = val ? 0 : -1
        })
      },
      selectedItems () {
        clearTimeout(this.searchTimeout)

        if (this.isAutocomplete) {
          this.$nextTick(this.$refs.menu.updateDimensions)
        }
      },
      value (val) {
        this.inputValue = val
        this.validate()
      }
    },

    created () {
      if (this.tags) this.selectedItems = this.inputValue
    },

    mounted () {
      this.$vuetify.load(() => {
        if (this._isDestroyed) return

        this.content = this.$refs.menu.$refs.content
        this.genSelectedItems()
      })
    },

    beforeDestroy () {
      if (this.isBooted) {
        if (this.content) {
          this.content.removeEventListener('scroll', this.onScroll, false)
        }
      }
    },

    methods: {
      blur () {
        this.$emit('blur')
        if (this.isAutocomplete && this.$refs.input) this.$refs.input.blur()
        this.$nextTick(() => (this.isActive = false))
      },
      changeSelectedIndex (keyCode) {
        if (keyCode === 32 ||
          ![8, 37, 39, 46].includes(keyCode)
        ) return

        const indexes = this.selectedItems.length - 1

        if (keyCode === 37) {
          this.selectedIndex = this.selectedIndex === -1
            ? indexes
            : this.selectedIndex - 1
        } else if (keyCode === 39) {
          this.selectedIndex = this.selectedIndex >= indexes
            ? -1
            : this.selectedIndex + 1
        } else if (this.selectedIndex === -1) {
          this.selectedIndex = indexes
          return
        }

        if (![8, 46].includes(keyCode)) return
        const newIndex = this.selectedIndex === indexes
          ? this.selectedIndex - 1
          : this.selectedItems[this.selectedIndex + 1]
          ? this.selectedIndex
          : -1

        this.selectedItems.splice(this.selectedIndex, 1)
        this.selectedIndex = newIndex
        this.$emit('change', this.selectedItems)
      },
      filterDuplicates (arr) {
        return arr.filter((el, i, self) => i === self.indexOf(el))
      },
      focus () {
        this.isActive = true
        this.isFocused = true

        if (this.$refs.input && this.isAutocomplete) {
          this.$refs.input.focus()
        }

        this.$emit('focus')
      },
      genDirectives () {
        return [{
          name: 'click-outside',
          value: () => (this.isActive = false)
        }]
      },
      genListeners () {
        const listeners = Object.assign({}, this.$listeners)
        delete listeners.input

        return {
          ...listeners,
          click: () => {
            if (this.disabled || this.readonly) return
            this.showMenuItems()
            this.selectedIndex = -1
          },
          focus: () => {
            if (this.disabled || this.readonly) return
            !this.isFocused && this.focus()
          },
          keydown: this.onKeyDown // Located in mixins/autocomplete.js
        }
      },
      genLabel () {
        const singleLine = this.singleLine || this.isDropdown
        if (singleLine && this.isDirty ||
          singleLine && this.isFocused && this.searchValue
        ) return null

        const data = {}

        if (this.id) data.attrs = { for: this.id }

        return this.$createElement('label', data, this.$slots.label || this.label)
      },
      getPropertyFromItem (item, field) {
        if (item !== Object(item)) return item

        const value = getObjectValueByPath(item, field)

        return typeof value === 'undefined' ? item : value
      },
      genSelectedItems (val) {
        val = val || this.inputValue

        let selectedItems = this.computedItems.filter(i => {
          if (!this.isMultiple) {
            return this.getValue(i) === this.getValue(val)
          } else {
            // Always return Boolean
            return val.find((j) => {
              return this.getValue(j) === this.getValue(i)
            }) !== undefined
          }
        })

        if (!selectedItems.length &&
          val != null &&
          this.tags
        ) {
          selectedItems = Array.isArray(val) ? val : [val]
        }

        this.selectedItems = selectedItems
      },
      getText (item) {
        return this.getPropertyFromItem(item, this.itemText)
      },
      getValue (item) {
        return this.getPropertyFromItem(item, this.itemValue)
      },
      inputAppendCallback () {
        if (this.clearable && this.isDirty) {
          const inputValue = this.isMultiple ? [] : null

          this.inputValue = inputValue
          this.$emit('change', inputValue)
          this.genSelectedItems()
        }

        this.showMenuItems()
        this.isAutocomplete && this.focus()
      },
      onScroll () {
        if (!this.isActive) {
          requestAnimationFrame(() => (this.content.scrollTop = 0))
        } else {
          const showMoreItems = (
            this.content.scrollHeight -
            (this.content.scrollTop +
            this.content.clientHeight)
          ) < 200

          if (showMoreItems) {
            this.lastItem += 20
          }
        }
      },
      selectItem (item) {
        if (!this.isMultiple) {
          this.inputValue = this.returnObject ? item : this.getValue(item)
          this.selectedItems = [item]
        } else {
          const selectedItems = []
          const inputValue = this.inputValue.slice()
          const i = this.inputValue.findIndex((i) => {
            return this.getValue(i) === this.getValue(item)
          })

          i !== -1 && inputValue.splice(i, 1) || inputValue.push(item)
          this.inputValue = inputValue.map((i) => {
            selectedItems.push(i)
            return this.returnObject ? i : this.getValue(i)
          })
          this.selectedItems = selectedItems
        }

        this.searchValue = !this.isMultiple || this.chips
          ? this.getText(this.selectedItem)
          : ''

        this.$emit('change', this.inputValue)

        this.$nextTick(() => {
          if (this.isAutocomplete &&
            this.$refs.input
          ) this.$refs.input.focus()
          else this.$el.focus()
        })
      },
      showMenuItems () {
        this.isActive = true
        this.menuIsActive = true
      }
    },

    render (h) {
      const data = {
        attrs: {
          tabindex: this.isAutocomplete || this.disabled ? -1 : this.tabindex,
          ...(this.isAutocomplete ? null : this.$attrs),
          role: this.isAutocomplete ? null : 'combobox'
        }
      }

      if (!this.isAutocomplete) {
        data.on = this.genListeners()
        data.directives = this.genDirectives()
      } else {
        data.on = {
          click: () => this.$refs.input.click()
        }
      }

      return this.genInputGroup([
        this.genSelectionsAndSearch(),
        this.genMenu()
      ], data)
    }
  }
</script>

<!-- TODO: Style between input-group and the components inside it should be
           better compartmentalized -->
<style lang="stylus" src="../../stylus/components/_text-fields.styl"></style>
<style lang="stylus" src="../../stylus/components/_input-groups.styl"></style>
<style lang="stylus" src="../../stylus/components/_select.styl"></style>


import VList from '~components/VList'
import { test } from '~util/testing'

test('VList.js', ({ mount }) => {
  it('should render component and match snapshot', () => {
    const wrapper = mount(VList)

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render a dense component and match snapshot', () => {
    const wrapper = mount(VList, {
      propsData: {
        dense: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render a subheader component and match snapshot', () => {
    const wrapper = mount(VList, {
      propsData: {
        subheader: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render a threeLine component and match snapshot', () => {
    const wrapper = mount(VList, {
      propsData: {
        threeLine: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render a twoLine component and match snapshot', () => {
    const wrapper = mount(VList, {
      propsData: {
        twoLine: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
  })
})


export default {
  methods: {
    genYears () {
      return this.$createElement('ul', {
        'class': 'picker--date__years',
        ref: 'years'
      }, this.genYearItems())
    },
    genYearItems () {
      const children = []
      for (let i = this.year + 100, length = this.year - 100; i > length; i--) {
        const date = new Date(i, this.month, this.day, 12)
        const buttonText = this.supportsLocaleFormat
          ? date.toLocaleDateString(this.locale, { year: 'numeric' })
          : i

        children.push(this.$createElement('li', {
          'class': {
            active: this.year === i
          },
          on: {
            click: e => {
              e.stopPropagation()

              let tableMonth = this.tableMonth + 1
              let day = this.day
              tableMonth = tableMonth < 10 ? `0${tableMonth}` : tableMonth
              day = day < 10 ? `0${day}` : day

              this.inputDate = `${i}-${tableMonth}-${day}`
              this.isSelected = false
            }
          }
        }, buttonText))
      }
      return children
    }
  }
}


it('should not start validating on input if validate-on-blur prop is set', async () => {

keep keep keep replace replace replace replace replace replace replace replace replace replace replace replace keep keep keep keep keep

// // it('should start validating on blur', async () => {

// //   const wrapper = mount(VTextField, {})

// //   const input = wrapper.find('input')[0]

// //   expect(wrapper.data().shouldValidate).toEqual(false)

// //   expect(wrapper.data().shouldValidate).toEqual(true)


require('./stylus/app.styl')
import Semver from 'semver'
import { devDependencies, version } from '../package.json'
import * as components from './components'
import * as directives from './directives'

function Vuetify (Vue) {
  const Vuetify = components.Vuetify

  Vue.use(Vuetify, {
    components,
    directives
  })
}

Vuetify.version = version

function checkVueVersion () {
  const vueDep = devDependencies.vue
  if (!Semver.satisfies(window.Vue.version, vueDep)) {
    console.warn(`Vuetify requires Vue version ${vueDep}`)
  }
}

if (typeof window !== 'undefined' && window.Vue) {
  window.Vue.version && checkVueVersion()
  window.Vue.use(Vuetify)
}

export default Vuetify


<script>
  import Input from '../../mixins/input'

  export default {
    name: 'v-text-field',

    mixins: [Input],

    inheritAttrs: false,

    data () {
      return {
        inputHeight: null,
        badInput: false
      }
    },

    props: {
      autofocus: Boolean,
      autoGrow: Boolean,
      box: Boolean,
      clearable: Boolean,
      counter: [Boolean, Number, String],
      fullWidth: Boolean,
      multiLine: Boolean,
      placeholder: String,
      prefix: String,
      rows: {
        default: 5
      },
      singleLine: Boolean,
      solo: Boolean,
      suffix: String,
      textarea: Boolean,
      type: {
        type: String,
        default: 'text'
      }
    },

    computed: {
      classes () {
        return {
          'input-group--text-field': true,
          'input-group--text-field-box': this.box,
          'input-group--single-line': this.singleLine || this.solo,
          'input-group--solo': this.solo,
          'input-group--multi-line': this.multiLine,
          'input-group--full-width': this.fullWidth,
          'input-group--prefix': this.prefix,
          'input-group--suffix': this.suffix,
          'input-group--textarea': this.textarea
        }
      },
      count () {
        let inputLength
        if (this.inputValue) inputLength = this.inputValue.toString().length
        else inputLength = 0

        return `${inputLength} / ${this.counterLength}`
      },
      counterLength () {
        const parsedLength = parseInt(this.counter, 10)
        return isNaN(parsedLength) ? 25 : parsedLength
      },
      inputValue: {
        get () {
          return this.value
        },
        set (val) {
          if (this.type === 'number') val = parseInt(val)

          this.$emit('input', val)

          this.lazyValue = val
        }
      },
      isDirty () {
        return this.lazyValue !== null &&
          typeof this.lazyValue !== 'undefined' &&
          this.lazyValue.toString().length > 0 ||
          this.badInput ||
          ['time', 'date', 'datetime-local', 'week', 'month'].includes(this.type)
      },
      shouldAutoGrow () {
        return (this.multiLine || this.textarea) && this.autoGrow
      }
    },

    watch: {
      isFocused (val) {
        if (!val) {
          const lazyValue = this.type === 'number'
            ? parseInt(this.lazyValue)
            : this.lazyValue

          this.$emit('change', lazyValue)
        }
      },
      value () {
        this.lazyValue = this.value
        !this.validateOnBlur && this.validate()
        this.shouldAutoGrow && this.calculateInputHeight()
      }
    },

    mounted () {
      this.$vuetify.load(() => {
        this.shouldAutoGrow && this.calculateInputHeight()
        this.autofocus && this.focus()
      })
    },

    methods: {
      calculateInputHeight () {
        this.inputHeight = null

        this.$nextTick(() => {
          const height = this.$refs.input
            ? this.$refs.input.scrollHeight
            : 0
          const minHeight = this.rows * 24
          const inputHeight = height < minHeight ? minHeight : height
          this.inputHeight = inputHeight + (this.textarea ? 4 : 0)
        })
      },
      onInput (e) {
        this.inputValue = e.target.value
        this.badInput = e.target.validity && e.target.validity.badInput
        this.shouldAutoGrow && this.calculateInputHeight()
      },
      blur (e) {
        this.$nextTick(() => {
          this.isFocused = false
          this.validate()
        })
        this.$emit('blur', e)
      },
      focus (e) {
        this.isFocused = true
        this.$refs.input.focus()
        this.$emit('focus', e)
      },
      genCounter () {
        return this.$createElement('div', {
          'class': {
            'input-group__counter': true,
            'input-group__counter--error': this.hasError
          }
        }, this.count)
      },
      genInput () {
        const tag = this.multiLine || this.textarea ? 'textarea' : 'input'

        const data = {
          style: {},
          domProps: {
            autofocus: this.autofocus,
            disabled: this.disabled,
            required: this.required,
            value: this.lazyValue
          },
          attrs: {
            ...this.$attrs,
            tabindex: this.tabindex,
            'aria-label': (!this.$attrs || !this.$attrs.id) && this.label // Label `for` will be set if we have an id
          },
          on: {
            ...this.$listeners,
            blur: this.blur,
            input: this.onInput,
            focus: this.focus,
            click: this.focus
          },
          ref: 'input'
        }

        if (this.shouldAutoGrow) {
          data.style.height = this.inputHeight && `${this.inputHeight}px`
        }

        if (this.placeholder) data.domProps.placeholder = this.placeholder

        if (!this.textarea && !this.multiLine) {
          data.domProps.type = this.type
        } else {
          data.domProps.rows = this.rows
        }

        const children = [this.$createElement(tag, data)]

        this.prefix && children.unshift(this.genFix('prefix'))
        this.suffix && children.push(this.genFix('suffix'))

        return children
      },
      genFix (type) {
        return this.$createElement('span', {
          'class': `input-group--text-field__${type}`
        }, this[type])
      },
      inputAppendCallback () {
        this.inputValue = null
        this.$nextTick(() => this.$refs.input.focus())
      }
    },

    render () {
      return this.genInputGroup(this.genInput(), { attrs: { tabindex: false } })
    }
  }
</script>

<style lang="stylus" src="../../stylus/components/_input-groups.styl"></style>
<style lang="stylus" src="../../stylus/components/_text-fields.styl"></style>


<script>
  import Themeable from '../../mixins/themeable'

  export default {
    name: 'v-toolbar',

    mixins: [Themeable],

    data: () => ({
      isExtended: false,
      isScrolling: false,
      marginTop: 0,
      previousScroll: null,
      target: null
    }),

    props: {
      absolute: Boolean,
      card: Boolean,
      dense: Boolean,
      extended: Boolean,
      fixed: Boolean,
      flat: Boolean,
      floating: Boolean,
      prominent: Boolean,
      scrollOffScreen: Boolean,
      scrollTarget: String,
      scrollThreshHold: {
        type: Number,
        default: 100
      }
    },

    computed: {
      classes () {
        return {
          'toolbar': true,
          'elevation-0': this.flat,
          'toolbar--absolute': this.absolute,
          'toolbar--card': this.card,
          'toolbar--dense': this.dense,
          'toolbar--fixed': this.fixed,
          'toolbar--floating': this.floating,
          'toolbar--prominent': this.prominent,
          'toolbar--extended': this.isExtended,
          'theme--dark': this.dark,
          'theme--light': this.light
        }
      },
      styles () {
        return {
          marginTop: this.marginTop
        }
      }
    },

    watch: {
      isScrolling (val) {
        if (!val) this.marginTop = 0
        else (this.marginTop = `${-this.$refs.content.clientHeight - 6}px`)
      }
    },

    methods: {
      onScroll () {
        if (typeof window === 'undefined') return

        if (!this.target) {
          this.target = this.scrollTarget
            ? document.querySelector(this.scrollTarget)
            : window
        }

        const currentScroll = this.scrollTarget
          ? this.target.scrollTop
          : this.target.pageYOffset || document.documentElement.scrollTop

        if (currentScroll < this.scrollThreshHold) return

        if (this.previousScroll === null) {
          this.previousScroll = currentScroll
        }

        this.isScrolling = this.previousScroll < currentScroll

        this.previousScroll = currentScroll
      }
    },

    render (h) {
      this.isExtended = this.extended || !!this.$slots.extension
      const children = []
      const data = {
        'class': this.classes,
        style: this.styles,
        on: this.$listeners
      }

      if (this.scrollOffScreen) {
        data.directives = [{
          name: 'scroll',
          value: {
            callback: this.onScroll,
            target: this.scrollTarget
          }
        }]
      }

      children.push(h('div', {
        'class': 'toolbar__content',
        ref: 'content'
      }, this.$slots.default))

      if (this.isExtended) {
        children.push(h('div', {
          'class': 'toolbar__extension'
        }, this.$slots.extension))
      }

      return h('nav', data, children)
    }
  }
</script>

<style lang="stylus" src="../../stylus/components/_toolbar.styl"></style>


keep keep keep keep keep replace replace replace replace replace replace replace keep

// This works in reality, but JSDOM failing again...

// it('should clear input value', async () => {

//   const wrapper = mount(VTextField, {


import VApp from '~components/VApp'
import { test } from '~util/testing'

test('VApp.js', ({ mount }) => {
  it('should have an application class', () => {
    const wrapper = mount(VApp)

    expect(wrapper.hasClass('application')).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should have a toolbar and footer class', () => {
    const wrapper = mount(VApp, {
      propsData: {
        toolbar: true,
        footer: true,
        fixedFooter: true
      }
    })

    expect(wrapper.hasClass('application--toolbar')).toBe(true)
    expect(wrapper.hasClass('application--footer')).toBe(true)
    expect(wrapper.hasClass('application--footer-fixed')).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should have data-app attribute', () => {
    const wrapper = mount(VApp)

    expect(wrapper.hasAttribute('data-app', 'true')).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should allow a custom id', () => {
    const wrapper = mount(VApp, {
      propsData: {
        id: 'inspire'
      }
    })

    expect(wrapper.hasAttribute('id', 'inspire')).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })
})


@import '../bootstrap'
@import '../theme'

/** Themes */
date-picker($material)
  .picker--date__years
    background: $material.cards

    li
      color: $material.text.primary

      &.active
        color: $theme.accent

  .picker--date__header
    color: $material.text.primary

  .picker--date__table
    table
      th
        color: $material.text.primary

      .btn
        &:not(.btn--disabled)\
        :not(.btn--active)\
        :not(.btn--current)\
        :not(:hover)
          color: $material.text.primary

        &--current
          color: $material.picker.background

        &--active
          color: $material.bg-color !important

        &--date-picker
          &:after
            background: $material.picker.background !important

theme(date-picker, "picker--date")

.picker--date
  color: #fff
  width: 100%

/** Years */
.picker--date__years
  background: #fff
  color: #000
  font-size: 18px
  font-weight: 400
  list-style-type: none
  max-height: 290px
  overflow: auto
  padding: 0
  text-align: center

  li
    cursor: pointer
    margin: 16px 0
    transition: .3s $transition.swing

    &:hover
      color: $theme.primary

    &.active
      color: $theme.primary
      font-size: 24px
      font-weight: 500
      margin: 20px 0

/** Title */
.picker--date__title
  justify-content: space-between
  flex-direction: column
  flex-wrap: wrap

  &-year
    align-items: center
    display: inline-flex
    font-size: 14px

  &-date
    font-size: 34px
    text-align: left

    > div
      position: relative

  &-year, &-date
    font-weight: 500
    transition: .3s $transition.swing
    width: 100%

    &:not(.active)
      cursor: pointer
      opacity: .6

    &:hover
      opacity: 1

/** Header */
.picker--date__header
  color: #000
  padding: 4px 16px

  &-selector
    align-items: center
    display: flex
    justify-content: space-between
    position: relative

    .btn
      color: #000
      margin: 0

    .icon
      cursor: pointer
      user-select: none

    &-date
      flex: 1
      text-align: center
      position: relative
      overflow: hidden

      strong
        transition: .3s $transition.swing
        display: block
        width: 100%

.picker--date
  &__table
    position: relative

  table
    transition: .3s $transition.swing
    top: 0

    thead
      th
        padding: 8px 0

    th
      color: rgba(#000, .54)
      font-weight: 600
      font-size: 12px

    th, td
      text-align: center
      width: 45px

    .btn
      margin: 0
      transition: none

      &:hover, .btn__content:before
        background-color: transparent !important

      &:after
        position: absolute
        content: ''
        left: 0
        top: 0
        height: 100%
        width: 100%

      &__content
        overflow: visible
        transition: none
        z-index: 1

    .btn.btn--current:not(.btn--active)
      color: $theme.primary

    .btn.btn--date-picker
      height: 32px
      width: 32px

      &:after
        background: $theme.primary !important
        opacity: 0
        transform: scale(0)

      &:not(.btn--active):not(.btn--current):not(.btn--disabled):hover
        color: #fff

        &:after
          opacity: .6
          transform: scale(1)

    .btn.btn--date-picker.btn--active
      color: #fff

      &:after
        background: $theme.primary !important
        opacity: 1
        transform: none

.picker--landscape
  .picker--date__years
    margin-left: 170px
    width: 330px


export default {
  props: {
    color: String
  },
  methods: {
    addColorClassChecks (classes) {
      const parts = this.color ? this.color.trim().split(' ') : ['']
      let color = parts[0] + '--text'
      if (parts.length > 1) color += ' text--' + parts[1]
      classes[color] = !!this.color
      return classes
    }
  }
}


//   const clear = wrapper.find('.input-group__append-icon')[0]


import VSnackbar from './VSnackbar'

VSnackbar.install = function install (Vue) {
  Vue.component(VSnackbar.name, VSnackbar)
}

export default VSnackbar


import { test } from '~util/testing'
import { VLayout } from '~components/VGrid'

const id = 'layout'

test('VLayout', ({ mount, functionalContext }) => {
  it('should render component and match snapshot', () => {
    const wrapper = mount(VLayout, functionalContext())

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component with custom id and match snapshot', () => {
    const wrapper = mount(VLayout, functionalContext({
      propsData: {
        id
      }
    }))

    expect(wrapper.html()).toMatchSnapshot()
  })
})


//   expect(wrapper.vm.inputValue).toBe('foo')


import VIcon from '../VIcon'
import Ripple from '../../directives/ripple'

export default {
  name: 'v-stepper-step',

  components: { VIcon },

  directives: { Ripple },

  inject: ['stepClick'],

  data () {
    return {
      isActive: false,
      isInactive: true
    }
  },

  props: {
    complete: Boolean,
    completeIcon: {
      type: String,
      default: 'check'
    },
    editIcon: {
      type: String,
      default: 'edit'
    },
    errorIcon: {
      type: String,
      default: 'warning'
    },
    editable: Boolean,
    rules: {
      type: Array,
      default: () => []
    },
    step: [Number, String]
  },

  computed: {
    classes () {
      return {
        'stepper__step': true,
        'stepper__step--active': this.isActive,
        'stepper__step--editable': this.editable,
        'stepper__step--inactive': this.isInactive,
        'stepper__step--error': this.hasError,
        'stepper__step--complete': this.complete
      }
    },
    hasError () {
      return this.rules.some(i => (i() !== true))
    }
  },

  methods: {
    click (e) {
      e.stopPropagation()

      if (this.editable) {
        this.stepClick(this.step)
      }
    },
    toggle (step) {
      this.isActive = step.toString() === this.step.toString()
      this.isInactive = Number(step) < Number(this.step)
    }
  },

  render (h) {
    const data = {
      'class': this.classes,
      directives: [{
        name: 'ripple',
        value: this.editable
      }],
      on: { click: this.click }
    }
    let stepContent

    if (this.hasError) {
      stepContent = [h('v-icon', {}, this.errorIcon)]
    } else if (this.complete) {
      if (this.editable) {
        stepContent = [h('v-icon', {}, this.editIcon)]
      } else {
        stepContent = [h('v-icon', {}, this.completeIcon)]
      }
    } else {
      stepContent = this.step
    }

    const step = h('span', { 'class': 'stepper__step__step' }, stepContent)
    const label = h('div', { 'class': 'stepper__label' }, this.$slots.default)

    return h('div', data, [step, label])
  }
}


import VForm from './VForm'

VForm.install = function install (Vue) {
  Vue.component(VForm.name, VForm)
}

export default VForm


import VBottomNav from './VBottomNav'
import VBtn from '../VBtn'
import { test } from '~util/testing'

test('VBottomNav.js', ({ mount, functionalContext }) => {
  it('should have a bottom-nav class', () => {
    const wrapper = mount(VBottomNav, {
      slots: {
        default: [VBtn, VBtn]
      }
    })

    expect(wrapper.hasClass('bottom-nav')).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should have prop classes', () => {
    const wrapper = mount(VBottomNav, {
      propsData: {
        absolute: true,
        shift: true
      },
      slots: {
        default: [VBtn, VBtn]
      }
    })

    expect(wrapper.hasClass('bottom-nav--absolute')).toBe(true)
    expect(wrapper.hasClass('bottom-nav--shift')).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should be hidden with a false value', () => {
    const wrapper = mount(VBottomNav, {
      propsData: { value: false },
      slots: {
        default: [VBtn, VBtn]
      }
    })

    expect(wrapper.hasClass('bottom-nav--active')).toBe(false)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should be visible with a true value', () => {
    const wrapper = mount(VBottomNav, {
      propsData: { value: true },
      slots: {
        default: [VBtn, VBtn]
      }
    })

    expect(wrapper.hasClass('bottom-nav--active')).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should output active btn when clicked', () => {
    const wrapper = mount(VBottomNav, {
      propsData: { value: true, active: 1 },
      slots: {
        default: [VBtn, VBtn]
      }
    })

    const change = jest.fn()
    wrapper.instance().$on('update:active', change)

    const btn = wrapper.find('.btn')[0]
    btn.trigger('click')

    expect(change).toBeCalledWith(0)
  })
})


import { test } from '~util/testing'
import VTextField from '~components/VTextField'

test('VTextField.js', ({ mount }) => {
  it('should render component and match snapshot', () => {
    const wrapper = mount(VTextField)

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should pass events to internal input field', () => {
    const keyup = jest.fn()
    const component = {
      render (h) {
        return h(VTextField, { on: { keyUp: keyup }, props: { download: '' }, attrs: {} })
      }
    }
    const wrapper = mount(component)

    const input = wrapper.find('input')[0]
    input.trigger('keyUp', { keyCode: 65 })

    expect(keyup).toBeCalled()
  })

  it('should render aria-label attribute on text field element with label value and no id', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        label: 'Test'
      },
      attrs: {}
    })

    const inputGroup = wrapper.find('input')[0]
    expect(inputGroup.hasAttribute('aria-label', 'Test')).toBe(true)
    expect(`$attrs is readonly`).toHaveBeenWarned()
  })

  it('should not render aria-label attribute on text field element with no label value or id', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        label: null
      },
      attrs: {}
    })

    const inputGroup = wrapper.find('input')[0]
    expect(inputGroup.element.getAttribute('aria-label')).toBeFalsy()
    expect(`$attrs is readonly`).toHaveBeenWarned()
  })

  it('should not render aria-label attribute on text field element with id', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        label: 'Test'
      },
      attrs: {
        id: 'Test'
      }
    })

    const inputGroup = wrapper.find('input')[0]
    expect(inputGroup.element.getAttribute('aria-label')).toBeFalsy()
    expect(`$attrs is readonly`).toHaveBeenWarned()
  })

  it('should start out as invalid', () => {
    const wrapper = mount(VTextField, {
      propsData: {
        rules: [(v) => !!v || 'Required']
      }
    })

    expect(wrapper.data().valid).toEqual(false)
  })

  it('should start validating on input', async () => {
    const wrapper = mount(VTextField, {})

    expect(wrapper.data().shouldValidate).toEqual(false)
    wrapper.setProps({ value: 'asd' })
    await wrapper.vm.$nextTick()
    expect(wrapper.data().shouldValidate).toEqual(true)
  })

  // This test breaks everything
  // it('should start validating on blur', async () => {
  //   const wrapper = mount(VTextField, {})

  //   const input = wrapper.find('input')[0]
  //   expect(wrapper.data().shouldValidate).toEqual(false)
  //   input.trigger('focus')
  //   await wrapper.vm.$nextTick()
  //   input.trigger('blur')
  //   await wrapper.vm.$nextTick()
  //   expect(wrapper.data().shouldValidate).toEqual(true)
  // })

  it('should not start validating on input if validate-on-blur prop is set', async () => {
    const wrapper = mount(VTextField, {
      propsData: {
        validateOnBlur: true
      }
    })

    expect(wrapper.data().shouldValidate).toEqual(false)
    wrapper.setProps({ value: 'asd' })
    await wrapper.vm.$nextTick()
    expect(wrapper.data().shouldValidate).toEqual(false)
  })

  it('should clear input value', async () => {
    const wrapper = mount(VTextField, {
      propsData: {
        clearable: true,
        value: 'foo'
      }
    })

    const clear = wrapper.find('.input-group__append-icon')[0]
    const input = jest.fn()
    wrapper.vm.$on('input', input)

    expect(wrapper.vm.inputValue).toBe('foo')

    clear.trigger('click')

    await wrapper.vm.$nextTick()

    expect(input).toHaveBeenCalledWith(null)
  })

  it('should not display counter when set to false', async () => {
    const wrapper = mount(VTextField, {
      propsData: {
        counter: true,
        max: 50
      }
    })

    expect(wrapper.find('.input-group__counter')[0]).not.toBe(undefined)
    expect(wrapper.html()).toMatchSnapshot()

    wrapper.setProps({ counter: false })
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.find('.input-group__counter')[0]).toBe(undefined)
  })
})


@import '../bootstrap'
@import '../theme'

/* Themes */
icon($material)
  color: $material.icons.active

  &:disabled
    color: $material.icons.inactive

theme(icon, "icon")

.icon
  align-items: center
  display: inline-flex
  font-size: 24px
  justify-content: center
  transition: $primary-transition
  vertical-align: middle

  &.icon--large
    font-size: 2.5rem

  &.icon--medium
    font-size: 2rem

  &.icon--x-large
    font-size: 3rem


@import '../bootstrap'
@import '../theme'

/** Theme */
switch($material)
  .input-group--selection-controls__container
    color: $material.selection-controls.active

  &:not(.input-group--dirty)
    .input-group--selection-controls__container
      // Must overwrite color helpers
      color: $material.selection-controls.track.inactive !important

  .input-group--selection-controls__ripple
    &:after
      background-color: $material.selection-controls.thumb.inactive

    &:not(.input-group--selection-controls__ripple--active)
      color: $material.selection-controls.track.inactive

    &--active:after
      background-color: $material.selection-controls.active

  .input-group--selection-controls__toggle
    color: $material.selection-controls.track.inactive

    &--active
      color: inherit

  &.input-group--disabled
    .input-group--selection-controls__ripple
      &:after
        background-color: $material.selection-controls.thumb.disabled !important

    .input-group--selection-controls__toggle
      color: $material.selection-controls.track.disabled !important

theme(switch, "switch")

.input-group.input-group--selection-controls
  z-index: 0

  &.switch
    min-width: 36px

    .input-group--selection-controls__container
      position: relative

      &[class*="--text"]
        .input-group--selection-controls__ripple--active:after
          background-color: currentColor

    .input-group--selection-controls__toggle
      background-color: currentColor
      color: inherit
      position: absolute
      height: 14px
      top: 50%
      left: 0
      width: 34px
      border-radius: 8px
      transform: translateY(-50%)

      &.input-group--selection-controls__toggle--active
        opacity: .5

    .input-group--selection-controls__ripple
      transform: translate3d(-15px, -24px, 0)
      transition: .3s $transition.fast-in-fast-out
      z-index: 1

      &:after
        content: ''
        position: absolute
        display: inline-block
        cursor: pointer
        width: 20px
        border-radius: 50%
        top: 50%
        left: 50%
        transform: translate3d(-50%, -50%, 0)
        height: 20px
        elevation(4)

      &--active
        transform: translate3d(2px, -24px, 0)

    label
      padding-left: 14px


import { test } from '~util/testing'
import VPagination from './VPagination'

test('VPagination.vue', ({ mount }) => {
  it('emits an event when next or previous is clicked', async () => {
    jest.useFakeTimers()
    const wrapper = mount(VPagination, {
      propsData: {
        length: 5
      }
    })
    jest.runAllTimers()

    await wrapper.vm.$nextTick()

    const previous = jest.fn()
    const next = jest.fn()

    wrapper.instance().$on('previous', previous)
    wrapper.instance().$on('next', next)

    const navigation = wrapper.find('.pagination__navigation')
    navigation[0].trigger('click')
    navigation[1].trigger('click')

    expect(next).toBeCalled()
    expect(previous).toBeCalled()
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should only render start and end of range if length is big', async () => {
    jest.useFakeTimers()
    const wrapper = mount(VPagination, {
      propsData: {
        length: 100
      }
    })
    jest.runAllTimers()

    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.find('.pagination__more').length).toEqual(1)
  })

  it('should only render middle of range if length is big and value is somewhere in the middle', async () => {
    jest.useFakeTimers()
    const wrapper = mount(VPagination, {
      propsData: {
        length: 100,
        value: 50
      }
    })
    jest.runAllTimers()

    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.find('.pagination__more').length).toEqual(2)
  })

  it('should use totalVisible prop if defined', async () => {
    jest.useFakeTimers()
    const wrapper = mount(VPagination, {
      propsData: {
        length: 100,
        value: 50,
        totalVisible: 10
      }
    })
    jest.runAllTimers()

    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.find('.pagination__more').length).toEqual(2)
    expect(wrapper.find('.pagination__item').length).toEqual(8)
  })
})


@import '../bootstrap'

.container
  flex: 1 1 100%
  margin: auto
  padding: $grid-gutters.lg
  width: 100%

  for size, width in $container-max-widths
    @media only screen and (min-width: width)
      max-width: width

  @media $display-breakpoints.xs-only
    padding: $grid-gutters.xl

  &.fluid
    max-width: 100%

  &.fill-height
    align-items: center
    display: flex

    .layout
      height: 100%
      flex: 1 1 auto

  &.grid-list
    for $size, $gutter in $grid-gutters
      &-{$size}
        padding: $gutter

        .layout
          .flex
            padding: ($gutter / 2)

        .layout:only-child
          margin: -($gutter / 2)

        .layout:not(:only-child)
          margin: auto -($gutter / 2)

        *:not(:only-child)
          .layout:first-child
            margin-top: -($gutter / 2)

          .layout:last-child
            margin-bottom: -($gutter / 2)

.layout
  display: flex
  flex: 1 1 auto
  flex-wrap: nowrap

  &.row
    flex-direction: row

    &.reverse
      flex-direction: row-reverse

  &.column
    flex-direction: column

    &.reverse
      flex-direction: column-reverse

  &.wrap
    flex-wrap: wrap

for size, width in $grid-breakpoints
  @media all and (min-width: width)
    for n in (1..$grid-columns)
      .flex.{size}{n}
        flex-basis: (n / $grid-columns * 100)%
        max-width: (n / $grid-columns * 100)%

      .flex.order-{size}{n}
        order: n

    for n in (0..$grid-columns)
      .flex.offset-{size}{n}
        margin-left: (n / $grid-columns * 100)%

.flex,
.child-flex > *
  flex: 1 1 auto

.align
  &-start
    align-items: flex-start

  &-end
    align-items: flex-end

  &-center
    align-items: center

  &-baseline
    align-items: baseline

.align-content
  &-start
    align-content: flex-start

  &-end
    align-content: flex-end

  &-center
    align-content: center

  &-space-between
    align-content: space-between

  &-space-around
    align-content: space-around

.justify
  &-start
    justify-content: flex-start

  &-end
    justify-content: flex-end

  &-center
    justify-content: center

  &-space-around
    justify-content: space-around

  &-space-between
    justify-content: space-between

.spacer
  flex-grow: 1 !important

.scroll-y
  overflow-y: auto

.fill-height
  height: 100%

.hide-overflow
  overflow: hidden !important

.show-overflow
  overflow: visible !important

.no-wrap
  white-space: nowrap

.ellipsis
  white-space: nowrap
  overflow: hidden
  text-overflow: ellipsis

// Display helpers
.d-flex
  display: flex

.d-inline-flex
  display: inline-flex

.d-flex,
.d-inline-flex
  > *
    flex: 1 1 auto

.d-block
  display: block

.d-inline-block
  display: inline-block


<script>
  import VIcon from '../VIcon'

  import Resize from '../../directives/resize'

  export default {
    name: 'v-pagination',

    directives: { Resize },

    props: {
      circle: Boolean,
      disabled: Boolean,
      length: {
        type: Number,
        default: 0,
        validator: val => val % 1 === 0
      },
      totalVisible: [Number, String],
      nextIcon: {
        type: String,
        default: 'chevron_right'
      },
      prevIcon: {
        type: String,
        default: 'chevron_left'
      },
      value: {
        type: Number,
        default: 0
      }
    },

    data () {
      return {
        maxButtons: 0
      }
    },

    watch: {
      value () {
        this.init()
      }
    },

    computed: {
      classes () {
        return {
          'pagination': true,
          'pagination--circle': this.circle,
          'pagination--disabled': this.disabled
        }
      },

      items () {
        const maxLength = this.totalVisible || this.maxButtons
        if (this.length <= maxLength) {
          return this.range(1, this.length)
        }

        const even = maxLength % 2 === 0 ? 1 : 0
        const left = Math.floor(maxLength / 2)
        const right = this.length - left + 1 + even

        if (this.value >= left && this.value <= right) {
          const start = this.value - left + 2
          const end = this.value + left - 2 - even

          return [1, '...', ...this.range(start, end), '...', this.length]
        } else {
          return [
            ...this.range(1, left),
            '...',
            ...this.range(this.length - left + 1 + even, this.length)
          ]
        }
      }
    },

    mounted () {
      this.$vuetify.load.call(this, this.init)
    },

    methods: {
      onResize () {
        const width = this.$el && this.$el.parentNode
          ? this.$el.parentNode.clientWidth
          : window.innerWidth

        this.maxButtons = Math.floor((width - 96) / 42)
      },
      init () {
        this.selected = null

        // Change this
        setTimeout(() => (this.selected = this.value), 100)
      },
      next (e) {
        e.preventDefault()
        this.$emit('input', this.value + 1)
        this.$emit('next')
      },
      previous (e) {
        e.preventDefault()
        this.$emit('input', this.value - 1)
        this.$emit('previous')
      },
      range (from, to) {
        const range = []

        from = from > 0 ? from : 1

        for (let i = from; i <= to; i++) {
          range.push(i)
        }

        return range
      },
      genIcon (h, icon, disabled, fn) {
        return h('li', [
          h('a', {
            class: {
              'pagination__navigation': true,
              'pagination__navigation--disabled': disabled
            },
            attrs: { href: '#!' },
            on: { click: fn }
          }, [h(VIcon, [icon])])
        ])
      },
      genItem (h, i) {
        return h('a', {
          class: {
            'pagination__item': true,
            'pagination__item--active': i === this.value
          },
          attrs: { href: '#!' },
          on: {
            click: (e) => {
              e.preventDefault()
              this.$emit('input', i)
            }
          }
        }, [i])
      },
      genItems (h) {
        return this.items.map((i) => {
          return h('li', [
            isNaN(i) && h('span', { class: 'pagination__more' }, [i]) || this.genItem(h, i)
          ])
        })
      }
    },

    render (h) {
      const children = [
        this.genIcon(h, this.prevIcon, this.value === 1, this.previous),
        this.genItems(h),
        this.genIcon(h, this.nextIcon, this.value === this.length, this.next)
      ]

      return h('ul', {
        directives: [{ name: 'resize', value: this.onResize }],
        class: this.classes
      }, children)
    }
  }
</script>

<style lang="stylus" src="../../stylus/components/_pagination.styl"></style>


it('should not display counter when set to false', async () => {

//   expect(input).toHaveBeenCalledWith(null)


<script>
  import {
    VFadeTransition,
    VSlideXTransition
  } from '../transitions'

  export default {
    name: 'v-progress-linear',

    components: {
      VFadeTransition,
      VSlideXTransition
    },

    props: {
      active: {
        type: Boolean,
        default: true
      },
      buffer: Boolean,
      bufferValue: Number,
      error: Boolean,
      height: {
        type: [Number, String],
        default: 7
      },
      indeterminate: Boolean,
      info: Boolean,
      secondary: Boolean,
      success: Boolean,
      query: Boolean,
      warning: Boolean,
      value: {
        type: [Number, String],
        default: 0
      },
      colorFront: {
        type: String,
        default: null
      },
      colorBack: {
        type: String,
        default: null
      }
    },

    computed: {
      classes () {
        return {
          'progress-linear--query': this.query,
          'progress-linear--secondary': this.secondary,
          'progress-linear--success': this.success,
          'progress-linear--info': this.info,
          'progress-linear--warning': this.warning,
          'progress-linear--error': this.error
        }
      },
      styles () {
        const styles = {}

        if (!this.active) {
          styles.height = 0
        }

        if (this.buffer) {
          styles.width = `${this.bufferValue}%`
        }

        return styles
      },
      bufferStyles () {
        const styles = {}

        if (!this.active) {
          styles.height = 0
        }

        return styles
      }
    },

    methods: {
      genDeterminate (h) {
        return h('div', {
          ref: 'front',
          class: ['progress-linear__bar__determinate', this.colorFront],
          style: { width: `${this.value}%` }
        })
      },
      genBar (h, name) {
        return h('div', {
          class: [
            'progress-linear__bar__indeterminate',
            name,
            this.colorFront
          ]
        })
      },
      genIndeterminate (h) {
        return h('div', {
          ref: 'front',
          class: {
            'progress-linear__bar__indeterminate': true,
            'progress-linear__bar__indeterminate--active': this.active
          }
        }, [
          this.genBar(h, 'long'),
          this.genBar(h, 'short')
        ])
      }
    },

    render (h) {
      const fade = h('v-fade-transition', [this.indeterminate && this.genIndeterminate(h)])
      const slide = h('v-slide-x-transition', [!this.indeterminate && this.genDeterminate(h)])

      const bar = h('div', { class: ['progress-linear__bar', this.colorBack], style: this.styles }, [fade, slide])

      return h('div', {
        class: ['progress-linear', this.classes],
        style: { height: `${this.height}px` },
        on: this.$listeners
      }, [bar])
    }
  }
</script>

<style lang="stylus" src="../../stylus/components/_progress-linear.styl"></style>


heading($material)
  color: $material.text.primary

theme(heading, "heading")

for heading, options in $headings
  {heading}
    font-size: options.size
    font-weight: options.weight
    if options.line-height
      line-height: options.line-height
    if options.letter-spacing
      letter-spacing: options.letter-spacing
    margin-bottom: $spacers.three.y

    @media screen and (max-width: $grid-breakpoints.sm)
      font-size: options.size * .6


if (Session.get('openedRoom') === message.rid) {

Meteor.methods({
	messageSearch(text, rid, limit) {
		const result = {
			messages: [],
			users: [],
			channels: []
		};
		const query = {};
		const options = {
			sort: {
				ts: -1
			},
			limit: limit || 20
		};

		check(text, String);
		check(rid, String);
		check(limit, Match.Optional(Number));

		const currentUserId = Meteor.userId();
		if (!currentUserId) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', {
				method: 'messageSearch'
			});
		}
		const currentUserName = Meteor.user().username;
		const currentUserTimezoneOffset = Meteor.user().utcOffset;

		// I would place these methods at the bottom of the file for clarity but travis doesn't appreciate that.
		// (no-use-before-define)

		function filterStarred() {
			query['starred._id'] = currentUserId;
			return '';
		}

		function filterUrl() {
			query['urls.0'] = {
				$exists: true
			};
			return '';
		}

		function filterPinned() {
			query.pinned = true;
			return '';
		}

		function filterLocation() {
			query.location = {
				$exist: true
			};
			return '';
		}

		function filterBeforeDate(_, day, month, year) {
			month--;
			const beforeDate = new Date(year, month, day);
			beforeDate.setHours(beforeDate.getUTCHours() + beforeDate.getTimezoneOffset()/60 + currentUserTimezoneOffset);
			query.ts = {
				$lte: beforeDate
			};
			return '';
		}

		function filterAfterDate(_, day, month, year) {
			month--;
			day++;
			const afterDate = new Date(year, month, day);
			afterDate.setUTCHours(afterDate.getUTCHours() + afterDate.getTimezoneOffset()/60 + currentUserTimezoneOffset);
			if (query.ts) {
				query.ts.$gte = afterDate;
			} else {
				query.ts = {
					$gte: afterDate
				};
			}
			return '';
		}

		function filterOnDate(_, day, month, year) {
			month--;
			const date = new Date(year, month, day);
			date.setUTCHours(date.getUTCHours() + date.getTimezoneOffset()/60 + currentUserTimezoneOffset);
			const dayAfter = new Date(date);
			dayAfter.setDate(dayAfter.getDate() + 1);
			delete query.ts;
			query.ts = {
				$gte: date,
				$lt: dayAfter
			};
			return '';
		}

		function sortByTimestamp(_, direction) {
			if (direction.startsWith('asc')) {
				options.sort.ts = 1;
			} else if (direction.startsWith('desc')) {
				options.sort.ts = -1;
			}
			return '';
		}

		/*
		 text = 'from:rodrigo mention:gabriel chat'
		 */

		// Query for senders
		const from = [];
		text = text.replace(/from:([a-z0-9.-_]+)/ig, function(match, username) {
			if (username === 'me' && !from.includes(currentUserName)) {
				username = currentUserName;
			}
			from.push(username);
			return '';
		});
		if (from.length > 0) {
			query['u.username'] = {
				$regex: from.join('|'),
				$options: 'i'
			};
		}
		// Query for senders
		const mention = [];
		text = text.replace(/mention:([a-z0-9.-_]+)/ig, function(match, username) {
			mention.push(username);
			return '';
		});
		if (mention.length > 0) {
			query['mentions.username'] = {
				$regex: mention.join('|'),
				$options: 'i'
			};
		}
		// Filter on messages that are starred by the current user.
		text = text.replace(/has:star/g, filterStarred);
		// Filter on messages that have an url.
		text = text.replace(/has:url|has:link/g, filterUrl);
		// Filter on pinned messages.
		text = text.replace(/is:pinned|has:pin/g, filterPinned);
		// Filter on messages which have a location attached.
		text = text.replace(/has:location|has:map/g, filterLocation);
		// Filtering before/after/on a date
		// matches dd-MM-yyyy, dd/MM/yyyy, dd-MM-yyyy, prefixed by before:, after: and on: respectively.
		// Example: before:15/09/2016 after: 10-08-2016
		// if "on:" is set, "before:" and "after:" are ignored.
		text = text.replace(/before:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterBeforeDate);
		text = text.replace(/after:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterAfterDate);
		text = text.replace(/on:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterOnDate);
		// Sort order
		text = text.replace(/(?:order|sort):(asc|ascend|ascending|desc|descend|descending)/g, sortByTimestamp);

		// Query in message text
		text = text.trim().replace(/\s\s/g, ' ');
		if (text !== '') {
			if (/^\/.+\/[imxs]*$/.test(text)) {
				const r = text.split('/');
				query.msg = {
					$regex: r[1],
					$options: r[2]
				};
			} else if (RocketChat.settings.get('Message_AlwaysSearchRegExp')) {
				query.msg = {
					$regex: text,
					$options: 'i'
				};
			} else {
				query.$text = {
					$search: text
				};
				options.fields = {
					score: {
						$meta: 'textScore'
					}
				};
			}
		}
		if (Object.keys(query).length > 0) {
			query.t = {
				$ne: 'rm'  //hide removed messages (useful when searching for user messages)
			};
			query._hidden = {
				$ne: true  // don't return _hidden messages
			};
			if (rid != null) {
				query.rid = rid;
				if (Meteor.call('canAccessRoom', rid, currentUserId) !== false) {
					if (!RocketChat.settings.get('Message_ShowEditedStatus')) {
						options.fields = {
							'editedAt': 0
						};
					}
					result.messages = RocketChat.models.Messages.find(query, options).fetch();
				}
			}
		}

		return result;
	}
});


this.VideoRecorder = new class {
	constructor() {
		this.started = false;
		this.cameraStarted = new ReactiveVar(false);
		this.recording = new ReactiveVar(false);
		this.recordingAvailable = new ReactiveVar(false);
	}

	start(videoel, cb) {
		navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
		window.URL = window.URL || window.webkitURL;

		this.videoel = videoel;
		const ok = stream => {
			this.startUserMedia(stream);
			return (cb != null ? cb.call(this) : undefined);
		};

		if (navigator.getUserMedia == null) {
			return cb(false);
		}

		return navigator.getUserMedia({audio: true, video: true}, ok, e => console.log(`No live video input: ${ e }`));
	}

	record() {
		this.chunks = [];
		if (this.stream == null) {
			return;
		}
		this.mediaRecorder = new MediaRecorder(this.stream);
		this.mediaRecorder.stream = this.stream;
		this.mediaRecorder.mimeType = 'video/webm';
		this.mediaRecorder.ondataavailable = blobev => {
			this.chunks.push(blobev.data);
			if (!this.recordingAvailable.get()) {
				return this.recordingAvailable.set(true);
			}
		};
		this.mediaRecorder.start();
		return this.recording.set(true);
	}

	startUserMedia(stream) {
		this.stream = stream;
		this.videoel.src = URL.createObjectURL(stream);
		this.videoel.onloadedmetadata = () => {
			return this.videoel.play();
		};

		this.started = true;
		return this.cameraStarted.set(true);
	}

	stop(cb) {
		if (this.started) {
			this.stopRecording();

			if (this.stream) {
				const vtracks = this.stream.getVideoTracks();
				for (const vtrack of Array.from(vtracks)) {
					vtrack.stop();
				}

				const atracks = this.stream.getAudioTracks();
				for (const atrack of Array.from(atracks)) {
					atrack.stop();
				}
			}

			if (this.videoel) {
				this.videoel.pause;
				this.videoel.src = '';
			}

			this.started = false;
			this.cameraStarted.set(false);
			this.recordingAvailable.set(false);

			if (cb && this.chunks) {
				const blob = new Blob(this.chunks, { 'type' :  'video/webm' });
				cb(blob);
			}

			delete this.recorder;
			delete this.stream;
			return delete this.videoel;
		}
	}

	stopRecording() {
		if (this.started && this.recording && this.mediaRecorder) {
			this.mediaRecorder.stop();
			this.recording.set(false);
			return delete this.mediaRecorder;
		}
	}
};


Meteor.methods({
	'livechat:getAgentData'(roomId) {
		check(roomId, String);

		const room = RocketChat.models.Rooms.findOneById(roomId);
		const user = Meteor.user();

		// allow to only user to send transcripts from their own chats
		if (!room || room.t !== 'l' || !room.v || !user.profile || room.v.token !== user.profile.token) {
			throw new Meteor.Error('error-invalid-room', 'Invalid room');
		}

		if (!room.servedBy) {
			return;
		}

		return RocketChat.models.Users.getAgentInfo(room.servedBy._id);
	}
});


// 400ms is popular among game devs as a good delay before transition starts

// ie. 50, 100, 200, 400, 800 are the favored timings


Meteor.methods({
	'livechat:saveDepartment'(_id, departmentData, departmentAgents) {
		if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'view-livechat-manager')) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveDepartment' });
		}

		return RocketChat.Livechat.saveDepartment(_id, departmentData, departmentAgents);
	}
});