'use strict';

const { dirname, join } = require('path');
const { mkdirs, rmdir, stat, unlink, writeFile } = require('hexo-fs');

const dateFormat = 'YYYY-MM-DD HH:mm:ss';

describe('asset', () => {
  const Hexo = require('../../../dist/hexo');
  const defaults = require('../../../dist/hexo/default_config');
  const baseDir = join(__dirname, 'asset_test');
  const hexo = new Hexo(baseDir);
  const asset = require('../../../dist/plugins/processor/asset')(hexo);
  const process = asset.process.bind(hexo);
  const { pattern } = asset;
  const { source } = hexo;
  const { File } = source;
  const Asset = hexo.model('Asset');
  const Page = hexo.model('Page');

  function newFile(options) {
    options.source = join(source.base, options.path);
    options.params = {
      renderable: options.renderable
    };

    return new File(options);
  }

  before(async () => {
    await mkdirs(baseDir);
    await hexo.init();
  });

  beforeEach(() => { hexo.config = Object.assign({}, defaults); });

  after(() => rmdir(baseDir));

  it('pattern', () => {
    // Renderable files
    pattern.match('foo.json').should.have.property('renderable', true);

    // Non-renderable files
    pattern.match('foo.txt').should.have.property('renderable', false);

    // Tmp files
    should.not.exist(pattern.match('foo.txt~'));
    should.not.exist(pattern.match('foo.txt%'));

    // Hidden files
    should.not.exist(pattern.match('_foo.txt'));
    should.not.exist(pattern.match('test/_foo.txt'));
    should.not.exist(pattern.match('.foo.txt'));
    should.not.exist(pattern.match('test/.foo.txt'));

    // Include files
    hexo.config.include = ['fff/**'];
    pattern.match('fff/_foo.txt').should.exist;
    hexo.config.include = [];

    // Exclude files
    hexo.config.exclude = ['fff/**'];
    should.not.exist(pattern.match('fff/foo.txt'));
    hexo.config.exclude = [];

    // Skip render files
    hexo.config.skip_render = ['fff/**'];
    pattern.match('fff/foo.json').should.have.property('renderable', false);
    hexo.config.skip_render = [];
  });

  it('asset - type: create', async () => {
    const file = newFile({
      path: 'foo.jpg',
      type: 'create',
      renderable: false
    });

    await writeFile(file.source, 'foo');
    await process(file);
    const id = 'source/' + file.path;
    const asset = Asset.findById(id);

    asset._id.should.eql(id);
    asset.path.should.eql(file.path);
    asset.modified.should.be.true;
    asset.renderable.should.be.false;

    return Promise.all([
      asset.remove(),
      unlink(file.source)
    ]);
  });

  it('asset - type: create (when source path is configed to parent directory)', async () => {
    const file = newFile({
      path: '../../source/foo.jpg',
      type: 'create',
      renderable: false
    });

    await writeFile(file.source, 'foo');
    await process(file);
    const id = '../source/foo.jpg'; // The id should a relative path,because the 'lib/models/assets.js' use asset path by joining base path with "_id" directly.
    const asset = Asset.findById(id);
    asset._id.should.eql(id);
    asset.path.should.eql(file.path);
    asset.modified.should.be.true;
    asset.renderable.should.be.false;

    asset.remove();
    await unlink(file.source);
    rmdir(dirname(file.source));
  });

  it('asset - type: update', async () => {
    const file = newFile({
      path: 'foo.jpg',
      type: 'update',
      renderable: false
    });

    const id = 'source/' + file.path;

    await Promise.all([
      writeFile(file.source, 'test'),
      Asset.insert({
        _id: id,
        path: file.path,
        modified: false
      })
    ]);
    await process(file);
    const asset = Asset.findById(id);

    asset._id.should.eql(id);
    asset.path.should.eql(file.path);
    asset.modified.should.be.true;
    asset.renderable.should.be.false;

    return Promise.all([
      asset.remove(),
      unlink(file.source)
    ]);
  });

  it('asset - type: skip', async () => {
    const file = newFile({
      path: 'foo.jpg',
      type: 'skip',
      renderable: false
    });

    const id = 'source/' + file.path;

    await Promise.all([
      writeFile(file.source, 'test'),
      Asset.insert({
        _id: id,
        path: file.path,
        modified: false
      })
    ]);
    await process(file);
    const asset = Asset.findById(id);
    asset.modified.should.be.false;
    await Promise.all([
      Asset.removeById(id),
      unlink(file.source)
    ]);
  });

  it('asset - type: delete', async () => {
    const file = newFile({
      path: 'foo.jpg',
      type: 'delete',
      renderable: false
    });

    const id = 'source/' + file.path;

    await Asset.insert({
      _id: id,
      path: file.path
    });
    await process(file);

    should.not.exist(Asset.findById(id));
  });

  it('page - type: create', async () => {
    const body = [
      'title: "Hello world"',
      'date: 2006-01-02 15:04:05',
      'updated: 2014-12-13 01:02:03',
      '---',
      'The quick brown fox jumps over the lazy dog'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);

    const page = Page.findOne({ source: file.path });
    page.title.should.eql('Hello world');
    page.date.format(dateFormat).should.eql('2006-01-02 15:04:05');
    page.updated.format(dateFormat).should.eql('2014-12-13 01:02:03');
    page._content.should.eql('The quick brown fox jumps over the lazy dog');
    page.source.should.eql(file.path);
    page.raw.should.eql(body);
    page.path.should.eql('hello.html');
    page.layout.should.eql('page');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - type: update', async () => {
    const body = [
      'title: "Hello world"',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'update',
      renderable: true
    });

    const doc = await Page.insert({ source: file.path, path: 'hello.html' });
    await writeFile(file.source, body);
    const id = doc._id;
    await process(file);
    const page = Page.findOne({ source: file.path });
    page._id.should.eql(id);
    page.title.should.eql('Hello world');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - type: delete', async () => {
    const file = newFile({
      path: 'hello.njk',
      type: 'delete',
      renderable: true
    });

    await Page.insert({
      source: file.path,
      path: 'hello.html'
    });
    await process(file);
    should.not.exist(Page.findOne({ source: file.path }));
  });

  it('page - use the status of the source file if date not set', async () => {
    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, '');
    await process(file);
    const stats = await stat(file.source);
    const page = Page.findOne({source: file.path});

    page.date.toDate().should.eql(stats.ctime);
    page.updated.toDate().should.eql(stats.mtime);

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - use the date for updated if updated_option = date', async () => {
    const body = [
      'date: 2011-4-5 14:19:19',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    hexo.config.updated_option = 'date';

    await writeFile(file.source, body);
    await process(file);
    const stats = await stat(file.source);
    const page = Page.findOne({source: file.path});

    page.updated.toDate().should.eql(page.date.toDate());
    page.updated.toDate().should.not.eql(stats.mtime);

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - use the status if updated_option = mtime', async () => {
    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    hexo.config.updated_option = 'mtime';

    await writeFile(file.source, '');
    await process(file);
    const stats = await stat(file.source);
    const page = Page.findOne({source: file.path});

    page.date.toDate().should.eql(stats.ctime);
    page.updated.toDate().should.eql(stats.mtime);

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - updated shouldn\'t exists if updated_option = empty', async () => {
    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    hexo.config.updated_option = 'empty';

    await writeFile(file.source, '');
    await process(file);
    const stats = await stat(file.source);
    const page = Page.findOne({source: file.path});

    page.date.toDate().should.eql(stats.ctime);
    should.not.exist(page.updated);

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - permalink', async () => {
    const body = [
      'title: "Hello world"',
      'permalink: foo.html',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({ source: file.path });
    page.path.should.eql('foo.html');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - permalink (without extension name)', async () => {
    const body = [
      'title: "Hello world"',
      'permalink: foo',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({ source: file.path });
    page.path.should.eql('foo.html');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - permalink (with trailing slash)', async () => {
    const body = [
      'title: "Hello world"',
      'permalink: foo/',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({ source: file.path });
    page.path.should.eql('foo/index.html');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - set layout to false if output is not html', async () => {
    const body = 'foo: 1';

    const file = newFile({
      path: 'test.yml',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({ source: file.path });
    page.layout.should.eql('false');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - don\'t set layout to false if layout is set but output is not html', async () => {
    const body = [
      'layout: something',
      '---',
      'foo: 1'
    ].join('\n');

    const file = newFile({
      path: 'test.yml',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({ source: file.path });
    page.layout.should.eql('something');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - parse date', async () => {
    const body = [
      'title: "Hello world"',
      'date: Apr 24 2014',
      'updated: May 5 2015',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({ source: file.path });
    page.date.format(dateFormat).should.eql('2014-04-24 00:00:00');
    page.updated.format(dateFormat).should.eql('2015-05-05 00:00:00');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - use file stats instead if date is invalid', async () => {
    const body = [
      'title: "Hello world"',
      'date: yomama',
      'updated: isfat',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const stats = await file.stat();
    const page = Page.findOne({source: file.path});

    page.date.toDate().should.eql(stats.ctime);
    page.updated.toDate().should.eql(page.date.toDate());

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - don\'t remove extension name', async () => {
    const body = '';

    const file = newFile({
      path: 'test.min.js',
      type: 'create',
      renderable: true
    });

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({ source: file.path });
    page.path.should.eql('test.min.js');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });

  it('page - timezone', async () => {
    const body = [
      'title: "Hello world"',
      'date: Apr 24 2014',
      'updated: May 5 2015',
      '---'
    ].join('\n');

    const file = newFile({
      path: 'hello.njk',
      type: 'create',
      renderable: true
    });

    hexo.config.timezone = 'UTC';

    await writeFile(file.source, body);
    await process(file);
    const page = Page.findOne({source: file.path});

    page.date.utc().format(dateFormat).should.eql('2014-04-24 00:00:00');
    page.updated.utc().format(dateFormat).should.eql('2015-05-05 00:00:00');

    await Promise.all([
      page.remove(),
      unlink(file.source)
    ]);
  });
});


keep keep keep keep replace keep keep keep keep

import abbrev from 'abbrev';
import page from './page';
import post from './post';
import route from './route';
import tag from './tag';
import category from './category';

const store = {
  page, post, route, tag, category
};

const alias = abbrev(Object.keys(store));

function listConsole(args) {
  const type = args._.shift();

  // Display help message if user didn't input any arguments
  if (!type || !alias[type]) {
    return this.call('help', {_: ['list']});
  }

  return this.load().then(() => Reflect.apply(store[alias[type]], this, [args]));
}

export = listConsole;


@lines = _.map(@root.childNodes, (lineNode, index) =>

line = new Tandem.Line(lineNode, @lineIdCounter, index)

keep keep keep replace keep keep keep keep

line = new Tandem.Line(this, lineNode, @lineIdCounter, index)

#= require underscore

class TandemLine
  @CLASS_NAME: 'line'

  constructor: (@node, @id, @index) ->
    @leaves = []
    @numLeaves = 0
    this.normalizeHTML()
    this.buildLeaves(@node, {})
    @length = @node.textContent.length
    @innerHTML = @node.innerHTML

  buildLeaves: (node, attributes) ->
    _.each(node.childNodes, (node) =>
      nodeAttributes = _.clone(attributes)
      [attr, value] = Tandem.Leaf.getAttribute(node)
      nodeAttributes[attr] = value if attr?
      if node.childNodes.length == 1 && node.firstChild.nodeType == node.TEXT_NODE
        @leaves.push(new Tandem.Leaf(node, nodeAttributes, @numLeaves))
        @numLeaves += 1
      else
        this.buildLeaves(node, nodeAttributes)
    )

  normalizeHTML: ->

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


constructor: (@line, @node, @attributes, @index) ->

#= require underscore

class TandemLeaf
  @getAttribute: (node) ->
    switch node.tagName
      when 'B' then return ['bold', true]
      when 'I' then return ['italic', true]
      when 'S' then return ['strike', true]
      when 'U' then return ['underline', true]
      else          return [null, null]

  @groupByLine: (leaves) ->
    return _.reduce(leaves, (lines, leaf) ->
      if !lines[leaf.line.id]?
        lines[leaf.line.id] = []
      lines[leaf.line.id].push(leaf)
    , {})

  constructor: (@node, @attributes, @index) ->
    @length = @node.textContent.length

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


/**
 * @license Serializer module for Rangy.
 * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
 * cookie or local storage and restore it on the user's next visit to the same page.
 *
 * Part of Rangy, a cross-browser JavaScript range and selection library
 * http://code.google.com/p/rangy/
 *
 * Depends on Rangy core.
 *
 * Copyright 2012, Tim Down
 * Licensed under the MIT license.
 * Version: 1.2.3
 * Build date: 26 February 2012
 */
rangy.createModule("Serializer", function(api, module) {
    api.requireModules( ["WrappedSelection", "WrappedRange"] );
    var UNDEF = "undefined";

    // encodeURIComponent and decodeURIComponent are required for cookie handling
    if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
        module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
    }

    // Checksum for checking whether range can be serialized
    var crc32 = (function() {
        function utf8encode(str) {
            var utf8CharCodes = [];

            for (var i = 0, len = str.length, c; i < len; ++i) {
                c = str.charCodeAt(i);
                if (c < 128) {
                    utf8CharCodes.push(c);
                } else if (c < 2048) {
                    utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
                } else {
                    utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
                }
            }
            return utf8CharCodes;
        }

        var cachedCrcTable = null;

        function buildCRCTable() {
            var table = [];
            for (var i = 0, j, crc; i < 256; ++i) {
                crc = i;
                j = 8;
                while (j--) {
                    if ((crc & 1) == 1) {
                        crc = (crc >>> 1) ^ 0xEDB88320;
                    } else {
                        crc >>>= 1;
                    }
                }
                table[i] = crc >>> 0;
            }
            return table;
        }

        function getCrcTable() {
            if (!cachedCrcTable) {
                cachedCrcTable = buildCRCTable();
            }
            return cachedCrcTable;
        }

        return function(str) {
            var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
            for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
                y = (crc ^ utf8CharCodes[i]) & 0xFF;
                crc = (crc >>> 8) ^ crcTable[y];
            }
            return (crc ^ -1) >>> 0;
        };
    })();

    var dom = api.dom;

    function escapeTextForHtml(str) {
        return str.replace(/</g, "<").replace(/>/g, ">");
    }

    function nodeToInfoString(node, infoParts) {
        infoParts = infoParts || [];
        var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
        var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
        var start = "", end = "";
        switch (nodeType) {
            case 3: // Text node
                start = escapeTextForHtml(node.nodeValue);
                break;
            case 8: // Comment
                start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
                break;
            default:
                start = "<" + nodeInfo + ">";
                end = "</>";
                break;
        }
        if (start) {
            infoParts.push(start);
        }
        for (var i = 0; i < childCount; ++i) {
            nodeToInfoString(children[i], infoParts);
        }
        if (end) {
            infoParts.push(end);
        }
        return infoParts;
    }

    // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
    // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
    // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
    // innerHTML whenever the user changes an input within the element.
    function getElementChecksum(el) {
        var info = nodeToInfoString(el).join("");
        return crc32(info).toString(16);
    }

    function serializePosition(node, offset, rootNode) {
        var pathBits = [], n = node;
        rootNode = rootNode || dom.getDocument(node).documentElement;
        while (n && n != rootNode) {
            pathBits.push(dom.getNodeIndex(n, true));
            n = n.parentNode;
        }
        return pathBits.join("/") + ":" + offset;
    }

    function deserializePosition(serialized, rootNode, doc) {
        if (rootNode) {
            doc = doc || dom.getDocument(rootNode);
        } else {
            doc = doc || document;
            rootNode = doc.documentElement;
        }
        var bits = serialized.split(":");
        var node = rootNode;
        var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;

        while (i--) {
            nodeIndex = parseInt(nodeIndices[i], 10);
            if (nodeIndex < node.childNodes.length) {
                node = node.childNodes[parseInt(nodeIndices[i], 10)];
            } else {
                throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
                        " has no child with index " + nodeIndex + ", " + i);
            }
        }

        return new dom.DomPosition(node, parseInt(bits[1], 10));
    }

    function serializeRange(range, omitChecksum, rootNode) {
        rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
        if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
            throw new Error("serializeRange: range is not wholly contained within specified root node");
        }
        var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
            serializePosition(range.endContainer, range.endOffset, rootNode);
        if (!omitChecksum) {
            serialized += "{" + getElementChecksum(rootNode) + "}";
        }
        return serialized;
    }

    function deserializeRange(serialized, rootNode, doc) {
        if (rootNode) {
            doc = doc || dom.getDocument(rootNode);
        } else {
            doc = doc || document;
            rootNode = doc.documentElement;
        }
        var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
        var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
        if (checksum && checksum !== getElementChecksum(rootNode)) {
            throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
                    ") and target root node (" + rootNodeChecksum + ") do not match");
        }
        var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
        var range = api.createRange(doc);
        range.setStart(start.node, start.offset);
        range.setEnd(end.node, end.offset);
        return range;
    }

    function canDeserializeRange(serialized, rootNode, doc) {
        if (rootNode) {
            doc = doc || dom.getDocument(rootNode);
        } else {
            doc = doc || document;
            rootNode = doc.documentElement;
        }
        var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
        var checksum = result[3];
        return !checksum || checksum === getElementChecksum(rootNode);
    }

    function serializeSelection(selection, omitChecksum, rootNode) {
        selection = selection || api.getSelection();
        var ranges = selection.getAllRanges(), serializedRanges = [];
        for (var i = 0, len = ranges.length; i < len; ++i) {
            serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
        }
        return serializedRanges.join("|");
    }

    function deserializeSelection(serialized, rootNode, win) {
        if (rootNode) {
            win = win || dom.getWindow(rootNode);
        } else {
            win = win || window;
            rootNode = win.document.documentElement;
        }
        var serializedRanges = serialized.split("|");
        var sel = api.getSelection(win);
        var ranges = [];

        for (var i = 0, len = serializedRanges.length; i < len; ++i) {
            ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
        }
        sel.setRanges(ranges);

        return sel;
    }

    function canDeserializeSelection(serialized, rootNode, win) {
        var doc;
        if (rootNode) {
            doc = win ? win.document : dom.getDocument(rootNode);
        } else {
            win = win || window;
            rootNode = win.document.documentElement;
        }
        var serializedRanges = serialized.split("|");

        for (var i = 0, len = serializedRanges.length; i < len; ++i) {
            if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
                return false;
            }
        }

        return true;
    }

    var cookieName = "rangySerializedSelection";

    function getSerializedSelectionFromCookie(cookie) {
        var parts = cookie.split(/[;,]/);
        for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
            nameVal = parts[i].split("=");
            if (nameVal[0].replace(/^\s+/, "") == cookieName) {
                val = nameVal[1];
                if (val) {
                    return decodeURIComponent(val.replace(/\s+$/, ""));
                }
            }
        }
        return null;
    }

    function restoreSelectionFromCookie(win) {
        win = win || window;
        var serialized = getSerializedSelectionFromCookie(win.document.cookie);
        if (serialized) {
            deserializeSelection(serialized, win.doc)
        }
    }

    function saveSelectionCookie(win, props) {
        win = win || window;
        props = (typeof props == "object") ? props : {};
        var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
        var path = props.path ? ";path=" + props.path : "";
        var domain = props.domain ? ";domain=" + props.domain : "";
        var secure = props.secure ? ";secure" : "";
        var serialized = serializeSelection(api.getSelection(win));
        win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
    }

    api.serializePosition = serializePosition;
    api.deserializePosition = deserializePosition;

    api.serializeRange = serializeRange;
    api.deserializeRange = deserializeRange;
    api.canDeserializeRange = canDeserializeRange;

    api.serializeSelection = serializeSelection;
    api.deserializeSelection = deserializeSelection;
    api.canDeserializeSelection = canDeserializeSelection;

    api.restoreSelectionFromCookie = restoreSelectionFromCookie;
    api.saveSelectionCookie = saveSelectionCookie;

    api.getElementChecksum = getElementChecksum;
});


@length = @node.textContent.length
window.Tandem ||= {}

getNextLeaf: ->
return @line.leaves[@index + 1]
getPrevLeaf: ->
return @line.leaves[@index - 1]

#= underscore
#= require rangy/rangy-core

class TandemRange
  @getSelection: (editor) ->
    rangySelection = rangy.getIframeSelection(editor.iframe)
    return null unless rangySelection.anchorNode? && rangySelection.focusNode?
    if !rangySelection.isBackwards()
      start = new Tandem.Position(editor, rangySelection.anchorNode, rangySelection.anchorOffset)
      end = new Tandem.Position(editor, rangySelection.focusNode, rangySelection.focusOffset)
    else
      start = new Tandem.Position(editor, rangySelection.focusNode, rangySelection.focusOffset)
      end = new Tandem.Position(editor, rangySelection.anchorNode, rangySelection.anchorOffset)
    return new TandemRange(editor, start, end)

  # constructor: (TandemEditor editor, Number startIndex, Number endIndex) ->
  # constructor: (TandemEditor editor, Object start, Object end) ->
  constructor: (@editor, @start, @end) ->
    # TODO initialize with index
    @start = new Tandem.Position(@editor, @start) if _.isNumber(@start)
    @end = new Tandem.Position(@editor, @end) if _.isNumber(@end)

  equals: (range) ->
    return false if range == null
    return range.start.node == @start.node && range.end.node == @end.node && range.start.offset == @start.offset && range.end.offset == @end.offset

  getAttributeIntersection: ->
    attributes = null
    _.all(this.getLeafPositions(), (position) ->
      leafAttributes = Tandem.Utils.Node.getAttributes(position.node)
      if attributes
        _.each(attributes, (value, key) ->
          if leafAttributes[key] != true
            delete attributes[key]
        )
      else
        attributes = leafAttributes

      for key,value of attributes when value == true
        return true
      return false
    )
    return attributes || {}

  getRangy: ->
    range = rangy.createRangyRange(@editor.iframe)
    range.setStart(@start.node.firstChild, @start.offset)
    range.setEnd(@end.node.firstChild, @end.offset)
    return range

  getLeafNodes: ->
    range = this.getRangy()
    if range.collapsed
      return [@start.node]
    else
      nodes = _.map(range.getNodes([3]), (node) ->
        return node.parentNode
      )
      nodes.pop() if nodes[nodes.length - 1] != @end.node || @end.offset == 0
      return nodes

  getLeafPositions: ->
    return _.map(this.getLeafNodes(), (node) =>
      return new Tandem.Position(@editor, node, 0)
    )

  groupNodesByLine: ->
    currentLine = 0
    return _.reduce(this.getLeafPositions(), (lines, position) ->
      line = Tandem.Utils.Node.getLine(position.node)
      if currentLine == line
        lines[lines.length - 1].push(position.node)
      else
        lines.push([position.node])
        currentLine = line
      return lines
    , [])

  split: (position, before = true) ->
    newNode = @editor.iframeDoc.createElement(position.node.tagName)
    beforeText = position.node.textContent.substring(0, position.offset)
    afterText = position.node.textContent.substring(position.offset)
    return if beforeText == '' || afterText == ''
    if before
      newNode.textContent = beforeText
      position.node.textContent = afterText
      position.node.parentNode.insertBefore(newNode, position.node)
    else
      position.node.textContent = beforeText
      newNode.textContent = afterText
      position.node.parentNode.insertBefore(newNode, position.node.nextSibling)

  splitBefore: ->
    this.split(@start, true)
    @end.offset -= @start.offset if @end.node == @start.node
    @start.offset -= @start.offset

  splitAfter: ->
    this.split(@end, false)

  splitEnds: ->
    this.splitBefore()
    this.splitAfter()

window.Tandem ||= {}
window.Tandem.Range = TandemRange


#= require underscore
class TandemLine
@CLASS_NAME: 'line'
constructor: (@node, @id, @index) ->
@leaves = []
@numLeaves = 0

keep keep keep keep keep replace keep keep

constructor: (@doc, @node, @id, @index) ->


@leaves = []
@numLeaves = 0
this.normalizeHTML()


this.normalizeHTML()
this.buildLeaves(@node, {})
@length = @node.textContent.length
@innerHTML = @node.innerHTML
buildLeaves: (node, attributes) ->
_.each(node.childNodes, (node) =>


[attr, value] = Tandem.Leaf.getAttribute(node)

if node.childNodes.length == 1 && node.firstChild.nodeType == node.TEXT_NODE

@leaves.push(new Tandem.Leaf(node, nodeAttributes, @numLeaves))

keep keep keep replace keep keep keep keep keep

@leaves.push(new Tandem.Leaf(this, node, nodeAttributes, @numLeaves))


getNextLine: ->
return @doc.lines[@index + 1]
getPrevLine: ->
return @doc.lines[@index - 1]


beforeText = position.node.textContent.substring(0, position.offset)

return if beforeText == '' || afterText == ''

keep keep keep keep add keep keep keep keep keep

return @start.node == @end.node && @start.offset == @end.offset


import { test } from '~util/testing'
import { mount } from 'avoriaz'
import VSelect from '~components/VSelect'
import VMenu from '~components/VMenu'

test('VSelect - tags', () => {
  const backspace = new Event('keydown')
  backspace.keyCode = 8

  it('should create new values when tagging', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: []
      }
    })

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

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()

    input.element.value = 'foo'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    await wrapper.vm.$nextTick()

    input.trigger('keydown.enter')
    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['foo'])
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should change selectedIndex with keyboard', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: ['foo', 'bar']
      }
    })

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

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()

    for (const index of [1, 0, -1]) {
      input.trigger('keydown.left')
      await wrapper.vm.$nextTick()
      expect(wrapper.vm.selectedIndex).toBe(index)
    }

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

  it('should delete a tagged item when selected and backspace/delete is pressed', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: ['foo', 'bar']
      }
    })

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

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    wrapper.vm.focus()

    input.trigger('keydown.left')
    expect(wrapper.vm.selectedIndex).toBe(1)

    input.trigger('keydown.delete')
    expect(change).toHaveBeenCalledWith(['foo'])
    expect(wrapper.vm.selectedIndex).toBe(0)

    input.element.dispatchEvent(backspace) // Avoriaz doesn't wrap keydown.backspace
    expect(change).toHaveBeenCalledWith([])
    expect(wrapper.vm.selectedIndex).toBe(-1)

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

  it('should add a tag on tab using the first suggestion', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: [],
        items: ['bar']
      }
    })

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

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()

    input.element.value = 'b'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()

    expect(change).toBeCalledWith(['bar'])
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should add a tag on tab using the current searchValue', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: [],
        items: ['bar']
      }
    })

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

    const change = jest.fn()
    const blur = jest.fn()
    wrapper.vm.$on('change', change)
    wrapper.vm.$on('blur', blur)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()
    wrapper.setProps({ searchInput: 'ba' })
    await wrapper.vm.$nextTick()
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()
    expect(change).toBeCalledWith(['bar'])

    wrapper.setProps({ searchInput: 'it' })
    await wrapper.vm.$nextTick()
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()
    expect(change).toBeCalledWith(['bar', 'it'])

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

  it('should add a tag on enter using the current searchValue', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: [],
        items: ['bar']
      }
    })

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

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()

    input.element.value = 'ba'
    input.trigger('input')
    input.element.setSelectionRange(2, 2)
    await wrapper.vm.$nextTick()
    input.trigger('keydown.right')
    await wrapper.vm.$nextTick()
    input.trigger('keydown.enter')
    await wrapper.vm.$nextTick()

    expect(change).toBeCalledWith(['ba'])
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should add a tag on left arrow and select the previous tag', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: ['foo'],
        items: ['foo', 'bar']
      }
    })

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

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()

    input.element.value = 'b'
    input.trigger('input')
    input.trigger('keydown.left')
    await wrapper.vm.$nextTick()

    expect(change).toBeCalledWith(['foo', 'b'])
    expect(wrapper.vm.selectedIndex).toBe(0)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should remove a duplicate tag and add it to the end', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true,
        value: ['foo', 'bar']
      }
    })

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

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()

    input.element.value = 'foo'
    input.trigger('input')
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()

    expect(change).toBeCalledWith(['bar', 'foo'])
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should add tag with valid search value on blur', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tags: true
      }
    })

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

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()

    input.element.value = 'bar'
    input.trigger('input')
    await wrapper.vm.$nextTick()

    wrapper.vm.blur()
    await wrapper.vm.$nextTick() // First tick processes blur, menu sets isActive false, adds tag
    await wrapper.vm.$nextTick() // Second tick processes change after tag added

    expect(change).toBeCalledWith(['bar'])
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })
})


import { mount } from 'avoriaz'
import { VCarouselItem } from '~components/VCarousel'

const imageSrc = 'https://vuetifyjs.com/static/doc-images/cards/sunshine.jpg'

describe('VCarouselItem.js', () => {
  it('should render component and match snapshot', () => {
    const wrapper = mount(VCarouselItem, {
      propsData: {
        src: imageSrc
      }
    })

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

  it('should render component with custom transition and match snapshot', () => {
    const wrapper = mount(VCarouselItem, {
      propsData: {
        src: imageSrc,
        transition: 'slide-y-transition'
      }
    })

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

  it('should render component with custom reverse transition and match snapshot', () => {
    const wrapper = mount(VCarouselItem, {
      propsData: {
        src: imageSrc,
        'reverse-ransition': 'slide-y-reverse-transition'
      }
    })

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


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


import { test } from '~util/testing'
import { mount } from 'avoriaz'
import VSelect from '~components/VSelect'

test('VSelect - autocomplete', () => {
  it('should have -1 tabindex when disabled', () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        disabled: true
      }
    })

    expect(wrapper.vm.$refs.input.tabIndex).toBe(-1)
    expect(wrapper.vm.$el.tabIndex).toBe(-1)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should have explicit tabindex passed through when autocomplete', () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        tabindex: 10
      }
    })

    expect(wrapper.vm.$refs.input.tabIndex).toBe(10)
    expect(wrapper.vm.$el.tabIndex).toBe(-1)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should have explicit tabindex passed through when not autocomplete', () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        tabindex: 10
      }
    })

    expect(wrapper.vm.$refs.input.tabIndex).toBe(-1)
    expect(wrapper.vm.$el.tabIndex).toBe(10)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should emit search input changes', async () => {
    const wrapper = mount(VSelect, {
      propsData: {
        autocomplete: true,
        debounceSearch: 0
      }
    })

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

    const update = jest.fn()
    wrapper.vm.$on('update:searchInput', update)

    await wrapper.vm.$nextTick()

    input.element.value = 'test'
    input.trigger('input')
    await new Promise(resolve => setTimeout(resolve, 1))

    expect(update).toBeCalledWith('test')
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should be able to clear autocomplete value', async () => {
    const wrapper = mount(VSelect, {
      propsData: {
        autocomplete: true,
        items: ['red'],
        debounceSearch: 0
      }
    })

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

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()
    wrapper.setProps({ searchInput: 're' })
    await wrapper.vm.$nextTick()
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()

    const change = jest.fn()
    wrapper.vm.$on('input', change)

    wrapper.vm.focus()
    await wrapper.vm.$nextTick()
    wrapper.setProps({ searchInput: '' })
    await wrapper.vm.$nextTick()
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()
    expect(change).toBeCalledWith(null)

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

  it('should filter autocomplete search results', () => {
    const wrapper = mount(VSelect, {
      propsData: {
        autocomplete: true,
        items: ['foo', 'bar']
      }
    })

    wrapper.vm.searchValue = 'foo'

    expect(wrapper.vm.filteredItems).toHaveLength(1)
    expect(wrapper.vm.filteredItems[0]).toBe('foo')
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should filter numeric primitives', () => {
    const wrapper = mount(VSelect, {
      propsData: {
        autocomplete: true,
        items: [1, 2]
      }
    })

    wrapper.vm.searchValue = 1

    expect(wrapper.vm.filteredItems).toHaveLength(1)
    expect(wrapper.vm.filteredItems[0]).toBe(1)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should activate when search changes and not active', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        items: [1, 2, 3, 4],
        multiple: true
      }
    })

    wrapper.vm.isActive = true
    await wrapper.vm.$nextTick()
    wrapper.vm.searchValue = 2
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isActive).toBe(true)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should set searchValue to null when deactivated', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        items: [1, 2, 3, 4],
        multiple: true
      }
    })

    wrapper.vm.isActive = true
    wrapper.vm.searchValue = 2
    await wrapper.vm.$nextTick()
    wrapper.vm.isActive = false
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.searchValue).toBe(null)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should render role=combobox correctly when autocomplete', async () => {
    const wrapper = mount(VSelect, {
      propsData: {
        autocomplete: true
      }
    })

    const inputGroup = wrapper.find('.input-group--select')[0]
    expect(inputGroup.element.getAttribute('role')).toBeFalsy()

    const input = wrapper.find('input')[0]
    expect(input.getAttribute('role')).toBe('combobox')

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

  it('should render role=combobox correctly when not autocomplete)', async () => {
    const wrapper = mount(VSelect)

    const inputGroup = wrapper.find('.input-group--select')[0]
    expect(inputGroup.element.getAttribute('role')).toBe('combobox')

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

  it('should not duplicate items after items update when caching is turned on', async () => {
    const wrapper = mount(VSelect, {
      propsData: {
        autocomplete: true,
        cacheItems: true,
        returnObject: true,
        itemText: 'text',
        itemValue: 'id',
        items: [],
      }
    })

    wrapper.setProps({ items: [{ id: 1, text: 'A' }] })
    expect(wrapper.vm.computedItems).toHaveLength(1)
    wrapper.setProps({ items: [{ id: 1, text: 'A' }] })
    expect(wrapper.vm.computedItems).toHaveLength(1)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should not display list with no items and autocomplete', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        items: []
      }
    })

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

    input.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.menuIsActive).toBe(false)
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should cache items', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        cacheItems: true,
        items: []
      }
    })

    wrapper.setProps({ items: ['bar', 'baz'] })
    expect(wrapper.vm.computedItems).toHaveLength(2)

    wrapper.setProps({ items: ['foo'] })
    expect(wrapper.vm.computedItems).toHaveLength(3)

    wrapper.setProps({ items: ['bar'] })
    expect(wrapper.vm.computedItems).toHaveLength(3)

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

  it('should allow changing of browser autocomplete', () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        browserAutocomplete: 'off'
      }
    })

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

    expect(input.getAttribute('autocomplete')).toBe('off')
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })

  it('should show input when focused and autocomplete', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true
      }
    })

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

    expect(input.hasStyle('display', 'none'))

    wrapper.trigger('focus')
    await wrapper.vm.$nextTick()
    expect(input.hasStyle('display', 'block'))

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

  it('should not filter text with no items', async () => {
    const wrapper = mount(VSelect, {
      attachToDocument: true,
      propsData: {
        autocomplete: true,
        items: ['foo', 'bar']
      }
    })

    wrapper.setProps({ searchInput: 'asdf' })
    wrapper.update()
    await wrapper.vm.$nextTick()
    const tile = wrapper.find('.list__tile__title')[0]

    expect(tile.text()).toBe('No data available')
    expect('Application is missing <v-app> component.').toHaveBeenTipped()
  })
})


keep keep keep add keep keep keep keep keep

require('../../stylus/components/_text-fields.styl')
require('../../stylus/components/_input-groups.styl')
require('../../stylus/components/_select.styl')

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

// Mixins
import Colorable from '../../mixins/colorable'
import Dependent from '../../mixins/dependent'
import Filterable from '../../mixins/filterable'
import Input from '../../mixins/input'
import Maskable from '../../mixins/maskable'

// Component level mixins
import Autocomplete from './mixins/select-autocomplete'
import Generators from './mixins/select-generators'

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

// Helpers
import { getObjectValueByPath } from '../../util/helpers'

export default {
  name: 'v-select',

  inheritAttrs: false,

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

  directives: {
    ClickOutside
  },

  mixins: [Autocomplete, Colorable, Dependent, Filterable, Generators, Input, Maskable],

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

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

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

      if (this.hasError) {
        classes['error--text'] = true
      } else {
        return this.addTextColorClassChecks(classes)
      }

      return classes
    },
    computedContentClass () {
      const children = [
        'menu__content--select',
        this.auto ? 'menu__content--auto' : '',
        this.isDropdown ? 'menu__content--dropdown' : '',
        this.isAutocomplete ? 'menu__content--autocomplete' : ''
      ]

      return children.join(' ')
    },
    computedItems () {
      return this.filterDuplicates(this.cachedItems.concat(this.items))
    },
    /**
     * The range of the current input text
     *
     * @return {Number}
     */
    currentRange () {
      return this.getText(this.selectedItem || '').length
    },
    filteredItems () {
      // If we are not actively filtering
      // Show all available items
      const items = this.isNotFiltering
        ? this.computedItems
        : this.filterSearch()

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

        this.lazySearch = val

        clearTimeout(this.searchTimeout)

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

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

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

      this.$emit('input', val)

      // When inputValue is changed
      // and combobox is true set
      // menu property to false
      if (this.combobox) this.menuIsActive = false
    },
    isActive (val) {
      if (val) {
        this.searchValue = this.getText(this.selectedItem)
        return
      }

      if (this.tags && this.searchValue) {
        this.updateTags(this.searchValue)
      }

      this.searchValue = null
      this.menuIsActive = false
      this.isFocused = false
      this.selectedIndex = -1

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

      this.resetMenuIndex()

      this.searchValue && this.$nextTick(() => this.setMenuIndex(0))

      this.genSelectedItems()
    },
    menuIsActive (val) {
      if (!val) return

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

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

      // Only reset list index
      // if typing in search
      val && this.resetMenuIndex()

      this.$nextTick(() => {
        val ? this.setMenuIndex(0) : this.resetMenuIndex()
      })
    },
    selectedItems () {
      clearTimeout(this.searchTimeout)

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

  mounted () {
    // If instance is being destroyed
    // do not run mounted functions
    if (this._isDestroyed) return

    // Evaluate the selected items immediately
    // to avoid a unnecessary label transition
    this.genSelectedItems()

    this.content = this.$refs.menu.$refs.content
  },

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

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

      const indexes = this.selectedItems.length - 1

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

      // backspace/delete
      if ([8, 46].includes(keyCode)) {
        const newIndex = this.selectedIndex === indexes
          ? this.selectedIndex - 1
          : this.selectedItems[this.selectedIndex + 1]
            ? this.selectedIndex
            : -1

        this.selectItem(this.selectedItems[this.selectedIndex])
        this.selectedIndex = newIndex
      }
    },
    compareObjects (a, b) {
      const aProps = Object.keys(a)
      const bProps = Object.keys(b)

      if (aProps.length !== bProps.length) return false

      for (let i = 0, length = aProps.length; i < length; i++) {
        const propName = aProps[i]

        if (a[propName] !== b[propName]) return false
      }

      return true
    },
    filterDuplicates (arr) {
      const values = arr.map(this.getValue)
      return arr.filter((el, i) => i === values.indexOf(values[i]))
    },
    focus () {
      this.isActive = true
      this.isFocused = true

      if (this.$refs.input && this.isAutocomplete) {
        this.$refs.input.focus()
        this.$nextTick(() => {
          this.$refs.input.select()
          this.shouldBreak && (
            this.$refs.input.scrollLeft = this.$refs.input.scrollWidth
          )
        })
      } else {
        this.$el.focus()
      }

      this.$emit('focus')
    },
    genDirectives () {
      return [{
        name: 'click-outside',
        value: e => {
          return (
            this.$refs.menu &&
            !this.$refs.menu.$refs.content.contains(e.target)
          )
        }
      }]
    },
    genListeners () {
      const listeners = Object.assign({}, this.$listeners)
      delete listeners.input

      return {
        ...listeners,
        click: () => {
          if (this.disabled || this.readonly) return
          this.showMenuItems()
          this.selectedIndex = -1
        },
        focus: () => {
          if (this.disabled || this.readonly) return

          !this.isFocused && this.focus()
        },
        keydown: this.onKeyDown // Located in mixins/select-autocomplete.js
      }
    },
    genLabel () {
      const singleLine = this.singleLine || this.isDropdown
      if (singleLine && this.isDirty ||
        singleLine && this.isFocused && this.searchValue
      ) return null

      const data = {}

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

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

      const value = getObjectValueByPath(item, field)

      return typeof value === 'undefined' ? item : value
    },
    genSelectedItems (val = this.inputValue) {
      // If we are using tags, don't filter results
      if (this.tags) return (this.selectedItems = val)

      // Combobox is the single version
      // of a taggable select element
      if (this.combobox) return (this.selectedItems = val ? [val] : [])

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

            if (a !== Object(a)) return a === b

            return this.compareObjects(a, b)
          }) !== undefined
        }
      })

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

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

      this.inputValue = inputValue
      this.searchValue = null
      this.$emit('change', inputValue)
      this.genSelectedItems()
      setTimeout(this.showMenu, 0)
    },
    showMenu () {
      this.showMenuItems()
      this.isAutocomplete && this.focus()
    },
    onScroll () {
      if (!this.isActive) {
        requestAnimationFrame(() => (this.content.scrollTop = 0))
      } else {
        if (this.lastItem >= this.computedItems.length) return

        const showMoreItems = (
          this.content.scrollHeight -
          (this.content.scrollTop +
          this.content.clientHeight)
        ) < 200

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

          if (a !== Object(a)) return a === b

          return this.compareObjects(a, b)
        })

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

        this.selectedItems = selectedItems
      }

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

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

      // List tile will re-render, reset index to
      // maintain highlighting
      const savedIndex = this.getMenuIndex()
      this.resetMenuIndex()

      // After selecting an item
      // refocus the input and
      // reset the caret pos
      this.$nextTick(() => {
        this.focus()
        this.setCaretPosition(this.currentRange)

        requestAnimationFrame(() => {
          (!this.isAutocomplete || this.searchValue) ? this.setMenuIndex(savedIndex) : this.resetMenuIndex()
        })
      })
    },
    showMenuItems () {
      this.isActive = true
      this.menuIsActive = true
      this.chips && this.resetMenuIndex()
    }
  },

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

    if (!this.isAutocomplete) {
      data.on = this.genListeners()
      data.directives = this.genDirectives()
    } else {
      data.on = {
        click: () => {
          if (this.disabled || this.readonly) return

          // Workaround for clicking select
          // when using autocomplete
          // and click doesn't target the input
          setTimeout(() => {
            if (this.menuIsActive) return

            this.focus()
            this.menuIsActive = true
          }, 100)
        }
      }
    }

    return this.genInputGroup([
      this.genSelectionsAndSearch(),
      this.genMenu()
    ], data, () => this.showMenu())
  }
}


import VCheckbox from './VCheckbox'

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

export default VCheckbox


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

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

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


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

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

`;

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

`;


if (this.combobox) this.menuIsActive = false

keep keep keep keep replace keep keep keep

if (!this.chips) this.searchValue = this.getText(this.selectedItem)


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

export default {
  name: 'v-content',

  props: {
    tag: {
      type: String,
      default: 'main'
    }
  },

  computed: {
    styles () {
      const {
        bar, top, right, bottom, left
      } = this.$vuetify.application

      return {
        paddingTop: `${top + bar}px`,
        paddingRight: `${right}px`,
        paddingBottom: `${bottom}px`,
        paddingLeft: `${left}px`
      }
    }
  },

  mounted () {
    // TODO: Deprecate
    if (this.$el.parentElement.tagName === 'MAIN') {
      console.warn('v-content no longer needs to be wrapped in a <main> tag', this.$el.parentElement)
    }
  },

  render (h) {
    const data = {
      staticClass: 'content',
      style: this.styles
    }

    return h('div', {
      staticClass: 'content--wrap'
    }, [
      h(this.tag, data, this.$slots.default)
    ])
  }
}


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

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

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

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

    &--active:after
      background-color: currentColor

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

    &--active
      color: inherit

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

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

theme(switch, "switch")

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

  &.switch
    min-width: 36px

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

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

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

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

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

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

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

    label
      padding-left: 14px


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

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

      radio_button_unchecked

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

`;

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

    Test

      radio_button_unchecked

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

`;

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

      radio_button_unchecked

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

`;

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

      radio_button_unchecked

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

`;


this.searchValue && (this.searchValue = null)


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

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

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

  it('should render component with specific padding applied', () => {
    const wrapper = mount(VCardTitle, functionalContext({
      props: {
        'primary-title': true
      }
    }))

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


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

stepper($material)
  background: $material.bg-color

  .stepper__step
    &:not(.stepper__step--active):not(.stepper__step--complete):not(.stepper__step--error)
      .stepper__step__step
        background: rgba($material.fg-color, $material.disabledORhints-text-percent)

    &__step
      color: $material.stepper.active

      .icon
        color: $material.stepper.active

  .stepper__header
    .divider
      background: rgba($material.fg-color, $material.divider-percent)

  .stepper__step
    &--active
      .stepper__label
        text-shadow: 0px 0px 0px rgba($material.fg-color, 1)

    &--editable:hover
      background: rgba($material.fg-color, .06)

      .stepper__label
        text-shadow: 0px 0px 0px rgba($material.fg-color, 1)

    &--complete
      .stepper__label
        color: $material.stepper.completed

    &--inactive
      &.stepper__step--editable
        &:hover
          .stepper__step__step
            background: $material.stepper.hover

  .stepper__label
    color: rgba($material.fg-color, $material.disabledORhints-text-percent)

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

  &--non-linear
    .stepper__step:not(.stepper__step--complete)
      .stepper__label
        color: rgba($material.fg-color, $material.secondary-text-percent)

  &--vertical
    .stepper__content:not(:last-child)
      border-left: 1px solid rgba($material.fg-color, $material.divider-percent)

theme(stepper, "stepper")

.stepper
  overflow: hidden
  position: relative
  elevation(2)

.stepper
  &__header
    align-items: stretch
    display: flex
    flex-wrap: wrap
    justify-content: space-between
    elevation(2)

    .divider
      align-self: center
      margin: 0 -16px

.stepper
  &__items
    position: relative
    overflow: hidden

.stepper
  &__step__step
    align-items: center
    border-radius: 50%
    display: inline-flex
    font-size: 12px
    justify-content: center
    height: 24px
    margin-right: 8px
    min-width: 24px
    width: 24px
    transition: .3s $transition.fast-in-fast-out

    .icon
      font-size: 18px

.stepper
  &__step
    align-items: center
    display: flex
    flex-direction: row
    padding: 24px
    position: relative

    &--active
      .stepper__label
        transition: .3s $transition.ease-in-out

    &--editable
      cursor: pointer

    &.stepper__step--error
      .stepper__step__step
        background: transparent
        color: inherit

        .icon
          font-size: 24px
          color: inherit

      .stepper__label
        color: inherit
        text-shadow: none
        font-weight: 500

        small
          color: inherit

.stepper
  &__label
    align-items: flex-start
    display: flex
    flex-direction: column
    text-align: left

    small
      font-size: 12px
      font-weight: 300
      text-shadow: none

.stepper
  &__wrapper
    overflow: hidden
    transition: none

.stepper
  &__content
    top: initial
    bottom: 0
    padding: 16px
    flex: 1
    // Chrome has an issue with overflow hidden for rendering
    // Originally used translateZ but this messes up fixed
    // elements within the stepper
    // Fix for #512 and #620
    // overflow: hidden
    width: 100%

    .btn
      margin-left: 0

  &--is-booted
    .stepper__content, .stepper__wrapper
      transition: .4s $transition.ease-in-out

.stepper
  &--vertical
    padding-bottom: 36px

    .stepper__content
      margin: -8px -36px -16px 36px
      padding: 16px 60px 16px 23px
      width: auto

    .stepper__step
      padding: 24px 24px 16px

    .stepper__step__step
      margin-right: 12px

  &--alt-labels
    .stepper__header
      .divider
        margin: 35px -67px 0
        align-self: flex-start

    .stepper__step
      flex-direction: column
      justify-content: flex-start
      align-items: center
      flex-basis: 175px

      small
        align-self: center

    .stepper__step__step
      margin-right: 0
      margin-bottom: 12px

@media $display-breakpoints.sm-and-down
  .stepper:not(.stepper--vertical)
    .stepper__label
      display: none

    .stepper__step__step
      margin-right: 0


.browser-list
  padding-left: $spacers.four.x

  &--unstyled
    list-style-type: none


// Wrap input to next line if overflowing

if (this.$refs.input.scrollWidth > this.$refs.input.clientWidth) {


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

/* Theme */
datatable($material)
  thead
    th.column.sortable
      i
        color: rgba($material.fg-color, $material.disabledORhints-text-percent)

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

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

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

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

    &__select
      .input-group--select
        .input-group__selections__comma
          color: rgba($material.fg-color, $material.secondary-text-percent) !important

        .input-group__append-icon
          color: rgba($material.fg-color, $material.secondary-text-percent) !important

theme(datatable, "datatable")

.datatable
  thead
    th.column.sortable
      cursor: pointer

      i
        font-size: 16px
        vertical-align: sub
        display: inline-block
        opacity: 0
        transition: .3s $transition.swing

      &:hover
        i
          opacity: .6

      &.active
        transform: none

        i
          opacity: 1

        &.desc
          i
            transform: rotate(-180deg)

/** Actions */
.datatable__actions
  display: flex
  justify-content: flex-end
  align-items: center
  font-size: 12px

  .btn
    color: inherit

    &:last-of-type
      margin-left: 18px

  &__pagination
    text-align: center
    margin: 0 26px 0 32px

  &__select
    display: flex
    align-items: center
    justify-content: center

    .input-group--select
      margin: 13px 0 13px 34px
      padding: 0
      position: initial

      .input-group__selections__comma
        font-size: 12px

.datatable__progress
  height: auto !important

  tr, td, th
    height: auto !important

  th
    padding: 0 !important

    .progress-linear
      top: -2px
      margin: 0 0 -2px

.datatable__expand
  &-row
    border: none !important

  &-col
    padding: 0 !important
    height: 0px !important

    &--expanded
      border-bottom: 1px solid rgba(0,0,0,0.12)

  &-content
    transition: height $primary-transition

    > .card
      border-radius: 0
      box-shadow: none


/**
 * Select autocomplete
 *
 * @mixin
 *
 * Handles logic when using the "autocomplete" prop
 */
export default {
  props: {
    filter: {
      type: Function,
      default: (item, queryText, itemText) => {
        const hasValue = val => val != null ? val : ''

        const text = hasValue(itemText)
        const query = hasValue(queryText)

        return text.toString()
          .toLowerCase()
          .indexOf(query.toString().toLowerCase()) > -1
      }
    }
  },

  methods: {
    filterSearch () {
      if (!this.isAutocomplete) return this.computedItems

      return this.computedItems.filter(i => this.filter(
        i, this.searchValue, this.getText(i))
      )
    },
    genFiltered (text) {
      if (!this.isAutocomplete ||
        !this.searchValue ||
        this.filteredItems.length < 1
      ) return text

      text = (text || '').toString()

      const { start, middle, end } = this.getMaskedCharacters(text)

      return `${start}${this.genHighlight(middle)}${end}`
    },
    genHighlight (text) {
      if (this.isNotFiltering) return text

      return `<span class="list__tile__mask">${text}</span>`
    },
    getMaskedCharacters (text) {
      const searchValue = (this.searchValue || '').toString().toLowerCase()
      const index = text.toLowerCase().indexOf(searchValue)

      if (index < 0) return { start: '', middle: text, end: '' }

      const start = text.slice(0, index)
      const middle = text.slice(index, index + searchValue.length)
      const end = text.slice(index + searchValue.length)
      return { start, middle, end }
    },
    getCurrentTag () {
      return this.isMenuItemSelected()
        ? this.filteredItems[this.getMenuIndex()]
        : (this.isAnyValueAllowed ? this.searchValue : null)
    },
    tabOut () {
      this.blur()

      if (this.isAutocomplete && !this.isMultiple && !this.searchValue) {
        // Single (not multiple) autocomplete select with an
        // empty search value should clear the input value
        this.inputValue = null
      } else if (this.combobox) {
        // For combo box use selected
        // menu item or searchValue
        this.inputValue = this.getCurrentTag()
      }
    },
    onTabDown (e) {
      // If tabbing through inputs and
      // and there is no need for an
      // update, blur the v-select
      if (!this.isAutocomplete || !this.getCurrentTag() || this.combobox) return this.tabOut()

      // When adding tags, if searching and
      // there is not a filtered options,
      // add the value to the tags list
      if (this.tags &&
        this.searchValue &&
        !this.filteredItems.length
      ) {
        e.preventDefault()

        return this.updateTags(this.searchValue)
      }

      // An item that is selected by
      // menu-index should toggled
      if (this.menuIsActive) {
        e.preventDefault()
        this.selectListTile(this.getMenuIndex())
      }
    },
    onEnterDown () {
      this.updateTags(this.getCurrentTag())
    },
    onEscDown (e) {
      e.preventDefault()
      this.menuIsActive = false
    },
    onKeyDown (e) {
      // If enter, space, up, or down is pressed, open menu
      if (!this.menuIsActive && [13, 32, 38, 40].includes(e.keyCode)) {
        e.preventDefault()
        return this.showMenuItems()
      }

      // If escape deactivate the menu
      if (e.keyCode === 27) return this.onEscDown(e)

      // If tab - select item or close menu
      if (e.keyCode === 9) return this.onTabDown(e)

      if (!this.isAutocomplete ||
        ![32].includes(e.keyCode) // space
      ) this.$refs.menu.changeListIndex(e)

      // Up or down
      if ([38, 40].includes(e.keyCode)) this.selectedIndex = -1

      if (this.isAutocomplete &&
        !this.hideSelections &&
        !this.searchValue
      ) this.changeSelectedIndex(e.keyCode)

      if (!this.tags || !this.searchValue) return

      // Enter
      if (e.keyCode === 13) return this.onEnterDown()

      // Left arrow
      if (e.keyCode === 37 && this.$refs.input.selectionStart === 0 && this.selectedItems.length) {
        this.updateTags(this.searchValue)
        this.$nextTick(() => {
          this.selectedIndex = Math.max(this.selectedItems.length - 2, 0)
        })
      }

      // Right arrow
      if (e.keyCode === 39 && this.$refs.input.selectionEnd === this.searchValue.length) {
        this.resetMenuIndex()
      }
    },
    selectListTile (index) {
      if (!this.$refs.menu.tiles[index]) return

      this.$refs.menu.tiles[index].click()
    },
    updateTags (content) {
      // If a duplicate item
      // exists, remove it
      if (this.selectedItems.includes(content)) {
        this.$delete(this.selectedItems, this.selectedItems.indexOf(content))
      }

      this.selectedItems.push(content)

      this.$nextTick(() => {
        this.searchValue = null
        this.$emit('change', this.selectedItems)
      })
    }
  }
}


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

test('VApp.js', ({ mount }) => {
  it('should match a snapshot', () => {
    const wrapper = mount(VApp)

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

  it('should have data-app attribute', () => {
    const wrapper = mount(VApp)
    const app = wrapper.find('.application')[0]

    expect(app.getAttribute('data-app')).toBe('true')
  })

  it('should allow a custom id', () => {
    const wrapper = mount(VApp, {
      propsData: {
        id: 'inspire'
      }
    })
    const app = wrapper.find('.application')[0]

    expect(app.getAttribute('id')).toBe('inspire')
    expect(wrapper.html()).toMatchSnapshot()
  })
})


}
// Only reset list index
// if typing in search
val && this.resetMenuIndex()
this.$nextTick(() => {


import VSelect from './VSelect'

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

export default VSelect


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

exports[`VBtn.js should render a <button> tag when using to prop 1`] = `

<button class="btn btn--router"
        data-ripple="true"
>
</button>

`;

exports[`VBtn.js should render an <a> tag when using href prop 1`] = `

`;

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

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

</button>

`;

exports[`VBtn.js should render specified tag when using tag prop 1`] = `

`;


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

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

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

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

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

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

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

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

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

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

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

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

    btn.trigger('click')
    expect(change).toBeCalledWith(0)
  })
})


val ? this.setMenuIndex(0) : this.resetMenuIndex()

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

/**
 * Select generators
 *
 * @mixin
 *
 * Used for creating the DOM elements for VSelect
 */
export default {
  computed: {
    menuItems () {
      return this.isHidingSelected ? this.filteredItems.filter(o => {
        return (this.selectedItems || []).indexOf(o) === -1
      }) : this.filteredItems
    }
  },

  methods: {
    genMenu () {
      const offsetY = this.isAutocomplete || this.offset || this.isDropdown
      let nudgeTop = 0

      if (this.auto) nudgeTop = -18
      else if (this.solo) nudgeTop = 0
      else if (this.isDropdown) nudgeTop = 26
      else if (offsetY) nudgeTop = 24

      const data = {
        ref: 'menu',
        props: {
          activator: this.$el,
          auto: this.auto,
          closeOnClick: false,
          closeOnContentClick: !this.isMultiple,
          contentClass: this.computedContentClass,
          disabled: this.disabled,
          maxHeight: this.maxHeight,
          nudgeTop,
          offsetY,
          offsetOverflow: this.isAutocomplete,
          openOnClick: false,
          value: this.menuIsActive &&
            this.computedItems.length &&
            (!this.tags || this.filteredItems.length > 0) &&
            (!this.combobox || this.filteredItems.length > 0),
          zIndex: this.menuZIndex
        },
        on: {
          input: val => {
            if (!val) {
              this.menuIsActive = false
            }
          }
        }
      }

      if (this.isAutocomplete) data.props.transition = ''

      this.minWidth && (data.props.minWidth = this.minWidth)

      return this.$createElement('v-menu', data, [this.genList()])
    },
    getMenuIndex () {
      return this.$refs.menu ? this.$refs.menu.listIndex : -1
    },
    setMenuIndex (index) {
      this.$refs.menu && (this.$refs.menu.listIndex = index)
    },
    resetMenuIndex () {
      this.setMenuIndex(-1)
    },
    isMenuItemSelected () {
      return this.menuIsActive && this.menuItems.length && this.getMenuIndex() > -1
    },
    genSelectionsAndSearch () {
      return this.$createElement('div', {
        'class': 'input-group__selections',
        style: { 'overflow': 'hidden' },
        ref: 'activator'
      }, [
        ...this.genSelections(),
        this.genSearch()
      ])
    },
    genSelections () {
      if (this.hideSelections) return []

      const children = []
      const chips = this.chips
      const slots = this.$scopedSlots.selection
      const length = this.selectedItems.length
      this.selectedItems.forEach((item, i) => {
        if (slots) {
          children.push(this.genSlotSelection(item, i))
        } else if (chips) {
          children.push(this.genChipSelection(item, i))
        } else if (this.segmented) {
          children.push(this.genSegmentedBtn(item, i))
        } else {
          children.push(this.genCommaSelection(item, i < length - 1, i))
        }
      })

      return children
    },
    genSearch () {
      const data = {
        staticClass: 'input-group--select__autocomplete',
        'class': {
          'input-group--select__autocomplete--index': this.selectedIndex > -1
        },
        style: {
          flex: this.shouldBreak ? '1 0 100%' : null
        },
        attrs: {
          ...this.$attrs,
          disabled: this.disabled || !this.isAutocomplete,
          readonly: this.readonly,
          tabindex: this.disabled || !this.isAutocomplete ? -1 : this.tabindex
        },
        domProps: {
          value: this.maskText(this.lazySearch || '')
        },
        directives: [{
          name: 'show',
          value: (this.isAutocomplete) ||
            (this.placeholder && !this.selectedItems.length)
        }],
        ref: 'input',
        key: 'input'
      }

      if (this.isAutocomplete) {
        data.attrs.role = 'combobox'
        data.domProps.autocomplete = this.browserAutocomplete

        data.on = {
          ...this.genListeners(),
          input: e => {
            this.searchValue = this.unmaskText(e.target.value)
          }
        }

        if (this.combobox) {
          // When using the combobox
          // update inputValue and
          // set the menu status
          data.on.blur = () => {
            this.inputValue = this.lazySearch
          }
        }

        data.directives = data.directives.concat(this.genDirectives())
      }

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

      return this.$createElement('input', data)
    },
    genSegmentedBtn (item) {
      if (!item.text || !item.callback) {
        console.warn('[Vuetify] Warn: When using the v-select component with \'segmented\' prop without a selection slot, items must contain both a text and callback property')
        return null
      }

      return this.$createElement('v-btn', {
        props: {
          flat: true
        },
        on: {
          click (e) {
            e.stopPropagation()
            item.callback(e)
          }
        }
      }, [item.text])
    },
    genSlotSelection (item, index) {
      return this.$scopedSlots.selection({
        parent: this,
        item,
        index,
        selected: index === this.selectedIndex,
        disabled: this.disabled || this.readonly
      })
    },
    genChipSelection (item, index) {
      const isDisabled = this.disabled || this.readonly
      const click = e => {
        if (isDisabled) return

        e.stopPropagation()
        this.focus()
        this.selectedIndex = index
      }

      return this.$createElement('v-chip', {
        staticClass: 'chip--select-multi',
        attrs: { tabindex: '-1' },
        props: {
          close: !isDisabled,
          dark: this.dark,
          disabled: isDisabled,
          selected: index === this.selectedIndex
        },
        on: {
          click: click,
          focus: click,
          input: () => this.selectItem(item)
        },
        key: this.getValue(item)
      }, this.getText(item))
    },
    genCommaSelection (item, comma, index) {
      return this.$createElement('div', {
        staticClass: 'input-group__selections__comma',
        'class': {
          'input-group__selections__comma--active': index === this.selectedIndex
        },
        key: JSON.stringify(this.getValue(item)) // Item may be an object
      }, `${this.getText(item)}${comma ? ', ' : ''}`)
    },
    genList () {
      const children = this.menuItems.map(o => {
        if (o.header) return this.genHeader(o)
        if (o.divider) return this.genDivider(o)
        else return this.genTile(o)
      })

      if (!children.length) {
        const noData = this.$slots['no-data']
        if (noData) {
          children.push(noData)
        } else {
          children.push(this.genTile(this.noDataText, true))
        }
      }

      return this.$createElement('v-card', [
        this.$createElement('v-list', {
          ref: 'list'
        }, children)
      ])
    },
    genHeader (item) {
      return this.$createElement('v-subheader', {
        props: item
      }, item.header)
    },
    genDivider (item) {
      return this.$createElement('v-divider', {
        props: item
      })
    },
    genTile (item, disabled) {
      const active = this.selectedItems.indexOf(item) !== -1

      if (typeof disabled === 'undefined') {
        disabled = getObjectValueByPath(item, this.itemDisabled)
      }

      const data = {
        on: {
          click: e => {
            if (disabled) return

            this.selectItem(item)
          }
        },
        props: {
          avatar: item === Object(item) && this.itemAvatar in item,
          ripple: true,
          value: active
        }
      }

      if (disabled) {
        data.props.disabled = disabled
      }

      data.props.activeClass = Object.keys(this.addTextColorClassChecks()).join(' ')

      if (this.$scopedSlots.item) {
        return this.$createElement('v-list-tile', data,
          [this.$scopedSlots.item({ parent: this, item })]
        )
      }

      return this.$createElement('v-list-tile', data,
        [this.genAction(item, active), this.genContent(item)]
      )
    },
    genAction (item, active) {
      if (!this.isMultiple || this.isHidingSelected) return null

      const data = {
        staticClass: 'list__tile__action--select-multi',
        on: {
          click: e => {
            e.stopPropagation()
            this.selectItem(item)
          }
        }
      }

      return this.$createElement('v-list-tile-action', data, [
        this.$createElement('v-checkbox', {
          props: {
            color: this.computedColor,
            inputValue: active
          }
        })
      ])
    },
    genContent (item) {
      const text = this.getText(item)

      return this.$createElement('v-list-tile-content',
        [this.$createElement('v-list-tile-title', {
          domProps: {
            innerHTML: this.genFiltered(text)
          }
        })]
      )
    }
  }
}


.ripple
  &__container
    color: inherit
    border-radius: inherit
    position: absolute
    width: 100%
    height: 100%
    left: 0
    top: 0
    overflow: hidden
    z-index: 0
    pointer-events: none
    contain: strict

  &__animation
    color: inherit
    position: absolute
    top: 0
    left: 0
    border-radius: 50%
    background: currentColor
    opacity: 0
    transition: $ripple-animation-transition
    pointer-events: none
    overflow: hidden
    will-change: transform, opacity

    &--enter
      transition: none

    &--visible
      opacity: $ripple-animation-visible-opacity


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

import VIcon from '../VIcon'
import Colorable from '../../mixins/colorable'
import Themeable from '../../mixins/themeable'
import Toggleable from '../../mixins/toggleable'

export default {
  name: 'v-chip',

  components: {
    VIcon
  },

  mixins: [Colorable, Themeable, Toggleable],

  props: {
    close: Boolean,
    disabled: Boolean,
    label: Boolean,
    outline: Boolean,
    // Used for selects/tagging
    selected: Boolean,
    small: Boolean,
    textColor: String,
    value: {
      type: Boolean,
      default: true
    }
  },

  computed: {
    classes () {
      const classes = this.addBackgroundColorClassChecks({
        'chip--disabled': this.disabled,
        'chip--selected': this.selected,
        'chip--label': this.label,
        'chip--outline': this.outline,
        'chip--small': this.small,
        'chip--removable': this.close,
        'theme--light': this.light,
        'theme--dark': this.dark
      })

      return (this.textColor || this.outline)
        ? this.addTextColorClassChecks(classes, this.textColor ? 'textColor' : 'color')
        : classes
    }
  },

  methods: {
    genClose (h) {
      const data = {
        staticClass: 'chip__close',
        on: {
          click: e => {
            e.stopPropagation()

            this.$emit('input', false)
          }
        }
      }

      return h('div', data, [
        h(VIcon, { props: { right: true } }, 'cancel')
      ])
    },
    genContent (h) {
      const children = [this.$slots.default]

      this.close && children.push(this.genClose(h))

      return h('span', {
        staticClass: 'chip__content'
      }, children)
    }
  },

  render (h) {
    const data = {
      staticClass: 'chip',
      'class': this.classes,
      attrs: { tabindex: this.disabled ? -1 : 0 },
      directives: [{
        name: 'show',
        value: this.isActive
      }],
      on: this.$listeners
    }

    return h('span', data, [this.genContent(h)])
  }
}


import VSwitch from './VSwitch'

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

export default VSwitch