<template>
  <v-card>
    <v-toolbar
      color="purple"
      dark
      flat
      prominent
    >
      <v-text-field
        append-icon="mic"
        class="mx-3"
        flat
        hide-details
        label="Search"
        prepend-inner-icon="search"
        solo-inverted
      ></v-text-field>

      <v-tabs
        slot="extension"
        v-model="tabs"
        centered
      >
        <v-tab
          v-for="n in 3"
          :key="n"
        >
          Item {{ n }}
        </v-tab>
      </v-tabs>
    </v-toolbar>

    <v-tabs-items v-model="tabs">
      <v-tab-item>
        <v-card flat>
          <v-card-text>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
          </v-card-text>
        </v-card>
      </v-tab-item>
      <v-tab-item>
        <v-card flat>
          <v-card-title class="headline">An awesome title</v-card-title>
          <v-card-text>

              Duis lobortis massa imperdiet quam. Donec vitae orci sed dolor rutrum auctor. Vestibulum facilisis, purus nec pulvinar iaculis, ligula mi congue nunc, vitae euismod ligula urna in dolor. Praesent congue erat at massa.

              Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc, eu sollicitudin urna dolor sagittis lacus. Pellentesque egestas, neque sit amet convallis pulvinar, justo nulla eleifend augue, ac auctor orci leo non est. Etiam sit amet orci eget eros faucibus tincidunt. Donec sodales sagittis magna.

              Ut leo. Suspendisse potenti. Duis vel nibh at velit scelerisque suscipit. Fusce pharetra convallis urna.

          </v-card-text>
        </v-card>
      </v-tab-item>
      <v-tab-item>
        <v-card flat>
          <v-card-title class="headline">An even better title</v-card-title>
          <v-card-text>

              Maecenas ullamcorper, dui et placerat feugiat, eros pede varius nisi, condimentum viverra felis nunc et lorem. Sed hendrerit. Maecenas malesuada. Vestibulum ullamcorper mauris at ligula. Proin faucibus arcu quis ante.

              Etiam vitae tortor. Curabitur ullamcorper ultricies nisi. Sed magna purus, fermentum eu, tincidunt eu, varius ut, felis. Aliquam lobortis. Suspendisse potenti.

          </v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>


keep keep keep keep add keep keep keep keep keep

<template>
  <v-toolbar
    color="orange accent-1"
    prominent
  >
    <v-app-bar-nav-icon></v-app-bar-nav-icon>
    <v-toolbar-title class="title mr-4">Cryptocurrency</v-toolbar-title>
    <v-autocomplete
      v-model="model"
      :items="items"
      :loading="isLoading"
      :search-input.sync="search"
      chips
      clearable
      hide-details
      hide-selected
      item-text="name"
      item-value="symbol"
      label="Search for a coin..."
      solo
    >
      <template v-slot:no-data>
        <v-list-item>
          <v-list-item-title>
            Search for your favorite
            Cryptocurrency
          </v-list-item-title>
        </v-list-item>
      </template>
      <template v-slot:selection="{ item, selected }">
        <v-chip
          :selected="selected"
          color="blue-grey"
          class="white--text"
        >
          <v-icon left>mdi-coin</v-icon>

        </v-chip>
      </template>
      <template v-slot:item="{ item }">
        <v-list-item-avatar
          color="indigo"
          class="headline font-weight-light white--text"
        >
          {{ item.name.charAt(0) }}
        </v-list-item-avatar>
        <v-list-item-content>
          <v-list-item-title v-text="item.name"></v-list-item-title>
          <v-list-item-subtitle v-text="item.symbol"></v-list-item-subtitle>
        </v-list-item-content>
        <v-list-item-action>
          <v-icon>mdi-coin</v-icon>
        </v-list-item-action>
      </template>
    </v-autocomplete>
    <template v-slot:extension>
      <v-tabs
        :hide-slider="!model"
        color="transparent"
        slider-color="blue-grey"
      >
        <v-tab :disabled="!model">News</v-tab>
        <v-tab :disabled="!model">Trading</v-tab>
        <v-tab :disabled="!model">Blog</v-tab>
      </v-tabs>
    </template>
  </v-toolbar>
</template>

<codepen-resources lang="json">
  {
    "css": ["https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css"]
  }
</codepen-resources>


border-bottom-right-radius: #{$size * 0.6666666666666667} !important

border-top-right-radius: #{$size * 0.6666666666666667} !important

@mixin list-shaped($size)
  .v-list-item,
  .v-list-item:before
    border-bottom-right-radius: #{$size * 0.6666666666666667}
    border-top-right-radius: #{$size * 0.6666666666666667}


@import '../../styles/styles.sass'

+theme(v-small-dialog) using ($material)
  a
    color: rgba(map-get($material, 'fg-color'), map-get($material, 'primary-text-percent'))

  &__content,
  &__actions
    background: map-get($material, 'cards')

.v-small-dialog
  display: block
  width: 100%
  height: 100%

  &__content
    padding: 0 24px

  &__actions
    text-align: right
    white-space: pre

  a
    display: flex
    align-items: center

    height: 100%
    text-decoration: none

    > *
      width: 100%

  .v-menu__activator
    height: 100%


// main.sass
@import '~vuetify/src/styles/styles.sass'


<script>
  // Extensions
  import Heading from './Heading'

  export default {
    name: 'Subheading',

    extends: Heading,

    props: {
      tag: {
        type: String,
        default: 'v-headline'
      }
    }
  }
</script>


keep keep keep keep replace keep keep keep keep keep

// Components
import VWindow from './VWindow'

// Mixins
import Bootable from '../../mixins/bootable'
import { factory as GroupableFactory } from '../../mixins/groupable'

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

// Utilities
import {
  addOnceEventListener,
  convertToUnit
} from '../../util/helpers'
import { ExtractVue } from './../../util/mixins'
import mixins from '../../util/mixins'

// Types
import Vue from 'vue'
import { VNode, VNodeDirective } from 'vue/types'

type VBaseWindow = InstanceType<typeof VWindow>

interface options extends Vue {
  windowGroup: VBaseWindow
}

export default mixins<options & ExtractVue<[typeof Bootable]>>(
  Bootable,
  GroupableFactory('windowGroup', 'v-window-item', 'v-window')
  /* @vue/component */
).extend({
  name: 'v-window-item',

  directives: {
    Touch
  },

  props: {
    reverseTransition: {
      type: [Boolean, String],
      default: undefined
    },
    transition: {
      type: [Boolean, String],
      default: undefined
    },
    value: {
      required: false
    }
  },

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

  computed: {
    computedTransition (): string | boolean {
      if (!this.windowGroup.internalReverse) {
        return typeof this.transition !== 'undefined'
          ? this.transition || ''
          : this.windowGroup.computedTransition
      }

      return typeof this.reverseTransition !== 'undefined'
        ? this.reverseTransition || ''
        : this.windowGroup.computedTransition
    }
  },

  methods: {
    genDefaultSlot () {
      return this.$slots.default
    },
    onAfterEnter () {
      if (this.wasCancelled) {
        this.wasCancelled = false
        return
      }

      requestAnimationFrame(() => {
        this.windowGroup.internalHeight = undefined
        this.windowGroup.isActive = false
      })
    },
    onBeforeEnter () {
      this.windowGroup.isActive = true
    },
    onBeforeLeave (el: HTMLElement) {
      this.windowGroup.internalHeight = convertToUnit(el.clientHeight)
    },
    onEnterCancelled () {
      this.wasCancelled = true
    },
    onEnter (el: HTMLElement, done: () => void) {
      const isBooted = this.windowGroup.isBooted

      if (isBooted) {
        addOnceEventListener(el, 'transitionend', done)
      }

      requestAnimationFrame(() => {
        this.windowGroup.internalHeight = convertToUnit(el.clientHeight)

        // On initial render, there is no transition
        // Vue leaves a `enter` transition class
        // if done is called too fast
        !isBooted && setTimeout(done, 100)
      })
    }
  },

  render (h): VNode {
    const div = h('div', {
      staticClass: 'v-window-item',
      directives: [{
        name: 'show',
        value: this.isActive
      }] as VNodeDirective[],
      on: this.$listeners
    }, this.showLazyContent(this.genDefaultSlot()))

    return h('transition', {
      props: {
        name: this.computedTransition
      },
      on: {
        afterEnter: this.onAfterEnter,
        beforeEnter: this.onBeforeEnter,
        beforeLeave: this.onBeforeLeave,
        enter: this.onEnter,
        enterCancelled: this.onEnterCancelled
      }
    }, [div])
  }
})


<template>
  <v-carousel>
    <v-carousel-item v-for="(item,i) in items" v-bind:src="item.src" :key="i"></v-carousel-item>
  </v-carousel>
</template>

<script>
  export default {
    data () {
      return {
        items: [
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/squirrel.jpg'
          },
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/sky.jpg'
          },
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/bird.jpg'
          },
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/planet.jpg'
          }
        ]
      }
    }
  }
</script>


import VApp from '@/components/VApp'
import VNavigationDrawer from '@/components/VNavigationDrawer'
import { test, resizeWindow } from '@/test'

beforeEach(() => resizeWindow(1920, 1080))

test('VNavigationDrawer', ({ mount }) => {
  // v-app is needed to initialise $vuetify.application
  const app = mount(VApp)

  it('should become temporary when the window resizes', async () => {
    const wrapper = mount(VNavigationDrawer)

    expect(wrapper.vm.isActive).toBe(true)
    await resizeWindow(1200)
    expect(wrapper.vm.isActive).toBe(false)
    expect(wrapper.vm.overlay).toBeFalsy()
  })

  it('should not resize the content when temporary', async () => {
    const wrapper = mount(VNavigationDrawer, { propsData: {
      app: true,
      temporary: true,
      value: true
    }})

    await wrapper.vm.$nextTick()
    expect(wrapper.vm.$vuetify.application.left).toBe(0)
    expect(wrapper.vm.overlay).toBeTruthy()
  })

  it('should not resize the content when permanent and stateless', async () => {
    const wrapper = mount(VNavigationDrawer, { propsData: {
      app: true,
      permanent: true,
      stateless: true
    }})

    await wrapper.vm.$nextTick()
    expect(wrapper.vm.$vuetify.application.left).toBe(300)

    await resizeWindow(1200)
    expect(wrapper.vm.$vuetify.application.left).toBe(300)
    expect(wrapper.vm.overlay).toBeFalsy()
  })

  it('should not resize the content when permanent and resize watcher is disabled', async () => {
    const wrapper = mount(VNavigationDrawer, {
      propsData: {
        app: true,
        permanent: true,
        disableResizeWatcher: true
      }
    })

    await wrapper.vm.$nextTick()
    expect(wrapper.vm.$vuetify.application.left).toBe(300)

    await resizeWindow(1200)
    expect(wrapper.vm.$vuetify.application.left).toBe(300)
    expect(wrapper.vm.overlay).toBeFalsy()
  })

  it('should stay active when resizing a temporary drawer', async () => {
    const wrapper = mount(VNavigationDrawer, {
      propsData: {
        app: true,
        temporary: true,
        value: true
      }
    })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isActive).toBe(true)
    expect(wrapper.vm.overlay).toBeTruthy()

    await resizeWindow(1200)

    expect(wrapper.vm.isActive).toBe(true)
    expect(wrapper.vm.overlay).toBeTruthy()
  })

  it('should open when changed to permanent', async () => {
    const wrapper = mount(VNavigationDrawer, {
      propsData: {
        value: null
      }
    })

    wrapper.setProps({ permanent: true })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isActive).toBe(true)
  })

  it('should not close when value changes and permanent', async () => {
    const wrapper = mount(VNavigationDrawer, {
      propsData: {
        permanent: true,
        value: true
      }
    })

    wrapper.setProps({ value: false })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isActive).toBe(true)
  })

  it('should update content padding when temporary state is changed', async () => {
    const wrapper = mount(VNavigationDrawer, { propsData: {
      app: true
    }})
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.$vuetify.application.left).toBe(300)

    wrapper.setProps({ temporary: true })
    expect(wrapper.vm.$vuetify.application.left).toBe(0)

    wrapper.setProps({ temporary: false })
    expect(wrapper.vm.$vuetify.application.left).toBe(300)
  })

  it('should update content padding when permanent state is changed', async () => {
    await resizeWindow(800)
    const wrapper = mount(VNavigationDrawer, { propsData: {
      app: true
    }})
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.$vuetify.application.left).toBe(0)

    wrapper.setProps({ permanent: true })
    expect(wrapper.vm.$vuetify.application.left).toBe(300)

    wrapper.setProps({ permanent: false })
    expect(wrapper.vm.$vuetify.application.left).toBe(0)
  })

  it('should update content padding when miniVariant is changed', async () => {
    const wrapper = mount(VNavigationDrawer, { propsData: {
      app: true
    }})
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.$vuetify.application.left).toBe(300)

    wrapper.setProps({ miniVariant: true })
    expect(wrapper.vm.$vuetify.application.left).toBe(80)

    wrapper.setProps({ miniVariant: false })
    expect(wrapper.vm.$vuetify.application.left).toBe(300)
  })

  it('should not remain mobile when temporary is toggled', async () => {
    await resizeWindow(800)
    const wrapper = mount(VNavigationDrawer, { propsData: {
      temporary: true
    }})

    await resizeWindow(1920)
    expect(wrapper.vm.isMobile).toBe(false)
  })

  it('should stay closed when mobile and temporary is enabled', async () => {
    await resizeWindow(800)
    const wrapper = mount(VNavigationDrawer)
    const input = jest.fn(value => wrapper.setProps({ value }))
    wrapper.vm.$on('input', input)

    wrapper.setProps({ temporary: true })
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isActive).toBe(false)
    expect(input.mock.calls).toHaveLength(0)
  })

  it('should update content padding when mobile is toggled', async () => {
    const input = jest.fn()
    const wrapper = mount(VNavigationDrawer, { propsData: {
      app: true,
      fixed: true,
      value: true
    }})
    await wrapper.vm.$nextTick()

    wrapper.vm.$on('input', input)
    expect(wrapper.vm.$vuetify.application.left).toBe(300)
    await resizeWindow(800)
    expect(wrapper.vm.$vuetify.application.left).toBe(0)
    expect(wrapper.vm.isActive).toBe(false)
    expect(input).toBeCalledWith(false)
    wrapper.setProps({ value: false })
    await wrapper.vm.$nextTick()
    wrapper.setProps({ value: true })
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.isActive).toBe(true)
    expect(wrapper.vm.$vuetify.application.left).toBe(0)
    await resizeWindow(1920)
    expect(wrapper.vm.isActive).toBe(true)
    expect(wrapper.vm.isMobile).toBe(false)
    expect(wrapper.vm.$vuetify.application.left).toBe(300)
  })

  it('should not have marginTop when temporary / isMobile', async () => {
    const wrapper = mount(VNavigationDrawer, {
      propsData: {
        app: true
      }
    })

    expect(wrapper.vm.marginTop).toBe(0)

    wrapper.vm.$vuetify.application.bar = 24

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.marginTop).toBe(24)

    await resizeWindow(640)

    expect(wrapper.vm.marginTop).toBe(0)

    await resizeWindow(1980)

    expect(wrapper.vm.marginTop).toBe(24)

    wrapper.setProps({ temporary: true })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.marginTop).toBe(0)

    wrapper.setProps({ app: false, temporary: false })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.marginTop).toBe(0)

    wrapper.setProps({ app: true })

    expect(wrapper.vm.marginTop).toBe(24)
  })
})


<template lang="pug">
  div(style="position: relative;").elevation-2.mb-3.hide-overflow
    input(
      :style="{ position: 'absolute', left: '-1000px', top: '-1000px' }"
      :value="copy"
      ref="copy"
    )
    v-snackbar(
      absolute
      v-model="copied"
      top
      left
    ) {{ $t('Components.TemplateList.init') }}
      v-btn(flat @click="copied = !copied" color="light-blue")
        |{{ $t('Components.TemplateList.close') }}
    help-me-choose(v-model="suggestion")
    v-divider
    v-expansion-panel(expand).elevation-0
      v-expansion-panel-content(
        lazy
        v-for="(template, i) in templates"
        :key="i"
      )
        v-layout(slot="header")
          v-icon(
            :color="isActive(template) ? 'primary' : ''"
            v-text="template.icon"
          ).mr-3
          div(
            v-html="template.title"
            :class="[isActive(template) ? 'primary--text' : '']"
          ).mr-3
        v-card(
          color="blue darken-3"
          tile
          flat
          dark
          style="min-height: 75px"
        )
          v-fade-transition(mode="out-in")
            v-layout(
              row
              justify-space-between
            )
              v-flex(xs10).layout.align-center
                v-card-text(v-html="template.desc")
              v-flex(xs2).layout.column.align-end.pa-3
                v-tooltip(
                  lazy
                  dark
                  left
                  v-if="template.link"
                )
                  v-btn(
                    icon
                    dark
                    color="secondary"
                    :href="template.link"
                    target="_blank"
                    rel="noopener"
                    slot="activator"
                  )
                    v-icon fab fa-codepen
                  span Codepen
                template(v-else)
                  v-tooltip(lazy left dark)
                    v-btn(
                      icon
                      dark
                      color="secondary"
                      :href="`https://github.com/vuetifyjs/${template.init}`"
                      target="_blank"
                      rel="noopener"
                      slot="activator"
                    ).elevation-20
                      v-icon fab fa-github
                    span Github
                  v-tooltip(lazy left dark)
                    v-btn(
                      icon
                      color="secondary"
                      dark
                      @click="copyMarkup(template)"
                      slot="activator"
                    )
                      v-icon content_copy
                    span Copy markup
</template>

<script>
  // Components
  import HelpMeChoose from './HelpMeChoose'

  export default {
    components: {
      HelpMeChoose
    },

    data: vm => ({
      copied: false,
      copy: null,
      copyTimeout: null,
      options: [
        {
          label: 'For Beginners',
          value: null
        }
      ],
      suggestion: null,
      templates: [
        {
          icon: 'mdi-codepen',
          title: vm.$t('Components.TemplateList.quickstart.title'),
          link: 'https://codepen.io/johnjleider/pen/jYZwVZ',
          cats: ['bwnn'],
          desc: vm.$t('Components.TemplateList.quickstart.desc')
        },
        {
          icon: 'landscape',
          title: vm.$t('Components.TemplateList.simple.title'),
          init: 'simple',
          cats: ['bwnn'],
          desc: vm.$t('Components.TemplateList.simple.desc')
        },
        {
          icon: 'web',
          title: vm.$t('Components.TemplateList.webpackSimple.title'),
          init: 'webpack-simple',
          cats: ['iwnn'],
          desc: vm.$t('Components.TemplateList.webpackSimple.desc')
        },
        {
          icon: 'layers',
          title: vm.$t('Components.TemplateList.webpack.title'),
          init: 'webpack',
          cats: ['bwnn', 'bwyn', 'iwnn', 'iwyn'],
          desc: vm.$t('Components.TemplateList.webpack.desc')
        },
        {
          icon: 'cloud_circle',
          title: vm.$t('Components.TemplateList.webpackSSR.title'),
          init: 'webpack-ssr',
          cats: ['awny'],
          desc: vm.$t('Components.TemplateList.webpackSSR.desc')
        },
        {
          icon: 'flash_on',
          title: vm.$t('Components.TemplateList.nuxt.title'),
          init: 'nuxt',
          cats: [
            'awny',
            'iwny'
          ],
          desc: vm.$t('Components.TemplateList.nuxt.desc')
        },
        {
          icon: 'featured_video',
          title: vm.$t('Components.TemplateList.pwa.title'),
          init: 'pwa',
          cats: ['iwyn', 'idyn', 'imyn', 'awyn', 'adyn', 'amyn'],
          desc: vm.$t('Components.TemplateList.pwa.desc')
        },
        {
          icon: 'power',
          title: vm.$t('Components.TemplateList.electron.title'),
          init: 'electron',
          cats: [
            'bdnn',
            'bdyn',
            'idnn',
            'idyn',
            'adnn',
            'adyn'
          ],
          desc: vm.$t('Components.TemplateList.electron.desc')
        },
        {
          icon: 'call_split',
          title: vm.$t('Components.TemplateList.aLaCarte.title'),
          init: 'a-la-carte',
          cats: [
            'idnn',
            'idyn',
            'adnn',
            'adyn'
          ],
          desc: vm.$t('Components.TemplateList.aLaCarte.desc')
        },
        {
          icon: 'phone_iphone',
          title: vm.$t('Components.TemplateList.cordova.title'),
          init: 'cordova',
          cats: [
            'bmnn',
            'bmyn',
            'bmny',
            'imnn',
            'imyn',
            'imny',
            'amnn',
            'amyn',
            'amny'
          ],
          desc: vm.$t('Components.TemplateList.cordova.desc')
        }
      ]
    }),

    watch: {
      copied (val) {
        !val && clearTimeout(this.copyTimeout)
      }
    },

    methods: {
      copyMarkup (template) {
        this.copy = `vue init vuetifyjs/${template.init}`
        clearTimeout(this.copyTimeout)
        this.copied = true
        this.copyTimeout = setTimeout(() => { this.copied = false }, 4000)

        setTimeout(() => {
          this.$refs.copy.select()
          document.execCommand('copy')
        }, 0)
      },
      isActive (template) {
        return template.cats.indexOf(this.suggestion) > -1
      }
    }
  }
</script>


keep keep keep keep keep replace keep keep keep keep keep


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

.v-input--radio-group
  &__input
    display: flex
    width: 100%

  &--column .v-input--radio-group__input > .v-label
    padding-bottom: 8px

  &--row .v-input--radio-group__input > .v-label
    padding-right: 8px

  &--row
    .v-input--radio-group__input
      flex-direction: row
      flex-wrap: wrap

  &--column
    .v-radio:not(:last-child):not(:only-child)
      margin-bottom: 8px

    .v-input--radio-group__input
      flex-direction: column


<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<!DOCTYPE svg  PUBLIC '-//W3C//DTD SVG 1.0//EN'  'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd' [
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
	<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
	<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
	<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
	<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
	<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
	<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
	<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
	<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
]>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 428.5 92.3" xml:space="preserve" viewBox="0 0 428.5 92.3" version="1.0" y="0px" x="0px" xmlns:xlink="http://www.w3.org/1999/xlink">
<switch>
	<foreignObject y="0" x="0" height="1" width="1" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/">
	</foreignObject>
	<g>
		<rect y="-36.1" x="-36.1" height="164.5" width="500.8" fill="#fff"/>
			<circle cy="46.1" cx="46.1" r="46.1" fill="#F5BB60"/>
			<circle cy="41.5" cx="41.5" r="41.5" fill="#E86F32"/>
			<circle cy="36" cx="47" r="36" fill="#E53D42"/>
			<circle cy="38.8" cx="49.9" r="33.1" fill="#BFD141"/>
			<circle cy="41.3" cx="47.5" r="30.7" fill="#6DB64C"/>
			<circle cy="36.4" cx="42.6" r="25.8" fill="#AFDBE7"/>
			<circle cy="32.2" cx="46.8" r="21.6" fill="#57BADF"/>
			<circle cy="34.8" cx="49.5" r="19" fill="#02B2D6"/>
			<radialGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" cy="37.575" cx="46.729" r="16.238">
				<stop stop-color="#797979" offset="0"/>
				<stop stop-color="#4C4C4C" offset="1"/>
			</radialGradient>
			<circle cy="37.6" cx="46.7" r="16.2" fill="url(#SVGID_1_)"/>
			<circle cy="37.6" cx="46.7" r="16.2" fill="#231F20"/>
				<ellipse rx="3.2" ry="5" transform="matrix(.9124 .4094 -.4094 .9124 17.628 -19.007)" cy="31.7" cx="53.2" fill="#fff"/>
		<path d="m107.1 28.5c0-0.3 0.2-0.5 0.6-0.5h14.5c8.3 0 12.2 3.8 12.2 9.6 0 4.1-2.1 6.4-4.5 7.6v0.1c2.4 0.8 5.2 3.7 5.2 8.1 0 7.2-4.9 10.6-13.4 10.6h-14c-0.3 0-0.6-0.2-0.6-0.5v-35zm14.9 14.5c3.4 0 5.6-1.9 5.6-4.7 0-2.9-2.1-4.8-5.6-4.8h-7.8c-0.2 0-0.3 0.1-0.3 0.3v8.9c0 0.2 0.1 0.3 0.3 0.3h7.8zm-7.8 15.5h8.2c3.8 0 5.9-2 5.9-5s-2.1-5-5.9-5h-8.2c-0.2 0-0.3 0.1-0.3 0.3v9.4c0 0.2 0.1 0.3 0.3 0.3z" fill="#382F27"/>
		<path d="m139.8 64.1c-0.3 0-0.6-0.2-0.6-0.5v-24.6c0-0.3 0.2-0.5 0.6-0.5h5.3c0.3 0 0.6 0.2 0.6 0.5v2.2h0.1c1.3-2 3.7-3.3 7.2-3.3 2.2 0 4.4 0.8 5.8 2.1 0.3 0.3 0.3 0.5 0.1 0.7l-3 3.6c-0.2 0.3-0.5 0.3-0.8 0.1-1.3-0.7-2.6-1.2-4.1-1.2-3.6 0-5.2 2.4-5.2 6.5v13.9c0 0.3-0.2 0.5-0.6 0.5h-5.4z" fill="#382F27"/>
		<path d="m159.9 57.6c-0.6-1.8-0.9-3.5-0.9-6.3s0.3-4.5 0.9-6.3c1.6-4.5 5.8-7.1 11.4-7.1s9.8 2.6 11.4 7.1c0.6 1.8 0.9 3.5 0.9 6.3s-0.3 4.5-0.9 6.3c-1.6 4.5-5.8 7.1-11.4 7.1-5.6-0.1-9.8-2.7-11.4-7.1zm16.6-1.6c0.5-1.3 0.6-2.5 0.6-4.7s-0.1-3.3-0.6-4.7c-0.8-2.1-2.6-3.3-5.2-3.3s-4.4 1.2-5.2 3.3c-0.5 1.4-0.6 2.5-0.6 4.7s0.1 3.4 0.6 4.7c0.8 2.1 2.6 3.3 5.2 3.3s4.4-1.2 5.2-3.3z" fill="#382F27"/>
		<path d="m211.5 64.1c-0.3 0-0.6-0.2-0.7-0.5l-5.3-16.4h-0.1l-5.4 16.4c-0.1 0.3-0.3 0.5-0.7 0.5h-4.7c-0.3 0-0.6-0.2-0.7-0.5l-8.8-24.6c-0.1-0.3 0.1-0.5 0.4-0.5h5.5c0.4 0 0.6 0.2 0.7 0.5l5.3 16.9h0.1l5.3-16.9c0.1-0.3 0.4-0.5 0.7-0.5h4.1c0.3 0 0.6 0.2 0.7 0.5l5.6 16.9h0.1l5.1-16.9c0.1-0.4 0.3-0.5 0.7-0.5h5.5c0.3 0 0.5 0.2 0.4 0.5l-8.7 24.5c-0.1 0.3-0.3 0.5-0.7 0.5h-4.4z" fill="#382F27"/>
		<path d="m226.7 60.6c-0.2-0.2-0.2-0.5 0-0.7l3.3-3.2c0.2-0.2 0.6-0.2 0.8 0 2 1.7 5.2 2.9 8.1 2.9 3.4 0 5.1-1.3 5.1-3 0-1.5-1-2.5-4.6-2.8l-3-0.3c-5.6-0.5-8.5-3.2-8.5-7.5 0-4.9 3.8-8.1 10.7-8.1 4.3 0 7.9 1.3 10.4 3.1 0.3 0.2 0.3 0.5 0.1 0.7l-2.8 3.1c-0.2 0.3-0.5 0.3-0.8 0.1-1.7-1.1-4.5-2.1-7.2-2.1-2.8 0-4.2 1.1-4.2 2.7 0 1.5 1 2.4 4.5 2.7l3 0.3c5.8 0.5 8.6 3.3 8.6 7.5 0 5.1-4 8.7-11.6 8.7-5.7-0.1-9.6-2.1-11.9-4.1z" fill="#382F27"/>
		<path d="m254.6 57.5c-0.6-1.6-1-3.6-1-6.3s0.3-4.7 0.9-6.3c1.6-4.5 5.8-7.1 11.3-7.1 5.7 0 9.8 2.7 11.4 7.1 0.6 1.8 0.9 3.6 0.9 7.7 0 0.3-0.2 0.5-0.6 0.5h-17.1c-0.2 0-0.3 0.1-0.3 0.3 0 0.7 0.2 1.4 0.4 2 0.9 2.5 3.1 3.9 6.2 3.9s5.1-1.1 6.5-2.4c0.3-0.3 0.6-0.3 0.9-0.1l3.4 2.8c0.3 0.2 0.3 0.5 0.1 0.7-2.4 2.5-6.3 4.4-11.4 4.4-5.9-0.1-10-2.7-11.6-7.2zm16.7-11.3c-0.7-2.1-2.8-3.3-5.4-3.3s-4.7 1.2-5.5 3.3c-0.2 0.6-0.3 1.3-0.3 2.2 0 0.2 0.1 0.3 0.3 0.3h10.9c0.2 0 0.3-0.1 0.3-0.3 0.1-0.9 0-1.6-0.3-2.2z" fill="#382F27"/>
		<path d="m282.9 64.1c-0.3 0-0.6-0.2-0.6-0.5v-24.6c0-0.3 0.2-0.5 0.6-0.5h5.3c0.3 0 0.6 0.2 0.6 0.5v2.2h0.1c1.3-2 3.7-3.3 7.2-3.3 2.2 0 4.4 0.8 5.8 2.1 0.3 0.3 0.3 0.5 0.1 0.7l-3 3.6c-0.2 0.3-0.5 0.3-0.8 0.1-1.3-0.7-2.6-1.2-4.1-1.2-3.6 0-5.2 2.4-5.2 6.5v13.9c0 0.3-0.2 0.5-0.6 0.5h-5.4z" fill="#382F27"/>
		<path d="m302.9 59.9c-0.2-0.2-0.3-0.5-0.1-0.8l3.6-3.8c0.2-0.3 0.6-0.3 0.8-0.1 2.5 1.9 6.3 3.8 10.6 3.8 4.7 0 7.4-2.2 7.4-5.2 0-2.6-1.7-4.3-7.1-5l-2.1-0.3c-7.5-1-11.8-4.3-11.8-10.3 0-6.5 5.2-10.9 13.2-10.9 4.9 0 9.5 1.4 12.6 3.6 0.3 0.2 0.3 0.4 0.1 0.7l-2.8 3.9c-0.2 0.3-0.5 0.3-0.8 0.2-3.2-1.9-6.1-2.8-9.4-2.8-4 0-6.1 2-6.1 4.8 0 2.5 1.9 4.2 7.2 4.9l2.1 0.3c7.5 1 11.7 4.2 11.7 10.5 0 6.4-5 11.2-14.7 11.2-5.8 0-11.3-2.1-14.4-4.7z" fill="#382F27"/>
		<path d="m343.9 64.5c-5.6 0-7.7-2.5-7.7-7.5v-13.5c0-0.2-0.1-0.3-0.3-0.3h-1.9c-0.3 0-0.6-0.2-0.6-0.5v-3.6c0-0.3 0.2-0.5 0.6-0.5h1.8c0.2 0 0.3-0.1 0.3-0.3v-7c0-0.3 0.2-0.5 0.6-0.5h5.3c0.3 0 0.6 0.2 0.6 0.5v7c0 0.2 0.1 0.3 0.3 0.3h3.7c0.3 0 0.6 0.2 0.6 0.5v3.6c0 0.3-0.2 0.5-0.6 0.5h-3.6c-0.2 0-0.3 0.1-0.3 0.3v13.3c0 1.8 0.8 2.4 2.6 2.4h1.4c0.3 0 0.6 0.2 0.6 0.5v4.3c0 0.3-0.2 0.5-0.6 0.5h-2.8z" fill="#382F27"/>
		<path d="m368.2 64.1c-0.3 0-0.6-0.2-0.6-0.5v-1.9h-0.1c-1.3 1.8-3.9 3-7.8 3-5.1 0-9.3-2.4-9.3-7.8 0-5.6 4.2-8.1 11.1-8.1h5.6c0.2 0 0.3-0.1 0.3-0.3v-1.2c0-2.9-1.5-4.2-6.1-4.2-3.1 0-5 0.8-6.3 1.8-0.3 0.2-0.6 0.2-0.8-0.1l-2.1-3.5c-0.2-0.3-0.1-0.5 0.1-0.7 2.2-1.5 5.3-2.5 9.8-2.5 8.4 0 11.5 2.7 11.5 8.9v16.7c0 0.3-0.2 0.5-0.6 0.5h-4.7zm-0.6-8.6v-2c0-0.2-0.1-0.3-0.3-0.3h-4.6c-4.1 0-6 1.1-6 3.5 0 2.1 1.7 3.2 4.8 3.2 3.9-0.1 6.1-1.5 6.1-4.4z" fill="#382F27"/>
		<path d="m378.7 57.6c-0.6-1.6-0.9-3.6-0.9-6.3s0.3-4.7 0.9-6.3c1.6-4.6 5.9-7.1 11.4-7.1 4.1 0 7.4 1.5 9.6 4 0.2 0.2 0.2 0.5-0.1 0.7l-3.6 2.9c-0.3 0.2-0.6 0.2-0.8-0.1-1.4-1.4-2.8-2.2-5.1-2.2-2.5 0-4.4 1.1-5.2 3.3-0.5 1.3-0.6 2.8-0.6 4.7s0.1 3.5 0.6 4.8c0.8 2.2 2.7 3.3 5.2 3.3 2.3 0 3.7-0.8 5.1-2.2 0.2-0.3 0.5-0.3 0.8-0.1l3.6 2.9c0.3 0.2 0.2 0.5 0.1 0.7-2.2 2.4-5.5 3.9-9.6 3.9-5.6 0.1-9.8-2.4-11.4-6.9z" fill="#382F27"/>
		<path d="m422.1 64.1c-0.4 0-0.7-0.2-0.9-0.5l-7-10.7-3.7 4v6.7c0 0.3-0.2 0.5-0.6 0.5h-5.3c-0.3 0-0.6-0.2-0.6-0.5v-35.1c0-0.3 0.2-0.5 0.6-0.5h5.3c0.3 0 0.6 0.2 0.6 0.5v20.8l9.5-10.3c0.3-0.4 0.6-0.5 1.1-0.5h5.9c0.3 0 0.5 0.3 0.2 0.5l-8.8 9.3 10.1 15.2c0.2 0.2 0.1 0.5-0.3 0.5h-6.1z" fill="#382F27"/>
	</g>
</switch>
</svg>


keep keep keep keep keep replace replace keep keep keep keep

import { test } from '@/test'
import { VWindow, VWindowItem } from '@/components/VWindow'

test('VWindowItem.ts', ({ mount }) => {
  it('should transition content', async () => {
    const wrapper = mount(VWindow, {
      slots: {
        default: [VWindowItem]
      }
    })

    const item = wrapper.first(VWindowItem.options)
    // Before enter
    expect(wrapper.vm.isActive).toBe(false)
    item.vm.onBeforeEnter()
    expect(wrapper.vm.isActive).toBe(true)

    // Enter
    const el = document.createElement('div')
    expect(wrapper.vm.internalHeight).toBe(undefined)
    item.vm.onEnter(el)
    await new Promise(resolve => window.requestAnimationFrame(resolve))
    expect(wrapper.vm.internalHeight).toBe('0px')

    // After enter
    item.vm.onAfterEnter()
    await new Promise(resolve => window.requestAnimationFrame(resolve))
    expect(wrapper.vm.internalHeight).toBe(undefined)
    expect(wrapper.vm.isActive).toBe(false)

    // Before leave
    item.vm.onBeforeLeave(el)
    expect(wrapper.vm.internalHeight).toBe('0px')

    // Canceling
    item.vm.onBeforeEnter()
    item.vm.onEnter(el)
    item.vm.onEnterCancelled()

    expect(item.vm.wasCancelled).toBe(true)
    expect(wrapper.vm.isActive).toBe(true)

    item.vm.onAfterEnter()

    await new Promise(resolve => requestAnimationFrame(resolve))

    expect(wrapper.vm.isActive).toBe(true)
  })

  it('should use custom transition', () => {
    const wrapper = mount(VWindowItem, {
      propsData: {
        transition: 'foo',
        reverseTransition: 'bar'
      },
      data: {
        windowGroup: {
          internalReverse: false,
          register: () => {},
          unregister: () => {}
        }
      }
    })

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

    wrapper.setProps({ transition: false })
    expect(wrapper.vm.computedTransition).toBe('')

    wrapper.vm.windowGroup.internalReverse = true
    expect(wrapper.vm.computedTransition).toBe('bar')

    wrapper.setProps({ reverseTransition: false })
    expect(wrapper.vm.computedTransition).toBe('')
  })
})


<template>
  <div>
    <v-slider
      v-model="ex1.val"
      :color="ex1.color"
      :label="ex1.label"
    ></v-slider>

    <v-slider
      v-model="ex2.val"
      :label="ex2.label"
      :track-color="ex2.color"
    ></v-slider>

    <v-slider
      v-model="ex3.val"
      :label="ex3.label"
      :thumb-color="ex3.color"
      thumb-label="always"
    ></v-slider>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        ex1: { label: 'color', val: 25, color: 'orange darken-3' },
        ex2: { label: 'track-color', val: 75, color: 'green lighten-1' },
        ex3: { label: 'thumb-color', val: 50, color: 'red' }
      }
    }
  }
</script>


<template>
  <div>
    <v-breadcrumbs icons divider="forward">
      <v-breadcrumbs-item
        v-for="item in items" :key="item.text"
        :disabled="item.disabled"
      >
        {{ item.text }}
      </v-breadcrumbs-item>
    </v-breadcrumbs>

    <v-breadcrumbs icons divider="chevron_right">
      <v-breadcrumbs-item
        v-for="item in items" :key="item.text"
        :disabled="item.disabled"
      >
        {{ item.text }}
      </v-breadcrumbs-item>
    </v-breadcrumbs>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        items: [
          {
            text: 'Dashboard',
            disabled: false
          },
          {
            text: 'Link 1',
            disabled: false
          },
          {
            text: 'Link 2',
            disabled: true
          }
        ]
      }
    }
  }
</script>


// Mixins
import Colorable from '../../mixins/colorable'
import DatePickerTable from './mixins/date-picker-table'
import Themeable from '../../mixins/themeable'

// Utils
import { pad, createNativeLocaleFormatter, monthChange } from './util'
import { createRange } from '../../util/helpers'

/* @vue/component */
export default {
  name: 'v-date-picker-date-table',

  mixins: [
    Colorable,
    DatePickerTable,
    Themeable
  ],

  props: {
    events: {
      type: [Array, Object, Function],
      default: () => null
    },
    eventColor: {
      type: [String, Function, Object],
      default: 'warning'
    },
    firstDayOfWeek: {
      type: [String, Number],
      default: 0
    },
    weekdayFormat: {
      type: Function,
      default: null
    }
  },

  computed: {
    formatter () {
      return this.format || createNativeLocaleFormatter(this.locale, { day: 'numeric', timeZone: 'UTC' }, { start: 8, length: 2 })
    },
    weekdayFormatter () {
      return this.weekdayFormat || createNativeLocaleFormatter(this.locale, { weekday: 'narrow', timeZone: 'UTC' })
    },
    weekDays () {
      const first = parseInt(this.firstDayOfWeek, 10)

      return this.weekdayFormatter
        ? createRange(7).map(i => this.weekdayFormatter(`2017-01-${first + i + 15}`)) // 2017-01-15 is Sunday
        : createRange(7).map(i => ['S', 'M', 'T', 'W', 'T', 'F', 'S'][(i + first) % 7])
    }
  },

  methods: {
    calculateTableDate (delta) {
      return monthChange(this.tableDate, Math.sign(delta || 1))
    },
    genTHead () {
      const days = this.weekDays.map(day => this.$createElement('th', day))
      return this.$createElement('thead', this.genTR(days))
    },
    genEvent (date) {
      let eventColor
      if (typeof this.eventColor === 'string') {
        eventColor = this.eventColor
      } else if (typeof this.eventColor === 'function') {
        eventColor = this.eventColor(date)
      } else {
        eventColor = this.eventColor[date]
      }
      return this.$createElement('div', this.setBackgroundColor(eventColor || this.color || 'accent', {
        staticClass: 'v-date-picker-table__event'
      }))
    },
    // Returns number of the days from the firstDayOfWeek to the first day of the current month
    weekDaysBeforeFirstDayOfTheMonth () {
      const firstDayOfTheMonth = new Date(`${this.displayedYear}-${pad(this.displayedMonth + 1)}-01T00:00:00+00:00`)
      const weekDay = firstDayOfTheMonth.getUTCDay()
      return (weekDay - parseInt(this.firstDayOfWeek) + 7) % 7
    },
    isEvent (date) {
      if (Array.isArray(this.events)) {
        return this.events.indexOf(date) > -1
      } else if (this.events instanceof Function) {
        return this.events(date)
      } else {
        return false
      }
    },
    genTBody () {
      const children = []
      const daysInMonth = new Date(this.displayedYear, this.displayedMonth + 1, 0).getDate()
      let rows = []
      let day = this.weekDaysBeforeFirstDayOfTheMonth()

      while (day--) rows.push(this.$createElement('td'))
      for (day = 1; day <= daysInMonth; day++) {
        const date = `${this.displayedYear}-${pad(this.displayedMonth + 1)}-${pad(day)}`

        rows.push(this.$createElement('td', [
          this.genButton(date, true),
          this.isEvent(date) ? this.genEvent(date) : null
        ]))

        if (rows.length % 7 === 0) {
          children.push(this.genTR(rows))
          rows = []
        }
      }

      if (rows.length) {
        children.push(this.genTR(rows))
      }

      return this.$createElement('tbody', children)
    },
    genTR (children) {
      return [this.$createElement('tr', children)]
    }
  },

  render () {
    return this.genTable('v-date-picker-table v-date-picker-table--date', [
      this.genTHead(),
      this.genTBody()
    ])
  }
}


// Imports
@import '../../styles/styles.sass'
@import './_variables.scss'

// Theme
+theme(v-icon) using ($material)
  color: map-deep-get($material, 'icons', 'active')

  &--disabled
    color: map-deep-get($material, 'icons', 'inactive') !important

.v-icon
  align-items: center
  display: inline-flex
  font-feature-settings: 'liga'
  font-size: $icon-size
  justify-content: center
  line-height: 1
  transition: $primary-transition
  vertical-align: text-bottom

  &--right
    margin-left: map-get($grid-gutters, 'md')
  &--left
    margin-right: map-get($grid-gutters, 'md')

  &.v-icon.v-icon--link
    cursor: pointer

  &--disabled
    pointer-events: none
    opacity: .6

  &--is-component
    height: $icon-size
    width: $icon-size

  &--svg
    height: $icon-size
    width: $icon-size
    fill: currentColor

  &--dense
    font-size: $icon-size-dense

    &--is-component
      height: $icon-size-dense


Vuetify tornou-se possível pelo fato de ter uma comunidade incrível que envia issues, cria pull requests e fornece um feedback inestimável. É nosso trabalho permitir que você crie aplicativos incríveis. Muitas vezes, você encontra algo que pode ser melhorado. Talvez você encontre um bug ou tenha uma ideia de funcionalidades adicionais. Isso é ótimo! É tão fácil como clonar o repositório do Vuetify para começar a trabalhar em um ambiente de desenvolvimento.

A lista de issues deste repositório é exclusivamente para lista de erros e solicitações de recursos. As issues não conformes serão fechadas imediatamente.

Para perguntas gerais, junte-se à [comunidade](https://chat.vuetifyjs.com).

Tente procurar por seu problema, ele pode ter sido respondido.

Veja se o erro é reprodutível com a versão mais recente.

Se reprodutível, forneça um simples [codepen](https://template.vuetifyjs.com) ou um repositório que pode ser clonado para produzir o comportamento esperado.

Por favor, use o [criador de issues](https://issues.vuetifyjs.com) para criar uma nova issue.


keep keep keep keep keep replace keep keep keep


<template>
  <v-layout row justify-center>
    <v-dialog v-model="dialog" persistent max-width="290">
      <template v-slot:activator="{ on }">
        <v-btn color="primary" dark v-on="on">Open Dialog</v-btn>
      </template>
      <v-card>
        <v-card-title class="headline">Use Google's location service?</v-card-title>
        <v-card-text>Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.</v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="green darken-1" text @click="dialog = false">Disagree</v-btn>
          <v-btn color="green darken-1" text @click="dialog = false">Agree</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-layout>
</template>

<script>
  export default {
    data () {
      return {
        dialog: false,
      }
    },
  }
</script>


Input: 연결된 모델을 업데이트 (The updated bound model)


// Imports
@import '../../styles/styles.sass'
@import './_variables.scss'

+theme(v-tabs) using ($material)
  .v-tab:not(.v-tab--active),
  .v-tab:not(.v-tab--active) > .v-icon,
  .v-tab--disabled
    color: map-get($material, 'tabs')

  .v-tabs-bar
    background-color: map-get($material, 'cards')

  .v-tab
    +states($material)

+theme(v-tabs-items) using ($material)
  background-color: map-get($material, 'cards')

.v-tabs-bar.theme--dark
  .v-tab:not(.v-tab--active):not(.v-tab--disabled)
    opacity: .7

// Block
.v-tabs
  flex: 1 1 auto
  width: 100%

  .v-menu__activator
    height: 100%

// Element
.v-tabs-bar
  border-radius: inherit
  height: $tabs-bar-height

  &.v-item-group > *
    cursor: initial

.v-tab
  align-items: center
  display: flex
  flex: 0 1 auto
  font-size: 0.875rem
  font-weight: 500
  justify-content: center
  min-width: $tabs-item-min-width
  max-width: $tabs-item-max-width
  outline: none
  padding: $tabs-item-padding
  position: relative
  text-decoration: none
  text-transform: uppercase
  transition: none
  user-select: none

  // Needs increased specificity
  &.v-tab
    color: inherit

  &:before
    background-color: currentColor
    bottom: 0
    content: ''
    left: 0
    opacity: 0
    pointer-events: none
    position: absolute
    right: 0
    top: 0
    transition: $primary-transition

  &:not(.v-tab-disabled)
    cursor: pointer

.v-tabs-slider
  background-color: currentColor
  height: $tabs-slider-height
  width: 100%

  &-wrapper
    bottom: 0
    margin: 0 !important
    position: absolute
    transition: $primary-transition

// Modifier
.v-tabs--align-with-title > .v-tabs-bar
  padding-left: $tabs-item-align-with-title-padding

.v-tabs--fixed-tabs,
.v-tabs--centered
  .v-tabs-bar__content > *:last-child
    margin-right: auto

  .v-tabs-bar__content > *:first-child:not(.v-tabs-slider-wrapper)
    margin-left: auto

  .v-tabs-slider-wrapper + *
    margin-left: auto

  +rtl()
    .v-tabs-bar__content > *:last-child
      margin-right: 0
      margin-left: auto

    .v-tabs-bar__content > *:first-child:not(.v-tabs-slider-wrapper)
      margin-left: 0
      margin-right: auto

    .v-tabs-slider-wrapper + *
      margin-left: 0
      margin-right: auto

.v-tabs--fixed-tabs
  .v-tab
    flex: 1 1 auto
    width: 100%

.v-tabs--grow
  .v-tab
    flex: 1 0 auto
    max-width: none

.v-tabs--icons-and-text
  .v-tabs-bar
    height: $tabs-icons-and-text-bar-height

  .v-tab
    flex-direction: column-reverse

    > *:first-child
      margin-bottom: 6px

.v-tabs--overflow
  .v-tab
    flex: 1 0 auto

.v-tabs--right
  .v-tab:first-child,
  .v-tabs-slider-wrapper + .v-tab
    margin-left: auto

  .v-tab:last-child
    margin-right: 0

.v-tabs--vertical
  display: flex

  & > .v-tabs-bar
    height: auto

    .v-tabs-bar__content
      flex-direction: column

    .v-tab
      height: $tabs-item-vertical-height

    .v-tabs-slider
      height: 100%

  & > .v-window
    flex: 1 1 auto

  &.v-tabs--icons-and-text
    & > .v-tabs-bar
      .v-tab
        height: $tabs-item-vertical-icons-and-text-height

.v-tab--active
  color: inherit

  &.v-tab:not(:focus)::before
    opacity: 0

  .v-icon
    color: inherit

.v-tab--disabled
  pointer-events: none
  opacity: .5


// Libraries
import Vue from 'vue'

// Components
import VItem from '../VItem'

// Utilities
import {
  createLocalVue,
  mount,
  Wrapper,
} from '@vue/test-utils'

const itemWarning = '[Vuetify] The v-item component must be used inside a v-item-group'

describe('VItem', () => {
  type Instance = ExtractVue<typeof VItem>
  let mountFunction: (options?: object) => Wrapper<Instance>
  let localVue: typeof Vue

  beforeEach(() => {
    localVue = createLocalVue()

    mountFunction = (options = {}) => {
      return mount(VItem, {
        localVue,
        ...options,
      })
    }
  })

  it('should warn if missing default scopedSlot', () => {
    mountFunction()

    expect('v-item is missing a default scopedSlot').toHaveBeenTipped()
    expect(itemWarning).toHaveBeenTipped()
  })

  it('should warn if multiple elements', () => {
    const Mock = {
      name: 'test',

      render: h => h(VItem, {
        scopedSlots: {
          default: () => '
foo
',
        },
      }),
    }

    mount(Mock)

    expect('v-item should only contain a single element').toHaveBeenTipped()
    expect(itemWarning).toHaveBeenTipped()
  })

  it('should match snapshot activeClass', async () => {
    const Mock = {
      name: 'test',

      render: h => h(VItem, {
        props: { activeClass: 'foo' },
        scopedSlots: {
          default: () => h('div'),
        },
      }),
    }

    const wrapper = mount(Mock)

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

    wrapper.vm.$children[0].isActive = true

    await wrapper.vm.$nextTick()

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


<template>
  <v-simple-table dense>

        Name
        Calories

        {{ item.name }}
        {{ item.calories }}

  </v-simple-table>
</template>


#= require underscore
#= require tandem/line

class TandemPosition
  @findLeafNode: (editor, node, offset) ->
    if offset <= node.textContent.length    # We are at right subtree, dive deeper
      if node.firstChild?
        TandemPosition.findLeafNode(editor, node.firstChild, offset)
      else
        node = node.parentNode if node.nodeType == node.TEXT_NODE
        if offset == node.textContent.length && node.nextSibling?
          TandemPosition.findLeafNode(editor, node.nextSibling, 0)
        else
          return [node, offset]
    else if node.nextSibling?               # Not at right subtree, advance to sibling
      if Tandem.Line.isLineNode(node)
        line = editor.doc.findLine(node)
        offset -= (line.length + 1)
      else
        offset -= node.textContent.length
      TandemPosition.findLeafNode(editor, node.nextSibling, offset)
    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(@editor, @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


body
  font-family: 'Helvetica'
  font-size: 13px
  padding: 0
  margin: 0

#content-container
  width: 100%
  height: 100%


#= require diff_match_patch
#= require coffee/ot/jetsync
#= require coffee/editor/attribution

DIFF_DELETE = -1
DIFF_INSERT = 1
DIFF_EQUAL = 0

# Note: JetStates have precedence in the following order: Text > Cursor > Chat > Comment
# That means on an update message, text's update is always called before cursor's

class JetState
  @dmp: new diff_match_patch
  @CURSOR = "cursor"
  @TEXT = "text"
  @CHAT = "chat"
  @COMMENT = "comment"

  constructor: (delegate, client, initialState, sessionId) ->

  reset: (state) ->
    console.assert(false, "Should be implemented by decendents")

  localUpdate: (change) ->
    console.assert(false, "Should be implemented by decendents")

  remoteUpdate: (change, authorId) ->
    console.assert(false, "Should be implemented by decendents")

  takeoff: ->
    console.assert(false, "Should be implemented by decendents")

  land: ->
    console.assert(false, "Should be implemented by decendents")

  @applyDelta: (newDelta, insertFn, deleteFn, context = null) ->
    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
    for delta in newDelta.deltas
      authorId = if delta.attributes? then delta.attributes.authorId else 1
      if JetDelta.isInsert(delta)
        insertFn.call(context || insertFn, index + offset, delta.text, authorId)
        offset += delta.getLength()
      else if JetDelta.isRetain(delta)
        console.assert(delta.start >= index, "Somehow delta.start became smaller than index")
        if delta.start > index
          deleteFn.call(context || deleteFn, index + offset, delta.start + offset, authorId)
          offset -= (delta.start - index)
        index = delta.end
      else
        console.assert(false, "Unrecognized type in delta", delta)

    # If end of text was deleted
    if newDelta.endLength < newDelta.startLength + offset
      deleteFn.call(context || deleteFn, newDelta.endLength, newDelta.startLength + offset)
    return

class JetTextState extends JetState
  constructor: (@editor, @client, shadow, @sessionId) ->
    @type = JetState.TEXT
    @userId = @client.settings.userId
    this.reset(shadow)

  reset: (shadow) ->
    @editor.setText(shadow)
    @arrived = JetDelta.getInitial(shadow)
    @inFlight = JetDelta.getIdentity(shadow.length)
    @inLine = JetDelta.getIdentity(shadow.length)

  localUpdate: (delta) ->
    this.applyDeltaToCursors(delta, @userId)
    @inLine = JetSync.compose(@inLine, delta)

  remoteUpdate: (delta, authorId) ->
    delta = JetDelta.makeDelta(delta)
    flightDeltaFollows = JetSync.follows(@inFlight, delta, false)
    textFollows = JetSync.follows(@inLine, flightDeltaFollows, false)

    @arrived = JetSync.compose(@arrived, delta)
    @inFlight = JetSync.follows(delta, @inFlight, true)
    @inLine = JetSync.follows(flightDeltaFollows, @inLine, true)

    # TODO use apply library function in JetSync
    this.applyDeltaToText(textFollows, authorId)

  takeoff: ->
    console.assert(@inFlight?, "inFlight is", @inFlight, "at takeoff")
    console.assert(@inLine?, "inLine is", @inLine, "at takeoff")
    if (@inFlight.isIdentity() && !@inLine.isIdentity())
      @inFlight = @inLine
      @inLine = JetDelta.getIdentity(@inFlight.endLength)
      return { change: @inFlight }
    return false

  land: ->
    @arrived = JetSync.compose(@arrived, @inFlight)
    @inFlight = JetDelta.getIdentity(@arrived.endLength)

  applyDeltaToCursors: (delta, authorId) ->
    JetState.applyDelta(delta, ((index, text, authorId) =>
      @editor.shiftCursors(index, text.length, authorId)
    ), ((start, end, authorId) =>
      @editor.shiftCursors(start, start-end, authorId)
    ))

  applyDeltaToText: (delta, authorId) ->
    if @client.states[JetClient.CURSOR]? && authorId != 1 && @editor.cursors[authorId] == undefined
      @editor.setCursor(authorId, 0)
    console.assert(delta.startLength == @editor.getText().length, "Delta startLength #{delta.startLength} and target string length #{@editor.getText().length} mismatch", delta)
    JetState.applyDelta(delta, @editor.insertTextAt, @editor.deleteTextAt, @editor)
    console.assert(delta.endLength == @editor.getText().length, "Delta endLength #{delta.endLength} and target string length #{@editor.getText().length} mismatch", delta)

class JetCursorState extends JetState
  constructor: (@editor, @client, @cursors, @sessionId) ->
    @type = JetState.CURSOR
    @userId = @client.settings.userId
    this.reset(@cursors)

  reset: (@cursors) ->
    @editor.clearCursors()
    @inFlight = @inLine = null
    for id,cursor of cursors when id != @userId     # Ignore our own cursor
      index = this.getCursorIndex(cursor)
      @editor.setCursor(cursor.userId, Math.max(index, 0))

  getCursorIndex: (cursor) ->
    # TODO for efficiency, check if text has changed, otherwise we can use cursor.index
    # TODO transform against inLine and inFlight... so we don't have to do match
    # Need to match since the cursor may have changed since last time...
    index = JetState.dmp.match_main(@editor.getText(), cursor.before+cursor.after, cursor.index) + cursor.before.length
    return Math.max(index, 0)

  localUpdate: (cursor) ->
    console.assert(JetCursorState.isCursor(cursor), "Non-cursor passed into JetCursorState's localUpdate")
    cursor.userId = @userId
    @inLine = cursor

  remoteUpdate: (cursor, authorId) ->
    index = this.getCursorIndex(cursor)
    @editor.setCursor(authorId, index)

  land: ->
    @inFlight = null

  takeoff: ->
    if @inFlight == null && @inLine != null
      @inFlight = @inLine
      @inLine = null
      return { change: @inFlight }
    return false

  @areCursorsEqual: (a, b) ->
    console.assert(JetCursorState.isCursor(a) && JetCursorState.isCursor(b), "comparing apples and oranges")
    return a.index == b.index && a.before == b.before && a.after == b.after

  @isCursor: (cursor) ->
    return typeof cursor.index == 'number' && typeof cursor.before == 'string' && typeof cursor.after == 'string'

class JetChatState extends JetState
  @COMPOSITION: "composition"
  @MAX_CHAT_HISTORY: 50

  constructor: (@chat, @client, @history, @sessionId) ->
    @type = JetState.CHAT
    $(@client).bind(JetChatState.COMPOSITION, (event, message) => this.remoteComposition.call(this, event, message))
    $(@client).bind(JetClient.LEAVE, (event, message) => this.chat.updateComposing("", message.userId))
    this.reset(@history)

  reset: (@history) ->
    @names = []
    @chat.clearMessages()
    for record in @history
      @chat.addMessage(record.text, record.authorId)

  localUpdate: (text) ->
    packet = @client.prepareChangePacket([{type: JetState.CHAT, change: text}])
    @client.send(packet)
    @history.push({text: text, authorId: @userId})
    @history.splice(0, 1) if @history.length > JetChatState.MAX_CHAT_HISTORY

  remoteUpdate: (text, authorId) ->
    @chat.addMessage(text, authorId)
    @history.push({text: text, authorId: authorId})
    @history.splice(0, 1) if @history.length > JetChatState.MAX_CHAT_HISTORY

  localComposition: (isComposing) ->
    @client.send({
      type: JetChatState.COMPOSITION
      composing: isComposing
    })

  remoteComposition: (event, message) ->
    @chat.updateComposing(message.composing, message.authorId)

  land: -> # Nothing to do

  takeoff: ->
    return false

# So other files can actually use this library
window.JetState = JetState if window?
window.JetTextState = JetTextState if window?
window.JetCursorState = JetCursorState if window?
window.JetChatState = JetChatState if window?


#= require socket.io
#= require coffee/ot/jetstate
#= require coffee/network
#= require coffee/editor/presence

# TODO fix sendUpdate code diversion from update

class JetClient
  @DEFAULTS:
    latency: 0
    delegate: null
    resendInterval: 5000
    userId: Stypi.configs.userId

  # Socketio events
  @CONNECT        : "connect"
  @MESSAGE        : "message"
  @DISCONNECT     : "disconnect"
  @RECONNECT      : "reconnect"
  @RECONNECTING   : "reconnecting"

  # jetclient events
  @CONNECTED      : "connected"
  @DISCONNECTED   : "disconnected"
  @LEAVING        : "leaving"
  @LEFT           : "left"
  @WARNING        : "warning"

  # Node packet types
  @ACK:       "ack"
  @HANDSHAKE: "handshake"                       # We have joined
  @HANDSHAKE_RECONNECT: "handshake-reconnect"   # We lost connection, trying to reconnect
  @JOIN:      "user/join"                            # Another user has joined
  @LEAVE:     "user/leave"                           # Another user has left
  @SYNC:      "editor/sync"
  @UPDATE:    "editor/update"                          # Update to some jetstate

  constructor: (options = {}) ->
    @settings = $.extend({}, JetClient.DEFAULTS, options)
    @states = {}
    @status = JetClient.DISCONNECTED
    @inFlight = true
    if Stypi? && Stypi.utils? && Stypi.utils.inDevelopment()
      @packetHistory = {
        sent: []
        recieved: []
      }
    @network = Network
    @lastChangePacket = null
    if @settings.delegate?
      $(@settings.delegate).bind(JetClient.UPDATE, =>
        this.checkRunwayReady.call(this)
      )
    window.onbeforeunload = =>
      status = @status
      @status = JetClient.LEAVING
      setTimeout( =>
        @status = status if @status == JetClient.LEAVING
      , 5000)
      return if @inFlight then return "This document has unsaved changes." else undefined
    window.onunload = =>
      @status = JetClient.LEFT

  connect: (docId, @version = 0) ->
    this.initSocket(docId)
    @status = JetClient.CONNECTED
    this.sync()
    @docId = docId

  initSocket: (docId) ->
    if @docId?
      @network.off(@docId, JetClient.UPDATE)
      @network.off(@docId, JetClient.JOIN)
      @network.off(@docId, JetClient.LEAVE)

    @network.on(docId, JetClient.UPDATE, (packet, callback) =>
      this.remoteUpdate(packet)
    )
    @network.on(docId, JetClient.JOIN, (packet) =>
      this.remoteJoin(packet)
    )
    @network.on(docId, JetClient.LEAVE, (packet) =>
      this.remoteLeave(packet)
    )
    @network.on(docId, JetClient.RECONNECTING, (timeout, attempts) =>
      $(this).trigger(JetClient.DISCONNECT) if attempts == 1 && @status != JetClient.LEAVING && @status != JetClient.LEFT
    )
    @network.on(docId, JetClient.RECONNECT, (transport, attempts) =>
      $(this).trigger(JetClient.RECONNECT)
      @inFlight = false
      syncPacket = {type: JetClient.SYNC, version: @version}
      syncPacket['update'] = @lastChangePacket if @lastChangePacket?
      this.send(syncPacket, true, (response) =>
        this.syncCallback(response)
      )
    )

  sync: ->
    syncPacket = { type: "editor/sync", version: @version }
    this.send(syncPacket, true, (response) =>
      this.syncCallback(response)
    )

  addState: (state) ->
    console.assert(@states[state.type] == undefined, "Adding already existing state of type:", state.type)
    @states[state.type] = state

  whenConnected: (callback, params) ->
    if @status == JetClient.CONNECTED
      callback.call(this, params)
    else
      $(this).one(JetClient.CONNECTED, =>
        callback.call(this, params)
      )

  remoteJoin: (message) ->
    $(Stypi).trigger(Stypi.events.CLIENT_JOIN, message)

  remoteLeave: (message) ->
    $(Stypi).trigger(Stypi.events.CLIENT_LEAVE, message)

  # Handle update from server
  remoteUpdate: (message) ->
    if message.docId != @docId
      console.warn  "Got update with invalid docId", message.docId
      return
    console.assert(@version < message.version, "Version from server update #{message.version} is not newer than ours #{@version}!")
    @version = message.version
    # Assumes server sends changes in order they should be applied
    for change in message.changes
      console.assert(@states[change.type] != undefined, "remote updating state for something we are not tracking: " + change.type)
      @states[change.type].remoteUpdate(change.change, message.authorId)

  # If we can send a packet to server (and have a packet to send), send the packet
  checkRunwayReady: ->
    if @inFlight == false
      changes = []
      for type,state of @states
        ready = state.takeoff()
        changes.push({type: type, change: ready.change}) if ready
      if changes.length > 0
        this.sendUpdate(changes, (message) =>
          this.updateCallback(message)
        )
        @inFlight = true

  prepareChangePacket: (changes) ->
    return {
      type: JetClient.UPDATE
      version: @version
      changes: changes
      changeId: Math.floor(Math.random() * 4294967295)
    }

  syncCallback: (response) ->
    if !response.error? || response.error.length == 0
      if @states[JetState.TEXT]?
        @states[JetState.TEXT].reset("") if @version == 0
        for delta in response.deltas
          @states[JetState.TEXT].remoteUpdate(delta)
      Stypi.Presence.setUsers(response.users)
      state.land() for type,state of @states
      @version = response.version
      $(this).trigger(JetClient.CONNECTED)
      @inFlight = false
      this.checkRunwayReady()
    else
      console.warn(error) for error in response.error

  updateCallback: (message) ->
    if message.docId != @docId
      console.warn  "Got ack with invalid docId", message.docId
      return
    console.assert(@version < message.version || message.duplicate == true, "Version from server ack:#{message.version} is not newer than ours:#{@version}")
    @version = message.version
    if !message.duplicate
      state.land() for type,state of @states
    @inFlight = false
    @checkRunwayReady()

  # Helper for sending a generic packet
  send: (packet, priority, callback) ->
    if typeof priority != 'boolean'
      callback = priority
      priority = false
    console.assert(packet.type?, "Sending undefined packet type")
    @packetHistory.sent.push(packet) if Stypi? && Stypi.utils? && Stypi.utils.inDevelopment()
    setTimeout( =>
      retry = true
      while true
        try
          @network.send(@docId, packet.type, packet, priority, callback)
          break
        catch e
          break if retry == false
          retry = false
    , @settings.latency)

  # Creates update packet for changeset and sends, will resend until acknowledged
  sendUpdate: (changes, clientCallback) ->
    packet = @lastChangePacket = this.prepareChangePacket(changes)
    this.send(packet, (args...) =>
      @lastChangePacket = null
      clientCallback.apply(this, args)
    )

# So other files can actually use this library
window.JetClient = JetClient if window?


#= 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; white-space: pre; }
    "
    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()
      if !delta.isIdentity()
        this.checkSelectionChange()
        Tandem.Range.setSelection(this, @currentSelection)
        this.emit(this.events.USER_TEXT_CHANGE, delta)
    , 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)
      parentNode = startNode.parentNode
      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)
        parentNode.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 attr == 'indent'
      Tandem.Utils.setIndent(line.node, nextIndent)
    else if Tandem.Constants.INDENT_ATTRIBUTES[attr]?
      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, nextIndent)

  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
        else
          # TODO could be more clever about if we need to call this
          Tandem.Document.fixListNumbering(@doc.root)
      )
    )
    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)
    parentNode = prevNode?.parentNode || startNode?.parentNode
    parentNode.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)
      parentNode = beforeNode?.parentNode || afterNode?.parentNode
      span = lineNode.ownerDocument.createElement('span')
      span.textContent = text
      parentNode.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?
      startLeaf = @currentSelection.start.getLeaf()
      if startLeaf?
        [selStart, selEnd] = this.transformSelection(modificationStart, @currentSelection, charAdditions)
        fn()
      else
        nativeSel = Tandem.Range.getNativeSelection(@currentSelection.editor)
        anchorNode = if nativeSel.anchorNode.parentNode? then nativeSel.anchorNode else nativeSel.anchorParent
        focusNode = if nativeSel.focusNode.parentNode? then nativeSel.focusNode else nativeSel.focusParent
        fn()
        startPos = new Tandem.Position(this, anchorNode, nativeSel.anchorOffset)
        endPos = new Tandem.Position(this, focusNode, nativeSel.focusOffset)
        range = new Tandem.Range(this, startPos, endPos)
        [selStart, selEnd] = this.transformSelection(modificationStart, range, charAdditions)
      savedSelectionRange = new Tandem.Range(@currentSelection.editor, selStart, selEnd)
      Tandem.Range.setSelection(this, savedSelectionRange)
    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: ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    oldDelta = @doc.toDelta()
    this.preserveSelection(null, 0, =>
      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
    )
    @ignoreDomChanges = oldIgnoreDomChange

    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 decompose

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


Editor
---
applyAttribute()
insertAt()
deleteAt()

Selection.current
Selection.getCurrent()
Selection.setCurrent()

Document
---
Storage for editor. Differences in storage data and DOM indicates change.

Position
---
Removes ambiguity in DOM positions.

Node node
Number offset

Range
---
Wrapper for rangy's range, using Positions. Has many useful functions for operations or analysis of the range.

Position start
Position end
equals()
getRangy()
getLeaves(inclusive = true)

Line
---

Leaf
---

Leaf.groupByLine()
split(offset)

Toolbar
---
Interface for 3rd parties implementing toolbar. Just

Utils
---


#= require underscore
#= require linked_list
#= require jetsync
#= require tandem/constants

class TandemLine extends LinkedList.Node
  @CLASS_NAME : 'line'
  @DIRTY_CLASS: 'dirty'
  @ID_PREFIX  : 'line-'

  @applyRules: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node, index) =>
      if node.nodeType == node.ELEMENT_NODE
        rules = Tandem.Constants.LINE_RULES[node.tagName]
        if rules?
          _.each(rules, (data, rule) ->
            switch rule
              when 'rename' then node = Tandem.Utils.switchTag(node, data)
              else return
          )
        else
          node = Tandem.Utils.unwrap(node)
      return node
    )

  @isLineNode: (node) ->
    return node? && node.classList? && node.classList.contains(TandemLine.CLASS_NAME)

  @mergeAdjacent: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node) ->
      if node.nodeType == node.ELEMENT_NODE && !TandemLine.isLineNode(node)
        next = node.nextSibling
        if next?.tagName == node.tagName && node.tagName != 'LI' && !Tandem.Utils.isIgnoreNode(node) && !Tandem.Utils.isIgnoreNode(next)
          [nodeAttr, nodeValue] = Tandem.Utils.getAttributeForContainer(node)
          [nextAttr, nextValue] = Tandem.Utils.getAttributeForContainer(next)
          if nodeAttr == nextAttr && nodeValue == nextValue
            node = Tandem.Utils.mergeNodes(node, next)
      return node
    )

  @normalizeHtml: (root) ->
    return if root.childNodes.length == 1 && root.firstChild.tagName == 'BR'
    this.applyRules(root)
    this.removeRedundant(root)
    # TODO need wrapText before and after for these cases:
    # Before: TestWhat -> TestWhat
    # After: BoldTest -> BoldTest
    this.wrapText(root)
    this.mergeAdjacent(root)
    this.wrapText(root)
    root.appendChild(root.ownerDocument.createElement('br')) if root.firstChild == null

  @removeRedundant: (root) ->
    tandemKey = '_tandemAttributes' + Math.floor(Math.random() * 100000000)
    root[tandemKey] = {}

    isRedudant = (node) ->
      if node.nodeType == node.ELEMENT_NODE && !Tandem.Utils.isIgnoreNode(node)
        if node.textContent.length == 0
          return true
        [attrName, attrValue] = Tandem.Utils.getAttributeForContainer(node)
        if attrName?
          return node.parentNode[tandemKey][attrName]?     # Parent attribute value will overwrite child's so no need to check attrValue
        else if node.tagName == 'SPAN'
          # Check if children need us
          if node.childNodes.length == 0 || !_.any(node.childNodes, (child) -> child.nodeType != child.ELEMENT_NODE)
            return true
          # Check if parent needs us
          if node.previousSibling == null && node.nextSibling == null && !TandemLine.isLineNode(node.parentNode) && node.parentNode.tagName != 'LI'
            return true
      return false

    Tandem.Utils.traversePreorder(root, 0, (node) =>
      if isRedudant(node)
        node = Tandem.Utils.unwrap(node)
      if node?
        node[tandemKey] = _.clone(node.parentNode[tandemKey])
        [attrName, attrValue] = Tandem.Utils.getAttributeForContainer(node)
        node[tandemKey][attrName] = attrValue if attrName?
      return node
    )
    delete root[tandemKey]
    Tandem.Utils.traversePreorder(root, 0, (node) ->
      delete node[tandemKey]
      return node
    )

  @wrapText: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node) =>
      node.normalize()
      if node.nodeType == node.TEXT_NODE && (node.nextSibling? || node.previousSibling? || node.parentNode == root || node.parentNode.tagName == 'LI')
        span = node.ownerDocument.createElement('span')
        Tandem.Utils.wrap(span, node)
        node = span
      return node
    )

  constructor: (@doc, @node) ->
    @id = _.uniqueId(Tandem.Line.ID_PREFIX)
    @node.id = @id
    @node.classList.add(TandemLine.CLASS_NAME)
    this.rebuild()
    super(@node)

  buildLeaves: (node, attributes) ->
    _.each(_.clone(node.childNodes), (node) =>
      node.normalize()
      nodeAttributes = _.clone(attributes)
      [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(node)
      nodeAttributes[attrName] = attrVal if attrName?
      if Tandem.Leaf.isLeafNode(node)
        @leaves.append(new Tandem.Leaf(this, node, nodeAttributes))
      else
        this.buildLeaves(node, nodeAttributes)
    )

  findLeaf: (leafNode) ->
    curLeaf = @leaves.first
    while curLeaf?
      return curLeaf if curLeaf.node == leafNode
      curLeaf = curLeaf.next
    return null

  rebuild: ->
    if @node.parentNode == @doc.root
      return if @outerHTML == @node.outerHTML
      while @leaves? && @leaves.length > 0
        @leaves.remove(@leaves.first)
      @leaves = new LinkedList()
      TandemLine.normalizeHtml(@node)
      this.buildLeaves(@node, {})
      this.resetContent()
    else
      @doc.removeLine(this)

  resetContent: ->
    @length = _.reduce(@leaves.toArray(), ((length, leaf) -> leaf.length + length), 0)
    @outerHTML = @node.outerHTML
    @attributes = {}
    [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(@node)
    @attributes[attrName] = attrVal if attrName?
    this.setDirty(false)
    @delta = this.toDelta()

  setDirty: (isDirty = true) ->
    if isDirty
      @node.classList.add(TandemLine.DIRTY_CLASS)
    else
      @node.classList.remove(TandemLine.DIRTY_CLASS)

  splitContents: (offset) ->
    this.setDirty()
    [node, offset] = Tandem.Utils.getChildAtOffset(@node, offset)
    if @node.tagName == 'OL' || @node.tagName == 'UL'
      [node, offset] = Tandem.Utils.getChildAtOffset(node, offset)
    return Tandem.Utils.splitNode(node, offset)

  toDelta: ->
    deltas = _.map(@leaves.toArray(), (leaf) ->
      return new JetInsert(leaf.text, leaf.getAttributes())
    )
    delta = new JetDelta(0, @length, deltas)
    delta.compact()
    return delta

window.Tandem ||= {}
window.Tandem.Line = TandemLine


@index += node.textContent.length + (if Tandem.Line.isLineNode(node) then 1 else 0) # Account for newline char

length = if node.tagName != 'BR' then node.textContent.length else 0

@index += length + (if Tandem.Line.isLineNode(node) then 1 else 0) # Account for newline char


#= require underscore

# Arrays must be alphabetized, so we can use binary search

TandemConstants =
  ALIGN_ATTRIBUTES: [
    'center'
    'justify'
    'left'
    'right'
  ]

  BLOCK_TAGS: [
    'ADDRESS'
    'BLOCKQUOTE'
    'DD'
    'DIV'
    'DL'
    'H1', 'H2', 'H3', 'H4', 'H5', 'H6'
    'LI'
    'OL'
    'P'
    'PRE'
    'TABLE'
    'TBODY'
    'TD'
    'TFOOT'
    'TH'
    'THEAD'
    'TR'
    'UL'
  ]

  BREAK_TAGS: [
    'BR'
    'HR'
  ]

  FONT_BACKGROUNDS: [
    'black'
    'blue'
    'green'
    'orange'
    'purple'
    'red'
    'white'
    'yellow'
  ]

  FONT_COLORS: [
    'black'
    'blue'
    'green'
    'orange'
    'red'
    'white'
    'yellow'
  ]

  FONT_FAMILIES: [
    'monospace'
    'serif'
  ]

  FONT_SIZES: [
    'huge'
    'large'
    'small'
  ]

  IGNORE_CLASSES: [
    'tab'
  ]

  # Missing rule implies removal
  LINE_RULES: {
    'A'         : {}
    'B'         : {}
    'BIG'       : {rename: 'span'}
    'CENTER'    : {rename: 'span'}
    'DEL'       : {rename: 's'}
    'EM'        : {rename: 'i'}
    'H1'        : {rename: 'span'}
    'H2'        : {rename: 'span'}
    'H3'        : {rename: 'span'}
    'H4'        : {rename: 'span'}
    'H5'        : {rename: 'span'}
    'H6'        : {rename: 'span'}
    'I'         : {}
    'INS'       : {rename: 'span'}
    'LI'        : {}
    'OL'        : {}
    'S'         : {}
    'SMALL'     : {rename: 'span'}
    'SPAN'      : {}
    'STRIKE'    : {rename: 's'}
    'STRONG'    : {rename: 'b'}
    'U'         : {}
    'UL'        : {}
  }

  INDENT_ATTRIBUTES:
    'bullet'  : [0..8]
    'indent'  : [0..8]
    'list'    : [0..8]

  LIST_TAGS: [
    'OL'
    'UL'
  ]

TandemConstants.LINE_ATTRIBUTES = [].concat(_.keys(TandemConstants.INDENT_ATTRIBUTES), TandemConstants.ALIGN_ATTRIBUTES).sort()

TandemConstants.SPAN_ATTRIBUTES =
  'font-background' : TandemConstants.FONT_BACKGROUNDS
  'font-color'      : TandemConstants.FONT_COLORS
  'font-family'     : TandemConstants.FONT_FAMILIES
  'font-size'       : TandemConstants.FONT_SIZES

# Array of possbile values mostly for consistency with SPAN_ATTRIBUTES, not actually used in codebase
TandemConstants.TAG_ATTRIBUTES =
  'bold'      : [true, false]
  'italic'    : [true, false]
  'link'      : [true, false]     # Link value could actually also be any string representing the href
  'strike'    : [true, false]
  'underline' : [true, false]

window.Tandem ||= {}
window.Tandem.Constants = TandemConstants


#= underscore
#= tandem/constants

TandemUtils =
  # All block nodes inside nodes are moved out
  breakBlocks: (root) ->
    lineNodes = [root]
    this.traversePreorder(root, 0, (node, index) =>
      if node.nodeType == node.ELEMENT_NODE && !Tandem.Utils.isIgnoreNode(node)
        toBreak = []
        if _.indexOf(Tandem.Constants.BLOCK_TAGS, node.tagName, true) > -1
          [left1, left2, didLeftSplit] = this.splitNode(root, index)
          if didLeftSplit
            toBreak = toBreak.concat([left1, left2])
          else
            [right1, right2, didRightSplit] = this.splitNode(root, node.textContent.length)
            toBreak = toBreak.concat([right1, right2]) if didRightSplit
        else if _.indexOf(Tandem.Constants.BREAK_TAGS, node.tagName, true) > -1
          next = node.nextSibling
          if node.previousSibling && node.nextSibling
            [left, right, didSplit] = this.splitNode(root, index, true)
            node.parentNode.removeChild(node)
            toBreak = [left, right]
            node = next
          else if node.previousSibling || node.nextSibling
            node.parentNode.removeChild(node)
            node = next
        _.each(toBreak, (line) =>
          if line? && line != root
            newLineNodes = this.breakBlocks(line)
            lineNodes.push(line)
            lineNodes = _.uniq(lineNodes.concat(newLineNodes))
        )
      return node
    )
    return lineNodes

  cleanHtml: (html, keepIdClass = false) ->
    # Remove leading and tailing whitespace
    html = html.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
    # Remove whitespace between tags
    html = html.replace(/>\s\s+</gi, '><')
    # Remove id or class classname
    html = html.replace(/\ (class|id)="[a-z0-9\-_]+"/gi, '') unless keepIdClass == true
    # Standardize br
    html = html.replace(/
<\/br>/, '
')
    return html

  createContainerForAttribute: (doc, attribute, value) ->
    switch (attribute)
      when 'bold'       then return doc.createElement('b')
      when 'italic'     then return doc.createElement('i')
      when 'strike'     then return doc.createElement('s')
      when 'underline'  then return doc.createElement('u')
      when 'link'
        link = doc.createElement('a')
        link.setAttribute('href', value)
        return link
      else
        span = doc.createElement('span')
        span.classList.add(attribute)
        span.classList.add(value)
        return span

  extractNodes: (startLineNode, startOffset, endLineNode, endOffset) ->
    [leftStart, rightStart] = Tandem.Utils.splitNode(startLineNode, startOffset, true)
    if startLineNode == endLineNode
      endLineNode = rightStart
      endOffset -= leftStart.textContent.length if leftStart? && startLineNode != rightStart
    [leftEnd, rightEnd] = Tandem.Utils.splitNode(endLineNode, endOffset, true)
    fragment = startLineNode.ownerDocument.createDocumentFragment()
    while rightStart != rightEnd
      next = rightStart.nextSibling
      fragment.appendChild(rightStart)
      rightStart = next
    Tandem.Utils.mergeNodes(leftStart, rightEnd)
    return fragment

  getAncestorCount: (container, tagName) ->
    count = 0
    while container.parentNode?
      if container.parentNode.tagName == tagName
        count += 1
      container = container.parentNode
    return count

  getAttributeDefault: (attribute) ->
    switch attribute
      when 'font-background'  then return 'white'
      when 'font-color'       then return 'black'
      when 'font-family'      then return 'san-serif'
      when 'font-size'        then return 'normal'
      else                         return false

  getAttributeForContainer: (container) ->
    switch container.tagName
      when 'A'  then return ['link', container.getAttribute('href')]
      when 'B'  then return ['bold', true]
      when 'I'  then return ['italic', true]
      when 'S'  then return ['strike', true]
      when 'U'  then return ['underline', true]
      when 'OL' then return ['list', Tandem.Utils.getIndent(container)]
      when 'UL' then return ['bullet', Tandem.Utils.getIndent(container)]
      when 'DIV'
        indent = Tandem.Utils.getIndent(container)
        if indent > 0
          return ['indent', indent]
        else
          return []
      when 'SPAN'
        attribute = []
        _.all(Tandem.Constants.SPAN_ATTRIBUTES, (list, attrName) ->
          if container.classList.contains(attrName)
            return _.all(container.classList, (css) ->
              if _.indexOf(list, css) > -1
                attribute = [attrName, css]
                return false
              return true
            )
          return true
        )
        return attribute
      else
        return []

  getChildAtOffset: (node, offset) ->
    child = node.firstChild
    while child? && offset > child.textContent.length
      if offset == child.textContent.length
        return [child, offset]
      offset -= child.textContent.length
      offset -= 1 if Tandem.Line.isLineNode(child)
      child = child.nextSibling
    return [child, offset]

  getIndent: (list) ->
    indent = 0
    _.any(list.classList, (css) ->
      if css.substring(0, Tandem.Document.INDENT_PREFIX.length) == Tandem.Document.INDENT_PREFIX
        indent = parseInt(css.substring(Tandem.Document.INDENT_PREFIX.length))
        return true
      return false
    )
    return indent

  isIgnoreNode: (node) ->
    return _.any(Tandem.Constants.IGNORE_CLASSES, (cssClass) ->
      return node.classList.contains(cssClass)
    )

  isTextNodeParent: (node) ->
    return node.childNodes.length == 1 && node.firstChild.nodeType == node.TEXT_NODE

  mergeNodes: (node1, node2) ->
    return node2 if !node1?
    return node1 if !node2?
    this.moveChildren(node1, node2)
    node2.parentNode.removeChild(node2)
    return node1

  moveChildren: (newParent, oldParent) ->
    _.each(_.clone(oldParent.childNodes), (child) ->
      newParent.appendChild(child)
    )

  removeAttributeFromSubtree: (subtree, attribute) ->
    children = _.clone(subtree.childNodes)
    attributes = Tandem.Utils.getAttributeForContainer(subtree)
    [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(subtree)
    if attrName == attribute
      Tandem.Utils.unwrap(subtree)
    _.each(children, (child) ->
      Tandem.Utils.removeAttributeFromSubtree(child, attribute)
    )

  setIndent: (list, indent) ->
    _.each(_.clone(list.classList), (css) ->
      if css.substring(0, Tandem.Document.INDENT_PREFIX.length) == Tandem.Document.INDENT_PREFIX
        list.classList.remove(css)
    )
    list.classList.add(Tandem.Document.INDENT_PREFIX + indent) if indent

  splitNode: (node, offset, force = false) ->
    if offset > node.textContent.length
      throw new Error('Splitting at offset greater than node length')

    if node.nodeType == node.TEXT_NODE
      node = this.wrap(node.ownerDocument.createElement('span'), node)

    # Check if split necessary
    if !force
      if offset == 0
        return [node.previousSibling, node, false]
      if offset == node.textContent.length
        return [node, node.nextSibling, false]

    left = node
    right = node.cloneNode(false)
    node.parentNode.insertBefore(right, left.nextSibling)

    if TandemUtils.isTextNodeParent(node)
      # Text split
      beforeText = node.textContent.substring(0, offset)
      afterText = node.textContent.substring(offset)
      left.textContent = beforeText
      right.textContent = afterText
      return [left, right, true]
    else
      # Node split
      [child, offset] = TandemUtils.getChildAtOffset(node, offset)
      [childLeft, childRight] = TandemUtils.splitNode(child, offset)
      while childRight != null
        nextRight = childRight.nextSibling
        right.appendChild(childRight)
        childRight = nextRight
      return [left, right, true]

  switchTag: (node, newTag) ->
    return if node.tagName == newTag
    newNode = node.ownerDocument.createElement(newTag)
    this.moveChildren(newNode, node)
    node.parentNode.replaceChild(newNode, node)
    newNode.className = node.className if node.className
    newNode.id = node.id if node.id
    return newNode

  traversePreorder: (root, offset, fn, context = fn, args...) ->
    return unless root?
    cur = root.firstChild
    while cur?
      nextOffset = offset + cur.textContent.length
      curHtml = cur.innerHTML
      cur = fn.apply(context, [cur, offset].concat(args))
      TandemUtils.traversePreorder.apply(TandemUtils.traversePreorder, [cur, offset, fn, context].concat(args))
      if cur? && cur.innerHTML == curHtml
        cur = cur.nextSibling
        offset = nextOffset

  traverseSiblings: (startNode, endNode, fn) ->
    while startNode?
      nextSibling = startNode.nextSibling
      fn(startNode)
      break if startNode == endNode
      startNode = nextSibling

  unwrap: (node) ->
    next = node.firstChild
    _.each(_.clone(node.childNodes), (child) ->
      node.parentNode.insertBefore(child, node)
    )
    node.parentNode.removeChild(node)
    return next

  wrap: (wrapper, node) ->
    node.parentNode.insertBefore(wrapper, node)
    wrapper.appendChild(node)
    return wrapper

  wrapChildren: (wrapper, node) ->
    Tandem.Utils.moveChildren(wrapper, node)
    node.appendChild(wrapper)
    return wrapper

window.Tandem ||= {}
window.Tandem.Utils = TandemUtils