import VBtn from '~components/VBtn'
import VCard from '~components/VCard'
import VMenu from '~components/VMenu'
import { test } from '~util/testing'

// TODO: Most of these have exactly the same snapshots
test('VMenu.js', ({ mount }) => {
  it('should work', async () => {
    const wrapper = mount(VMenu, {
      propsData: {
        value: false
      },
      slots: {
        activator: [VBtn],
        default: [VCard]
      }
    })

    const activator = wrapper.find('.menu__activator')[0]
    const input = jest.fn()
    wrapper.instance().$on('input', input)
    activator.trigger('click')

    await wrapper.vm.$nextTick()

    expect(input).toBeCalledWith(true)
    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom top and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        top: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom bottom and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        bottom: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom left and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        left: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom right and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        right: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom fullWidth and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        fullWidth: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom auto and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        auto: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom offsetX and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        offsetX: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom offsetY and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        offsetY: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom disabled and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        disabled: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom maxHeight and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        maxHeight: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom nudgeTop and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        nudgeTop: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom nudgeBottom and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        nudgeBottom: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom nudgeLeft and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        nudgeLeft: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom nudgeRight and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        nudgeRight: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom nudgeWidth and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        nudgeWidth: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom openOnClick and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        openOnClick: false
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom openOnHover and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        openOnHover: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom lazy and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        lazy: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom closeOnClick and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        closeOnClick: false
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom closeOnContentClick and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        closeOnContentClick: false
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom activator and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        activator: [VBtn]
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom origin and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        origin: 'top right'
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom transition and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        transition: 'fade-transition'
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom positionX and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        positionX: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom positionY and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        positionY: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom positionAbsolutely and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        positionAbsolutely: true
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom maxWidth and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        maxWidth: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render component with custom minWidth and match snapshot', () => {
    const wrapper = mount(VMenu, {
      propsData: {
        minWidth: 100
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })
})


const transition = h(VExpandTransition, [group])

return h('div', { 'class': 'list--group__container' }, [item, transition])

return h('li', { 'class': 'list--group__container' }, [item, transition])

require('../../stylus/components/_small-dialog.styl')

export default {
  name: 'v-edit-dialog',

  data () {
    return {
      isActive: false,
      isSaving: false
    }
  },

  props: {
    cancelText: {
      default: 'Cancel'
    },
    large: Boolean,
    lazy: Boolean,
    saveText: {
      default: 'Save'
    },
    transition: {
      type: String,
      default: 'slide-x-reverse-transition'
    }
  },

  watch: {
    isActive (val) {
      val &&
        this.$emit('open') &&
        setTimeout(this.focus, 50) // Give DOM time to paint

      if (!val) {
        !this.isSaving && this.$emit('cancel')
        this.isSaving && this.$emit('close')
        this.isSaving = false
      }
    }
  },

  methods: {
    cancel () {
      this.isActive = false
    },
    focus () {
      const input = this.$refs.content.querySelector('input')
      input && input.focus()
    },
    save () {
      this.isSaving = true
      this.isActive = false
      this.$emit('save')
    },
    genButton (fn, text) {
      return this.$createElement('v-btn', {
        props: {
          flat: true,
          color: 'primary',
          light: true
        },
        on: { click: fn }
      }, text)
    },
    genActions () {
      return this.$createElement('div', {
        'class': 'small-dialog__actions'
      }, [
        this.genButton(this.cancel, this.cancelText),
        this.genButton(this.save, this.saveText)
      ])
    },
    genContent () {
      return this.$createElement('div', {
        on: {
          keydown: e => {
            e.keyCode === 27 && this.cancel()
            e.keyCode === 13 && this.save()
          }
        },
        ref: 'content'
      }, [this.$slots.input])
    }
  },

  render (h) {
    return h('v-menu', {
      'class': 'small-dialog',
      props: {
        contentClass: 'small-dialog__content',
        transition: this.transition,
        origin: 'top right',
        right: true,
        value: this.isActive,
        closeOnContentClick: false,
        lazy: this.lazy
      },
      on: {
        input: val => (this.isActive = val)
      }
    }, [
      h('a', {
        slot: 'activator'
      }, this.$slots.default),
      this.genContent(),
      this.large ? this.genActions() : null
    ])
  }
}


require('../../stylus/components/_pickers.styl')
require('../../stylus/components/_date-picker.styl')

import { createRange } from '../../util/helpers'

import DateYears from './mixins/date-years'
import DateTitle from './mixins/date-title'
import DateHeader from './mixins/date-header'
import DateTable from './mixins/date-table'
import MonthTable from './mixins/month-table'
import Picker from '../../mixins/picker'
import VBtn from '../VBtn'
import VCard from '../VCard'
import VIcon from '../VIcon'

import Touch from '../../directives/touch'

const pad = n => (n * 1 < 10) ? `0${n * 1}` : `${n}`

export default {
  name: 'v-date-picker',

  components: {
    VBtn,
    VCard,
    VIcon
  },

  mixins: [Picker, DateYears, DateTitle, DateHeader, DateTable, MonthTable],

  directives: { Touch },

  data () {
    const now = new Date()
    return {
      activePicker: this.type.toUpperCase(),
      currentDay: null,
      currentMonth: null,
      currentYear: null,
      isReversing: false,
      originalDate: this.value,
      // tableDate is a string in 'YYYY' / 'YYYY-M' format (leading zero for month is not required)
      tableDate: this.type === 'month'
        ? `${now.getFullYear()}`
        : `${now.getFullYear()}-${now.getMonth() + 1}`
    }
  },

  props: {
    allowedDates: {
      type: [Array, Object, Function],
      default: () => (null)
    },
    // Function formatting the day in date picker table
    dayFormat: {
      type: Function,
      default: null
    },
    firstDayOfWeek: {
      type: [String, Number],
      default: 0
    },
    // Function formatting the tableDate in the day/month table header
    headerDateFormat: {
      type: Function,
      default: null
    },
    locale: {
      type: String,
      default: 'en-us'
    },
    // Function formatting month in the months table
    monthFormat: {
      type: Function,
      default: null
    },
    // Function formatting currently selected date in the picker title
    titleDateFormat: {
      type: Function,
      default: null
    },
    type: {
      type: String,
      default: 'date',
      validator: type => ['date', 'month'/*, 'year'*/].includes(type)
    },
    value: String,
    // Function formatting the year in table header and pickup title
    yearFormat: {
      type: Function,
      default: null
    },
    yearIcon: String
  },

  computed: {
    weekDays () {
      const first = parseInt(this.firstDayOfWeek, 10)

      return this.formatters.weekDay
        ? createRange(7).map(i => this.formatters.weekDay(`2017-01-${first + i + 15}`)) // 2017-01-15 is Sunday
        : createRange(7).map(i => ['S', 'M', 'T', 'W', 'T', 'F', 'S'][(i + first) % 7])
    },
    firstAllowedDate () {
      const now = new Date()
      const year = now.getFullYear()
      const month = now.getMonth()

      if (this.allowedDates) {
        for (let date = now.getDate(); date <= 31; date++) {
          const dateString = `${year}-${month + 1}-${date}`
          if (isNaN(new Date(dateString).getDate())) break

          const sanitizedDateString = this.sanitizeDateString(dateString, 'date')
          if (this.isAllowed(sanitizedDateString)) {
            return sanitizedDateString
          }
        }
      }

      return this.sanitizeDateString(`${year}-${month + 1}-${now.getDate()}`, 'date')
    },
    firstAllowedMonth () {
      const now = new Date()
      const year = now.getFullYear()

      if (this.allowedDates) {
        for (let month = now.getMonth(); month < 12; month++) {
          const dateString = `${year}-${month + 1}`
          const sanitizedDateString = this.sanitizeDateString(dateString, 'month')
          if (this.isAllowed(sanitizedDateString)) {
            return sanitizedDateString
          }
        }
      }

      return this.sanitizeDateString(`${year}-${now.getMonth() + 1}`, 'month')
    },
    // inputDate MUST be a string in ISO 8601 format (including leading zero for month/day)
    // YYYY-MM for month picker
    // YYYY-MM-DD for date picker
    inputDate: {
      get () {
        if (this.value) {
          return this.sanitizeDateString(this.value, this.type)
        }

        return this.type === 'month' ? this.firstAllowedMonth : this.firstAllowedDate
      },
      set (value) {
        const date = value == null ? this.originalDate : this.sanitizeDateString(value, this.type)
        this.$emit('input', date)
      }
    },
    day () {
      return this.inputDate.split('-')[2] * 1
    },
    month () {
      return this.inputDate.split('-')[1] - 1
    },
    year () {
      return this.inputDate.split('-')[0] * 1
    },
    tableMonth () {
      return this.tableDate.split('-')[1] - 1
    },
    tableYear () {
      return this.tableDate.split('-')[0] * 1
    },
    computedTransition () {
      return this.isReversing ? 'tab-reverse-transition' : 'tab-transition'
    },
    formatters () {
      return {
        day: this.dayFormat || this.createNativeLocaleFormatter(this.locale, { day: 'numeric', timeZone: 'UTC' }, { start: 8, length: 2 }),
        headerDate: this.headerDateFormat || this.createNativeLocaleFormatter(this.locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }, { length: 7 }),
        month: this.monthFormat || this.createNativeLocaleFormatter(this.locale, { month: 'short', timeZone: 'UTC' }, { start: 5, length: 2 }),
        year: this.yearFormat || this.createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 }),
        weekDay: this.createNativeLocaleFormatter(this.locale, { weekday: 'narrow', timeZone: 'UTC' }),
        titleDate: this.titleDateFormat || this.defaultTitleDateFormatter
      }
    },
    defaultTitleDateFormatter () {
      const titleFormats = {
        year: { year: 'numeric', timeZone: 'UTC' },
        month: { month: 'long', timeZone: 'UTC' },
        date: { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }
      }

      const titleDateFormatter = this.createNativeLocaleFormatter(this.locale, titleFormats[this.type], {
        start: 0,
        length: { date: 10, month: 7, year: 4 }[this.type]
      })

      const landscapeFormatter = date => titleDateFormatter(date)
        .replace(/([^\d\s])([\d])/g, (match, nonDigit, digit) => `${nonDigit} ${digit}`)
        .replace(', ', ',<br>')

      return this.landscape ? landscapeFormatter : titleDateFormatter
    }
  },

  watch: {
    activePicker (val, prev) {
      if (val !== 'YEAR') return

      // That's a quirk, setting timeout stopped working after fixing #1649
      // It worked but for timeouts significantly longer than the transition duration
      const interval = setInterval(() => {
        if (this.$refs.years) {
          this.$refs.years.scrollTop = this.$refs.years.scrollHeight / 2 - 125
          clearInterval(interval)
        }
      }, 100)
    },
    tableDate (val, prev) {
      // Make a ISO 8601 strings from val and prev for comparision, otherwise it will incorrectly
      // compare for example '2000-9' and '2000-10'
      const sanitizeType = this.type === 'month' ? 'year' : 'month'
      this.isReversing = this.sanitizeDateString(val, sanitizeType) < this.sanitizeDateString(prev, sanitizeType)
    },
    value (val) {
      if (val) {
        this.tableDate = this.type === 'month' ? `${this.year}` : `${this.year}-${this.month + 1}`
      }
    },
    type (val) {
      if (val === 'month' && this.activePicker === 'DATE') {
        this.activePicker = 'MONTH'
      } else if (val === 'year') {
        this.activePicker = 'YEAR'
      }
    }
  },

  methods: {
    save () {
      if (this.originalDate) {
        this.originalDate = this.value
      } else {
        this.originalDate = this.inputDate
      }

      if (this.$parent && this.$parent.isActive) this.$parent.isActive = false
    },
    cancel () {
      this.inputDate = this.originalDate
      if (this.$parent && this.$parent.isActive) this.$parent.isActive = false
    },
    isAllowed (date) {
      if (!this.allowedDates) return true

      // date parameter must be in ISO 8601 format with leading zero
      // If allowedDates is an array its values must be in ISO 8601 format with leading zero
      // If allowedDates is on object its min/max properties must be in ISO 8601 with leading zero
      if (Array.isArray(this.allowedDates)) {
        return this.allowedDates.indexOf(date) > -1
      } else if (this.allowedDates instanceof Function) {
        return this.allowedDates(date)
      } else if (this.allowedDates instanceof Object) {
        const min = this.allowedDates.min
        const max = this.allowedDates.max
        return (!min || min <= date) && (!max || max >= date)
      }

      return true
    },
    genTableTouch (touchCallback) {
      return {
        name: 'touch',
        value: {
          left: e => (e.offsetX < -15) && touchCallback(1),
          right: e => (e.offsetX > 15) && touchCallback(-1)
        }
      }
    },
    genTable (tableChildren, touchCallback) {
      const wheel = this.activePicker === 'MONTH' ? this.monthWheelScroll : this.dateWheelScroll
      const options = {
        staticClass: 'picker--date__table',
        'class': {
          'picker--month__table': this.activePicker === 'MONTH'
        },
        on: this.scrollable ? { wheel } : undefined,
        directives: [this.genTableTouch(touchCallback)]
      }

      const table = this.$createElement('table', {
        key: this.activePicker === 'MONTH' ? this.tableYear : this.tableMonth
      }, tableChildren)

      return this.$createElement('div', options, [
        this.$createElement('transition', {
          props: { name: this.computedTransition }
        }, [table])
      ])
    },
    genPickerBody (h) {
      const pickerBodyChildren = []
      if (this.activePicker === 'DATE') {
        pickerBodyChildren.push(h('div', { staticClass: 'picker--date__header' }, [this.genSelector()]))
        pickerBodyChildren.push(this.genTable([
          this.dateGenTHead(),
          this.dateGenTBody()
        ], value => this.updateTableMonth(this.tableMonth + value)))
      } else if (this.activePicker === 'MONTH') {
        pickerBodyChildren.push(h('div', { staticClass: 'picker--date__header' }, [this.genSelector()]))
        pickerBodyChildren.push(this.genTable([
          this.monthGenTBody()
        ], value => this.tableDate = `${this.tableYear + value}`))
      } else if (this.activePicker === 'YEAR') {
        pickerBodyChildren.push(this.genYears())
      }

      return pickerBodyChildren
    },
    createNativeLocaleFormatter (locale, options, { start, length } = { start: 0, length: 0 }) {
      const makeIsoString = dateString => {
        const [year, month, date] = dateString.trim().split(' ')[0].split('-')
        return [year, pad(month || 1), pad(date || 1)].join('-')
      }

      try {
        const intlFormatter = new Intl.DateTimeFormat(locale || undefined, options)
        return dateString => intlFormatter.format(new Date(`${makeIsoString(dateString)}T00:00:00+00:00`))
      } catch (e) {
        return (start || length) ? dateString => makeIsoString(dateString).substr(start, length) : null
      }
    },
    // Adds leading zero to month/day if necessary, returns 'YYYY' if type = 'year',
    // 'YYYY-MM' if 'month' and 'YYYY-MM-DD' if 'date'
    sanitizeDateString (dateString, type) {
      const [year, month, date] = dateString.split('-')
      return `${year}-${pad(month)}-${pad(date)}`.substr(0, { date: 10, month: 7, year: 4 }[type])
    },
    // For month = 12 it sets the tableDate to January next year
    // For month = -1 it sets the tableDate to December previous year
    // Otherwise it just changes the table month
    updateTableMonth (month /* -1..12 */) {
      if (month === 12) {
        this.tableDate = `${this.tableYear + 1}-01`
      } else if (month === -1) {
        this.tableDate = `${this.tableYear - 1}-12`
      } else {
        this.tableDate = `${this.tableYear}-${month + 1}`
      }
    }
  },

  created () {
    this.tableDate = this.type === 'month' ? `${this.year}` : `${this.year}-${this.month + 1}`
  },

  mounted () {
    const date = new Date()
    this.currentDay = date.getDate()
    this.currentMonth = date.getMonth()
    this.currentYear = date.getFullYear()
  },

  render (h) {
    const children = []

    !this.noTitle && children.push(this.genTitle(this.formatters.titleDate(this.inputDate)))

    children.push(h('transition', {
      props: {
        origin: 'center center',
        mode: 'out-in',
        name: 'scale-transition'
      }
    }, [h('div', {
      staticClass: 'picker__body',
      key: this.activePicker
    }, this.genPickerBody(h))]))

    this.$scopedSlots.default && children.push(this.genSlot())

    return h('v-card', {
      staticClass: 'picker picker--date',
      'class': {
        'picker--landscape': this.landscape,
        ...this.themeClasses
      }
    }, children)
  }

}


keep keep keep keep keep replace replace replace keep keep keep

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VListGroup.js should render a component with no padding for action icon and match snapshot 1`] = `

`;

exports[`VListGroup.js should render a component with route namespace and match snapshot 1`] = `

`;

exports[`VListGroup.js should render a lazy component and match snapshot 1`] = `

`;

exports[`VListGroup.js should render component and match snapshot 1`] = `

`;


@import '../bootstrap'

.menu
  display: inline-block
  position: relative
  vertical-align: middle

  &--disabled
    cursor: default

    .menu__activator,
    .menu__activator > *
      cursor: default
      pointer-events: none

  &__activator
    align-items: center
    cursor: pointer
    position: relative

    input[readonly]
      cursor: pointer

    .toolbar__side-icon
      margin: 0

  &__content
    position: absolute
    display: inline-block
    border-radius: 2px
    max-width: 80%
    overflow-y: auto
    overflow-x: hidden
    transition: .3s $transition.swing
    contain: content
    elevation(8)

    &--active
      pointer-events: none

    &--dropdown
      border-top-left-radius: 0
      border-top-right-radius: 0
      border-top: 1px solid $material-light.dividers

    & > .card
      contain: content
      backface-visibility: hidden

  &-transition
    &-enter
      .list__tile
        min-width: 0
        transition-delay: .4s
        opacity: 0
        transform: translateY(-15px)
        pointer-events: none

    &-enter-to
      .list__tile
        pointer-events: auto
        opacity: 1

      .list__tile--active
        transform: none !important

    &-leave-to
      transform: translateY(-10px)

    &-leave-active, &-leave-to
      pointer-events: none

    &-enter, &-leave-to
      opacity: 0

    &-enter-to, &-leave
      opacity: 1

    &-enter-active, &-leave-active
      transition: all .5s $transition.swing

.menu-transition-enter
  &.menu__content--auto
    .list__tile--active
      opacity: 1
      transform: none !important
      pointer-events: auto


/**
 * Loadable
 *
 * @mixin
 *
 * Used to add linear progress bar to components
 * Can use a default bar with a specific color
 * or designate a custom progress linear bar
 */
export default {
  props: {
    loading: {
      type: [Boolean, String],
      default: false
    }
  },

  methods: {
    genProgress () {
      if (this.loading === false) return null

      return this.$slots.progress || this.$createElement('v-progress-linear', {
        props: {
          color: (this.loading === true || this.loading === '')
            ? (this.color || 'primary')
            : this.loading,
          height: 2,
          indeterminate: true
        }
      })
    }
  }
}


global-color(color_name, color_value)
  .{color_name}
    background-color color_value !important
    border-color: color_value !important

  .{color_name}--text
    color color_value !important

global-color-accent(color_name, color_value)
  .{color_name}
    &.{color_type}
      background-color color_value !important
      border-color: color_value !important

  .{color_name}--text
    &.text--{color_type}
      color: color_value !important


exports[`VListGroup.js should render a component with route namespace and match snapshot 1`] = `

keep keep keep keep replace keep keep keep keep keep

import { VExpandTransition } from '../transitions'

import Bootable from '../../mixins/bootable'
import Toggleable from '../../mixins/toggleable'

export default {
  name: 'v-list-group',

  inject: ['listClick', 'listClose'],

  mixins: [Bootable, Toggleable],

  props: {
    group: String,
    noAction: Boolean
  },

  computed: {
    classes () {
      return {
        'list--group__header': true,
        'list--group__header--active': this.isActive,
        'list--group__header--no-action': this.noAction
      }
    }
  },

  watch: {
    isActive () {
      this.isBooted = true

      if (!this.isActive) {
        this.listClose(this._uid)
      }
    },
    $route (to) {
      const isActive = this.matchRoute(to.path)

      if (this.group) {
        if (isActive && this.isActive !== isActive) {
          this.listClick(this._uid)
        }
        this.isActive = isActive
      }
    }
  },

  mounted () {
    this.isBooted = this.isActive

    if (this.group) {
      this.isActive = this.matchRoute(this.$route.path)
    }

    if (this.isActive) {
      this.listClick(this._uid)
    }
  },

  methods: {
    click () {
      if (!this.$refs.item.querySelector('.list__tile--disabled')) {
        requestAnimationFrame(() => this.listClick(this._uid))
      }
    },
    toggle (uid) {
      this.isActive = this._uid === uid
    },
    matchRoute (to) {
      if (!this.group) return false
      return to.match(this.group) !== null
    }
  },

  render (h) {
    const group = h('ul', {
      'class': 'list list--group',
      directives: [{
        name: 'show',
        value: this.isActive
      }],
      ref: 'group'
    }, this.showLazyContent(this.$slots.default))

    const item = h('div', {
      'class': this.classes,
      on: Object.assign({}, { click: this.click }, this.$listeners),
      ref: 'item'
    }, [this.$slots.item])

    const transition = h(VExpandTransition, [group])

    return h('div', { 'class': 'list--group__container' }, [item, transition])
  }
}


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VCard.vue should render a card with custom height 1`] = `

`;

exports[`VCard.vue should render a flat card 1`] = `

`;

exports[`VCard.vue should render a raised card 1`] = `

`;

exports[`VCard.vue should render a tile card 1`] = `

`;

exports[`VCard.vue should render a tile card 2`] = `

`;

exports[`VCard.vue should render card with img 1`] = `

`;

exports[`VCard.vue should render component and match snapshot 1`] = `

`;


import VRadioGroup from './VRadioGroup'
import VRadio from './VRadio'

export { VRadioGroup, VRadio }

/* istanbul ignore next */
VRadioGroup.install = function install (Vue) {
  Vue.component(VRadioGroup.name, VRadioGroup)
  Vue.component(VRadio.name, VRadio)
}

export default VRadioGroup


import { test } from '~util/testing'
import { maskText, unmaskText } from './mask'

test('mask.js', ({ mount }) => {
  // Mask
  it('should add delimiter', () => {
    expect(maskText('5', '(#')).toBe('(5')
  })

  it('should add delimiter if explicity typed', () => {
    expect(maskText('(', '(#')).toBe('(')
  })

  it('should add delimiters and masks', () => {
    expect(maskText('4567', '(###) #')).toBe('(456) 7')
    expect(maskText('444444444', '#### - #### - #### - ####')).toBe('4444 - 4444 - 4')
    expect(maskText('A314444', 'A## - ####')).toBe('A31 - 4444')
  })

  it('should fill mask blanks', () => {
    expect(maskText('55', '## - ##')).toBe('55 - ')
  })

  it('should not fill mask blanks if told not to', () => {
    expect(maskText('55', '## - ##', true)).toBe('55')
  })

  it('should not fill if no value is provided', () => {
    expect(maskText('', '## - ##')).toBe('')
  })

  it('should only allow exact input if dontFillMaskBlanks is true', () => {
    expect(maskText('4567', '(###) #', true)).toBe('')
    expect(maskText('(456)', '(###) #', true)).toBe('(456)')
  })

  it('should convert alphanumeric to the proper case', () => {
    expect(maskText('aa', 'Aa')).toBe('Aa')
    expect(maskText('AA', 'aa')).toBe('aa')
    expect(maskText('A1', 'Aa')).toBe('A')
    expect(maskText('12abAB', 'NnNnNn')).toBe('12AbAb')
  })

  it('should not fill if wrong value is provided even if it is correct later in mask', () => {
    expect(maskText('a', '#a')).toBe('')
  })

  /* Not sure how to implement this
  it('should not fill in delimiter value if it does not match', () => {
    expect(maskText('a', '(')).toBe('')
  })
  */

  it('should fill last characters if they are all delimiters', () => {
    expect(maskText('1', '#)')).toBe('1)')
    expect(maskText('123', '(###)!!')).toBe('(123)!!')
  })

  // Unmasks
  it('should remove delimiter', () => {
    expect(unmaskText('(5')).toBe('5')
  })

  it('should return proper text length', () => {
    expect(unmaskText('1111')).toBe('1111')
  })

  it('should remove delimiters and masks', () => {
    expect(unmaskText('(123)4')).toBe('1234')
    expect(unmaskText('(456) 7)')).toBe('4567')
    expect(unmaskText('4444 - 4444 - 4')).toBe('444444444')
    expect(unmaskText('A31 - 4444')).toBe('A314444')
  })

  it('should include all possible matching values', () => {
    expect(unmaskText('555')).toBe('555')
  })

  it('should mask numbers', () => {
    expect(maskText(1234, '##.##')).toBe('12.34')
  })
})


keep keep keep replace replace replace keep keep keep keep


const THEME_DEFAULTS = {
  primary: '#1976D2',
  secondary: '#424242',
  accent: '#82B1FF',
  error: '#FF5252',
  info: '#2196F3',
  success: '#4CAF50',
  warning: '#FFC107'
}

export default function theme (theme) {
  theme = theme || {}

  return Object.assign({}, THEME_DEFAULTS, theme)
}


export function createSimpleFunctional (c, el = 'div', name) {
  name = name || c.replace(/__/g, '-')

  return {
    name: `v-${name}`,
    functional: true,

    render: (h, { data, children }) => {
      data.staticClass = (`${c} ${data.staticClass || ''}`).trim()

      return h(el, data, children)
    }
  }
}

export function createSimpleTransition (name, origin = 'top center 0', mode) {
  return {
    name,

    functional: true,

    props: {
      origin: {
        type: String,
        default: origin
      }
    },

    render (h, context) {
      context.data = context.data || {}
      context.data.props = { name }
      context.data.on = context.data.on || {}
      if (!Object.isExtensible(context.data.on)) {
        context.data.on = { ...context.data.on }
      }

      if (mode) context.data.props.mode = mode

      context.data.on.beforeEnter = el => {
        el.style.transformOrigin = context.props.origin
        el.style.webkitTransformOrigin = context.props.origin
      }

      return h('transition', context.data, context.children)
    }
  }
}

export function createJavaScriptTransition (name, functions, css = true, mode = 'in-out') {
  return {
    name,

    functional: true,

    props: {
      css: {
        type: Boolean,
        default: css
      },
      mode: {
        type: String,
        default: mode
      }
    },

    render (h, context) {
      const data = {
        props: {
          ...context.props,
          name
        },
        on: functions
      }

      return h('transition', data, context.children)
    }
  }
}

export function directiveConfig (binding, defaults = {}) {
  return Object.assign({},
    defaults,
    binding.modifiers,
    { value: binding.arg },
    binding.value || {}
  )
}

export function addOnceEventListener (el, event, cb) {
  var once = () => {
    cb()
    el.removeEventListener(event, once, false)
  }

  el.addEventListener(event, once, false)
}

export function getObjectValueByPath (obj, path) {
  // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621
  if (!path || path.constructor !== String) return
  path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
  path = path.replace(/^\./, '') // strip a leading dot
  const a = path.split('.')
  for (var i = 0, n = a.length; i < n; ++i) {
    var k = a[i]
    if (obj instanceof Object && k in obj) {
      obj = obj[k]
    } else {
      return
    }
  }
  return obj
}

export function createRange (length) {
  return [...Array.from({ length }, (v, k) => k)]
}

export function getZIndex (el) {
  if (!el || el.nodeType !== Node.ELEMENT_NODE) return 0
  var zi = document.defaultView.getComputedStyle(el).getPropertyValue('z-index')
  if (isNaN(zi)) return getZIndex(el.parentNode)

  return zi
}

const tagsToReplace = {
  '&': '&',
  '<': '<',
  '>': '>'
}
export function escapeHTML (str) {
  return str.replace(/[&<>]/g, tag => tagsToReplace[tag] || tag)
}


exports[`VListGroup.js should render a lazy component and match snapshot 1`] = `

keep keep keep replace keep keep keep keep keep


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VSelect should escape items in menu 1`] = `

  <strong>foo</strong>

`;

exports[`VSelect should escape items in menu 2`] = `

  <

    str

  ong>foo</strong>

`;

exports[`VSelect should render buttons correctly when using items array with segmented prop 1`] = `

      <button type="button"
              class="btn btn--flat"
              data-ripple="true"
      >

          Hello

      </button>
      <input disabled="disabled"
             tabindex="-1"
             class="input-group--select__autocomplete"
             style="display: none;"
      >

                    Hello

      arrow_drop_down

`;

exports[`VSelect should render buttons correctly when using slot with segmented prop 1`] = `

        Hello

      <input disabled="disabled"
             tabindex="-1"
             class="input-group--select__autocomplete"
             style="display: none;"
      >

                    Hello

      arrow_drop_down

`;


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VProgressLinear.js should render component and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render component with buffer value and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render component with buffer value and value > buffer value and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render component with color and background color and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render component with color and background color and opacity and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render component with color and background opacity and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render component with color and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render indeterminate progress and match snapshot 1`] = `

`;

exports[`VProgressLinear.js should render indeterminate progress with query prop and match snapshot 1`] = `

`;


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VApp.js should allow a custom id 1`] = `

`;

exports[`VApp.js should match a snapshot 1`] = `

`;


@import './settings/_theme.styl'

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

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

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

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


require('../../stylus/components/_bottom-sheets.styl')

import VDialog from '../VDialog/VDialog'

export default {
  name: 'v-bottom-sheet',

  props: {
    disabled: Boolean,
    fullWidth: Boolean,
    inset: Boolean,
    lazy: Boolean,
    maxWidth: {
      type: [String, Number],
      default: 'auto'
    },
    persistent: Boolean,
    value: null
  },

  render (h) {
    const activator = h('template', {
      slot: 'activator'
    }, this.$slots.activator)

    const contentClass = [
      'bottom-sheet',
      this.inset ? 'bottom-sheet--inset' : ''
    ].join(' ')

    return h(VDialog, {
      attrs: {
        ...this.$props
      },
      on: {
        ...this.$listeners
      },
      props: {
        contentClass: contentClass,
        transition: 'bottom-sheet-transition',
        value: this.value
      }
    }, [activator, this.$slots.default])
  }
}


exports[`VListGroup.js should render component and match snapshot 1`] = `


$shadow-key-umbra-opacity = rgba(0,0,0, .2)
$shadow-key-penumbra-opacity = rgba(0,0,0, .14)
$shadow-key-ambient-opacity = rgba(0,0,0, .12)

$shadow-key-umbra = 0px 0px 0px 0px $shadow-key-umbra-opacity,
                    0px 2px 1px -1px $shadow-key-umbra-opacity,
                    0px 3px 1px -2px $shadow-key-umbra-opacity,
                    0px 3px 3px -2px $shadow-key-umbra-opacity,
                    0px 2px 4px -1px $shadow-key-umbra-opacity,
                    0px 3px 5px -1px $shadow-key-umbra-opacity,
                    0px 3px 5px -1px $shadow-key-umbra-opacity,
                    0px 4px 5px -2px $shadow-key-umbra-opacity,
                    0px 5px 5px -3px $shadow-key-umbra-opacity,
                    0px 5px 6px -3px $shadow-key-umbra-opacity,
                    0px 6px 6px -3px $shadow-key-umbra-opacity,
                    0px 6px 7px -4px $shadow-key-umbra-opacity,
                    0px 7px 8px -4px $shadow-key-umbra-opacity,
                    0px 7px 8px -4px $shadow-key-umbra-opacity,
                    0px 7px 9px -4px $shadow-key-umbra-opacity,
                    0px 8px 9px -5px $shadow-key-umbra-opacity,
                    0px 8px 10px -5px $shadow-key-umbra-opacity,
                    0px 8px 11px -5px $shadow-key-umbra-opacity,
                    0px 9px 11px -5px $shadow-key-umbra-opacity,
                    0px 9px 12px -6px $shadow-key-umbra-opacity,
                    0px 10px 13px -6px $shadow-key-umbra-opacity,
                    0px 10px 13px -6px $shadow-key-umbra-opacity,
                    0px 10px 14px -6px $shadow-key-umbra-opacity,
                    0px 11px 14px -7px $shadow-key-umbra-opacity,
                    0px 11px 15px -7px $shadow-key-umbra-opacity

$shadow-key-penumbra = 0px 0px 0px 0px $shadow-key-penumbra-opacity,
                        0px 1px 1px 0px $shadow-key-penumbra-opacity,
                        0px 2px 2px 0px $shadow-key-penumbra-opacity,
                        0px 3px 4px 0px $shadow-key-penumbra-opacity,
                        0px 4px 5px 0px $shadow-key-penumbra-opacity,
                        0px 5px 8px 0px $shadow-key-penumbra-opacity,
                        0px 6px 10px 0px $shadow-key-penumbra-opacity,
                        0px 7px 10px 1px $shadow-key-penumbra-opacity,
                        0px 8px 10px 1px $shadow-key-penumbra-opacity,
                        0px 9px 12px 1px $shadow-key-penumbra-opacity,
                        0px 10px 14px 1px $shadow-key-penumbra-opacity,
                        0px 11px 15px 1px $shadow-key-penumbra-opacity,
                        0px 12px 17px 2px $shadow-key-penumbra-opacity,
                        0px 13px 19px 2px $shadow-key-penumbra-opacity,
                        0px 14px 21px 2px $shadow-key-penumbra-opacity,
                        0px 15px 22px 2px $shadow-key-penumbra-opacity,
                        0px 16px 24px 2px $shadow-key-penumbra-opacity,
                        0px 17px 26px 2px $shadow-key-penumbra-opacity,
                        0px 18px 28px 2px $shadow-key-penumbra-opacity,
                        0px 19px 29px 2px $shadow-key-penumbra-opacity,
                        0px 20px 31px 3px $shadow-key-penumbra-opacity,
                        0px 21px 33px 3px $shadow-key-penumbra-opacity,
                        0px 22px 35px 3px $shadow-key-penumbra-opacity,
                        0px 23px 36px 3px $shadow-key-penumbra-opacity,
                        0px 24px 38px 3px $shadow-key-penumbra-opacity

$shadow-key-ambient = 0px 0px 0px 0px $shadow-key-ambient-opacity,
                      0px 1px 3px 0px $shadow-key-ambient-opacity,
                      0px 1px 5px 0px $shadow-key-ambient-opacity,
                      0px 1px 8px 0px $shadow-key-ambient-opacity,
                      0px 1px 10px 0px $shadow-key-ambient-opacity,
                      0px 1px 14px 0px $shadow-key-ambient-opacity,
                      0px 1px 18px 0px $shadow-key-ambient-opacity,
                      0px 2px 16px 1px $shadow-key-ambient-opacity,
                      0px 3px 14px 2px $shadow-key-ambient-opacity,
                      0px 3px 16px 2px $shadow-key-ambient-opacity,
                      0px 4px 18px 3px $shadow-key-ambient-opacity,
                      0px 4px 20px 3px $shadow-key-ambient-opacity,
                      0px 5px 22px 4px $shadow-key-ambient-opacity,
                      0px 5px 24px 4px $shadow-key-ambient-opacity,
                      0px 5px 26px 4px $shadow-key-ambient-opacity,
                      0px 6px 28px 5px $shadow-key-ambient-opacity,
                      0px 6px 30px 5px $shadow-key-ambient-opacity,
                      0px 6px 32px 5px $shadow-key-ambient-opacity,
                      0px 7px 34px 6px $shadow-key-ambient-opacity,
                      0px 7px 36px 6px $shadow-key-ambient-opacity,
                      0px 8px 38px 7px $shadow-key-ambient-opacity,
                      0px 8px 40px 7px $shadow-key-ambient-opacity,
                      0px 8px 42px 7px $shadow-key-ambient-opacity,
                      0px 9px 44px 8px $shadow-key-ambient-opacity,
                      0px 9px 46px 8px $shadow-key-ambient-opacity

// MIXINS
elevation($z, important = false)
  box-shadow: $shadow-key-umbra[$z],
              $shadow-key-penumbra[$z],
              $shadow-key-ambient[$z] (important ? !important : )

elevationTransition($duration = 280ms, $easing = cubic-bezier(.4, 0, .2, 1))
  transition: box-shadow $duration $easing
  will-change: box-shadow


import Colorable from './colorable'
import Themeable from './themeable'

export default {
  mixins: [Colorable, Themeable],

  data () {
    return {
      defaultColor: 'accent',
      isSaving: false
    }
  },

  props: {
    actions: Boolean,
    autosave: Boolean,
    headerColor: String,
    landscape: Boolean,
    noTitle: Boolean,
    scrollable: Boolean,
    value: {
      required: true
    }
  },

  computed: {
    titleColor () {
      const darkTheme = this.dark || (!this.light && this.$vuetify.dark)
      const defaultTitleColor = darkTheme ? null : 'primary'
      return this.headerColor || this.color || defaultTitleColor
    }
  },

  methods: {
    save () {},
    cancel () {},
    genSlot () {
      return this.$scopedSlots.default({
        save: this.save,
        cancel: this.cancel
      })
    },
    genPickerTitle (children) {
      return this.$createElement('div', {
        staticClass: 'picker__title',
        'class': this.addBackgroundColorClassChecks({}, 'titleColor')
      }, children)
    }
  }
}


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VCardTitle.js should render component and match snapshot 1`] = `

`;

exports[`VCardTitle.js should render component with specific padding applied 1`] = `

`;


keep keep keep replace replace replace keep keep keep

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VListGroup.js should render a component with no padding for action icon and match snapshot 1`] = `

`;

exports[`VListGroup.js should render a component with route namespace and match snapshot 1`] = `

`;

exports[`VListGroup.js should render a lazy component and match snapshot 1`] = `

`;

exports[`VListGroup.js should render component and match snapshot 1`] = `

`;


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

/* Theme */
input-group($material)
  input,
  textarea
    color: $material.text.primary

    &:disabled
      color: $material.text.disabled

  &:not(.input-group--error) .input-group__messages
    color: $material.text.secondary

  &.input-group--textarea:not(.input-group--full-width)
    .input-group__input
      border: 2px solid $material.text.secondary

  // This is specific for selects
  &.input-group--dirty
    .input-group__selections__comma
      &:not(.input-group__selections__comma--active)
        color: $material.text.primary

  &:not(.input-group--error)
    label
      color: $material.text.secondary

    &.input-group--disabled
      label,
      .input-group__selections__comma
        color: $material.text.disabled

    .input-group__details:before
      background-color: $material.input-bottom-line

    &:not(.input-group--focused):not(.input-group--disabled)
      .input-group__input
        .input-group__append-icon,
        .input-group__prepend-icon
          &:not(:hover)
            color: $material.text.secondary

      &:not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover
        .input-group__details:before
          background-color: $material.text.primary

        &.input-group--textarea:not(.input-group--full-width)
          .input-group__input
            border-color: $material.text.primary

  &.input-group
    &--overflow, &--segmented, &--editable
      .input-group__input,
      .input-group__details
        &:before
          background-color: $material.dividers

  &.input-group--disabled
    .input-group__input
      .input-group__append-icon,
      .input-group__prepend-icon
        color: $material.text.disabled

    .input-group__details:before
      background-color: transparent
      background-image: linear-gradient(
        to right,
        $material.text.disabled 0,
        $material.text.disabled 33%,
        transparent 0
      )

  .input-group--text-field
    &__prefix, &__suffix
      color: $material.text.secondary

    &.input-group--disabled
      &__prefix, &__suffix
        color: $material.text.disabled

theme(input-group, "input-group")

.input-group
  display: flex
  flex: 1 1
  flex-wrap: wrap
  min-width: 24px
  padding: $input-group-padding
  position: relative
  width: 100%
  outline: none
  transition: box-shadow $primary-transition

/** Label */
.input-group
  label
    display: inline-block
    font-size: $input-font-size
    line-height: $input-height
    height: $input-height
    max-width: 90%
    min-width: 0
    overflow: hidden
    pointer-events: none
    text-align: left
    text-overflow: ellipsis
    transform-origin: top left
    transition: .4s $transition.fast-in-fast-out
    white-space: nowrap
    width: 100%
    z-index: 0

/** Blocks */
.input-group
  &__input
    display: flex
    flex: 1 0 100%
    min-width: 0
    min-height: $input-height

/** Icons */
.input-group
  &__icon-cb
    cursor: pointer

.input-group
  &.input-group--focused,
  &.input-group--error
    .input-group__append-icon,
    .input-group__prepend-icon
      color: inherit

  &.input-group--append-icon, &.input-group--selection-controls
    label
      max-width: 75%

  &.input-group--prepend-icon
    label
      max-width: 75%

  &.input-group--append-icon.input-group--prepend-icon
    label
      max-width: 65%

  .input-group__append-icon
    padding: 0 6px
    transition: .3s $transition.ease-in-out

  .input-group__append-icon,
  .input-group__prepend-icon
    align-self: center
    user-select: none

/** States */
.input-group
  // Single Line
  &.input-group--single-line,
  &.input-group--solo
    &.input-group--dirty
      label
        display: none

  // Solo
  &.input-group--solo
    background: #fff
    min-height: 46px
    border-radius: $card-border-radius
    padding: 0
    elevation(2)

    label
      top: 8px
      padding-left: 16px
      transform: none !important

    .input-group__input
      align-items: center
      padding: 8px 16px

    .input-group__details
      display: none

  &--disabled
    .input-group__details:before
      background-color: transparent
      background-position: bottom
      background-size: 3px 1px
      background-repeat: repeat-x

  // Focused
  &.input-group--text-field
    &:not(.input-group--single-line):not(.input-group--error)
      &.input-group--focused
        label
          color: inherit

        .input-group__input
          border-color: inherit

    &.input-group--focused
      &:not(.input-group--disabled)
        .input-group__details
          &:after
            transform: scaleX(1)

  // Required
  &--required
    label:after
      content: '*'

  // Error
  &.input-group--error
    label
      animation: shake .6s $transition.swing

    .input-group__messages
      color: inherit

    .input-group__details
      &:before
        background-color: currentColor

/** Details */
.input-group
  .slide-y-transition-leave,
  .slide-y-transition-leave-to
    position: absolute

  &__details
    color: inherit
    display: flex
    padding-top: 4px
    flex: 1 0 100%
    font-size: 12px
    min-height: 26px
    overflow: hidden
    position: relative
    width: 100%

    &:after, &:before
      content: ''
      position: absolute
      left: 0
      transition: .3s $transition.fast-out-slow-in

    &:after
      background-color: currentColor
      color: inherit
      top: 0
      height: 2px
      transform: scaleX(0)
      transform-origin: center center 0
      width: 100%
      z-index: 1

    &:before
      top: 0
      height: 1px
      width: 100%
      z-index: 0

  &--hide-details
    .input-group__details
      min-height: 2px
      padding: 0

  &--async-loading
    .input-group__details
      &:before,
      &:after
        display: none

  /** Async loading indicator */
  .progress-linear
    position: absolute
    top: 0
    left: 0
    margin: 0

/** Hint */
.input-group
  &__hint
    // flex: 1 0
    transition: .3s $transition.fast-in-fast-out

/** Error */
.input-group
  .input-group__error
    color: inherit
    flex: 1 0
    transition: .3s $transition.fast-in-fast-out

/** Types */
.input-group
  &--overflow,
  &--segmented,
  &--editable
    &.input-group--active
      background-color: #fff


/**
 * Delayable
 *
 * @mixin
 *
 * Changes the open or close
 * delay time for elements
 */
export default {
  data: () => ({
    openTimeout: null,
    closeTimeout: null
  }),

  props: {
    openDelay: {
      type: [Number, String],
      default: 0
    },
    closeDelay: {
      type: [Number, String],
      default: 200
    }
  },

  methods: {
    /**
     * Clear any pending delay
     * timers from executing
     *
     * @return {void}
     */
    clearDelay () {
      clearTimeout(this.openTimeout)
      clearTimeout(this.closeTimeout)
    },
    /**
     * Runs callback after
     * a specified delay
     *
     * @param  {String}   type
     * @param  {Function} cb
     *
     * @return {void}
     */
    runDelay (type, cb) {
      this.clearDelay()

      const delay = parseInt(this[`${type}Delay`], 10)

      this[`${type}Timeout`] = setTimeout(cb, delay)
    }
  }
}


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

/** Theme */
switch($material)
  &: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: currentColor

  .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
      color: inherit
      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: translate(-15px, -24px)
      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: translate(-50%, -50%)
        height: 20px
        elevation(4)

      &--active
        transform: translate(2px, -24px)

    label
      padding-left: 14px


<ul class="list list--group"
style="display: none;"
>
</ul>
</div>
</ul>
`;

keep keep keep keep replace keep keep keep

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

/* Themes */
list($material)
  background: $material.cards

  .list__tile:not(.list__tile--active)
    color: $material.text.primary

  .list__tile__sub-title
    color: $material.text.secondary

  .divider
    background-color: $material.dividers

  .list__tile__mask
    color: $material.text.disabled

  &--group
    .list__tile--active
      .list__tile__title
        color: $material.text.link

theme(list, "list")

.list
  list-style-type: none
  padding: $list-top-padding 0 $list-bottom-padding
  transition: height .3s $transition.fast-out-slow-in

  .input-group
    margin: 0

  > .list__tile ~ .list__tile
    margin-top: 0

  &__tile
    font-size: $list-tile-font-size
    font-weight: $list-tile-font-weight
    display: flex
    height: $list-item-single-height
    text-decoration: none
    align-items: center
    padding: 0 16px
    margin: 0
    transition: $primary-transition
    position: relative
    user-select: none

    &:after
      content: ''
      position: absolute
      left: 0
      top: 0
      height: 1px
      opacity: 0
      width: 100%
      transition: $primary-transition
      background-color: $material-twelve-percent-dark

    &--link
      cursor: pointer

    &--link:hover, &--highlighted
      background: $material-twelve-percent-dark

    &__content,
    &__action,
    &__avatar
      height: 100%

    &__title, &__sub-title
      white-space: nowrap
      overflow: hidden
      text-overflow: ellipsis
      width: 100%

    &__title
      transition: $primary-transition
      height: 24px
      line-height: 24px
      position: relative
      text-align: left

    &__sub-title
      font-size: $list-sub-title-font-size

    .avatar
      justify-content: flex-start
      min-width: 56px

    &__action
      display: flex
      justify-content: flex-start
      min-width: 56px
      align-items: center

      .input-group--selection-controls
        align-items: center
        padding: 0

      .input-group__details
        display: none

      .icon
        transition: $primary-transition

      .btn //fix button padding if used
        padding 0
        margin 0
        &--icon
          margin -8px

      &-text
        color: $list-action-text-font-color
        font-size: $list-action-text-font-size

      &--stack
        align-items: flex-end
        justify-content: space-between
        padding-top: $list-top-padding
        padding-bottom: $list-bottom-padding
        white-space: nowrap
        flex-direction: column

    &__content
      text-align: left
      flex: 1 1 auto
      overflow: hidden
      display: flex
      align-items: flex-start
      justify-content: center
      flex-direction: column

      ~ .avatar
        justify-content: flex-end

      ~ .list__tile__action:not(.list__tile__action--stack)
        justify-content: flex-end

    &--active
      .list__tile__action:first-of-type
        .icon
          color: inherit

    &--disabled
      opacity: 0.4 !important
      pointer-events: none

    &--avatar
      height: $list-item-avatar-single-height

      .avatar
        img,
        .icon
          height: 40px
          width: 40px

  &--dense
    padding-top: $list-item-dense-top-padding
    padding-bottom: $list-item-dense-top-padding

    .subheader
      font-size: $subheader-dense-font-size
      height: $list-dense-subheader-height
    .list--group
      .subheader
        height $list-dense-subheader-group-height
    .list__tile
      font-size: $list-item-dense-font-size

      &--avatar
        height: $list-item-dense-avatar-single-height

      &:not(.list__tile--avatar)
        height: $list-item-dense-single-height

      .icon
        font-size: $list-item-dense-icon-font-size

    .list__tile__sub-title
      font-size: $list-item-dense-font-size

  &--two-line
    .list__tile
      height: $list-item-double-height

    &.list--dense
      .list__tile
        height: $list-item-dense-double-height

  &--three-line
    .list__tile
      height: $list-item-triple-height

    .list__tile__sub-title
      white-space: initial
      -webkit-line-clamp: 2
      -webkit-box-orient: vertical
      display: -webkit-box

    &.list--dense
      .list__tile
        height: $list-item-dense-triple-height

  &--group
    position: relative
    padding: 0

    &:after
      content: ''
      position: absolute
      left: 0
      bottom: 0
      height: 1px
      opacity: 0
      width: 100%
      background-color: $material-twelve-percent-dark

    .list__tile
      padding-left: $list-item-left-padding

    &__header
      > li:first-child
        cursor: pointer

        &:hover
          background: $material-twelve-percent-dark

      + .list--group
        &:after
          opacity: 1

      &--active
        .list__tile
          background: $material-twelve-percent-dark

          &:after
            opacity: 1

          .list__tile__title
            color: inherit

          .list__tile__action:last-of-type
            .icon
              transform: rotate(-180deg)

      &--no-action + .list--group
        .list__tile
          padding-left: $list-left-padding

  &--subheader
    padding-top: 0

  &.list--two-line,
  &.list--three-line
    .list__tile__avatar
      align-items: flex-start


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

small-dialog($material)
  &__content
    background: $material.cards

  &__actions
    background: $material.cards

  a
    color: rgba($material.fg-color, $material.primary-text-percent)

theme(small-dialog, "small-dialog")

.small-dialog
  display: block
  height: 100%

  &__content
    padding: 0 24px

  &__actions
    text-align: right

  a
    display: flex
    align-items: center

    height: 100%
    text-decoration: none

    > *
      width: 100%

  .menu__activator
    height: 100%


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

/* Theme */
table($material)
  background-color: $material.cards
  color: $material.text.primary

  thead
    border-bottom: 1px solid rgba($material.fg-color, $material.divider-percent)

    th
      color: rgba($material.text-color, $material.secondary-text-percent)

  tbody
    tr
      &:not(:last-child)
        border-bottom: 1px solid rgba($material.fg-color, $material.divider-percent)

      &[active]
        background: $material.table.active

      &:hover:not(.datatable__expand-row)
        background: $material.table.hover

  tfoot
    tr
      border-top: 1px solid rgba($material.fg-color, $material.divider-percent)

theme(table, "table")

.table
  &__overflow
    width: 100%
    overflow-x: auto
    overflow-y: hidden

table.table
  border-radius: 2px
  border-collapse: collapse
  border-spacing: 0
  width: 100%
  max-width: 100%

  thead, tbody
    td, th
      &:not(:nth-child(1)), &:first-child
        padding: 0 24px

  thead
    tr
      height: 56px

    th
      font-weight: 500
      font-size: 12px
      transition: .3s $transition.swing
      white-space: nowrap
      user-select: none

      &.sortable
        pointer-events: auto

      > div
        width: 100%

  tbody
    tr
      transition: background $primary-transition
      will-change: background

    td, th
      height: 48px

    td
      font-weight: 400
      font-size: 13px

  .input-group--selection-controls
    padding: 0

    .input-group__details
      display: none

    &.checkbox
      .icon
        left: 50%
        transform: translateX(-50%)

      .input-group--selection-controls__ripple
        left: 50%
        transform: translate(-50%, -50%)
  tfoot
    tr:not(:last-child)
      height: 48px

      &:not(:only-child)
        td
          padding: 0 24px

    tr
      height: 56px


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VRadio.vue should not render aria-label attribute with no label value on input group 1`] = `

      radio_button_unchecked

    <input name="name"
           type="radio"
           style="display: none;"
    >

`;

exports[`VRadio.vue should render aria-label attribute with label value on input group 1`] = `

    Test

      radio_button_unchecked

    <input name="name"
           type="radio"
           style="display: none;"
    >

`;

exports[`VRadio.vue should render proper input name 1`] = `

      radio_button_unchecked

    <input name="name"
           type="radio"
           style="display: none;"
    >

`;

exports[`VRadio.vue should render role and aria-checked attributes on input group 1`] = `

      radio_button_unchecked

    <input name="name"
           type="radio"
           style="display: none;"
    >

`;


import Routable from '../../mixins/routable'
import Toggleable from '../../mixins/toggleable'
import Ripple from '../../directives/ripple'

export default {
  name: 'v-list-tile',

  mixins: [Routable, Toggleable],

  directives: {
    Ripple
  },

  inheritAttrs: false,

  data: () => ({
    proxyClass: 'list__tile--active'
  }),

  props: {
    activeClass: {
      type: String,
      default: 'primary--text'
    },
    avatar: Boolean,
    inactive: Boolean,
    tag: String
  },

  computed: {
    classes () {
      return {
        'list__tile': true,
        'list__tile--link': this.isLink && !this.inactive,
        'list__tile--avatar': this.avatar,
        'list__tile--disabled': this.disabled,
        'list__tile--active': !this.to && this.isActive,
        [this.activeClass]: this.isActive
      }
    },
    isLink () {
      return this.href || this.to ||
        (this.$listeners && (this.$listeners.click || this.$listeners['!click']))
    }
  },

  render (h) {
    const isRouteLink = !this.inactive && this.isLink
    const { tag, data } = isRouteLink ? this.generateRouteLink() : {
      tag: this.tag || 'div',
      data: {
        class: this.classes
      }
    }

    data.attrs = Object.assign({}, data.attrs, this.$attrs)

    return h('li', {
      attrs: {
        disabled: this.disabled
      },
      on: {
        ...this.$listeners
      }
    }, [h(tag, data, this.$slots.default)])
  }
}


import VSlider from './VSlider'

/* istanbul ignore next */
VSlider.install = function install (Vue) {
  Vue.component(VSlider.name, VSlider)
}

export default VSlider


import VProgressLinear from './VProgressLinear'

/* istanbul ignore next */
VProgressLinear.install = function install (Vue) {
  Vue.component(VProgressLinear.name, VProgressLinear)
}

export default VProgressLinear


keep keep keep keep keep replace keep keep keep keep

# TODO editor.update should mark dirty lines

if options.ignoreDirty || child.classList.contains(Tandem.Line.DIRTY_CLASS) || true

#= require underscore
#= require rangy/rangy-core
#= require diff_match_patch
#= require eventemitter2
#= require jetsync
#= require tandem/document
#= require tandem/range
#= require tandem/keyboard

class TandemEditor extends EventEmitter2
  @editors: []

  events:
    API_TEXT_CHANGE       : 'api-text-change'
    USER_SELECTION_CHANGE : 'user-selection-change'
    USER_TEXT_CHANGE      : 'user-text-change'

  options:
    POLL_INTERVAL: 500

  constructor: (@container) ->
    @container = document.getElementById(@container) if _.isString(@container)
    @iframe = this.createIframe(@container)
    @doc = new Tandem.Document(this, @iframe.contentWindow.document.body)
    @ignoreDomChanges = false
    @currentSelection = null
    @keyboard = new Tandem.Keyboard(this)
    this.initContentListeners()
    this.initSelectionListeners()
    TandemEditor.editors.push(this)

  createIframe: (parent) ->
    html = parent.innerHTML
    parent.innerHTML = ''
    iframe = document.createElement('iframe')
    iframe.frameborder = 0
    iframe.src = 'javascript:;'
    iframe.height = iframe.width = '100%'
    parent.appendChild(iframe)
    doc = iframe.contentWindow.document
    head = doc.getElementsByTagName('head')[0]
    style = doc.createElement('style')
    style.type = 'text/css'
    css = "
      body {
        font-family: 'Helvetica', 'Arial', san-serif;
        font-size: 13px;
        line-height: 15px;
        margin: 0px;
        padding: 10px;
      }

      a { text-decoration: underline; }
      b { font-weight: bold; }
      i { font-style: italic; }
      s { text-decoration: line-through; }
      u { text-decoration: underline; }

      ol, ul { margin: 0px; padding: 0px; }
      ul { list-style-type: disc; }

      ol.indent-1, ol.indent-4, ol.indent-7 { list-style-type: decimal; }
      ol.indent-2, ol.indent-5, ol.indent-8 { list-style-type: lower-alpha; }
      ol.indent-3, ol.indent-6, ol.indent-9 { list-style-type: lower-roman; }

      .font-background.black  { background-color: black; }
      .font-background.red    { background-color: red; }
      .font-background.orange { background-color: orange; }
      .font-background.yellow { background-color: yellow; }
      .font-background.green  { background-color: green; }
      .font-background.blue   { background-color: blue; }
      .font-background.purple { background-color: purple; }
      .font-color.white       { color: white; }
      .font-color.red         { color: red; }
      .font-color.orange      { color: orange; }
      .font-color.yellow      { color: yellow; }
      .font-color.green       { color: green; }
      .font-color.blue        { color: blue; }
      .font-color.purple      { color: purple; }
      .font-family.monospace  { font-family: 'Courier New', monospace; }
      .font-family.serif      { font-family: 'Times New Roman', serif; }
      .font-size.huge         { font-size: 32px; line-height: 36px; }
      .font-size.large        { font-size: 18px; line-height: 22px; }
      .font-size.small        { font-size: 10px; line-height: 12px; }

      .indent-1 { margin-left: 2em; }
      .indent-2 { margin-left: 4em; }
      .indent-3 { margin-left: 6em; }
      .indent-4 { margin-left: 8em; }
      .indent-5 { margin-left: 10em; }
      .indent-6 { margin-left: 12em; }
      .indent-7 { margin-left: 14em; }
      .indent-8 { margin-left: 16em; }
      .indent-9 { margin-left: 18em; }

      .tab { display: inline-block; margin: 0px; width: 2em; }
    "
    if style.styleSheet?
      style.styleSheet.cssText = css
    else
      style.appendChild(doc.createTextNode(css))
    head.appendChild(style)
    doc.body.setAttribute('contenteditable', true)
    doc.body.innerHTML = html
    return iframe

  initContentListeners: ->
    onEdit = _.debounce( =>
      delta = this.update()
      this.emit(this.events.USER_TEXT_CHANGE, delta) if !delta.isIdentity()
    , 100)

    @doc.root.addEventListener('DOMSubtreeModified', =>
      return if @ignoreDomChanges
      onEdit()
    )

  initSelectionListeners: ->
    debounceCheckSelectionChange = _.debounce( =>
      this.checkSelectionChange()
    , 100)
    @doc.root.addEventListener('keyup', debounceCheckSelectionChange)
    @doc.root.addEventListener('mouseup', debounceCheckSelectionChange)
    setInterval((=> this.checkSelectionChange()), this.options.POLL_INTERVAL)

  # applyAttribute: (TandemRange range, String attr, Mixed value) ->
  # applyAttribute: (Number startIndex, Number length, String attr, Mixed value) ->
  applyAttribute: (startIndex, length, attr, value, emitEvent = true) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    if !_.isNumber(startIndex)
      [range, attr, value] = [startIndex, length, attr]
      startIndex = range.start.getIndex()
      length = range.end.getIndex() - startIndex
    else
      range = new Tandem.Range(this, startIndex, startIndex + length)

    this.preserveSelection(range.start, 0, =>
      [startLine, startLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
      [endLine, endLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex + length)
      if startLine == endLine
        this.applyAttributeToLine(startLine, startLineOffset, endLineOffset, attr, value)
      else
        curLine = startLine.nextSibling
        while curLine? && curLine != endLine
          nextSibling = curLine.nextSibling
          this.applyAttributeToLine(curLine, 0, curLine.textContent.length, attr, value)
          curLine = nextSibling
        this.applyAttributeToLine(startLine, startLineOffset, startLine.textContent.length, attr, value)
        this.applyAttributeToLine(endLine, 0, endLineOffset, attr, value)
      Tandem.Document.fixListNumbering(@doc.root) if attr == 'list'
      @doc.rebuildDirty()
    )
    if emitEvent
      deltas = []
      deltas.push(new JetRetain(0, startIndex)) if startIndex > 0
      attribute = {}
      attribute[attr] = value
      deltas.push(new JetRetain(startIndex, startIndex + length, attribute))
      deltas.push(new JetRetain(startIndex + length, @doc.length)) if startIndex + length < @doc.length
      delta = new JetDelta(@doc.length, @doc.length, deltas)
      this.emit(this.events.API_TEXT_CHANGE, delta)
    @ignoreDomChanges = oldIgnoreDomChange

  applyAttributeToLine: (lineNode, startOffset, endOffset, attr, value) ->
    line = @doc.findLine(lineNode)
    if _.indexOf(Tandem.Constants.LINE_ATTRIBUTES, attr, true) > -1
      this.applyLineAttribute(line, attr, value)
    else
      return if startOffset == endOffset
      [prevNode, startNode] = line.splitContents(startOffset)
      [endNode, nextNode] = line.splitContents(endOffset)
      if value && Tandem.Utils.getAttributeDefault(attr) != value
        fragment = @doc.root.ownerDocument.createDocumentFragment()
        Tandem.Utils.traverseSiblings(startNode, endNode, (node) ->
          fragment.appendChild(node)
        )
        attrNode = Tandem.Utils.createContainerForAttribute(@doc.root.ownerDocument, attr, value)
        attrNode.appendChild(fragment)
        lineNode.insertBefore(attrNode, nextNode)
      else
        Tandem.Utils.traverseSiblings(startNode, endNode, (node) ->
          Tandem.Utils.removeAttributeFromSubtree(node, attr)
        )
    @doc.updateLine(line)

  applyDelta: (delta) ->
    console.assert(delta.startLength == @doc.length, "Trying to apply delta to incorrect doc length", delta, @doc, @doc.root)
    index = 0       # Stores where the last retain end was, so if we see another one, we know to delete
    offset = 0      # Tracks how many characters inserted to correctly offset new text
    _.each(delta.deltas, (delta) =>
      if JetDelta.isInsert(delta)
        this.insertAt(index + offset, delta.text, false)
        _.each(delta.attributes, (value, attr) =>
          this.applyAttribute(index + offset, delta.text.length, attr, value, false)
        )
        offset += delta.getLength()
      else if JetDelta.isRetain(delta)
        if delta.start > index
          this.deleteAt(index + offset, delta.start - index, false)
          offset -= (delta.start - index)
        else
          _.each(delta.attributes, (value, attr) =>
            this.applyAttribute(index + offset, delta.end - delta.start, attr, value, false)
          )
        index = delta.end
      else
        console.warn('Unrecognized type in delta', delta)
    )
    # If end of text was deleted
    if delta.endLength < delta.startLength + offset
      this.deleteAt(delta.endLength, delta.startLength + offset - delta.endLength, false)

  applyLineAttribute: (line, attr, value) ->
    curIndent = if line.attributes[attr]? then line.attributes[attr] else 0
    nextIndent = if _.isNumber(value) then value else (if value then 1 else 0)
    if _.indexOf(Tandem.Constants.LIST_ATTRIBUTES, attr, true) > -1
      lineNode = line.node
      expectedTag = if value then (if attr == 'list' then 'OL' else 'UL') else 'DIV'
      if lineNode.tagName != expectedTag
        if value && lineNode.firstChild.tagName != 'LI'
          li = lineNode.ownerDocument.createElement('li')
          Tandem.Utils.wrapChildren(li, lineNode)
        else if !value && lineNode.firstChild.tagName == 'LI'
          Tandem.Utils.unwrap(lineNode.firstChild)
        line.node = Tandem.Utils.switchTag(lineNode, expectedTag)
      Tandem.Utils.setIndent(line.node, curIndent + value)
    if attr == 'indent'
      Tandem.Utils.setIndent(line.node, curIndent + value)

  checkSelectionChange: ->
    return if @ignoreDomChanges
    selection = this.getSelection()
    if selection != @currentSelection && ((selection? && !selection.equals(@currentSelection)) || !@currentSelection.equals(selection))
      this.emit(this.events.USER_SELECTION_CHANGE, selection)
      @currentSelection = selection

  deleteAt: (startIndex, length, emitEvent = true) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    originalDocLength = @doc.length
    if !_.isNumber(startIndex)
      range = startIndex
      startPos = range.start
      endPos = range.end
      startIndex = range.start.getIndex()
      length = range.end.getIndex() - startIndex
    else
      startPos = Tandem.Position.makePosition(this, startIndex)
      endPos = Tandem.Position.makePosition(this, startIndex + length)
    startIndex = startPos.getIndex()
    endIndex = endPos.getIndex()
    this.preserveSelection(startPos, 0 - length, =>
      [startLineNode, startOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
      [endLineNode, endOffset] = Tandem.Utils.getChildAtOffset(@doc.root, endIndex)
      fragment = Tandem.Utils.extractNodes(startLineNode, startOffset, endLineNode, endOffset)
      lineNodes = _.values(fragment.childNodes).concat(_.uniq([startLineNode, endLineNode]))
      _.each(lineNodes, (lineNode) =>
        line = @doc.findLine(lineNode)
        @doc.updateLine(line) if line?
      )
    )
    if emitEvent
      deltas = []
      deltas.push(new JetRetain(0, startIndex)) if 0 < startIndex
      deltas.push(new JetRetain(startIndex + length, originalDocLength)) if startIndex + length < originalDocLength
      delta = new JetDelta(originalDocLength, @doc.length, deltas)
      this.emit(this.events.API_TEXT_CHANGE, delta)
    @ignoreDomChanges = oldIgnoreDomChange

  getAt: (startIndex, length) ->
    # - Returns array of {text: "", attr: {}}
    # 1. Get all nodes in the range
    # 2. For first and last, change the text
    # 3. Return array
    # - Helper to get nodes in given index range
    # - In the case of 0 lenght, text will always be "", but attributes should be properly applied

  getDelta: ->
    return @doc.toDelta()

  getSelection: ->
    return Tandem.Range.getSelection(this)

  insertAt: (startIndex, text, emitEvent = true) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    originalDocLength = @doc.length
    position = Tandem.Position.makePosition(this, startIndex)
    index = startIndex = position.getIndex()
    this.preserveSelection(position, text.length, =>
      lines = text.split("\n")
      _.each(lines, (line, lineIndex) =>
        strings = line.split("\t")
        _.each(strings, (str, strIndex) =>
          this.insertTextAt(index, str)
          index += str.length
          if strIndex < strings.length - 1
            this.insertTabAt(index)
            index += 1
        )
        if lineIndex < lines.length - 1
          this.insertNewlineAt(index)
          index += 1
      )
    )
    if emitEvent
      deltas = []
      deltas.push(new JetRetain(0, startIndex)) if 0 < startIndex
      deltas.push(new JetInsert(text))
      deltas.push(new JetRetain(startIndex, originalDocLength)) if startIndex < originalDocLength
      delta = new JetDelta(originalDocLength, @doc.length, deltas)
      this.emit(this.events.API_TEXT_CHANGE, delta)
    @ignoreDomChanges = oldIgnoreDomChange

  insertNewlineAt: (startIndex) ->
    [line, offset] = @doc.findLineAtOffset(startIndex)
    @doc.splitLine(line, offset)

  insertTabAt: (startIndex) ->
    [startLineNode, startLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
    startLine = @doc.findLine(startLineNode)
    [prevNode, startNode] = startLine.splitContents(startLineOffset)
    tab = startLineNode.ownerDocument.createElement('span')
    tab.classList.add(Tandem.Leaf.TAB_NODE_CLASS)
    startLineNode.insertBefore(tab, startNode)
    @doc.updateLine(startLine)

  # insertTextAt: (Number startIndex, String text) ->
  # insertTextAt: (TandemRange startIndex, String text) ->
  insertTextAt: (startIndex, text) ->
    return if text.length <= 0
    position = Tandem.Position.makePosition(this, startIndex)
    startIndex = position.getIndex()
    leaf = position.getLeaf()
    if _.keys(leaf.attributes).length > 0
      [lineNode, lineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
      line = @doc.findLine(lineNode)
      [beforeNode, afterNode] = line.splitContents(lineOffset)
      span = lineNode.ownerDocument.createElement('span')
      span.textContent = text
      lineNode.insertBefore(span, afterNode)
    else
      if leaf.node.nodeName == 'BR'
        parent = leaf.node.parentNode
        parent.removeChild(leaf.node)
        leaf.node = parent.ownerDocument.createElement('span')
        leaf.setText(text)
        parent.appendChild(leaf.node)
      else
        leaf.setText(leaf.node.textContent.substr(0, position.offset) + text + leaf.node.textContent.substr(position.offset))
    @doc.updateLine(leaf.line)

  preserveSelection: (modificationStart, charAdditions, fn) ->
    if @currentSelection?
      [selStart, selEnd] = this.transformSelection(modificationStart, @currentSelection, charAdditions)
      fn()
      savedSelectionRange = new Tandem.Range(@currentSelection.editor, selStart, selEnd)
      rangySel = rangy.getSelection(@iframe.contentWindow)
      range = savedSelectionRange.getRangy()
      rangySel.setSingleRange(range)
    else
      fn()

  transformSelection: (modificationStart, selectionRange, charAdditions) ->
    modPos = if modificationStart? then modificationStart.getIndex() else 0
    selStart = selectionRange.start.getIndex()
    selEnd = selectionRange.end.getIndex()
    selStart = Math.max(selStart + charAdditions, modPos) if modPos <= selStart
    selEnd = Math.max(selEnd + charAdditions, modPos) if modPos < selEnd
    selEnd = Math.max(selStart, selEnd)
    return [selStart, selEnd]

  update: ->
    oldDelta = @doc.toDelta()
    #this.preserveSelection(null, 0, =>
    # TODO this is incorrect, we need to search for dirty lines and mark them, normalize only operates on dirty lines
    Tandem.Document.normalizeHtml(@doc.root)
    lines = @doc.lines.toArray()
    lineNode = @doc.root.firstChild
    _.each(lines, (line, index) =>
      while line.node != lineNode
        if line.node.parentNode == @doc.root
          newLine = @doc.insertLineBefore(lineNode, line)
          lineNode = lineNode.nextSibling
        else
          @doc.removeLine(line)
          return
      @doc.updateLine(line)
      lineNode = lineNode.nextSibling
    )
    while lineNode != null
      newLine = @doc.appendLine(lineNode)
      lineNode = lineNode.nextSibling
    #)
    newDelta = @doc.toDelta()
    decompose = JetSync.decompose(oldDelta, newDelta)
    compose = JetSync.compose(oldDelta, decompose)
    if !_.isEqual(compose, newDelta)
      console.info(JSON.stringify(oldDelta))
      console.info(JSON.stringify(newDelta))
      console.info(JSON.stringify(decompose))
      console.info(JSON.stringify(compose))
      console.assert(false, oldDelta, newDelta, decompose, compose)
    return JetSync.decompose(oldDelta, newDelta)

window.Tandem ||= {}
window.Tandem.Editor = TandemEditor


$(document).ready( ->
  editor = new Tandem.Editor('editor-container')
  toolbar = new Tandem.Toolbar(editor)
  _.each(['bold', 'italic', 'strike', 'underline'], (format) ->
    $("#formatting-container .#{format}").click( ->
      toolbar.applyAttribute(format, !$(this).hasClass('active'))
    )
  )
  toolbar.on('update', (attributes) ->
    $("#formatting-container .format-button").removeClass('active')
    for key,value of attributes when value == true
      $("#formatting-container .#{key}").addClass('active')
  )

  rangy.init()

  editor.getText = -> ""
  editor.setText = ->
  editor.setCursor = ->
  editor.clearCursors = ->
  editor.clearMessages = ->
  editor.addMessage = ->
  editor.updateComposing = ->

  delegate = new Object()
  jetClient = new JetClient({delegate: delegate})
  textState = new JetTextState(editor, jetClient, "", Stypi.configs.sessionId)
  chatState = new JetChatState(editor, jetClient, [], Stypi.configs.sessionId)
  cursorState = new JetCursorState(editor, jetClient, {}, Stypi.configs.sessionId)
  jetClient.addState(textState)
  jetClient.addState(cursorState)
  jetClient.addState(chatState)
  jetClient.connect(Stypi.configs.docId, Stypi.configs.version)

  textState.applyDeltaToText = (delta, authorId) ->   # Hacky overwrite
    editor.applyDelta(delta)
  textState.applyDeltaToCursors = ->
  Stypi.Presence = {
    setUsers: ->
  }

  editor.on(editor.events.API_TEXT_CHANGE, (delta) ->
    textState.localUpdate(delta)
    jetClient.checkRunwayReady()
  )
  editor.on(editor.events.USER_TEXT_CHANGE, (delta) ->
    textState.localUpdate(delta)
    jetClient.checkRunwayReady()
  )
)


/* http://meyerweb.com/eric/tools/css/reset/
   v2.0b1 | 201101
   NOTE: WORK IN PROGRESS
   USE WITH CAUTION AND TEST WITH ABANDON */

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
	margin: 0;
	padding: 0;
	border: 0;
	outline: 0;
	font-size: 100%;
	font: inherit;
	vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
	display: block;
}
body {
	line-height: 1;
}
ol, ul {
	list-style: none;
}
blockquote, q {
	quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
	content: '';
	content: none;
}

/* remember to define visible focus styles!
:focus {
	outline: ?????;
} */

/* remember to highlight inserts somehow! */
ins {
	text-decoration: none;
}
del {
	text-decoration: line-through;
}

table {
	border-collapse: collapse;
	border-spacing: 0;
}


!!! 5
%html
  %head
    %title Stypi Editor Normalize Test
    %link{:rel => 'stylesheet', :type => 'text/css', :href => '../../../lib/mocha.css'}

%body
  #mocha
  #editor-container

  %script{:type => 'text/javascript', :src => '../../../lib/jquery.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/underscore.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/eventemitter2.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/rangy/rangy-core.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/diff_match_patch.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/jetsync.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/linked_list.js'}
  %script{:type => 'text/javascript', :src => '../../src/constants.js'}
  %script{:type => 'text/javascript', :src => '../../src/utils.js'}
  %script{:type => 'text/javascript', :src => '../../src/leaf.js'}
  %script{:type => 'text/javascript', :src => '../../src/line.js'}
  %script{:type => 'text/javascript', :src => '../../src/document.js'}
  %script{:type => 'text/javascript', :src => '../../src/position.js'}
  %script{:type => 'text/javascript', :src => '../../src/range.js'}
  %script{:type => 'text/javascript', :src => '../../src/keyboard.js'}
  %script{:type => 'text/javascript', :src => '../../src/editor.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/mocha.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/chai.js'}
  :javascript
    mocha.setup('bdd');
    expect = chai.expect
  %script{:type => 'text/javascript', :src => 'scripts/normalize.js'}
  :javascript
    mocha.run();


else if !value && lineNode.firstChild.tagName == 'LI'

line.node = Tandem.Utils.switchTag(lineNode, expectedTag)

Tandem.Utils.setIndent(line.node, curIndent + value)


notification :off

guard 'haml', :output => 'bin', :run_at_start => true do
  watch %r{(.+\.haml)}
end

guard 'coffeescript', :output => 'bin', :all_on_start => true do
  watch %r{(.+\.coffee)}
end

guard 'sass', :output => 'bin', :all_on_start => true do
  watch %r{(.+\.sass)}
end


#= require underscore
#= require linked_list

class TandemLeaf extends LinkedList.Node
  @ID_PREFIX: 'leaf-'
  @TAB_NODE_CLASS: 'tab'

  @isLeafNode: (node) ->
    return false if !node?
    return true if node.childNodes.length == 1 && node.firstChild.nodeType == node.TEXT_NODE
    return true if node.tagName == 'BR'
    return true if node.classList?.contains(Tandem.Leaf.TAB_NODE_CLASS)
    return false

  constructor: (@line, @node, attributes) ->
    @attributes = _.clone(attributes)
    @id = _.uniqueId(Tandem.Leaf.ID_PREFIX)
    if !@node.classList.contains(Tandem.Leaf.TAB_NODE_CLASS)
      @text = @node.textContent
      @length = @text.length
    else
      @length = 1
      @text = "\t"
      @node.textContent = "\t"
      @node.setAttribute('contenteditable', false)

  getAttributes: ->
    return _.extend({}, @attributes, @line.attributes)

  setText: (@text) ->
    @node.textContent = @text
    @length = @text.length

class TandemLeafIterator
  # Start and end are both inclusive
  # Otherwise if end is inclusive, we cannot express end of line unambiguously
  constructor: (@start, @end) ->
    @cur = @start

  next: ->
    ret = @cur
    if @cur == @end || @cur == null
      @cur = null
    else if @cur.next?
      @cur = @cur.next
    else
      line = @cur.line.next
      while line? && line.leaves.length == 0
        line = line.next
      @cur = if line? then line.leaves.first else null
    return ret

  toArray: ->
    arr = []
    itr = new TandemLeafIterator(@start, @end)
    while next = itr.next()
      arr.push(next)
    return arr

window.Tandem ||= {}
window.Tandem.Leaf = TandemLeaf
window.Tandem.LeafIterator = TandemLeafIterator


#= require underscore
#= require tandem/line

class TandemPosition
  @findLeafNode: (node, offset) ->
    if offset <= node.textContent.length    # We are at right subtree, dive deeper
      if node.firstChild?
        TandemPosition.findLeafNode(node.firstChild, offset)
      else
        node = node.parentNode if node.nodeType == node.TEXT_NODE
        if offset == node.textContent.length && node.nextSibling?
          TandemPosition.findLeafNode(node.nextSibling, 0)
        else
          return [node, offset]
    else if node.nextSibling?               # Not at right subtree, advance to sibling
      offset -= 1 if Tandem.Line.isLineNode(node)
      TandemPosition.findLeafNode(node.nextSibling, offset - node.textContent.length)
    else
      throw new Error('Diving exceeded offset')

  @makePosition: (editor, index) ->
    if _.isNumber(index)
      position = new Tandem.Position(editor, index)
    else if index instanceof Tandem.Range
      position = index.start
      index = position.getIndex()
    else
      position = index
      index = position.getIndex()
    return position

  # constructor: (TandemEditor editor, Object node, Number offset) ->
  # constructor: (TandemEditor editor, Number index) ->
  constructor: (@editor, @leafNode, @offset) ->
    if _.isNumber(@leafNode)
      @offset = @leafNode
      @index = @leafNode
      @leafNode = @editor.doc.root.firstChild
    [@leafNode, @offset] = TandemPosition.findLeafNode(@leafNode, @offset)

  getIndex: ->
    return @index if @index?
    @index = @offset
    node = @leafNode
    while node.tagName != 'BODY'
      while node.previousSibling?
        node = node.previousSibling
        @index += node.textContent.length + (if Tandem.Line.isLineNode(node) then 1 else 0) # Account for newline char
      node = node.parentNode
    return @index

  getLeaf: ->
    return @leaf if @leaf?
    @leaf = @editor.doc.findLeaf(@leafNode)
    return @leaf

window.Tandem ||= {}
window.Tandem.Position = TandemPosition


!!! 5
%html
  %head
    %title Stypi OT Demo
    %link{:rel => 'stylesheet', :type => 'text/css', :href => '../lib/reset.css'}
    %link{:rel => 'stylesheet', :type => 'text/css', :href => 'styles/style.css'}

  %body
    #content-container
      .editor-wrapper.first
        #formatting-container.formatting-container
          .format-group
            .format-button.first<>
              .format.bold
            .format-button<>
              .format.italic
            .format-button<>
              .format.underline
            .format-button.last<>
              .format.strike
        #editor-container.editor-container

    :javascript
      var Stypi = {
        configs: {
          "nodeHost": "devnode.stypi.com",
          "registered": null,
          "auth": ["6W9E9Rup+pRGNexIrf5zPw==","Q27mpDHvVoe7OEIcvgMtXjSbhzb5ZRrGIcY2pD0gXe5LSO2w/FUm6z65fo1A\nAdZd"],
          "docId": "504e6d488fc60a5212000003",
          "sessionId": "81mxY8NDl3Wm1Hu6",
          "userId": "504e6d478fc60a5212000001",
          "version": 0,
          "environment":"development"
        }
      };
    %script{:type => 'text/javascript', :src => '../lib/jquery.js'}
    %script{:type => 'text/javascript', :src => '../lib/underscore.js'}
    %script{:type => 'text/javascript', :src => '../lib/eventemitter2.js'}
    %script{:type => 'text/javascript', :src => '../lib/rangy/rangy-core.js'}
    %script{:type => 'text/javascript', :src => '../lib/linked_list.js'}
    %script{:type => 'text/javascript', :src => '../lib/jetsync.js'}
    %script{:type => 'text/javascript', :src => '../src/constants.js'}
    %script{:type => 'text/javascript', :src => '../src/utils.js'}
    %script{:type => 'text/javascript', :src => '../src/leaf.js'}
    %script{:type => 'text/javascript', :src => '../src/line.js'}
    %script{:type => 'text/javascript', :src => '../src/document.js'}
    %script{:type => 'text/javascript', :src => '../src/position.js'}
    %script{:type => 'text/javascript', :src => '../src/range.js'}
    %script{:type => 'text/javascript', :src => '../src/keyboard.js'}
    %script{:type => 'text/javascript', :src => '../src/editor.js'}
    %script{:type => 'text/javascript', :src => '../src/toolbar.js'}
    %script{:type => 'text/javascript', :src => '../src/debug.js'}

    %script{:type => 'text/javascript', :src => '../lib/diff_match_patch.js'}
    %script{:type => 'text/javascript', :src => '../lib/async.js'}
    %script{:type => 'text/javascript', :src => '../lib/socket.io.js'}
    %script{:type => 'text/javascript', :src => 'scripts/stypi/network.js'}
    %script{:type => 'text/javascript', :src => 'scripts/stypi/ot/jetclient.js'}
    %script{:type => 'text/javascript', :src => 'scripts/stypi/ot/jetstate.js'}

    %script{:type => 'text/javascript', :src => 'scripts/ot.js'}


GIT
  remote: git://github.com/manufaktor/guard-haml.git
  revision: ee19e704c5afface900d18ba7f9ad90993712d02
  branch: development
  specs:
    guard-haml (0.4)
      guard (>= 1.1)
      haml (>= 3.0)

GEM
  remote: http://rubygems.org/
  specs:
    coffee-script (2.2.0)
      coffee-script-source
      execjs
    coffee-script-source (1.3.3)
    execjs (1.4.0)
      multi_json (~> 1.0)
    ffi (1.1.5)
    guard (1.3.0)
      listen (>= 0.4.2)
      thor (>= 0.14.6)
    guard-coffeescript (1.2.0)
      coffee-script (>= 2.2.0)
      guard (>= 1.1.0)
    guard-sass (1.0.0)
      guard (>= 1.1.0)
      sass (>= 3.1)
    haml (3.1.7)
    listen (0.4.7)
      rb-fchange (~> 0.0.5)
      rb-fsevent (~> 0.9.1)
      rb-inotify (~> 0.8.8)
    multi_json (1.3.6)
    rb-fchange (0.0.5)
      ffi
    rb-fsevent (0.9.1)
    rb-inotify (0.8.8)
      ffi (>= 0.5.0)
    sass (3.1.20)
    thor (0.15.4)

PLATFORMS
  ruby

DEPENDENCIES
  guard
  guard-coffeescript
  guard-haml!
  guard-sass


# TODO could be more clever about if we need to call this


ALPHABET = 'abcdefghijklmnopqrstuvwxyz'.split('').concat(_.map([0..15], -> return "\n"))
NUM_OPERATIONS = 1000
attributes = _.extend({}, Tandem.Constants.SPAN_ATTRIBUTES, Tandem.Constants.TAG_ATTRIBUTES)
attributeKeys = _.keys(attributes)

getRandomLength = ->
  rand = Math.random()
  if rand < 0.1
    return 1
  else if rand < 0.6
    return Math.floor(Math.random() * 3)
  else if rand < 0.8
    return Math.floor(Math.random() * 5)
  else if rand < 0.9
    return Math.floor(Math.random() * 10)
  else
    return Math.floor(Math.random() * 50)

getRandomOperation = (editor) ->
  rand = Math.random()
  if rand < 0.2
    index = 0
  else if rand < 0.4
    index = editor.doc.length
  else
    index = Math.floor(Math.random() * editor.doc.length)
  length = getRandomLength() + 1
  rand = Math.random()
  if rand < 0.5
    return {op: 'insertAt', args: [index, getRandomString(length)]}
  length = Math.min(length, editor.doc.length - index)
  return null if length <= 0
  if rand < 0.75
    return {op: 'deleteAt', args: [index, length]}
  else
    attr = attributeKeys[Math.floor(Math.random() * attributeKeys.length)]
    value = attributes[attr][Math.floor(Math.random() * attributes[attr].length)]
    if attr == 'link' && value == true
      value = 'http://www.google.com'
    return {op: 'applyAttribute', args: [index, length, attr, value]}

getRandomString = (length) ->
  return _.map([0..(length - 1)], ->
    return ALPHABET[Math.floor(Math.random()*length)]
  ).join('')

$(document).ready( ->
  rangy.init()
  $editors = $('.editor-container')
  writer = new Tandem.Editor($editors.get(0))
  reader = new Tandem.Editor($editors.get(1))
  start = new Date()

  writer.on(writer.events.API_TEXT_CHANGE, (delta) ->
    reader.applyDelta(delta)
  )

  finishedFuzzing = _.after(NUM_OPERATIONS, ->
    _.defer( ->
      writerHtml = Tandem.Utils.cleanHtml(writer.doc.root.innerHTML)
      readerHtml = Tandem.Utils.cleanHtml(reader.doc.root.innerHTML)
      writerDelta = writer.getDelta()
      readerDelta = reader.getDelta()

      if writerHtml == readerHtml && _.isEqual(writerDelta, readerDelta)
        time = (new Date() - start) / 1000
        console.info "Fuzzing passed"
        console.info time, 'seconds'
        console.info (NUM_OPERATIONS / time).toPrecision(2), 'ops/sec'
      else
        console.error "Fuzzing failed", writer, reader
    )
  )

  _.each([1..NUM_OPERATIONS], ->
    _.defer( ->
      operation = getRandomOperation(writer)
      if operation?
        writer[operation.op].apply(writer, operation.args)
      finishedFuzzing()
    )
  )
)