# Deny Account.createUser in client and set Meteor.loginTokenExpires
accountsConfig = { forbidClientAccountCreation: true, loginExpirationInDays: RocketChat.settings.get 'Accounts_LoginExpiration' }
Accounts.config accountsConfig

Accounts.emailTemplates.siteName = RocketChat.settings.get 'Site_Name';
Accounts.emailTemplates.from = "#{RocketChat.settings.get 'Site_Name'} <#{RocketChat.settings.get 'From_Email'}>";

verifyEmailHtml = Accounts.emailTemplates.verifyEmail.text
Accounts.emailTemplates.verifyEmail.html = (user, url) ->
	url = url.replace Meteor.absoluteUrl(), Meteor.absoluteUrl() + 'login/'
	verifyEmailHtml user, url

resetPasswordHtml = Accounts.emailTemplates.resetPassword.text
Accounts.emailTemplates.resetPassword.html = (user, url) ->
	url = url.replace /\/#\//, '/'
	resetPasswordHtml user, url

Accounts.emailTemplates.enrollAccount.subject = (user) ->
	if RocketChat.settings.get 'Accounts_Enrollment_Customized'
		subject = RocketChat.settings.get 'Accounts_Enrollment_Email_Subject'
	else
		subject = TAPi18n.__('Accounts_Enrollment_Email_Subject_Default', { lng: user?.language || RocketChat.settings.get('language') || 'en' })

	return RocketChat.placeholders.replace(subject);

Accounts.emailTemplates.enrollAccount.html = (user, url) ->

	if RocketChat.settings.get 'Accounts_Enrollment_Customized'
		html = RocketChat.settings.get 'Accounts_Enrollment_Email'
	else
		html = TAPi18n.__('Accounts_Enrollment_Email_Default', { lng: user?.language || RocketChat.settings.get('language') || 'en' })

	header = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Header') || "")
	footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer') || "")
	html = RocketChat.placeholders.replace(html, {
		name: user.name,
		email: user.emails?[0]?.address
	});

	return header + html + footer;

Accounts.onCreateUser (options, user) ->
	# console.log 'onCreateUser ->',JSON.stringify arguments, null, '  '
	# console.log 'options ->',JSON.stringify options, null, '  '
	# console.log 'user ->',JSON.stringify user, null, '  '

	RocketChat.callbacks.run 'beforeCreateUser', options, user

	user.status = 'offline'
	user.active = not RocketChat.settings.get 'Accounts_ManuallyApproveNewUsers'

	if not user?.name? or user.name is ''
		if options.profile?.name?
			user.name = options.profile?.name

	if user.services?
		for serviceName, service of user.services
			if not user?.name? or user.name is ''
				if service.name?
					user.name = service.name
				else if service.username?
					user.name = service.username

			if not user.emails? and service.email?
				user.emails = [
					address: service.email
					verified: true
				]

	return user

# Wrap insertUserDoc to allow executing code after Accounts.insertUserDoc is run
Accounts.insertUserDoc = _.wrap Accounts.insertUserDoc, (insertUserDoc, options, user) ->
	roles = []
	if Match.test(user.globalRoles, [String]) and user.globalRoles.length > 0
		roles = roles.concat user.globalRoles

	delete user.globalRoles

	user.type ?= 'user'

	_id = insertUserDoc.call(Accounts, options, user)

	if roles.length is 0
		# when inserting first user give them admin privileges otherwise make a regular user
		hasAdmin = RocketChat.models.Users.findOne({ roles: 'admin' }, {fields: {_id: 1}})
		if hasAdmin?
			roles.push 'user'
		else
			roles.push 'admin'

	RocketChat.authz.addUserRoles(_id, roles)

	Meteor.defer ->
		RocketChat.callbacks.run 'afterCreateUser', options, user

	return _id

Accounts.validateLoginAttempt (login) ->
	login = RocketChat.callbacks.run 'beforeValidateLogin', login

	if login.allowed isnt true
		return login.allowed

	# bypass for livechat users
	if login.user.type is 'visitor'
		return true

	if !!login.user?.active isnt true
		throw new Meteor.Error 'error-user-is-not-activated', 'User is not activated', { function: 'Accounts.validateLoginAttempt' }
		return false

	# If user is admin, no need to check if email is verified
	if 'admin' not in login.user?.roles and login.type is 'password' and RocketChat.settings.get('Accounts_EmailVerification') is true
		validEmail = login.user.emails.filter (email) ->
			return email.verified is true

		if validEmail.length is 0
			throw new Meteor.Error 'error-invalid-email', 'Invalid email __email__'
			return false

	RocketChat.models.Users.updateLastLoginById login.user._id

	Meteor.defer ->
		RocketChat.callbacks.run 'afterValidateLogin', login

	return true

Accounts.validateNewUser (user) ->
	# bypass for livechat users
	if user.type is 'visitor'
		return true

	if RocketChat.settings.get('Accounts_Registration_AuthenticationServices_Enabled') is false and RocketChat.settings.get('LDAP_Enable') is false and not user.services?.password?
		throw new Meteor.Error 'registration-disabled-authentication-services', 'User registration is disabled for authentication services'
	return true

Accounts.validateNewUser (user) ->
	# bypass for livechat users
	if user.type is 'visitor'
		return true

	domainWhiteList = RocketChat.settings.get('Accounts_AllowedDomainsList')

	if _.isEmpty s.trim(domainWhiteList)
		return true

	domainWhiteList = _.map(domainWhiteList.split(','), (domain) -> domain.trim())

	if user.emails?.length > 0
		ret = false
		email = user.emails[0].address
		for domain in domainWhiteList
			if email.match('@' + RegExp.escape(domain) + '$')
				ret = true
				break

		return ret

	return true


if RocketChat.settings.get('Accounts_CustomFields') isnt ''

@customFields = JSON.parse RocketChat.settings.get('Accounts_CustomFields')

console.error('Invalid JSON for Accounts_CustomFields')

keep replace replace replace replace replace keep keep keep

if typeof Accounts_CustomFields is 'string' and Accounts_CustomFields.trim() isnt ''

Template.adminImport.helpers
	isAdmin: ->
		return RocketChat.authz.hasRole(Meteor.userId(), 'admin')
	isImporters: ->
		return Object.keys(Importer.Importers).length > 0
	getDescription: (importer) ->
		return TAPi18n.__('Importer_From_Description', { from: importer.name })
	importers: ->
		importers = []
		_.each Importer.Importers, (importer, key) ->
			importer.key = key
			importers.push importer
		return importers

Template.adminImport.events
	'click .start-import': (event) ->
		importer = @

		Meteor.call 'setupImporter', importer.key, (error, data) ->
			if error
				console.log t('importer_setup_error'), importer.key, error
				return handleError(error)
			else
				FlowRouter.go '/admin/import/prepare/' + importer.key


@validSecretURL = new ReactiveVar(false)

validateCustomFields = (formObj, validationObj) ->

keep keep keep keep keep replace keep keep

Meteor.startup(function() {
	Meteor.autorun(function() {
		let CustomTranslations = RocketChat.settings.get('Custom_Translations');
		try {
			CustomTranslations = JSON.parse(CustomTranslations);
		} catch (e) {
			console.error('Invalid setting Custom_Translations', e);
		}

		for (const lang in CustomTranslations) {
			if (CustomTranslations.hasOwnProperty(lang)) {
				const translations = CustomTranslations[lang];
				TAPi18next.addResourceBundle(lang, 'project', translations);
			}
		}
	});
});


/* globals openRoom */

RocketChat.roomTypes.add('l', 5, {
	template: 'livechat',
	icon: 'icon-chat-empty',
	route: {
		name: 'live',
		path: '/live/:code(\\d+)',
		action(params/*, queryParams*/) {
			openRoom('l', params.code);
			RocketChat.TabBar.showGroup('livechat', 'search');
		},
		link(sub) {
			return {
				code: sub.code
			};
		}
	},

	findRoom(identifier) {
		return ChatRoom.findOne({ code: parseInt(identifier) });
	},

	roomName(roomData) {
		if (!roomData.name) {
			return roomData.label;
		} else {
			return roomData.name;
		}
	},

	condition() {
		return RocketChat.settings.get('Livechat_enabled') && RocketChat.authz.hasAllPermission('view-l-room');
	},

	canSendMessage(roomId) {
		let room = ChatRoom.findOne({ _id: roomId }, { fields: { open: 1 } });
		return room && room.open === true;
	},

	notSubscribedTpl: {
		template: 'livechatNotSubscribed'
	}
});


RocketChat.Migrations.add
	version: 32
	up: ->
		RocketChat.models.Settings.update {'_id':/Accounts_OAuth_Custom_/}, {$set:{'group':'OAuth'}}, {multi: true}


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

		check(_id, String);

		var department = RocketChat.models.LivechatDepartment.findOneById(_id, { fields: { _id: 1 } });

		if (!department) {
			throw new Meteor.Error('department-not-found', 'Department not found', { method: 'livechat:removeDepartment' });
		}

		return RocketChat.models.LivechatDepartment.removeById(_id);
	}
});


keep keep replace replace keep keep keep keep keep

for field, value of formObj when customFields[field]?

Template.loginForm.helpers
	userName: ->
		return Meteor.user()?.username

	namePlaceholder: ->
		return if RocketChat.settings.get 'Accounts_RequireNameForSignUp' then t('Name') else t('Name_optional')

	showFormLogin: ->
		return RocketChat.settings.get 'Accounts_ShowFormLogin'

	state: (state..., data) ->
		return state.indexOf(Template.instance().state.get()) > -1

	btnLoginSave: ->
		switch Template.instance().state.get()
			when 'register'
				return t('Submit')
			when 'login'
				return t('Login')
			when 'email-verification'
				return t('Send_confirmation_email')
			when 'forgot-password'
				return t('Reset_password')

	loginTerms: ->
		return RocketChat.settings.get 'Layout_Login_Terms'

	registrationAllowed: ->
		return RocketChat.settings.get('Accounts_RegistrationForm') is 'Public' or Template.instance().validSecretURL?.get()

	linkReplacementText: ->
		return RocketChat.settings.get('Accounts_RegistrationForm_LinkReplacementText')

	passwordResetAllowed: ->
		return RocketChat.settings.get 'Accounts_PasswordReset'

	requirePasswordConfirmation: ->
		return RocketChat.settings.get 'Accounts_RequirePasswordConfirmation'

	emailOrUsernamePlaceholder: ->
		return RocketChat.settings.get('Accounts_EmailOrUsernamePlaceholder') or t("Email_or_username")

	passwordPlaceholder: ->
		return RocketChat.settings.get('Accounts_PasswordPlaceholder') or t("Password")

	hasOnePassword: ->
		return OnePassword?.findLoginForUrl? && device?.platform?.toLocaleLowerCase() is 'ios'

	customFields: ->
		if not Template.instance().customFields
			return []

		customFieldsArray = []
		for key, value of Template.instance().customFields
			customFieldsArray.push
				fieldName: key,
				field: value

		return customFieldsArray

Template.loginForm.events
	'submit #login-card': (event, instance) ->
		event.preventDefault()

		button = $(event.target).find('button.login')
		RocketChat.Button.loading(button)

		formData = instance.validate()
		if formData
			if instance.state.get() is 'email-verification'
				Meteor.call 'sendConfirmationEmail', s.trim(formData.email), (err, result) ->
					RocketChat.Button.reset(button)
					RocketChat.callbacks.run('userConfirmationEmailRequested');
					toastr.success t('We_have_sent_registration_email')
					instance.state.set 'login'
				return

			if instance.state.get() is 'forgot-password'
				Meteor.call 'sendForgotPasswordEmail', s.trim(formData.email), (err, result) ->
					RocketChat.Button.reset(button)
					RocketChat.callbacks.run('userForgotPasswordEmailRequested');
					toastr.success t('We_have_sent_password_email')
					instance.state.set 'login'
				return

			if instance.state.get() is 'register'
				formData.secretURL = FlowRouter.getParam 'hash'
				Meteor.call 'registerUser', formData, (error, result) ->
					RocketChat.Button.reset(button)

					if error?
						if error.reason is 'Email already exists.'
							toastr.error t 'Email_already_exists'
						else
							handleError(error)
						return

					RocketChat.callbacks.run('userRegistered');

					Meteor.loginWithPassword s.trim(formData.email), formData.pass, (error) ->
						if error?.error is 'error-invalid-email'
							toastr.success t('We_have_sent_registration_email')
							instance.state.set 'login'
						else if error?.error is 'error-user-is-not-activated'
							instance.state.set 'wait-activation'

			else
				loginMethod = 'loginWithPassword'
				if RocketChat.settings.get('LDAP_Enable')
					loginMethod = 'loginWithLDAP'
				if RocketChat.settings.get('CROWD_Enable')
					loginMethod = 'loginWithCrowd'
				Meteor[loginMethod] s.trim(formData.emailOrUsername), formData.pass, (error) ->
					RocketChat.Button.reset(button)
					if error?
						if error.error is 'no-valid-email'
							instance.state.set 'email-verification'
						else
							toastr.error t 'User_not_found_or_incorrect_password'
						return
					localStorage.setItem('userLanguage', Meteor.user()?.language)
					setLanguage(Meteor.user()?.language)

	'click .register': ->
		Template.instance().state.set 'register'
		RocketChat.callbacks.run('loginPageStateChange', Template.instance().state.get());

	'click .back-to-login': ->
		Template.instance().state.set 'login'
		RocketChat.callbacks.run('loginPageStateChange', Template.instance().state.get());

	'click .forgot-password': ->
		Template.instance().state.set 'forgot-password'
		RocketChat.callbacks.run('loginPageStateChange', Template.instance().state.get());

	'click .one-passsword': ->
		if not OnePassword?.findLoginForUrl?
			return

		succesCallback = (credentials) ->
			$('input[name=emailOrUsername]').val(credentials.username)
			$('input[name=pass]').val(credentials.password)

		errorCallback = ->
			console.log 'OnePassword errorCallback', arguments

		OnePassword.findLoginForUrl(succesCallback, errorCallback, Meteor.absoluteUrl())

Template.loginForm.onCreated ->
	instance = @

	if RocketChat.settings.get('Accounts_CustomFields') isnt ''
		try
			@customFields = JSON.parse RocketChat.settings.get('Accounts_CustomFields')
		catch e
			console.error('Invalid JSON for Accounts_CustomFields')

	if Meteor.settings.public.sandstorm
		@state = new ReactiveVar('sandstorm')
	else if Session.get 'loginDefaultState'
		@state = new ReactiveVar(Session.get 'loginDefaultState')
	else
		@state = new ReactiveVar('login')

	@validSecretURL = new ReactiveVar(false)

	validateCustomFields = (formObj, validationObj) ->
		if not instance.customFields
			return

		for field, value of formObj when instance.customFields[field]?
			customField = instance.customFields[field]

			if customField.required is true and not value
				return validationObj[field] = t('Field_required')

			if customField.maxLength? and value.length > customField.maxLength
				return validationObj[field] = t('Max_length_is', customField.maxLength)

			if customField.minLength? and value.length < customField.minLength
				return validationObj[field] = t('Min_length_is', customField.minLength)

	@validate = ->
		formData = $("#login-card").serializeArray()
		formObj = {}
		validationObj = {}

		for field in formData
			formObj[field.name] = field.value

		if instance.state.get() isnt 'login'
			unless formObj['email'] and /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+\b/i.test(formObj['email'])
				validationObj['email'] = t('Invalid_email')

		if instance.state.get() isnt 'forgot-password'
			unless formObj['pass']
				validationObj['pass'] = t('Invalid_pass')

		if instance.state.get() is 'register'
			if RocketChat.settings.get('Accounts_RequireNameForSignUp') and not formObj['name']
				validationObj['name'] = t('Invalid_name')

			if RocketChat.settings.get('Accounts_RequirePasswordConfirmation') and formObj['confirm-pass'] isnt formObj['pass']
				validationObj['confirm-pass'] = t('Invalid_confirm_pass')

			validateCustomFields(formObj, validationObj)

		$("#login-card h2").removeClass "error"
		$("#login-card input.error, #login-card select.error").removeClass "error"
		$("#login-card .input-error").text ''

		unless _.isEmpty validationObj
			button = $('#login-card').find('button.login')
			RocketChat.Button.reset(button)
			$("#login-card h2").addClass "error"
			for key, value of validationObj
				$("#login-card input[name=#{key}], #login-card select[name=#{key}]").addClass "error"
				$("#login-card input[name=#{key}]~.input-error, #login-card select[name=#{key}]~.input-error").text value
			return false

		return formObj

	if FlowRouter.getParam('hash')
		Meteor.call 'checkRegistrationSecretURL', FlowRouter.getParam('hash'), (err, success) =>
			@validSecretURL.set true

Template.loginForm.onRendered ->
	Session.set 'loginDefaultState'
	Tracker.autorun =>
		RocketChat.callbacks.run('loginPageStateChange', this.state.get())
		switch this.state.get()
			when 'login', 'forgot-password', 'email-verification'
				Meteor.defer ->
					$('input[name=email]').select().focus()

			when 'register'
				Meteor.defer ->
					$('input[name=name]').select().focus()


<template name="listPrivateGroupsFlex">

{{_ "Private_Groups"}}

					<form class="search-form" role="form">

							<select class="c-select" id="sort">
								<option value="name" selected="{{sortSelected 'name'}}">{{_ "Name"}}</option>
								<option value="ls" selected="{{sortSelected 'ls'}}">{{_ "Last_seen"}}</option>
							</select>

							<input type="text" id="channel-search" class="search" placeholder="{{_ "Search_Private_Groups"}}" autocomplete="off" />

					</form>

{{_ "Private_Groups_list"}}

			{{#each groups}}

						{{name}}

							{{#if hidden}}

							{{/if}}

			{{/each}}
			{{#if hasMore}}

					{{#if Template.subscriptionsReady}}
						<button>{{_ "Has_more"}}...</button>
					{{else}}

{{_ "Loading..."}}

					{{/if}}

			{{/if}}

			<button class="button clean create">{{_ "Create_new"}}</button>

</template>


Template.room.events({
	'click .action-link'(event) {
		event.preventDefault();
		event.stopPropagation();
		const data = Blaze.getData(event.currentTarget);

		if (data && data._arguments && data._arguments[1] && data._arguments[1]._id) {
			Meteor.call('actionLinkHandler', event.currentTarget.childNodes[1].name, data._arguments[1]._id, (err) => {
				if (err) {
					handleError(err);
				}
			});
		}
	}
});


[leaf, offset] = @quill.editor.doc.findLeafAt(Math.max(0, range.start - 1))

keep keep keep keep replace keep keep keep keep keep

[leaf, offset] = @quill.editor.doc.findLeafAt(range.start, true)

_          = require('lodash')
DOM        = require('../dom')
Normalizer = require('../normalizer')

class Tooltip
  @DEFAULTS:
    offset: 10
    styles:
      '.tooltip':
        'background-color': '#fff'
        'border': '1px solid #000'
        'white-space': 'nowrap'
      '.tooltip a':
        'cursor': 'pointer'
        'text-decoration': 'none'
    template: ''

  constructor: (@quill, @options) ->
    @quill.addStyles(@options.styles)
    @container = @quill.addContainer('tooltip')
    @container.innerHTML = Normalizer.stripWhitespace(@options.template)
    @container.style.position = 'absolute'    # Set immediately so style.left has effect to avoid initial flicker
    DOM.addEventListener(@quill.root, 'focus', _.bind(this.hide, this))
    this.hide()
    @quill.on(@quill.constructor.events.TEXT_CHANGE, (delta, source) =>
      if source == 'user'
        @range = null
        this.hide()
    )

  initTextbox: (textbox, enterCallback, escapeCallback) ->
    DOM.addEventListener(textbox, 'keyup', (event) =>
      switch event.which
        when DOM.KEYS.ENTER   then enterCallback.call(this)
        when DOM.KEYS.ESCAPE  then escapeCallback.call(this)
        else return true
    )

  hide: ->
    @container.style.left = '-10000px'
    @quill.setSelection(@range) if @range
    @range = null

  show: (reference) ->
    @range = @quill.getSelection()
    [left, top] = this._position(reference)
    [left, top] = this._limit(left, top)
    @container.style.left = "#{left}px"
    @container.style.top = "#{top}px"
    @container.focus()

  _limit: (left, top) ->
    editorRect = @quill.root.getBoundingClientRect()
    toolbarRect = @container.getBoundingClientRect()
    left = Math.min(editorRect.right - toolbarRect.width, left)   # right boundary
    left = Math.max(editorRect.left, left)                        # left boundary
    top = Math.min(editorRect.bottom - toolbarRect.height, top)   # bottom boundary
    top = Math.max(editorRect.top, top)                           # top boundary
    return [left, top]

  _position: (reference) ->
    toolbarRect = @container.getBoundingClientRect()
    editorRect = @quill.root.getBoundingClientRect()
    if reference?
      referenceBounds = reference.getBoundingClientRect()
      left = referenceBounds.left + referenceBounds.width/2 - toolbarRect.width/2
      top = referenceBounds.top + referenceBounds.height + @options.offset
      if top + toolbarRect.height > editorRect.bottom
        top = referenceBounds.top - toolbarRect.height - @options.offset
    else
      left = editorRect.left + editorRect.width/2 - toolbarRect.width/2
      top = editorRect.top + editorRect.height/2 - toolbarRect.height/2
    return [left, top]

module.exports = Tooltip


doctype html
html
  head
    title Quill Mobile Editor Demo
    link(rel='stylesheet', type='text/css', href='styles/mobile.css')
    meta(charset='utf-8')
    meta(name='apple-mobile-web-app-capable', content='yes')
    meta(name='viewport', content='initial-scale=1.5, minimum-scale=1.5, maximum-scale=1.5, user-scalable=no')

  body
    #content-container
      #formatting-container
        span.sc-format-button.sc-bold(title='Bold')
        span.sc-format-button.sc-italic(title='Italic')
        span.sc-format-button.sc-underline(title='Underline')
        span.sc-format-button.sc-strike(title='Strikethrough')
      #editor-container.editor-container

    script(type='text/javascript', src='../quill.js')
    script(type='text/javascript').
      var editor = new Quill('#editor-container', {
        logLevel: 'info',
        modules: {
          toolbar: { container: '#formatting-container' }
        }
      });


describe('Keyboard', ->
  beforeEach( ->
    @container = $('#editor-container').get(0)
  )

  describe('toggleFormat()', ->
    beforeEach( ->
      @container.innerHTML = '

<s>01</s>2345

'
      @quill = new Quill(@container.firstChild)
      @keyboard = @quill.getModule('keyboard')
    )

    it('set if all unset', ->
      @keyboard.toggleFormat(new Quill.Lib.Range(3, 6), 'strike')
      expect(@quill.root.firstChild).toEqualHTML('<s>01</s>2<s>3</s><s>45</s>')
    )

    it('unset if all set', ->
      @keyboard.toggleFormat(new Quill.Lib.Range(2, 4), 'bold')
      expect(@quill.root.firstChild).toEqualHTML('<s>01</s>2345')
    )

    it('set if partially set', ->
      @keyboard.toggleFormat(new Quill.Lib.Range(3, 5), 'bold')
      expect(@quill.root.firstChild).toEqualHTML('<s>01</s>2345')
    )
  )

  describe('hotkeys', ->
    beforeEach( ->
      @container.innerHTML = '

0123

'
      @quill = new Quill(@container.firstChild)
      @keyboard = @quill.getModule('keyboard')
    )

    it('trigger', (done) ->
      hotkey = { key: 'B', metaKey: true }
      @keyboard.addHotkey(hotkey, (range) ->
        expect(range.start).toEqual(1)
        expect(range.end).toEqual(2)
        done()
      )
      @quill.setSelection(1, 2)
      Quill.DOM.triggerEvent(@quill.root, 'keydown', hotkey)
    )

    it('format', ->
      @quill.setSelection(0, 4)
      Quill.DOM.triggerEvent(@quill.root, 'keydown', Quill.Module.Keyboard.hotkeys.BOLD)
      expect(@quill.root).toEqualHTML('
0123
', true)
    )

    it('tab', ->
      @quill.setSelection(1, 3)
      Quill.DOM.triggerEvent(@quill.root, 'keydown', Quill.Module.Keyboard.hotkeys.INDENT)
      expect(@quill.root).toEqualHTML('
0\t3
', true)
    )

    it('shift + tab', ->
      @quill.setSelection(0, 2)
      Quill.DOM.triggerEvent(@quill.root, 'keydown', Quill.Module.Keyboard.hotkeys.OUTDENT)
      expect(@quill.root).toEqualHTML('
0123
', true)
    )
  )
)


describe('Tooltip', ->
  makeBounder = (left, top, width, height) ->
    return {
      getBoundingClientRect: ->
        return { left: left, top: top, right: left + width, bottom: top + height, width: width, height: height }
    }

  beforeEach( ->
    @container = $('#editor-container').html('

').get(0)
    @quill = new Quill(@container.firstChild)
    @tooltip = @quill.addModule('tooltip', { offset: 20 })
  )

  describe('show/hide', ->
    it('restore range', ->
      @quill.setSelection(0, 0)
      @tooltip.show()
      @tooltip.container.focus()
      @tooltip.hide()
      range = @quill.getSelection()
      expect(range.start).toEqual(0)
      expect(range.end).toEqual(0)
    )
  )

  describe('_position()', ->
    beforeEach( ->
      @quill.root = makeBounder(50, 50, 600, 400)
      @tooltip.container = makeBounder(60, 60, 200, 100)
    )

    it('no reference', ->
      [left, top] = @tooltip._position()
      expect(left).toEqual(250)
      expect(top).toEqual(200)
    )

    it('place below', ->
      reference = makeBounder(100, 100, 100, 50)
      [left, top] = @tooltip._position(reference)
      expect(left).toEqual(50)
      expect(top).toEqual(170)
    )

    it('place above', ->
      reference = makeBounder(100, 500, 100, 50)
      [left, top] = @tooltip._position(reference)
      expect(left).toEqual(50)
      expect(top).toEqual(380)
    )
  )
)


@container.style.position = 'absolute'    # Set immediately so style.left has effect to avoid initial flicker


describe('Format', ->
  beforeEach( ->
    @container = $('#editor-container').html('').get(0)
  )

  tests =
    tag:
      format: new Quill.Format(document, Quill.Format.FORMATS.bold)
      existing: 'Text'
      missing: 'Text'
      value: true
    style:
      format: new Quill.Format(document, Quill.Format.FORMATS.color)
      existing: 'Text'
      missing: 'Text'
      value: 'blue'
    image:
      format: new Quill.Format(document, Quill.Format.FORMATS.image)
      existing: ''
      missing: 'Text'
      removed: Quill.DOM.EMBED_TEXT
      value: 'http://quilljs.com/images/cloud.png'
    link:
      format: new Quill.Format(document, Quill.Format.FORMATS.link)
      existing: 'Text'
      missing: 'Text'
      value: 'http://quilljs.com'
    class:
      format: new Quill.Format(document, { class: 'author-' })
      existing: 'Text'
      missing: 'Text'
      value: 'jason'
    line:
      format: new Quill.Format(document, Quill.Format.FORMATS.align)
      existing: '
Text
'
      missing: '
Text
'
      value: 'right'
    complex:
      format: new Quill.Format(document, Quill.Format.FORMATS.bold)
      existing: 'Text<s>Strike</s>Italic'
      missing: 'Text<s>Strike</s>Italic'
      value: true

  describe('match()', ->
    _.each(tests, (test, name) ->
      it("#{name} existing", ->
        @container.innerHTML = test.existing
        expect(test.format.match(@container.firstChild)).toBe(true)
      )

      it("#{name} missing", ->
        @container.innerHTML = test.missing
        expect(test.format.match(@container.firstChild)).toBe(false)
      )
    )

    it('default', ->
      @container.innerHTML = 'Text'
      format = new Quill.Format(document, Quill.Format.FORMATS.color)
      expect(format.match(@container.firstChild)).toBe(false)
    )
  )

  describe('value()', ->
    _.each(tests, (test, name) ->
      it("#{name} existing", ->
        @container.innerHTML = test.existing
        expect(test.format.value(@container.firstChild)).toEqual(test.value)
      )

      it("#{name} missing", ->
        @container.innerHTML = test.missing
        expect(test.format.value(@container.firstChild)).toBe(undefined)
      )
    )

    it('default', ->
      @container.innerHTML = 'Text'
      format = new Quill.Format(document, Quill.Format.FORMATS.color)
      expect(format.value(@container.firstChild)).toBe(undefined)
    )
  )

  describe('add()', ->
    _.each(tests, (test, name) ->
      it("#{name} add value", ->
        @container.innerHTML = test.missing
        test.format.add(@container.firstChild, test.value)
        expect(@container).toEqualHTML(test.added or test.existing)
      )

      it("#{name} add value to exisitng", ->
        @container.innerHTML = test.existing
        test.format.add(@container.firstChild, test.value)
        expect(@container).toEqualHTML(test.existing)
      )

      it("#{name} add falsy value to existing", ->
        @container.innerHTML = test.existing
        test.format.add(@container.firstChild, false)
        expect(@container).toEqualHTML(test.removed or test.missing)
      )

      it("#{name} add falsy value to missing", ->
        @container.innerHTML = test.missing
        test.format.add(@container.firstChild, false)
        expect(@container).toEqualHTML(test.missing)
      )
    )

    it('change value', ->
      @container.innerHTML = 'Text'
      format = new Quill.Format(document, Quill.Format.FORMATS.color)
      format.add(@container.firstChild, 'red')
      expect(@container).toEqualHTML('Text')
    )

    it('default value', ->
      @container.innerHTML = 'Text'
      format = new Quill.Format(document, Quill.Format.FORMATS.size)
      format.add(@container.firstChild, Quill.Format.FORMATS.size.default)
      expect(@container).toEqualHTML('Text')
    )

    it('text node tag', ->
      @container.innerHTML = 'Text'
      format = new Quill.Format(document, Quill.Format.FORMATS.bold)
      format.add(@container.firstChild, true)
      expect(@container).toEqualHTML('Text')
    )

    it('text node style', ->
      @container.innerHTML = 'Text'
      format = new Quill.Format(document, Quill.Format.FORMATS.size)
      format.add(@container.firstChild, '18px')
      expect(@container).toEqualHTML('Text')
    )

    it('class over existing', ->
      @container.innerHTML = 'Text'
      format = new Quill.Format(document, { class: 'author-' })
      format.add(@container.firstChild, 'jason')
      expect(@container).toEqualHTML('Text')
    )
  )

  describe('remove()', ->
    _.each(tests, (test, name) ->
      it("#{name} existing", ->
        @container.innerHTML = test.existing
        test.format.remove(@container.firstChild)
        expect(@container).toEqualHTML(test.removed or test.missing)
      )

      it("#{name} missing", ->
        @container.innerHTML = test.missing
        test.format.remove(@container.firstChild)
        expect(@container).toEqualHTML(test.missing)
      )
    )
  )
)


describe('Picker', ->
  beforeEach( ->
    @container = $('#editor-container').html(Quill.Normalizer.stripWhitespace('
      <select title="Font" class="sc-font">
        <option value="sans-serif" selected>Sans Serif</option>
        <option value="serif">Serif</option>
        <option value="monospace">Monospace</option>
      </select>
    ')).get(0)
    @select = @container.querySelector('select')
    @picker = new Quill.Lib.Picker(@select)
  )

  it('constructor', ->
    expect(@container.querySelector('.sc-picker').outerHTML).toEqualHTML('

Sans Serif

Sans Serif

Serif

Monospace

    ')
  )

  it('expand/close', ->
    label = @container.querySelector('.sc-picker-label')
    picker = @container.querySelector('.sc-picker')
    Quill.DOM.triggerEvent(label, 'click')
    expect(Quill.DOM.hasClass(picker, 'sc-expanded')).toBe(true)
    Quill.DOM.triggerEvent(label, 'click')
    expect(Quill.DOM.hasClass(picker, 'sc-expanded')).toBe(false)
  )

  it('select picker item', ->
    Quill.DOM.triggerEvent(@container.querySelector('.sc-picker-options').lastChild, 'click')
    expect(@container.querySelector('.sc-picker')).toEqualHTML('

Monospace

Sans Serif

Serif

Monospace

    ')
  )

  it('select option', ->
    Quill.DOM.selectOption(@select, 'serif')
    expect(@container.querySelector('.sc-picker')).toEqualHTML('

Serif

Sans Serif

Serif

Monospace

    ')
  )

  it('select option mixed', ->
    Quill.DOM.selectOption(@select, '')
    expect(@container.querySelector('.sc-picker')).toEqualHTML('

Sans Serif

Serif

Monospace

    ')
  )
)


describe('Renderer', ->
  beforeEach( ->
    @container = $('#editor-container').html('').get(0)
  )

  it('objToCss()', ->
    css = Quill.Renderer.objToCss(
      '.editor-container a':
        'font-style': 'italic'
        'text-decoration': 'underline'
      '.editor-container b':
        'font-weight': 'bold'
    )
    expect(css).toEqual([
      '.editor-container a { font-style: italic; text-decoration: underline; }'
      '.editor-container b { font-weight: bold; }'
    ].join('\n'))
  )

  it('buildFrame()', ->
    [root, iframe] = Quill.Renderer.buildFrame(@container)
    expect(@container.querySelectorAll('iframe').length).toEqual(1)
    expect(root.ownerDocument.body.firstChild).toEqual(root)
    expect(root.innerHTML).toEqual('')
  )

  it('constructor', ->
    renderer = new Quill.Renderer(@container)
    expect(@container.querySelectorAll('iframe').length).toEqual(1)
    expect(renderer.root.id).not.toBe(null)
  )
)


DOM.addEventListener(@quill.root, 'focus', _.bind(this.hide, this))

@quill.on(@quill.constructor.events.TEXT_CHANGE, (delta, source) =>

keep keep keep keep replace keep keep keep

if source == 'user' and @container.style.left != Tooltip.HIDE_MARGIN


_     = require('lodash')
DOM   = require('./dom')
Leaf  = require('./leaf')
Range = require('./lib/range')

class Selection
  constructor: (@doc, @emitter) ->
    @document = @doc.root.ownerDocument
    @range = this.getRange()
    @nullDelay = false

  checkFocus: ->
    return @document.activeElement == @doc.root

  getRange: ->
    return null unless this.checkFocus()
    nativeRange = this._getNativeRange()
    return null unless nativeRange?
    start = this._positionToIndex(nativeRange.startContainer, nativeRange.startOffset)
    if nativeRange.endContainer != nativeRange.startContainer
      end = this._positionToIndex(nativeRange.endContainer, nativeRange.endOffset)
    else
      end = start - nativeRange.startOffset + nativeRange.endOffset
    return new Range(Math.min(start, end), Math.max(start, end))

  preserve: (fn) ->
    nativeRange = this._getNativeRange()
    if nativeRange? and this.checkFocus()
      [startNode, startOffset] = this._encodePosition(nativeRange.startContainer, nativeRange.startOffset)
      [endNode, endOffset] = this._encodePosition(nativeRange.endContainer, nativeRange.endOffset)
      fn()
      [startNode, startOffset] = this._decodePosition(startNode, startOffset)
      [endNode, endOffset] = this._decodePosition(endNode, endOffset)
      this._setNativeRange(startNode, startOffset, endNode, endOffset)
    else
      fn()

  setRange: (range, source) ->
    if range?
      [startNode, startOffset] = this._indexToPosition(range.start)
      if range.isCollapsed()
        [endNode, endOffset] = [startNode, startOffset]
      else
        [endNode, endOffset] = this._indexToPosition(range.end)
      this._setNativeRange(startNode, startOffset, endNode, endOffset)
    else
      this._setNativeRange(null)
    this.update(source)

  shiftAfter: (index, length, fn) ->
    range = this.getRange()
    fn()
    if range?
      range.shift(index, length)
      this.setRange(range, 'silent')

  update: (source) ->
    range = this.getRange()
    emit = source != 'silent' and !Range.compare(range, @range)
    # If range changes to null, require two update cycles to update and emit
    if range == null and source =='user' and !@nullDelay
      @nullDelay = true
    else
      @nullDelay = false
      @range = range
      # Set range before emitting to prevent infinite loop if listeners call quill.getSelection()
      @emitter.emit(@emitter.constructor.events.SELECTION_CHANGE, range, source) if emit

  _decodePosition: (node, offset) ->
    if DOM.isElement(node)
      childIndex = _.indexOf(DOM.getChildNodes(node.parentNode), node)
      offset += childIndex
      node = node.parentNode
    return [node, offset]

  _encodePosition: (node, offset) ->
    while true
      if DOM.isTextNode(node) or node.tagName == DOM.DEFAULT_BREAK_TAG or DOM.EMBED_TAGS[node.tagName]?
        return [node, offset]
      else if offset < node.childNodes.length
        node = node.childNodes[offset]
        offset = 0
      else if node.childNodes.length == 0
        return [node, 0]
      else
        node = node.lastChild
        offset = node.childNodes.length + 1

  _getNativeSelection: ->
    return if @document.getSelection? then @document.getSelection() else null

  _getNativeRange: ->
    selection = this._getNativeSelection()
    return if selection?.rangeCount > 0 then selection.getRangeAt(0) else null

  _indexToPosition: (index) ->
    return [@doc.root, 0] if @doc.lines.length == 0
    [leaf, offset] = @doc.findLeafAt(index, true)
    return this._decodePosition(leaf.node, offset)

  _positionToIndex: (node, offset) ->
    [leafNode, offset] = this._encodePosition(node, offset)
    line = @doc.findLine(leafNode)
    # TODO move to linked list
    return 0 unless line?   # Occurs on empty document
    leaf = line.findLeaf(leafNode)
    lineOffset = 0
    while line.prev?
      line = line.prev
      lineOffset += line.length
    return lineOffset unless leaf?
    leafOffset = 0
    while leaf.prev?
      leaf = leaf.prev
      leafOffset += leaf.length
    return lineOffset + leafOffset + offset

  _setNativeRange: (startNode, startOffset, endNode, endOffset) ->
    selection = this._getNativeSelection()
    return unless selection
    if startNode?
      @doc.root.focus()   # Some reason need to focus before removing ranges otherwise cannot set them
      nativeRange = this._getNativeRange()
      if !nativeRange? or startNode != nativeRange.startContainer or startOffset != nativeRange.startOffset or endNode != nativeRange.endContainer or endOffset != nativeRange.endOffset
        selection.removeAllRanges() if nativeRange?
        selection.removeAllRanges()
        nativeRange = @document.createRange()
        nativeRange.setStart(startNode, startOffset)
        nativeRange.setEnd(endNode, endOffset)
        selection.addRange(nativeRange)
        @doc.root.focus()   # IE11 needs refocus
    else
      selection.removeAllRanges()
      @doc.root.blur()

module.exports = Selection


_     = require('lodash')
_.str = require('underscore.string')

lastKeyEvent = null    # Workaround for fact we can't dispatch keyboard event via javascript

DOM =
  ELEMENT_NODE: 1
  NOBREAK_SPACE:  " "
  TEXT_NODE: 3
  ZERO_WIDTH_NOBREAK_SPACE:  "\uFEFF"

  DEFAULT_BLOCK_TAG: 'P'
  DEFAULT_BREAK_TAG: 'BR'
  DEFAULT_INLINE_TAG: 'SPAN'
  EMBED_TEXT: '!' # No reason we picked ! besides it being one character (so delta cannot split it up)

  FONT_SIZES:
    '10px': 1
    '13px': 2
    '16px': 3
    '18px': 4
    '24px': 5
    '32px': 6
    '48px': 7

  KEYS:
    BACKSPACE : 8
    TAB       : 9
    ENTER     : 13
    ESCAPE    : 27
    LEFT      : 37
    UP        : 38
    RIGHT     : 39
    DOWN      : 40
    DELETE    : 46

  BLOCK_TAGS: {
    'ADDRESS'
    'ARTICLE'
    'ASIDE'
    'AUDIO'
    'BLOCKQUOTE'
    'CANVAS'
    'DD'
    'DIV'
    'DL'
    'FIGCAPTION'
    'FIGURE'
    'FOOTER'
    'FORM'
    'H1', 'H2', 'H3', 'H4', 'H5', 'H6'
    'HEADER'
    'HGROUP'
    'LI'
    'OL'
    'OUTPUT'
    'P'
    'PRE'
    'SECTION'
    'TABLE'
    'TBODY'
    'TD'
    'TFOOT'
    'TH'
    'THEAD'
    'TR'
    'UL'
    'VIDEO'
  }

  EMBED_TAGS: {
    'IMG'
  }

  VOID_TAGS: {
    'AREA'
    'BASE'
    'BR'
    'COL'
    'COMMAND'
    'EMBED'
    'HR'
    'IMG'
    'INPUT'
    'KEYGEN'
    'LINK'
    'META'
    'PARAM'
    'SOURCE'
    'TRACK'
    'WBR'
  }

  addClass: (node, cssClass) ->
    return if DOM.hasClass(node, cssClass)
    if node.classList?
      node.classList.add(cssClass)
    else if node.className?
      node.className = _.str.trim(node.className + ' ' + cssClass)

  addEventListener: (node, eventName, listener) ->
    node.addEventListener(eventName, (event) ->
      arg = if lastKeyEvent and (eventName == 'keydown' or eventName == 'keyup') then lastKeyEvent else event
      propogate = listener(arg)
      unless propogate
        event.preventDefault()
        event.stopPropagation()
      return propogate
    )

  clearAttributes: (node, exception = []) ->
    exception = [exception] if _.isString(exception)
    _.each(DOM.getAttributes(node), (value, name) ->
      node.removeAttribute(name) unless _.indexOf(exception, name) > -1
    )

  getAttributes: (node) ->
    return {} unless node.attributes?
    attributes = {}
    for value,i in node.attributes
      attr = node.attributes[i]
      attributes[attr.name] = attr.value
    return attributes

  getChildNodes: (parent) ->
    return _.map(parent.childNodes)

  getChildren: (parent) ->
    return _.map(parent.children)

  getDescendants: (parent) ->
    return _.map(parent.getElementsByTagName('*'))

  getClasses: (node) ->
    return node.className.split(/\s+/)

  getDefaultOption: (select) ->
    return select.querySelector('option[selected]')

  getSelectValue: (select) ->
    return if select.selectedIndex > -1 then select.options[select.selectedIndex].value else ''

  getStyles: (node) ->
    styleString = node.getAttribute('style') or ''
    obj = _.reduce(styleString.split(';'), (styles, str) ->
      [name, value] = str.split(':')
      if name and value
        name = _.str.trim(name)
        value = _.str.trim(value)
        styles[name.toLowerCase()] = value
      return styles
    , {})
    return obj

  getText: (node) ->
    switch node.nodeType
      when DOM.ELEMENT_NODE
        return "" if node.tagName == DOM.DEFAULT_BREAK_TAG
        return DOM.EMBED_TEXT if DOM.EMBED_TAGS[node.tagName]?
        return node.textContent if node.textContent?
        return node.innerText.replace(/[\r\n]/g, '') if node.innerText?
        return ""
      when DOM.TEXT_NODE then return node.data or ""
      else return ""

  getWindow: (node) ->
    return node.ownerDocument.defaultView or node.ownerDocument.parentWindow

  hasClass: (node, cssClass) ->
    if node.classList?
      return node.classList.contains(cssClass)
    else if node.className?
      return _.indexOf(DOM.getClasses(node), cssClass) > -1
    return false

  isElement: (node) ->
    return node?.nodeType == DOM.ELEMENT_NODE

  isTextNode: (node) ->
    return node?.nodeType == DOM.TEXT_NODE

  moveChildren: (newParent, oldParent) ->
    _.each(DOM.getChildNodes(oldParent), (child) ->
      newParent.appendChild(child)
    )

  # IE normalize is broken
  normalize: (node) ->
    curNode = node.firstChild
    while curNode?
      nextNode = curNode.nextSibling
      if DOM.isTextNode(curNode)
        if DOM.getText(curNode).length == 0
          DOM.removeNode(curNode)
        else if DOM.isTextNode(nextNode)
          nextNode = nextNode.nextSibling
          newText = DOM.getText(curNode) + DOM.getText(curNode.nextSibling)
          DOM.setText(curNode, newText)
          DOM.removeNode(curNode.nextSibling)
      curNode = nextNode

  isIE: (maxVersion) ->
    version = document.documentMode
    return version and maxVersion >= version

  removeClass: (node, cssClass) ->
    return unless DOM.hasClass(node, cssClass)
    if node.classList?
      return node.classList.remove(cssClass)
    else if node.className?
      classArray = DOM.getClasses(node)
      classArray.splice(_.indexOf(classArray, cssClass), 1)
      node.className = classArray.join(' ')

  removeNode: (node) ->
    node.parentNode?.removeChild(node)

  replaceNode: (newNode, oldNode) ->
    oldNode.parentNode.replaceChild(newNode, oldNode)
    return newNode

  resetSelect: (select, trigger = true) ->
    option = DOM.getDefaultOption(select)
    if option?
      option.selected = true
    else
      select.selectedIndex = 0
    DOM.triggerEvent(select, 'change') if trigger

  selectOption: (select, option, trigger = true) ->
    value = if _.isElement(option) then option.value else option
    if value
      select.value = value
    else
      select.selectedIndex = -1  # PhantomJS
    DOM.triggerEvent(select, 'change') if trigger

  setStyles: (node, styles) ->
    styleString = _.map(styles, (style, name) ->
      return "#{name}: #{style}"
    ).join('; ') + ';'
    node.setAttribute('style', styleString)

  setText: (node, text) ->
    switch node.nodeType
      when DOM.ELEMENT_NODE
        if node.textContent?
          node.textContent = text
        else
          node.innerText = text
      when DOM.TEXT_NODE then node.data = text
      else return # Noop

  switchTag: (node, newTag) ->
    newTag = newTag.toUpperCase()
    return if node.tagName == newTag
    newNode = node.ownerDocument.createElement(newTag)
    attributes = DOM.getAttributes(node)
    this.moveChildren(newNode, node) unless DOM.VOID_TAGS[newTag]?
    node.parentNode.replaceChild(newNode, node)
    _.each(attributes, (value, name) ->
      newNode.setAttribute(name, value)
    )
    return newNode

  toggleClass: (node, className, state) ->
    state = !DOM.hasClass(node, className) unless state?
    if state
      DOM.addClass(node, className)
    else
      DOM.removeClass(node, className)

  triggerEvent: (elem, eventName, options = {}) ->
    if _.indexOf(['keypress', 'keydown', 'keyup'], eventName) < 0
      event = elem.ownerDocument.createEvent('Event')
      event.initEvent(eventName, options.bubbles, options.cancelable)
    else
      event = elem.ownerDocument.createEvent('KeyboardEvent')
      lastKeyEvent = _.clone(options)
      if _.isNumber(options.key)
        lastKeyEvent.which = options.key
      else if _.isString(options.key)
        lastKeyEvent.which = options.key.toUpperCase().charCodeAt(0)
      else
        lastKeyEvent.which = 0
      if DOM.isIE(10)
        modifiers = []
        modifiers.push('Alt') if options.altKey
        modifiers.push('Control') if options.ctrlKey
        modifiers.push('Meta') if options.metaKey
        modifiers.push('Shift') if options.shiftKey
        event.initKeyboardEvent(eventName, options.bubbles, options.cancelable, elem.ownerDocument.defaultView.window, 0, 0, modifiers.join(' '), null, null)
      else
        # FF uses initKeyEvent, Webkit uses initKeyboardEvent
        initFn = if _.isFunction(event.initKeyboardEvent) then 'initKeyboardEvent' else 'initKeyEvent'
        event[initFn](eventName, options.bubbles, options.cancelable, elem.ownerDocument.defaultView.window, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, 0)
    elem.dispatchEvent(event)
    lastKeyEvent = null

  unwrap: (node) ->
    ret = node.firstChild
    next = node.nextSibling
    _.each(DOM.getChildNodes(node), (child) ->
      node.parentNode.insertBefore(child, next)
    )
    DOM.removeNode(node)
    return ret

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

module.exports = DOM


_          = require('lodash')
DOM        = require('./dom')
Document   = require('./document')
Line       = require('./line')
Renderer   = require('./renderer')
Selection  = require('./selection')
Tandem     = require('tandem-core')

class Editor
  constructor: (@iframeContainer, @quill, @options = {}) ->
    @renderer = new Renderer(@iframeContainer, @quill, @options)
    @root = @renderer.root
    @doc = new Document(@root, @options)
    @delta = @doc.toDelta()
    @selection = new Selection(@doc, @quill)
    @timer = setInterval(_.bind(this.checkUpdate, this), @options.pollInterval)
    @quill.on(@quill.constructor.events.SELECTION_CHANGE, (range) =>
      @savedRange = range
    )
    this.enable() unless @options.readOnly

  disable: ->
    this.enable(false)

  enable: (enabled = true) ->
    @root.setAttribute('contenteditable', enabled)

  applyDelta: (delta, source) ->
    localDelta = this._update()
    if localDelta
      tempDelta = localDelta
      localDelta = localDelta.transform(delta, true)
      delta = delta.transform(tempDelta, false)
      @delta = @doc.toDelta()   # Only for our error check below, otherwise dont need to update yet
    unless delta.isIdentity()   # Follows may have turned delta into the identity
      throw new Error("Trying to apply delta to incorrect doc length") if delta.startLength != @delta.endLength
      delta = this._trackDelta( =>
        delta.apply(this._insertAt, this._deleteAt, this._formatAt, this)
        @selection.shiftAfter(0, 0, _.bind(@doc.optimizeLines, @doc))
      )
      @delta = @doc.toDelta()
      @innerHTML = @root.innerHTML
      @quill.emit(@quill.constructor.events.TEXT_CHANGE, delta, source) if delta and source != 'silent'
    if localDelta and !localDelta.isIdentity() and source != 'silent'
      @quill.emit(@quill.constructor.events.TEXT_CHANGE, localDelta, 'user')

  checkUpdate: (source = 'user') ->
    return clearInterval(@timer) if !@renderer.iframe.parentNode? or !@root.parentNode?
    delta = this._update()
    if delta
      oldDelta = @delta
      @delta = oldDelta.compose(delta)
      @quill.emit(@quill.constructor.events.TEXT_CHANGE, delta, source)
    source = 'silent' if delta
    @selection.update(source)

  getDelta: ->
    return @delta

  _deleteAt: (index, length) ->
    return if length <= 0
    @selection.shiftAfter(index, -1 * length, =>
      [firstLine, offset] = @doc.findLineAt(index)
      curLine = firstLine
      mergeFirstLine = firstLine.length - offset <= length and offset > 0
      while curLine? and length > 0
        nextLine = curLine.next
        deleteLength = Math.min(curLine.length - offset, length)
        if offset == 0 and length >= curLine.length
          @doc.removeLine(curLine)
        else
          curLine.deleteText(offset, deleteLength)
        length -= deleteLength
        curLine = nextLine
        offset = 0
      @doc.mergeLines(firstLine, firstLine.next) if mergeFirstLine and firstLine.next
    )

  _formatAt: (index, length, name, value) ->
    @selection.shiftAfter(index, 0, =>
      [line, offset] = @doc.findLineAt(index)
      while line? and length > 0
        formatLength = Math.min(length, line.length - offset - 1)
        line.formatText(offset, formatLength, name, value)
        length -= formatLength
        line.format(name, value) if length > 0
        length -= 1
        offset = 0
        line = line.next
    )

  _insertAt: (index, text, formatting = {}) ->
    @selection.shiftAfter(index, text.length, =>
      text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
      lineTexts = text.split('\n')
      [line, offset] = @doc.findLineAt(index)
      _.each(lineTexts, (lineText, i) =>
        if !line? or line.length <= offset    # End of document
          if i < lineTexts.length - 1 or lineText.length > 0
            line = @doc.appendLine(@root.ownerDocument.createElement(DOM.DEFAULT_BLOCK_TAG))
            offset = 0
            line.insertText(offset, lineText, formatting)
            line.format(formatting)
            nextLine = null
        else
          line.insertText(offset, lineText, formatting)
          if i < lineTexts.length - 1       # Are there more lines to insert?
            nextLine = @doc.splitLine(line, offset + lineText.length)
            line.format(formatting)
            offset = 0
        line = nextLine
      )
    )

  _trackDelta: (fn) ->
    oldIndex = @savedRange?.start
    fn()
    newDelta = @doc.toDelta()
    try
      newIndex = @selection.getRange()?.start
      if oldIndex? and newIndex? and oldIndex <= @delta.endLength and newIndex <= newDelta.endLength
        [oldLeftDelta, oldRightDelta] = @delta.split(oldIndex)
        [newLeftDelta, newRightDelta] = newDelta.split(newIndex)
        decomposeLeft = newLeftDelta.decompose(oldLeftDelta)
        decomposeRight = newRightDelta.decompose(oldRightDelta)
        decomposeA = decomposeLeft.merge(decomposeRight)
    catch ignored
    decomposeB = newDelta.decompose(@delta)
    if decomposeA and decomposeB
      decompose = if decomposeA.ops.length < decomposeB.ops.length then decomposeA else decomposeB
    else
      decompose = decomposeA or decomposeB
    return decompose

  _update: ->
    return false if @innerHTML == @root.innerHTML
    delta = this._trackDelta( =>
      @selection.preserve(_.bind(@doc.rebuild, @doc))
      @selection.shiftAfter(0, 0, _.bind(@doc.optimizeLines, @doc))
    )
    @innerHTML = @root.innerHTML
    return if delta.isIdentity() then false else delta

module.exports = Editor


keep keep keep replace keep keep keep keep


describe('UndoManager', ->
  beforeEach( ->
    @container = $('#editor-container').get(0)
    @container.innerHTML = '

The lazy fox

'
    @quill = new Quill(@container.firstChild, {
      modules: {
        'undo-manager': { delay: 10 }
      }
    })
    @undoManager = @quill.getModule('undo-manager')
    @original = @quill.getContents()
  )

  tests =
    'insert':
      delta: Tandem.Delta.makeInsertDelta(13, 9, 'hairy ')
      index: 15
    'delete':
      delta: Tandem.Delta.makeDeleteDelta(13, 4, 5)
      index: 4
    'format':
      delta: Tandem.Delta.makeRetainDelta(13, 4, 5, { bold: true })
      index: 9
    'multiple':
      delta: Tandem.Delta.makeDelta({
        startLength: 13
        endLength: 14
        ops: [
          { start: 0, end: 4, attributes: { bold: true } }
          { value: 'hairy' }
          { start: 8, end: 13 }
        ]
      })
      index: 9

  describe('_getLastChangeIndex', ->
    _.each(tests, (test, name) ->
      it(name, ->
        index = @undoManager._getLastChangeIndex(test.delta)
        expect(index).toEqual(test.index)
      )
    )
  )

  describe('undo/redo', ->
    _.each(tests, (test, name) ->
      it(name, ->
        @quill.updateContents(test.delta)
        changed = @quill.getContents()
        expect(changed).not.toEqualDelta(@original)
        @undoManager.undo()
        expect(@quill.getContents()).toEqualDelta(@original)
        @undoManager.redo()
        expect(@quill.getContents()).toEqualDelta(changed)
      )
    )

    it('user change', ->
      @quill.root.firstChild.innerHTML = 'The lazy foxes'
      @quill.editor.checkUpdate()
      changed = @quill.getContents()
      expect(changed).not.toEqualDelta(@original)
      @undoManager.undo()
      expect(@quill.getContents()).toEqualDelta(@original)
      @undoManager.redo()
      expect(@quill.getContents()).toEqualDelta(changed)
    )

    it('merge changes', ->
      expect(@undoManager.stack.undo.length).toEqual(0)
      @quill.updateContents(Tandem.Delta.makeInsertDelta(13, 12, 'e'))
      expect(@undoManager.stack.undo.length).toEqual(1)
      @quill.updateContents(Tandem.Delta.makeInsertDelta(14, 13, 's'))
      expect(@undoManager.stack.undo.length).toEqual(1)
      @undoManager.undo()
      expect(@quill.getContents()).toEqual(@original)
      expect(@undoManager.stack.undo.length).toEqual(0)
    )

    it('dont merge changes', (done) ->
      expect(@undoManager.stack.undo.length).toEqual(0)
      @quill.updateContents(Tandem.Delta.makeInsertDelta(13, 12, 'e'))
      expect(@undoManager.stack.undo.length).toEqual(1)
      setTimeout( =>
        @quill.updateContents(Tandem.Delta.makeInsertDelta(14, 13, 's'))
        expect(@undoManager.stack.undo.length).toEqual(2)
        done()
      , @undoManager.options.delay * 1.25)
    )

    it('hotkeys', ->
      @quill.updateContents(Tandem.Delta.makeInsertDelta(13, 0, 'A '))
      changed = @quill.getContents()
      expect(changed).not.toEqualDelta(@original)
      Quill.DOM.triggerEvent(@quill.root, 'keydown', Quill.Module.UndoManager.hotkeys.UNDO)
      expect(@quill.getContents()).toEqualDelta(@original)
      Quill.DOM.triggerEvent(@quill.root, 'keydown', Quill.Module.UndoManager.hotkeys.REDO)
      expect(@quill.getContents()).toEqualDelta(changed)
    )
  )
)


compareNodes = (node1, node2, ignoredAttributes = []) ->
  return false unless node1.nodeType == node2.nodeType
  if Quill.DOM.isElement(node1)
    return false unless Quill.DOM.isElement(node2)
    return false unless node1.tagName == node2.tagName
    [attr1, attr2] = _.map([node1, node2], (node) ->
      attr = Quill.DOM.getAttributes(node)
      _.each(ignoredAttributes, (name) ->
        delete attr[name]
      )
      attr.style = attr.style.trim() if attr.style?
      return attr
    )
    return false unless _.isEqual(attr1, attr2)
    return false unless node1.childNodes.length == node2.childNodes.length
    equal = true
    _.each(Quill.DOM.getChildNodes(node1), (child1, i) ->
      if !compareNodes(child1, node2.childNodes[i], ignoredAttributes)
        equal = false
        return false
    )
    return equal
  else
    return Quill.DOM.getText(node1) == Quill.DOM.getText(node2)

beforeEach( ->
  matchers =
    toEqualDelta: ->
      return {
        compare: (actual, expected) ->
          pass = actual.isEqual(expected)
          if pass
            message = 'Deltas equal'
          else
            message = "Deltas unequal: \n#{jasmine.pp(actual)}\n\n#{jasmine.pp(expected)}\n"
          return { message: message, pass: pass }
      }

    toEqualHTML: ->
      return {
        compare: (actual, expected, ignoreClassId) ->
          [div1, div2] = _.map([actual, expected], (html) ->
            html = html.join('') if _.isArray(html)
            html = html.innerHTML if _.isElement(html)
            div = document.createElement('div')
            div.innerHTML = Quill.Normalizer.stripWhitespace(html)
            return div
          )
          ignoredAttributes = if ignoreClassId then ['class', 'id'] else []
          ignoredAttributes = ignoredAttributes.concat(['width', 'height'])   # IE adds automatically
          pass = compareNodes(div1, div2, ignoredAttributes)
          if pass
            message = 'HTMLs equal'
          else
            message = "HTMLs unequal: \n#{jasmine.pp(div1.innerHTML)}\n\n#{jasmine.pp(div2.innerHTML)}\n"
          return { message: message, pass: pass }
      }

  jasmine.addMatchers(matchers)
)


describe('Leaf', ->
  beforeEach( ->
    @container = $('#editor-container').html('').get(0)
  )

  describe('constructor', ->
    tests =
      'image':
        html: ''
        text: Quill.DOM.EMBED_TEXT
      'break':
        html: '
'
        text: ''
      'empty element':
        html: ''
        text: ''
      'text':
        html: 'Text'
        text: 'Text'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.html
        leaf = new Quill.Leaf(@container.firstChild, {})
        expect(leaf.text).toEqual(test.text)
      )
    )
  )

  describe('isLeafNode()', ->
    tests =
      'text node':
        html: 'Test'
        expected: true
      'empty element':
        html: ''
        expected: true
      'break':
        html: '
'
        expected: true
      'image':
        html: ''
        expected: true
      'element with element child':
        html: ''
        expected: false
      'element with text child':
        html: 'Test'
        expected: false

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.html
        expect(Quill.Leaf.isLeafNode(@container.firstChild)).toBe(test.expected)
      )
    )
  )

  describe('deleteText()', ->
    beforeEach( ->
      @container.innerHTML = 'Test'
      @leaf = new Quill.Leaf(@container.firstChild, {})
    )

    tests =
      'remove middle':
        expected: 'Tt'
        offset: 1, length: 2
      'remove till end':
        expected: 'Te'
        offset: 2, length: 2
      'remove all':
        expected: ''
        offset: 0, length: 4

    _.each(tests, (test, name) ->
      it(name, ->
        @leaf.deleteText(test.offset, test.length)
        expect(@leaf.text).toEqualHTML(test.expected)
        expect(Quill.DOM.getText(@leaf.node)).toEqualHTML(test.expected)
      )
    )
  )

  describe('insertText()', ->
    tests =
      'element with text node':
        initial:  'Test'
        expected: 'Te|st'
        text: 'Test'
      'element without text node':
        initial:  ''
        expected: '|'
      'break':
        initial:  '
'
        expected: '|'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        leaf = new Quill.Leaf(@container.firstChild, {})
        text = test.text or ''
        length = text.length
        expect(leaf.text).toEqual(text)
        expect(leaf.length).toEqual(length)
        leaf.insertText(length/2, '|')
        expect(@container).toEqualHTML(test.expected)
        expect(leaf.text).toEqual(Quill.DOM.getText(leaf.node))
        expect(leaf.length).toEqual(length + 1)
      )
    )
  )
)


keep keep keep keep replace keep keep keep keep

export default {
  data: () => ({
    style: null
  }),

  watch: {
    type () {
      this.applyType()
    },
    '$vuetify.theme': {
      deep: true,
      handler () {
        this.applyType()
      }
    }
  },

  mounted () {
    this.genStyle()
    this.genThemeClasses()
    this.applyType()
  },

  methods: {
    applyType () {
      this.genThemeClasses()
      this.$vuetify.theme.type = this.type
    },
    genThemeClasses () {
      this.updateTheme(this.genTheme())
    },
    genTheme () {
      return this.genColors(this.$vuetify.theme)
    },
    genColors (theme) {
      return Object.keys(theme).map(key => {
        const value = theme[key]

        return (
          this.genBackgroundColor(key, value) +
          this.genTextColor(key, value)
        )
      }).join('')
    },
    genBackgroundColor (key, value) {
      return `.${key}{background-color:${value} !important;border-color:${value} !important;}`
    },
    genTextColor (key, value) {
      return `.${key}--text{color:${value} !important;}`
    },
    genStyle () {
      const style = document.createElement('style')
      style.type = 'text/css'
      document.head.appendChild(style)
      this.style = style
    },
    updateTheme (classes) {
      this.style.innerHTML = classes
    }
  }
}


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

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

import VCard from '../VCard'

import Picker from '../../mixins/picker'
import TimeTitle from './mixins/time-title'
import TimeBody from './mixins/time-body'

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

  components: {
    VCard
  },

  mixins: [Picker, TimeBody, TimeTitle],

  data () {
    return {
      isDragging: false,
      rotate: 0,
      originalTime: this.value,
      period: 'am',
      selectingHour: true,
      ranges: {
        hours: createRange(24),
        minutes: createRange(60)
      }
    }
  },

  props: {
    format: {
      type: String,
      default: 'ampm',
      validator (val) {
        return ['ampm', '24hr'].includes(val)
      }
    },
    allowedHours: {
      type: [Array, Object, Function],
      default: () => (null)
    },
    allowedMinutes: {
      type: [Array, Object, Function],
      default: () => (null)
    }
  },

  computed: {
    is24hr () {
      return this.format !== 'ampm'
    },
    is24hrAfter12 () {
      return this.selectingHour && this.is24hr && this.hour >= 12
    },
    divider () {
      return this.selectingHour ? 12 : 60
    },
    degrees () {
      return this.degreesPerUnit * Math.PI / 180
    },
    degreesPerUnit () {
      return 360 / this.divider
    },
    inputTime: {
      get () {
        if (this.value && !(this.value instanceof Date)) {
          if (!this.is24hr) {
            this.period = this.value.match(/pm/i)
              ? 'pm'
              : 'am'
          }

          return this.value
        }
        let value = new Date()

        if (this.value instanceof Date) {
          value = this.value
        }

        let hour = value.getHours()
        let minute = value.getMinutes()
        let period = ''

        if (!this.is24hr) {
          period = hour >= 12 ? 'pm' : 'am'
          hour = hour > 12 ? hour - 12 : hour
          hour = hour === 0 ? 12 : hour
        }

        period && (this.period = period)

        hour = this.firstAllowed('hour', hour)
        minute = this.firstAllowed('minute', minute)

        minute = minute < 10 ? `0${minute}` : minute > 59 ? '00' : minute

        return `${hour}:${minute}${period}`
      },
      set (val) {
        return this.$emit('input', val)
      }
    },
    timeArray () {
      return this.inputTime.replace(/(am|pm)/, '').split(':')
    },
    hour: {
      get () {
        return parseInt(this.timeArray[0])
      },
      set (val) {
        if (!this.is24hr) {
          val = val > 12 ? val - 12 : val < 1 ? 12 : val
        } else {
          val = val < 10 ? `0${val}` : val > 23 ? '00' : val
        }

        this.inputTime = `${val}:${this.minute}${!this.is24hr ? this.period : ''}`
      }
    },
    minute: {
      get () {
        const minute = parseInt(this.timeArray[1])

        return minute < 10 ? `0${minute}` : minute > 59 ? '00' : minute
      },
      set (val) {
        val = val < 10 ? `0${parseInt(val)}` : val > 59 ? '00' : val
        let hour = this.hour

        if (this.is24hr && hour < 10) {
          hour = `0${hour}`
        }

        this.inputTime = `${hour}:${val}${!this.is24hr ? this.period : ''}`
      }
    },
    clockHand () {
      if (this.selectingHour) return this.degreesPerUnit * this.hour
      return this.degreesPerUnit * this.minute
    },
    radius () {
      return this.clockSize / 2
    },
    clockSize: {
      get () {
        return this.size
      },
      set (val) {
        this.size = val
      }
    },
    size () {
      return this.landscape ? 250 : 280
    }
  },

  watch: {
    period (val) {
      const hour = !!this.allowedHours && this.selectingHour ? this.firstAllowed('hour', this.hour - 1) : this.hour
      this.inputTime = `${hour}:${this.minute}${val}`
    },
    value (val) {
      if (this.isSaving) {
        this.originalTime = this.inputTime
        this.isSaving = false
      }
    }
  },

  methods: {
    save () {
      if (this.originalTime) {
        this.originalTime = this.value
      } else {
        this.inputTime = this.inputTime
        this.originalTime = this.inputTime
      }

      if (this.$parent && this.$parent.isActive) this.$parent.isActive = false

      // Fix for #1818
      // Wait for data to persist
      // then set selectingHour
      this.$nextTick(() => (this.selectingHour = true))
    },
    cancel () {
      this.inputTime = this.originalTime
      if (this.$parent && this.$parent.isActive) this.$parent.isActive = false

      // Fix for #1818
      // Wait for data to persist
      // then set selectingHour
      this.$nextTick(() => (this.selectingHour = true))
    },
    isAllowed (type, value) {
      const allowed = this[`allowed${type.charAt(0).toUpperCase() + type.slice(1)}s`]
      const val = type === 'hour' &&
        !this.is24hr &&
        this.period === 'pm'
        ? value + 12
        : value

      if (!allowed) return true

      if (Array.isArray(allowed)) {
        return !!allowed.some(v => v === value)
      } else if (allowed instanceof Function) {
        return allowed(val)
      } else if (allowed === Object(allowed)) {
        const range = type === 'minute' ? this.ranges.minutes : this.ranges.hours
        const mod = type === 'minute' ? 60 : 24

        if (allowed.min === String(allowed.min)) {
          allowed.min = this.convert12to24hr(allowed.min)
        }

        if (allowed.max === String(allowed.max)) {
          allowed.max = this.convert12to24hr(allowed.max)
        }

        const steps = allowed.max - allowed.min
        value = type === 'hour' && !this.is24hr && this.period === 'pm' ? value + 12 : value

        for (let i = 0; i <= steps; i++) {
          const index = (allowed.min + i) % mod
          if (range[index] === value) return true
        }

        return false
      }

      return true
    },
    convert12to24hr (input) {
      input = input.toLowerCase()
      const pm = input.indexOf('pm') !== -1
      const hour = parseInt(input.slice(0, input.indexOf(pm ? 'pm' : 'am')))

      return pm ? hour + 12 : hour
    },
    generateRange (type, start) {
      let range = type === 'hour' ? this.ranges.hours : this.ranges.minutes
      let offset = 1

      if (type === 'hour' && !this.is24hr) {
        range = range.slice(1, 13)
        offset = 0
      }

      return range.slice(start + offset, range.length)
        .concat(range.slice(0, start + offset))
    },
    firstAllowed (type, value) {
      const allowed = this[`allowed${type.charAt(0).toUpperCase() + type.slice(1)}s`]

      if (!allowed) return value

      const range = this.generateRange(type, value)

      const first = range.find(v => this.isAllowed(type, v))

      return first || value
    }
  },

  render (h) {
    const children = [this.genBody()]

    !this.noTitle && children.unshift(this.genTitle())
    this.$scopedSlots.default && children.push(this.genSlot())

    return h('v-card', {
      'class': {
        'picker picker--time': true,
        'picker--landscape': this.landscape,
        'picker--time--hours': this.selectingHour,
        ...this.themeClasses
      }
    }, children)
  }
}


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

exports[`VRadioGroup.vue should render role on radio group 1`] = `

`;


import VIcon from '~components/VIcon'
import { test, functionalContext } from '~util/testing'
import { mount } from 'avoriaz'

test('VIcon.js', () => {
  it('should render component', () => {
    const context = functionalContext({}, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.text()).toBe('add')
    expect(wrapper.element.className).toBe('material-icons icon')
  })

  it('should render a colored component', () => {
    const context = functionalContext({ props: { color: 'green lighten-1' } }, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.element.classList).toContain('green--text')
    expect(wrapper.element.classList).toContain('text--lighten-1')
  })

  it('should render a disabled component', () => {
    const context = functionalContext({ props: { disabled: true } }, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.element.classList).toContain('icon--disabled')
  })

  it('should render a large size component', () => {
    const context = functionalContext({ props: { large: true } }, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.element.classList).toContain('icon--large')
  })

  it('should render a medium size component', () => {
    const context = functionalContext({ props: { medium: true } }, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.element.classList).toContain('icon--medium')
  })

  it('should render a xLarge size component', () => {
    const context = functionalContext({ props: { xLarge: true } }, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.element.classList).toContain('icon--x-large')
  })

  it('should render a left aligned component', () => {
    const context = functionalContext({ props: { left: true } }, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.element.classList).toContain('icon--left')
  })

  it('should render a right aligned component', () => {
    const context = functionalContext({ props: { right: true } }, 'add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.element.classList).toContain('icon--right')
  })

  it('should allow third-party icons when using <icon>- prefix', () => {
    const context = functionalContext({ props: {} }, 'fa-add')
    const wrapper = mount(VIcon, context)

    expect(wrapper.text()).toBe('')
    expect(wrapper.element.className).toBe('fa icon fa-add')
  })

  it('should allow the use of v-text', () => {
    const wrapper = mount(VIcon, functionalContext({
      domProps: { textContent: 'fa-home' }
    }))

    expect(wrapper.text()).toBe('')
    expect(wrapper.element.className).toBe('fa icon fa-home')
  })

  it('should allow the use of v-html', () => {
    const wrapper = mount(VIcon, functionalContext({
      domProps: { innerHTML: 'fa-home' }
    }))

    expect(wrapper.text()).toBe('')
    expect(wrapper.element.className).toBe('fa icon fa-home')
  })
})


keep keep keep keep replace replace keep keep keep

dark () {
this.$vuetify.dark = this.dark

import load from '../../util/load'
import application from './mixins/application'
import theme from './mixins/theme'

const Vuetify = {
  install (Vue, opts = {}) {
    if (this.installed) return

    this.installed = true

    const $vuetify = { load }
    Vue.util.defineReactive($vuetify, 'breakpoint', {})
    Vue.util.defineReactive($vuetify, 'application', application)
    Vue.util.defineReactive($vuetify, 'theme', theme(opts.theme))
    Vue.util.defineReactive($vuetify, 'touchSupport', false)

    Vue.prototype.$vuetify = $vuetify

    if (opts.transitions) {
      Object.keys(opts.transitions).forEach(key => {
        const t = opts.transitions[key]
        if (t.name !== undefined && t.name.startsWith('v-')) {
          Vue.component(t.name, t)
        }
      })
    }

    if (opts.directives) {
      Object.keys(opts.directives).forEach(key => {
        const d = opts.directives[key]
        Vue.directive(d.name, d)
      })
    }

    if (opts.components) {
      Object.keys(opts.components).forEach(key => {
        const c = opts.components[key]
        Vue.use(c)
      })
    }
  }
}

export default Vuetify


import { test } from '~util/testing'
import { VRadioGroup /*, VRadio */ } from '~components/VRadioGroup'

test('VRadioGroup.vue', ({ mount }) => {
  it('should render role on radio group', () => {
    const wrapper = mount(VRadioGroup)

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

    const radioGroup = wrapper.find('.radio-group')[0]
    expect(radioGroup.getAttribute('role')).toBe('radiogroup')
  })

  // TODO: Test ability to toggle multiple data types
})


export default {
  methods: {
    genYearIcon () {
      return this.yearIcon
        ? this.$createElement('v-icon', {
          props: {
            dark: true
          }
        }, this.yearIcon)
        : null
    },

    getYearBtn () {
      return this.$createElement('div', {
        'class': {
          'picker--date__title-year': true,
          'active': this.activePicker === 'YEAR'
        },
        on: {
          click: e => {
            e.stopPropagation()
            this.activePicker = 'YEAR'
          }
        }
      }, [
        this.formatters.year(`${this.year}`),
        this.genYearIcon()
      ])
    },

    genTitleText (title) {
      return this.$createElement('transition', {
        props: {
          name: 'slide-y-reverse-transition',
          mode: 'out-in'
        }
      }, [
        this.$createElement('div', {
          domProps: { innerHTML: title },
          key: title
        })
      ])
    },

    genTitleDate (title) {
      return this.$createElement('div', {
        staticClass: 'picker--date__title-date',
        'class': {
          'active': this.activePicker === this.type.toUpperCase()
        },
        on: {
          click: e => {
            e.stopPropagation()
            this.activePicker = this.type.toUpperCase()
          }
        }
      }, [this.genTitleText(title)])
    },

    genTitle (title) {
      return this.genPickerTitle([
        this.getYearBtn(),
        this.genTitleDate(title)
      ])
    }
  }
}


require('../../stylus/components/_carousel.styl')

import VBtn from '../VBtn'
import VIcon from '../VIcon'

import Bootable from '../../mixins/bootable'
import Themeable from '../../mixins/themeable'

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

export default {
  name: 'v-carousel',

  mixins: [Bootable, Themeable],

  directives: { Touch },

  data () {
    return {
      inputValue: null,
      items: [],
      slideTimeout: null,
      reverse: false
    }
  },

  props: {
    cycle: {
      type: Boolean,
      default: true
    },
    hideControls: Boolean,
    icon: {
      type: String,
      default: 'fiber_manual_record'
    },
    interval: {
      type: [Number, String],
      default: 6000,
      validator: value => value > 0
    },
    leftControlIcon: {
      type: [Boolean, String],
      default: 'chevron_left'
    },
    rightControlIcon: {
      type: [Boolean, String],
      default: 'chevron_right'
    },
    value: Number
  },

  watch: {
    inputValue () {
      // Evaluate items when inputValue changes to account for
      // dynamic changing of children
      this.items = this.$children.filter(i => {
        return i.$el.classList && i.$el.classList.contains('carousel__item')
      })

      this.items.forEach(i => i.open(
        this.items[this.inputValue]._uid,
        this.reverse
      ))

      this.$emit('input', this.inputValue)
      this.restartTimeout()
    },
    value (val) {
      this.inputValue = val
    },
    interval () {
      this.restartTimeout()
    },
    cycle (val) {
      if (val) {
        this.restartTimeout()
      } else {
        clearTimeout(this.slideTimeout)
        this.slideTimeout = null
      }
    }
  },

  mounted () {
    this.init()
  },

  methods: {
    genControls () {
      return this.$createElement('div', {
        staticClass: 'carousel__controls'
      }, this.genItems())
    },
    genIcon (direction, icon, fn) {
      if (!icon) return null

      return this.$createElement('div', {
        staticClass: `carousel__${direction}`
      }, [
        this.$createElement(VBtn, {
          props: {
            icon: true,
            dark: this.dark || !this.light,
            light: this.light
          },
          on: { click: fn }
        }, [this.$createElement(VIcon, icon)])
      ])
    },
    genItems () {
      return this.items.map((item, index) => {
        return this.$createElement(VBtn, {
          class: {
            'carousel__controls__item': true,
            'carousel__controls__item--active': index === this.inputValue
          },
          props: {
            icon: true,
            dark: this.dark || !this.light,
            light: this.light
          },
          key: index,
          on: { click: this.select.bind(this, index) }
        }, [this.$createElement(VIcon, this.icon)])
      })
    },
    restartTimeout () {
      this.slideTimeout && clearTimeout(this.slideTimeout)
      this.slideTimeout = null

      const raf = requestAnimationFrame || setTimeout
      raf(this.startTimeout)
    },
    init () {
      this.inputValue = this.value || 0
    },
    next () {
      this.reverse = false
      this.inputValue = (this.inputValue + 1) % this.items.length
    },
    prev () {
      this.reverse = true
      this.inputValue = (this.inputValue + this.items.length - 1) % this.items.length
    },
    select (index) {
      this.reverse = index < this.inputValue
      this.inputValue = index
    },
    startTimeout () {
      if (!this.cycle) return

      this.slideTimeout = setTimeout(() => this.next(), this.interval > 0 ? this.interval : 6000)
    }
  },

  render (h) {
    return h('div', {
      staticClass: 'carousel',
      directives: [{
        name: 'touch',
        value: {
          left: this.next,
          right: this.prev
        }
      }]
    }, [
      this.genIcon('left', this.leftControlIcon, this.prev),
      this.genIcon('right', this.rightControlIcon, this.next),
      this.hideControls ? null : this.genControls(),
      this.$slots.default
    ])
  }
}


style: null
}),
watch: {
type () {
this.applyType()
},
'$vuetify.theme': {
deep: true,
handler () {

keep keep keep keep replace replace replace keep keep keep

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

export default {
  mixins: [Colorable, Themeable],

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

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

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

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


require('../../stylus/components/_footer.styl')

import Applicationable from '../../mixins/applicationable'
import Colorable from '../../mixins/colorable'
import Themeable from '../../mixins/themeable'

export default {
  name: 'v-footer',

  mixins: [Applicationable, Colorable, Themeable],

  props: {
    absolute: Boolean,
    fixed: Boolean
  },

  computed: {
    paddingLeft () {
      return this.fixed || !this.app
        ? 0
        : this.$vuetify.application.left
    },
    paddingRight () {
      return this.fixed || !this.app
        ? 0
        : this.$vuetify.application.right
    }
  },

  destroyed () {
    if (this.app) this.$vuetify.application.bottom = 0
  },

  methods: {
    updateApplication () {
      if (!this.app) return

      this.$vuetify.application.bottom = this.fixed
        ? this.$el && this.$el.clientHeight
        : 0
    }
  },

  mounted () {
    this.updateApplication()
  },

  render (h) {
    this.updateApplication()

    const data = {
      staticClass: 'footer',
      'class': this.addBackgroundColorClassChecks({
        'footer--absolute': this.absolute,
        'footer--fixed': this.fixed,
        'theme--dark': this.dark,
        'theme--light': this.light
      }),
      style: {
        paddingLeft: `${this.paddingLeft}px`,
        paddingRight: `${this.paddingRight}px`
      }
    }

    return h('footer', data, this.$slots.default)
  }
}


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

import ButtonGroup from '../../mixins/button-group'
import Colorable from '../../mixins/colorable'

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

  mixins: [ButtonGroup, Colorable],

  data: () => ({
    defaultColor: 'primary'
  }),

  props: {
    absolute: Boolean,
    active: [Number, String],
    shift: Boolean,
    value: { required: false }
  },

  watch: {
    active () {
      this.update()
    }
  },

  computed: {
    classes () {
      return {
        'bottom-nav': true,
        'bottom-nav--absolute': this.absolute,
        'bottom-nav--shift': this.shift,
        'bottom-nav--active': this.value
      }
    }
  },

  methods: {
    isSelected (i) {
      const item = this.getValue(i)
      return this.active === item
    },
    updateValue (i) {
      const item = this.getValue(i)
      this.$emit('update:active', item)
    }
  },

  render (h) {
    return h('div', {
      class: this.addBackgroundColorClassChecks(this.classes)
    }, this.$slots.default)
  }
}


@import '../bootstrap'

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

  for size, width in $container-max-widths
    @media only screen and (min-width: width)
      max-width: (width * 0.9375) // Credit to bootstrap

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

  &.fluid
    max-width: 100%

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

    .layout
      height: 100%
      flex: 1 1 auto

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

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

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

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

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

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

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

  &.row
    flex-direction: row

    &.reverse
      flex-direction: row-reverse

  &.column
    flex-direction: column

    &.reverse
      flex-direction: column-reverse

  &.wrap
    flex-wrap: wrap

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

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

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

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

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

  &-end
    align-items: flex-end

  &-center
    align-items: center

  &-baseline
    align-items: baseline

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

  &-end
    align-content: flex-end

  &-center
    align-content: center

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

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

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

  &-end
    justify-content: flex-end

  &-center
    justify-content: center

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

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

.spacer
  flex-grow: 1 !important

.scroll-y
  overflow-y: auto

.fill-height
  height: 100%

.hide-overflow
  overflow: hidden !important

.show-overflow
  overflow: visible !important

.no-wrap
  white-space: nowrap

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

// Display helpers
.d-flex
  display: flex

.d-inline-flex
  display: inline-flex

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

.d-block
  display: block

.d-inline-block
  display: inline-block


'$vuetify.theme': {
deep: true,
handler () {
this.applyType()
}
}
},

require('../../stylus/components/_app.styl')

// Component level mixins
import AppTheme from './mixins/app-theme'
import AppBreakpoint from './mixins/app-breakpoint'

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

// Utilities
import PointerSupport from '../../util/pointerSupport'

export default {
  name: 'v-app',

  mixins: [
    AppBreakpoint,
    AppTheme,
    PointerSupport
  ],

  directives: {
    Resize
  },

  props: {
    id: {
      type: String,
      default: 'app'
    },
    dark: Boolean
  },

  computed: {
    classes () {
      return {
        [`theme--${this.dark ? 'dark' : 'light'}`]: true
      }
    }
  },

  mounted () {
    this.$vuetify.theme.type = this.type
    window.addEventListener('load', this.runCallbacks)
  },

  watch: {
    type () {
      this.$vuetify.theme.type = this.type
    }
  },

  methods: {
    // Run all load callbacks created
    // from the load helper utility
    runCallbacks () {
      // For unit tests
      if (!document._loadCallbacks) return

      while (document._loadCallbacks.length) {
        document._loadCallbacks.pop()()
      }
    }
  },

  render (h) {
    const data = {
      staticClass: 'application',
      'class': this.classes,
      attrs: { 'data-app': true },
      domProps: { id: this.id },
      directives: [{
        name: 'resize',
        value: this.onResize
      }]
    }

    const wrapper = h('div', { staticClass: 'application--wrap' }, this.$slots.default)

    return h('div', data, [wrapper])
  }
}


import { createSimpleFunctional } from '../../util/helpers'
import VStepper from './VStepper'
import VStepperStep from './VStepperStep'
import VStepperContent from './VStepperContent'

/* istanbul ignore next */
VStepper.install = function install (Vue) {
  const VStepperHeader = createSimpleFunctional('stepper__header')
  const VStepperItems = createSimpleFunctional('stepper__items')

  Vue.component(VStepper.name, VStepper)
  Vue.component(VStepperContent.name, VStepperContent)
  Vue.component(VStepperStep.name, VStepperStep)
  Vue.component(VStepperHeader.name, VStepperHeader)
  Vue.component(VStepperItems.name, VStepperItems)
}

export default VStepper


require('../stylus/components/_overlay.styl')

export default {
  data () {
    return {
      overlay: null,
      overlayOffset: 0,
      overlayTimeout: null,
      overlayTransitionDuration: 500 + 150 // transition + delay
    }
  },

  props: {
    hideOverlay: Boolean
  },

  beforeDestroy () {
    this.removeOverlay()
  },

  methods: {
    genOverlay () {
      // If fn is called and timeout is active
      // or overlay already exists
      // cancel removal of overlay and re-add active
      if ((!this.isActive || this.hideOverlay) ||
        (this.isActive && this.overlayTimeout) ||
        this.overlay
      ) {
        clearTimeout(this.overlayTimeout)

        return this.overlay &&
          this.overlay.classList.add('overlay--active')
      }

      this.overlay = document.createElement('div')
      this.overlay.className = 'overlay'

      if (this.absolute) this.overlay.className += ' overlay--absolute'

      this.hideScroll()

      const parent = this.absolute
        ? this.$el.parentNode
        : document.querySelector('[data-app]')

      parent && parent.insertBefore(this.overlay, parent.firstChild)

      this.overlay.clientHeight // Force repaint
      requestAnimationFrame(() => {
        this.overlay.className += ' overlay--active'

        if (this.activeZIndex !== undefined) {
          this.overlay.style.zIndex = this.activeZIndex - 1
        }
      })

      return true
    },
    removeOverlay () {
      if (!this.overlay) {
        return this.showScroll()
      }

      this.overlay.classList.remove('overlay--active')

      this.overlayTimeout = setTimeout(() => {
        // IE11 Fix
        try {
          this.overlay.parentNode.removeChild(this.overlay)
          this.overlay = null
          this.showScroll()
        } catch (e) {}

        clearTimeout(this.overlayTimeout)
        this.overlayTimeout = null
      }, this.overlayTransitionDuration)
    },
    /**
     * @param {Event} e
     * @returns void
     */
    scrollListener (e) {
      if (e.type === 'keydown') {
        if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return

        const up = [38, 33]
        const down = [40, 34]

        if (up.includes(e.keyCode)) {
          e.deltaY = -1
        } else if (down.includes(e.keyCode)) {
          e.deltaY = 1
        } else {
          return
        }
      }

      if (e.target === this.overlay ||
        (e.type !== 'keydown' && e.target === document.body) ||
        this.checkPath(e)) e.preventDefault()
    },
    hasScrollbar (el) {
      const style = window.getComputedStyle(el)
      return ['auto', 'scroll'].includes(style['overflow-y']) && el.scrollHeight > el.clientHeight
    },
    shouldScroll (el, delta) {
      if (el.scrollTop === 0 && delta < 0) return true
      return el.scrollTop + el.clientHeight === el.scrollHeight && delta > 0
    },
    isInside (el, parent) {
      if (el === parent) {
        return true
      } else if (el === null || el === document.body) {
        return false
      } else {
        return this.isInside(el.parentNode, parent)
      }
    },
    /**
     * @param {Event} e
     * @returns boolean
     */
    checkPath (e) {
      const path = e.path || this.composedPath(e)
      const delta = e.deltaY || -e.wheelDelta

      if (e.type === 'keydown' && path[0] === document.body) {
        const dialog = this.$refs.dialog
        const selected = window.getSelection().anchorNode
        if (this.hasScrollbar(dialog) && this.isInside(selected, dialog)) {
          return this.shouldScroll(dialog, delta)
        }
        return true
      }

      for (const el of path) {
        if ([document, document.documentElement, this.$refs.content].includes(el)) return true
        if (this.hasScrollbar(el)) return this.shouldScroll(el, delta)
      }

      return true
    },
    /**
     * Polyfill for Event.prototype.composedPath
     * @param {Event} e
     * @returns Element[]
     */
    composedPath (e) {
      if (e.composedPath) return e.composedPath()

      const path = []
      let el = e.target

      while (el) {
        path.push(el)

        if (el.tagName === 'HTML') {
          path.push(document)
          path.push(window)

          return path
        }

        el = el.parentElement
      }
    },
    hideScroll () {
      if (this.$vuetify.breakpoint.mdAndDown) {
        document.documentElement.classList.add('overflow-y-hidden')
      } else {
        window.addEventListener('wheel', this.scrollListener)
        window.addEventListener('keydown', this.scrollListener)
      }
    },
    showScroll () {
      document.documentElement.classList.remove('overflow-y-hidden')
      window.removeEventListener('wheel', this.scrollListener)
      window.removeEventListener('keydown', this.scrollListener)
    }
  }
}


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

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

`;

exports[`VSpeedDial.js should render component with custom direction and match snapshot 1`] = `

`;


import VProgressCircular from './VProgressCircular'

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

export default VProgressCircular


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

exports[`VParallax.js should render 1`] = `

`;

exports[`VParallax.js should use alt tag when supplied 1`] = `

`;


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


require('../../stylus/components/_progress-circular.styl')

import Colorable from '../../mixins/colorable'

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

  mixins: [Colorable],

  props: {
    button: Boolean,

    fill: {
      type: String,
      default () { return this.indeterminate ? 'none' : 'transparent' }
    },

    indeterminate: Boolean,

    rotate: {
      type: Number,
      default: 0
    },

    size: {
      type: [Number, String],
      default: 32
    },

    width: {
      type: Number,
      default: 4
    },

    value: {
      type: Number,
      default: 0
    }
  },

  computed: {
    calculatedSize () {
      let size = Number(this.size)

      if (this.button) {
        size += 8
      }

      return size
    },

    circumference () {
      return 2 * Math.PI * this.radius
    },

    classes () {
      return this.addTextColorClassChecks({
        'progress-circular': true,
        'progress-circular--indeterminate': this.indeterminate,
        'progress-circular--button': this.button
      })
    },

    cxy () {
      return this.indeterminate && !this.button ? 50 : this.calculatedSize / 2
    },

    normalizedValue () {
      if (this.value < 0) {
        return 0
      }

      if (this.value > 100) {
        return 100
      }

      return this.value
    },

    radius () {
      return this.indeterminate &&
        !this.button
        ? 20
        : (this.calculatedSize - this.width) / 2
    },

    strokeDashArray () {
      return Math.round(this.circumference * 1000) / 1000
    },

    strokeDashOffset () {
      return ((100 - this.normalizedValue) / 100) * this.circumference + 'px'
    },

    styles () {
      return {
        height: `${this.calculatedSize}px`,
        width: `${this.calculatedSize}px`
      }
    },

    svgSize () {
      return this.indeterminate ? false : this.calculatedSize
    },

    svgStyles () {
      return {
        transform: `rotate(${this.rotate}deg)`
      }
    },

    viewBox () {
      return this.indeterminate ? '25 25 50 50' : false
    }
  },

  methods: {
    genCircle (h, name, offset) {
      return h('circle', {
        class: `progress-circular__${name}`,
        attrs: {
          fill: 'transparent',
          cx: this.cxy,
          cy: this.cxy,
          r: this.radius,
          'stroke-width': this.width,
          'stroke-dasharray': this.strokeDashArray,
          'stroke-dashoffset': offset
        }
      })
    },
    genSvg (h) {
      const children = [
        !this.indeterminate && this.genCircle(h, 'underlay', 0),
        this.genCircle(h, 'overlay', this.strokeDashOffset)
      ]

      return h('svg', {
        style: this.svgStyles,
        attrs: {
          xmlns: 'http://www.w3.org/2000/svg',
          height: this.svgSize,
          width: this.svgSize,
          viewBox: this.viewBox
        }
      }, children)
    }
  },

  render (h) {
    const info = h('div', { class: 'progress-circular__info' }, [this.$slots.default])
    const svg = this.genSvg(h)

    return h('div', {
      class: this.classes,
      style: this.styles,
      on: this.$listeners
    }, [svg, info])
  }
}


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

/* Themes */

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

  .picker__body
    background: $material.picker.body

picker-title($material)
  .picker__title
    background: $material.picker.title

dark(picker-title, 'picker')
theme(picker, "picker")

.picker
  border-radius: $card-border-radius
  display: flex
  flex-direction: column
  width: 290px
  contain: content

  .card__row--actions
    border: none
    margin-top: -20px

  &__title
    color: #fff
    border-top-left-radius: $card-border-radius
    border-top-right-radius: $card-border-radius
    height: 105px
    padding: 16px

  &__body
    height: 290px
    overflow: hidden
    position: relative

  &--landscape
    flex-direction: row
    flex-wrap: wrap
    width: 500px

    .picker__title
      border-top-right-radius: 0
      border-bottom-right-radius: 0
      flex: 0 1 170px
      width: 170px
      height: auto
      position: absolute
      top: 0
      left: 0
      height: 100%
      z-index: 1

    .picker__body
      flex: 1 0
      width: 330px
      margin-left: 170px

    .card__row--actions
      margin-left: 170px
      width: 330px


require('../../stylus/components/_grid.styl')

import Grid from './grid'

export default Grid('container')


style.type = 'text/css'
document.head.appendChild(style)
this.style = style
},
updateTheme (classes) {
this.style.innerHTML = classes
}
}
}

keep keep keep replace replace replace keep keep keep


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

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

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

Vuetify.version = version

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

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

export default Vuetify