<commit_before>/**
 *    Copyright 2015-2017 ppy Pty. Ltd.
 *
 *    This file is part of osu!web. osu!web is distributed with the hope of
 *    attracting more community contributions to the core ecosystem of osu!.
 *
 *    osu!web is free software: you can redistribute it and/or modify
 *    it under the terms of the Affero GNU General Public License version 3
 *    as published by the Free Software Foundation.
 *
 *    osu!web is distributed WITHOUT ANY WARRANTY; without even the implied
 *    warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *    See the GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with osu!web.  If not, see <http://www.gnu.org/licenses/>.
 */

const { mix } = require('laravel-mix');
const path = require('path');

// .js doesn't support globbing by itself, so we need to glob
// and spread the values in.
const glob = require('glob');
let min = '';
if (mix.config.inProduction) {
  min = '.min';
}

// relative from root?
const node_root = 'node_modules';

mix
.webpackConfig({
  module: {
    rules: [
      { test: /\.coffee$/, loader: 'coffee-loader' }
    ]
  }
})
.copy('node_modules/font-awesome/fonts', 'public/vendor/fonts/font-awesome')
.copy('node_modules/photoswipe/dist/default-skin', 'public/vendor/_photoswipe-default-skin')
.copy('node_modules/timeago/locales', 'public/vendor/js/timeago-locales')
.less('resources/assets/less/app.less', 'public/build/css')
.js([
  ...glob.sync('resources/assets/coffee/_classes/*.coffee'),

  ...glob.sync('resources/assets/coffee/react-namespaces.coffee'),
  ...glob.sync('resources/assets/coffee/react/_components/*.coffee'),
  ...glob.sync('resources/assets/coffee/react/_mixins/*.coffee'),

  'resources/assets/coffee/jquery-pubsub.coffee',
  'resources/assets/coffee/osu!live.coffee',
  'resources/assets/coffee/osu_common.coffee',

  'resources/assets/coffee/navbar-mobile.coffee',
  'resources/assets/coffee/spoilerbox.coffee',
  'resources/assets/coffee/store.coffee',
  'resources/assets/coffee/forum/post-box.coffee',
  'resources/assets/coffee/forum/topic-ajax.coffee',
  'resources/assets/coffee/ujs-common.coffee',
  'resources/assets/coffee/bootstrap-modal.coffee',
  'resources/assets/coffee/logout.coffee',
  'resources/assets/coffee/shared.coffee',
  'resources/assets/coffee/turbolinks-overrides.coffee',

  'resources/assets/coffee/main.coffee'
], 'resources/assets/js/build/app-main.js')
.scripts([
  path.join(node_root, 'turbolinks/dist/turbolinks.js'),
  path.join(node_root, 'jquery/dist/jquery' + min + '.js'),
  path.join(node_root, 'jquery-ujs/src/rails.js'),
  path.join(node_root, 'qtip2/dist/jquery.qtip' + min + '.js'),
  path.join(node_root, 'jquery.scrollto/jquery.scrollTo.js'),
  path.join(node_root, 'jquery-ui/ui/data.js'),
  path.join(node_root, 'jquery-ui/ui/scroll-parent.js'),
  path.join(node_root, 'jquery-ui/ui/widget.js'),
  path.join(node_root, 'jquery-ui/ui/widgets/mouse.js'),
  path.join(node_root, 'jquery-ui/ui/widgets/sortable.js'),
  path.join(node_root, 'timeago/jquery.timeago.js'),
  path.join(node_root, 'blueimp-file-upload/js/jquery.fileupload.js'),
  path.join(node_root, 'bootstrap/dist/js/bootstrap.js'),
  path.join(node_root, 'lodash/lodash.js'),
  path.join(node_root, 'layzr.js/dist/layzr.js'),
  path.join(node_root, 'react/dist/react-with-addons' + min + '.js'),
  path.join(node_root, 'react-dom/dist/react-dom' + min + '.js'),
  path.join(node_root, 'photoswipe/dist/photoswipe.js'),
  path.join(node_root, 'photoswipe/dist/photoswipe-ui-default.js'),
  path.join(node_root, 'd3/build/d3' + min + '.js'),
  path.join(node_root, 'moment/moment.js'),
  path.join(node_root, 'js-cookie/src/js.cookie.js'),

  path.join(node_root, 'react-height/build/react-height' + min + '.js'),
  path.join(node_root, 'react-motion/build/react-motion.js'),
  path.join(node_root, 'react-collapse/build/react-collapse' + min + '.js'),
  path.join(node_root, 'react-disqus-thread/dist/react-disqus-thread' + min + '.js'),
], 'public/js/vendor.js')
.scripts([
  'resources/assets/js/ga.js',
  'resources/assets/js/messages.js',
  'resources/assets/js/laroute.js',
  'resources/assets/js/build/app-main.js',
], 'public/js/app.js')
.js([
  ...glob.sync('resources/assets/coffee/react/profile-page/*.coffee'),
  'resources/assets/coffee/react/profile-page.coffee',
], 'public/js/react/profile-page.js')
.js([
  ...glob.sync('resources/assets/coffee/react/beatmaps/*.coffee'),
  'resources/assets/coffee/react/beatmaps.coffee'
], 'public/js/react/beatmaps.js')
.js([
  ...glob.sync('resources/assets/coffee/react/status-page/*.coffee'),
  'resources/assets/coffee/react/status-page.coffee'
], 'public/js/react/status-page.js')
.js([
  ...glob.sync('resources/assets/coffee/react/beatmap-discussions/*.coffee'),
  'resources/assets/coffee/react/beatmap-discussions.coffee'
], 'public/js/react/beatmap-discussions.js')
.js([
  ...glob.sync('resources/assets/coffee/react/beatmapset-page/*.coffee'),
  'resources/assets/coffee/react/beatmapset-page.coffee'
], 'public/js/react/beatmapset-page.js')
.js([
  ...glob.sync('resources/assets/coffee/react/mp-history/*.coffee'),
  'resources/assets/coffee/react/mp-history.coffee'
], 'public/js/react/mp-history.js')
.js([
  'resources/assets/coffee/react/artist-page.coffee',
], 'public/js/react/artist-page.js')
.js([
  // 'resources/assets/coffee/react/contest/voting/_base-entry-list.coffee',
  ...glob.sync('resources/assets/coffee/react/contest/voting/*.coffee'),
  'resources/assets/coffee/react/contest-voting.coffee',
], 'public/js/react/contest-voting.js')
.js([
  ...glob.sync('resources/assets/coffee/react/contest/entry/*.coffee'),
  'resources/assets/coffee/react/contest-entry.coffee',
], 'public/js/react/contest-entry.js')
.version([
  'public/build/css/app.css'
])
.sourceMaps(!mix.config.inProduction);
<commit_msg>Don't actually need to put anything in `version()`<commit_after>/**
 *    Copyright 2015-2017 ppy Pty. Ltd.
 *
 *    This file is part of osu!web. osu!web is distributed with the hope of
 *    attracting more community contributions to the core ecosystem of osu!.
 *
 *    osu!web is free software: you can redistribute it and/or modify
 *    it under the terms of the Affero GNU General Public License version 3
 *    as published by the Free Software Foundation.
 *
 *    osu!web is distributed WITHOUT ANY WARRANTY; without even the implied
 *    warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *    See the GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with osu!web.  If not, see <http://www.gnu.org/licenses/>.
 */

const { mix } = require('laravel-mix');
const path = require('path');

// .js doesn't support globbing by itself, so we need to glob
// and spread the values in.
const glob = require('glob');
let min = '';
if (mix.config.inProduction) {
  min = '.min';
}

// relative from root?
const node_root = 'node_modules';

mix
.webpackConfig({
  module: {
    rules: [
      { test: /\.coffee$/, loader: 'coffee-loader' }
    ]
  }
})
.copy('node_modules/font-awesome/fonts', 'public/vendor/fonts/font-awesome')
.copy('node_modules/photoswipe/dist/default-skin', 'public/vendor/_photoswipe-default-skin')
.copy('node_modules/timeago/locales', 'public/vendor/js/timeago-locales')
.less('resources/assets/less/app.less', 'public/build/css')
.js([
  ...glob.sync('resources/assets/coffee/_classes/*.coffee'),

  ...glob.sync('resources/assets/coffee/react-namespaces.coffee'),
  ...glob.sync('resources/assets/coffee/react/_components/*.coffee'),
  ...glob.sync('resources/assets/coffee/react/_mixins/*.coffee'),

  'resources/assets/coffee/jquery-pubsub.coffee',
  'resources/assets/coffee/osu!live.coffee',
  'resources/assets/coffee/osu_common.coffee',

  'resources/assets/coffee/navbar-mobile.coffee',
  'resources/assets/coffee/spoilerbox.coffee',
  'resources/assets/coffee/store.coffee',
  'resources/assets/coffee/forum/post-box.coffee',
  'resources/assets/coffee/forum/topic-ajax.coffee',
  'resources/assets/coffee/ujs-common.coffee',
  'resources/assets/coffee/bootstrap-modal.coffee',
  'resources/assets/coffee/logout.coffee',
  'resources/assets/coffee/shared.coffee',
  'resources/assets/coffee/turbolinks-overrides.coffee',

  'resources/assets/coffee/main.coffee'
], 'resources/assets/js/build/app-main.js')
.scripts([
  path.join(node_root, 'turbolinks/dist/turbolinks.js'),
  path.join(node_root, 'jquery/dist/jquery' + min + '.js'),
  path.join(node_root, 'jquery-ujs/src/rails.js'),
  path.join(node_root, 'qtip2/dist/jquery.qtip' + min + '.js'),
  path.join(node_root, 'jquery.scrollto/jquery.scrollTo.js'),
  path.join(node_root, 'jquery-ui/ui/data.js'),
  path.join(node_root, 'jquery-ui/ui/scroll-parent.js'),
  path.join(node_root, 'jquery-ui/ui/widget.js'),
  path.join(node_root, 'jquery-ui/ui/widgets/mouse.js'),
  path.join(node_root, 'jquery-ui/ui/widgets/sortable.js'),
  path.join(node_root, 'timeago/jquery.timeago.js'),
  path.join(node_root, 'blueimp-file-upload/js/jquery.fileupload.js'),
  path.join(node_root, 'bootstrap/dist/js/bootstrap.js'),
  path.join(node_root, 'lodash/lodash.js'),
  path.join(node_root, 'layzr.js/dist/layzr.js'),
  path.join(node_root, 'react/dist/react-with-addons' + min + '.js'),
  path.join(node_root, 'react-dom/dist/react-dom' + min + '.js'),
  path.join(node_root, 'photoswipe/dist/photoswipe.js'),
  path.join(node_root, 'photoswipe/dist/photoswipe-ui-default.js'),
  path.join(node_root, 'd3/build/d3' + min + '.js'),
  path.join(node_root, 'moment/moment.js'),
  path.join(node_root, 'js-cookie/src/js.cookie.js'),

  path.join(node_root, 'react-height/build/react-height' + min + '.js'),
  path.join(node_root, 'react-motion/build/react-motion.js'),
  path.join(node_root, 'react-collapse/build/react-collapse' + min + '.js'),
  path.join(node_root, 'react-disqus-thread/dist/react-disqus-thread' + min + '.js'),
], 'public/js/vendor.js')
.scripts([
  'resources/assets/js/ga.js',
  'resources/assets/js/messages.js',
  'resources/assets/js/laroute.js',
  'resources/assets/js/build/app-main.js',
], 'public/js/app.js')
.js([
  ...glob.sync('resources/assets/coffee/react/profile-page/*.coffee'),
  'resources/assets/coffee/react/profile-page.coffee',
], 'public/js/react/profile-page.js')
.js([
  ...glob.sync('resources/assets/coffee/react/beatmaps/*.coffee'),
  'resources/assets/coffee/react/beatmaps.coffee'
], 'public/js/react/beatmaps.js')
.js([
  ...glob.sync('resources/assets/coffee/react/status-page/*.coffee'),
  'resources/assets/coffee/react/status-page.coffee'
], 'public/js/react/status-page.js')
.js([
  ...glob.sync('resources/assets/coffee/react/beatmap-discussions/*.coffee'),
  'resources/assets/coffee/react/beatmap-discussions.coffee'
], 'public/js/react/beatmap-discussions.js')
.js([
  ...glob.sync('resources/assets/coffee/react/beatmapset-page/*.coffee'),
  'resources/assets/coffee/react/beatmapset-page.coffee'
], 'public/js/react/beatmapset-page.js')
.js([
  ...glob.sync('resources/assets/coffee/react/mp-history/*.coffee'),
  'resources/assets/coffee/react/mp-history.coffee'
], 'public/js/react/mp-history.js')
.js([
  'resources/assets/coffee/react/artist-page.coffee',
], 'public/js/react/artist-page.js')
.js([
  // 'resources/assets/coffee/react/contest/voting/_base-entry-list.coffee',
  ...glob.sync('resources/assets/coffee/react/contest/voting/*.coffee'),
  'resources/assets/coffee/react/contest-voting.coffee',
], 'public/js/react/contest-voting.js')
.js([
  ...glob.sync('resources/assets/coffee/react/contest/entry/*.coffee'),
  'resources/assets/coffee/react/contest-entry.coffee',
], 'public/js/react/contest-entry.js')
.version()
.sourceMaps(!mix.config.inProduction);
<|endoftext|>


<commit_before>// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

'use strict';

//#region plugin imports
const Autoprefixer = require('autoprefixer');
const CopyPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const SentryPlugin = require('webpack-sentry-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

//#endregion

// #region non-plugin imports
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');

//#endregion

//#region env
const inProduction = process.env.NODE_ENV === 'production' || process.argv.includes('-p');
const paymentSandbox = !(process.env.PAYMENT_SANDBOX === '0'
                         || process.env.PAYMENT_SANDBOX === 'false'
                         || !process.env.PAYMENT_SANDBOX);

//#endregion

//#region helpers
function outputFilename(name, ext = 'js', hashType = 'contenthash:8') {
  return `${name}.${ext}?id=[${hashType}]`;
  // return `${name}.[${hashType}].${ext}`;
}

function resolvePath(...segments) {
  return path.resolve(__dirname, ...segments);
}

//#endregion

//#region Custom plugins
// Custom manifest dumper
// Dumps a manifest file and:
// Strip the hashes out - the problem is when adding assets via additionalAssets, as the copy and concat plugins do,
// we currently don't have the option to change th asset name to be different from the file name.
// Prefix the manifest keys with / - the existing php mix helper doesn't work properly without them.
class Manifest {
  constructor(options = {}) {
    this.fileName = options.fileName;
  }

  apply(compiler) {
    compiler.hooks.afterEmit.tap({ name: 'Manifest' }, (compilation) => {
      const json = compilation.getStats().toJson({
        // Disable data generation of everything we don't use
        all: false,
        // Add asset Information
        assets: true,
        // Show cached assets (setting this to `false` only shows emitted files)
        cachedAssets: true,
      });

      const manifest = {};
      json.assets.forEach((asset) => {
        let assetName = asset.name;
        // ensure lookup name starts with / because mix helper is dumb.
        // also ensure assets are relative to root.
        if (!assetName.startsWith('/')) {
          assetName = `/${assetName}`;
        }

        let name = assetName;
        // remove hash from name.
        if (name.lastIndexOf('?') > 0) {
          // querystring version
          name = name.substring(0, name.lastIndexOf('?'));
        } else {
          // hash in filename version
          let extname = path.extname(name);
          let basename = name.substring(0, name.lastIndexOf(extname));
          if (extname === '.map') {
            extname = `${path.extname(basename)}.map`;
          }

          basename = name.substring(0, name.lastIndexOf(extname));
          basename = basename.substring(0, basename.lastIndexOf('.'));

          name = `${basename}${extname}`;
        }

        manifest[name] = assetName;
      });

      fs.writeFileSync(this.fileName, JSON.stringify(manifest, null, 2));
    });
  }
}

//#endregion

//#region entrypoints and output
const entry = {
  'app': [
    './resources/assets/app.js',
    './resources/assets/less/app.less',
  ],
};

const coffeeReactComponents = [
  'artist-page',
  'beatmap-discussions',
  'beatmap-discussions-history',
  'beatmapset-page',
  'changelog-build',
  'changelog-index',
  'comments-index',
  'comments-show',
  'mp-history',
  'modding-profile',
  'profile-page',
  'admin/contest',
  'contest-entry',
  'contest-voting',
];

const tsReactComponents = [
  'account-edit',
  'beatmaps',
  'chat',
  'friends-index',
  'groups-show',
  'news-index',
  'news-show',
  'notifications-index',
  'scores-show',
  'store-bootstrap',
];

for (const name of coffeeReactComponents) {
  entry[`react/${name}`] = [resolvePath(`resources/assets/coffee/react/${name}.coffee`)];
}

for (const name of tsReactComponents) {
  entry[`react/${name}`] = [resolvePath(`resources/assets/lib/${name}.ts`)];
}

const output = {
  chunkFilename: outputFilename('js/[name]'),
  filename: outputFilename('js/[name]'),
  path: resolvePath('public'),
};

//#endregion

//#region plugin list
const plugins = [
  new webpack.ProvidePlugin({
    $: 'jquery',
    _: 'lodash',
    Cookies: 'js-cookie',
    d3: 'd3', // TODO: d3 is fat and probably should have it's own chunk
    jQuery: 'jquery',
    moment: 'moment',
    React: 'react',
    ReactDOM: 'react-dom',
    Turbolinks: 'turbolinks',
  }),
  new webpack.DefinePlugin({
    'process.env.DOCS_URL': JSON.stringify(process.env.DOCS_URL || 'https://docs.ppy.sh'),
    'process.env.PAYMENT_SANDBOX': JSON.stringify(paymentSandbox),
    'process.env.SHOPIFY_DOMAIN': JSON.stringify(process.env.SHOPIFY_DOMAIN),
    'process.env.SHOPIFY_STOREFRONT_TOKEN': JSON.stringify(process.env.SHOPIFY_STOREFRONT_TOKEN),
  }),
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), // don't add moment locales to bundle.
  new MiniCssExtractPlugin({
    chunkFilename: outputFilename('css/[name]', 'css'),
    filename: outputFilename('css/[name]', 'css'),
  }),
  new CopyPlugin({
    patterns: [
      { from: 'resources/assets/build/locales', to: outputFilename('js/locales/[name]', '[ext]') },
      { from: 'node_modules/@fortawesome/fontawesome-free/webfonts', to: outputFilename('vendor/fonts/font-awesome/[name]', '[ext]') },
      { from: 'node_modules/photoswipe/dist/default-skin', to: outputFilename('vendor/_photoswipe-default-skin/[name]', '[ext]') },
      { from: 'node_modules/moment/locale', to: outputFilename('vendor/js/moment-locales/[name]', '[ext]') },
    ],
  }),
  new Manifest({ fileName: 'public/mix-manifest.json'}),
];

if (process.env.SENTRY_RELEASE === '1') {
  plugins.push(
    new SentryPlugin({
      apiKey: process.env.SENTRY_API_KEY,
      organisation: process.env.SENTRY_ORG,
      project: process.env.SENTRY_PROJ,

      deleteAfterCompile: true,
      exclude: /\.css(\.map)?$/,
      filenameTransform: function(filename) {
        return '~' + filename;
      },
      release: function() {
        return process.env.GIT_SHA;
      },
    }),
  );
}

//#endregion

//#region Loader rules
const rules = [
  {
    enforce: 'pre',
    exclude: /(node_modules)/,
    loader: 'import-glob-loader',
    test: /\.(js|coffee)$/,
  },
  {
    exclude: /(node_modules|bower_components)/,
    test: /\.jsx?$/,
    use: [
      {
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
          plugins: [
            '@babel/plugin-syntax-dynamic-import',
            '@babel/plugin-proposal-object-rest-spread',
            [
              '@babel/plugin-transform-runtime',
              {
                helpers: false,
              },
            ],
          ],
          presets: [
            [
              '@babel/preset-env',
              {
                forceAllTransforms: true,
                modules: false,
              },
            ],
          ],
        },
      },
    ],
  },
  {
    exclude: /node_modules/,
    loader: 'ts-loader',
    test: /\.tsx?$/,
  },
  {
    // loader for preexisting global coffeescript
    exclude: [
      resolvePath('resources/assets/coffee/react'),
    ],
    include: [
      resolvePath('resources/assets/coffee'),
    ],
    test: /\.coffee$/,
    use: ['imports-loader?jQuery=jquery,$=jquery,this=>window', 'coffee-loader'],
  },
  {
    // loader for import-based coffeescript
    include: [
      resolvePath('resources/assets/coffee/react'),
      resolvePath('resources/assets/lib'),
    ],
    test: /\.coffee$/,
    use: ['coffee-loader'],
  },
  {
    test: /\.less$/,
    use: [
      MiniCssExtractPlugin.loader,
      {
        loader: 'css-loader',
        options: {
          importLoaders: 1,
          sourceMap: true,
          url: (url) => !url.startsWith('/'),
        },
      },
      {
        loader: 'postcss-loader',
        options: {
          plugins: [Autoprefixer],
          sourceMap: true,
        },
      },
      { loader: 'less-loader', options: { sourceMap: true } },
    ],
  },
  {
    loaders: [
      {
        loader: 'file-loader',
        options: {
          name: (filepath) => {
            if (!/node_modules|bower_components/.test(filepath)) {
              return '/images/[name].[ext]?[hash]';
            }

            const cleanPath = filepath
              .replace(/\\/g, '/')
              .replace(/((.*(node_modules|bower_components))|images|image|img|assets)\//g, '');

            return `/images/vendor/${cleanPath}?[hash]`;
          },
        },
      },
      {
        loader: 'img-loader',
      },
    ],
    test: /(\.(png|jpe?g|gif|webp)$|^((?!font).)*\.svg$)/,
  },
  {
    loader: 'file-loader',
    options: {
      name: (filepath) => {
        if (!/node_modules|bower_components/.test(filepath)) {
          return '/fonts/[name].[ext]?[hash]';
        }

        const cleanPath = filepath
          .replace(/\\/g, '/')
          .replace(/((.*(node_modules|bower_components))|fonts|font|assets)\//g, '');

        return `/fonts/vendor/${cleanPath}?[hash]`;
      },
    },
    test: /(\.(woff2?|ttf|eot|otf)$|font.*\.svg$)/,
  },
];

//#endregion

//#region resolvers
const resolve = {
  alias: {
    'layzr': resolvePath('node_modules/layzr.js/dist/layzr.module.js'),
    'ziggy': resolvePath('resources/assets/js/ziggy.js'),
    'ziggy-route': resolvePath('vendor/tightenco/ziggy/dist/js/route.js'),
  },
  extensions: ['*', '.js', '.coffee', '.ts', '.tsx'],
  modules: [
    resolvePath('resources/assets/coffee'),
    resolvePath('resources/assets/lib'),
    resolvePath('resources/assets/coffee/react/_components'),
    resolvePath('node_modules'),
  ],
  plugins: [new TsconfigPathsPlugin()],
};

//#endregion

//#region optimization and chunk splitting settings
const cacheGroups = {
  commons: {
    chunks: 'initial',
    minChunks: 2,
    name: 'commons',
    priority: -20,
  },
  vendor: {
    chunks: 'initial',
    name: 'vendor',
    priority: -10,
    reuseExistingChunk: true,
    test: (module, chunks) => {
      // Doing it this way doesn't split the css imported via app.less from the main css bundle.
      return module.resource && module.resource.includes(`${path.sep}node_modules${path.sep}`);
    },
  },
};

const optimization = {
  moduleIds: 'hashed',
  runtimeChunk: {
    name: 'runtime',
  },
  splitChunks: {
    cacheGroups,
  },

};

if (inProduction) {
  optimization.minimizer = [
    new TerserPlugin({
      sourceMap: true,
      terserOptions: {
        safari10: true,
      },
    }),
    new CssMinimizerPlugin({
      sourceMap: true,
    }),
  ];
}

//#endregion

module.exports = {
  devtool: 'source-map',
  entry,
  mode: inProduction ? 'production' : 'development',
  module: {
    rules,
  },
  optimization,
  output,
  plugins,
  resolve,
  stats: {
    entrypoints: false,
    errorDetails: false,
    excludeAssets: [
      // exclude copied files
      /^js\/locales\//,
      /^\/fonts\//,
      /^vendor\//,
    ],
  },
};
<commit_msg>add comment for url-loader's absolute path exception<commit_after>// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

'use strict';

//#region plugin imports
const Autoprefixer = require('autoprefixer');
const CopyPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const SentryPlugin = require('webpack-sentry-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

//#endregion

// #region non-plugin imports
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');

//#endregion

//#region env
const inProduction = process.env.NODE_ENV === 'production' || process.argv.includes('-p');
const paymentSandbox = !(process.env.PAYMENT_SANDBOX === '0'
                         || process.env.PAYMENT_SANDBOX === 'false'
                         || !process.env.PAYMENT_SANDBOX);

//#endregion

//#region helpers
function outputFilename(name, ext = 'js', hashType = 'contenthash:8') {
  return `${name}.${ext}?id=[${hashType}]`;
  // return `${name}.[${hashType}].${ext}`;
}

function resolvePath(...segments) {
  return path.resolve(__dirname, ...segments);
}

//#endregion

//#region Custom plugins
// Custom manifest dumper
// Dumps a manifest file and:
// Strip the hashes out - the problem is when adding assets via additionalAssets, as the copy and concat plugins do,
// we currently don't have the option to change th asset name to be different from the file name.
// Prefix the manifest keys with / - the existing php mix helper doesn't work properly without them.
class Manifest {
  constructor(options = {}) {
    this.fileName = options.fileName;
  }

  apply(compiler) {
    compiler.hooks.afterEmit.tap({ name: 'Manifest' }, (compilation) => {
      const json = compilation.getStats().toJson({
        // Disable data generation of everything we don't use
        all: false,
        // Add asset Information
        assets: true,
        // Show cached assets (setting this to `false` only shows emitted files)
        cachedAssets: true,
      });

      const manifest = {};
      json.assets.forEach((asset) => {
        let assetName = asset.name;
        // ensure lookup name starts with / because mix helper is dumb.
        // also ensure assets are relative to root.
        if (!assetName.startsWith('/')) {
          assetName = `/${assetName}`;
        }

        let name = assetName;
        // remove hash from name.
        if (name.lastIndexOf('?') > 0) {
          // querystring version
          name = name.substring(0, name.lastIndexOf('?'));
        } else {
          // hash in filename version
          let extname = path.extname(name);
          let basename = name.substring(0, name.lastIndexOf(extname));
          if (extname === '.map') {
            extname = `${path.extname(basename)}.map`;
          }

          basename = name.substring(0, name.lastIndexOf(extname));
          basename = basename.substring(0, basename.lastIndexOf('.'));

          name = `${basename}${extname}`;
        }

        manifest[name] = assetName;
      });

      fs.writeFileSync(this.fileName, JSON.stringify(manifest, null, 2));
    });
  }
}

//#endregion

//#region entrypoints and output
const entry = {
  'app': [
    './resources/assets/app.js',
    './resources/assets/less/app.less',
  ],
};

const coffeeReactComponents = [
  'artist-page',
  'beatmap-discussions',
  'beatmap-discussions-history',
  'beatmapset-page',
  'changelog-build',
  'changelog-index',
  'comments-index',
  'comments-show',
  'mp-history',
  'modding-profile',
  'profile-page',
  'admin/contest',
  'contest-entry',
  'contest-voting',
];

const tsReactComponents = [
  'account-edit',
  'beatmaps',
  'chat',
  'friends-index',
  'groups-show',
  'news-index',
  'news-show',
  'notifications-index',
  'scores-show',
  'store-bootstrap',
];

for (const name of coffeeReactComponents) {
  entry[`react/${name}`] = [resolvePath(`resources/assets/coffee/react/${name}.coffee`)];
}

for (const name of tsReactComponents) {
  entry[`react/${name}`] = [resolvePath(`resources/assets/lib/${name}.ts`)];
}

const output = {
  chunkFilename: outputFilename('js/[name]'),
  filename: outputFilename('js/[name]'),
  path: resolvePath('public'),
};

//#endregion

//#region plugin list
const plugins = [
  new webpack.ProvidePlugin({
    $: 'jquery',
    _: 'lodash',
    Cookies: 'js-cookie',
    d3: 'd3', // TODO: d3 is fat and probably should have it's own chunk
    jQuery: 'jquery',
    moment: 'moment',
    React: 'react',
    ReactDOM: 'react-dom',
    Turbolinks: 'turbolinks',
  }),
  new webpack.DefinePlugin({
    'process.env.DOCS_URL': JSON.stringify(process.env.DOCS_URL || 'https://docs.ppy.sh'),
    'process.env.PAYMENT_SANDBOX': JSON.stringify(paymentSandbox),
    'process.env.SHOPIFY_DOMAIN': JSON.stringify(process.env.SHOPIFY_DOMAIN),
    'process.env.SHOPIFY_STOREFRONT_TOKEN': JSON.stringify(process.env.SHOPIFY_STOREFRONT_TOKEN),
  }),
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), // don't add moment locales to bundle.
  new MiniCssExtractPlugin({
    chunkFilename: outputFilename('css/[name]', 'css'),
    filename: outputFilename('css/[name]', 'css'),
  }),
  new CopyPlugin({
    patterns: [
      { from: 'resources/assets/build/locales', to: outputFilename('js/locales/[name]', '[ext]') },
      { from: 'node_modules/@fortawesome/fontawesome-free/webfonts', to: outputFilename('vendor/fonts/font-awesome/[name]', '[ext]') },
      { from: 'node_modules/photoswipe/dist/default-skin', to: outputFilename('vendor/_photoswipe-default-skin/[name]', '[ext]') },
      { from: 'node_modules/moment/locale', to: outputFilename('vendor/js/moment-locales/[name]', '[ext]') },
    ],
  }),
  new Manifest({ fileName: 'public/mix-manifest.json'}),
];

if (process.env.SENTRY_RELEASE === '1') {
  plugins.push(
    new SentryPlugin({
      apiKey: process.env.SENTRY_API_KEY,
      organisation: process.env.SENTRY_ORG,
      project: process.env.SENTRY_PROJ,

      deleteAfterCompile: true,
      exclude: /\.css(\.map)?$/,
      filenameTransform: function(filename) {
        return '~' + filename;
      },
      release: function() {
        return process.env.GIT_SHA;
      },
    }),
  );
}

//#endregion

//#region Loader rules
const rules = [
  {
    enforce: 'pre',
    exclude: /(node_modules)/,
    loader: 'import-glob-loader',
    test: /\.(js|coffee)$/,
  },
  {
    exclude: /(node_modules|bower_components)/,
    test: /\.jsx?$/,
    use: [
      {
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
          plugins: [
            '@babel/plugin-syntax-dynamic-import',
            '@babel/plugin-proposal-object-rest-spread',
            [
              '@babel/plugin-transform-runtime',
              {
                helpers: false,
              },
            ],
          ],
          presets: [
            [
              '@babel/preset-env',
              {
                forceAllTransforms: true,
                modules: false,
              },
            ],
          ],
        },
      },
    ],
  },
  {
    exclude: /node_modules/,
    loader: 'ts-loader',
    test: /\.tsx?$/,
  },
  {
    // loader for preexisting global coffeescript
    exclude: [
      resolvePath('resources/assets/coffee/react'),
    ],
    include: [
      resolvePath('resources/assets/coffee'),
    ],
    test: /\.coffee$/,
    use: ['imports-loader?jQuery=jquery,$=jquery,this=>window', 'coffee-loader'],
  },
  {
    // loader for import-based coffeescript
    include: [
      resolvePath('resources/assets/coffee/react'),
      resolvePath('resources/assets/lib'),
    ],
    test: /\.coffee$/,
    use: ['coffee-loader'],
  },
  {
    test: /\.less$/,
    use: [
      MiniCssExtractPlugin.loader,
      {
        loader: 'css-loader',
        options: {
          importLoaders: 1,
          sourceMap: true,
          // url-loader didn't try to resolve aboslute paths before 4.1
          // https://github.com/webpack-contrib/css-loader/commit/f9ba0ce11789770c4c9220478e9c98dbd432a5d6
          url: (url) => !url.startsWith('/'),
        },
      },
      {
        loader: 'postcss-loader',
        options: {
          plugins: [Autoprefixer],
          sourceMap: true,
        },
      },
      { loader: 'less-loader', options: { sourceMap: true } },
    ],
  },
  {
    loaders: [
      {
        loader: 'file-loader',
        options: {
          name: (filepath) => {
            if (!/node_modules|bower_components/.test(filepath)) {
              return '/images/[name].[ext]?[hash]';
            }

            const cleanPath = filepath
              .replace(/\\/g, '/')
              .replace(/((.*(node_modules|bower_components))|images|image|img|assets)\//g, '');

            return `/images/vendor/${cleanPath}?[hash]`;
          },
        },
      },
      {
        loader: 'img-loader',
      },
    ],
    test: /(\.(png|jpe?g|gif|webp)$|^((?!font).)*\.svg$)/,
  },
  {
    loader: 'file-loader',
    options: {
      name: (filepath) => {
        if (!/node_modules|bower_components/.test(filepath)) {
          return '/fonts/[name].[ext]?[hash]';
        }

        const cleanPath = filepath
          .replace(/\\/g, '/')
          .replace(/((.*(node_modules|bower_components))|fonts|font|assets)\//g, '');

        return `/fonts/vendor/${cleanPath}?[hash]`;
      },
    },
    test: /(\.(woff2?|ttf|eot|otf)$|font.*\.svg$)/,
  },
];

//#endregion

//#region resolvers
const resolve = {
  alias: {
    'layzr': resolvePath('node_modules/layzr.js/dist/layzr.module.js'),
    'ziggy': resolvePath('resources/assets/js/ziggy.js'),
    'ziggy-route': resolvePath('vendor/tightenco/ziggy/dist/js/route.js'),
  },
  extensions: ['*', '.js', '.coffee', '.ts', '.tsx'],
  modules: [
    resolvePath('resources/assets/coffee'),
    resolvePath('resources/assets/lib'),
    resolvePath('resources/assets/coffee/react/_components'),
    resolvePath('node_modules'),
  ],
  plugins: [new TsconfigPathsPlugin()],
};

//#endregion

//#region optimization and chunk splitting settings
const cacheGroups = {
  commons: {
    chunks: 'initial',
    minChunks: 2,
    name: 'commons',
    priority: -20,
  },
  vendor: {
    chunks: 'initial',
    name: 'vendor',
    priority: -10,
    reuseExistingChunk: true,
    test: (module, chunks) => {
      // Doing it this way doesn't split the css imported via app.less from the main css bundle.
      return module.resource && module.resource.includes(`${path.sep}node_modules${path.sep}`);
    },
  },
};

const optimization = {
  moduleIds: 'hashed',
  runtimeChunk: {
    name: 'runtime',
  },
  splitChunks: {
    cacheGroups,
  },

};

if (inProduction) {
  optimization.minimizer = [
    new TerserPlugin({
      sourceMap: true,
      terserOptions: {
        safari10: true,
      },
    }),
    new CssMinimizerPlugin({
      sourceMap: true,
    }),
  ];
}

//#endregion

module.exports = {
  devtool: 'source-map',
  entry,
  mode: inProduction ? 'production' : 'development',
  module: {
    rules,
  },
  optimization,
  output,
  plugins,
  resolve,
  stats: {
    entrypoints: false,
    errorDetails: false,
    excludeAssets: [
      // exclude copied files
      /^js\/locales\//,
      /^\/fonts\//,
      /^vendor\//,
    ],
  },
};
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import {
    View,
    ViewPropTypes,
    TouchableOpacity,
    StyleSheet,
    Text
} from 'react-native'
import PropTypes from 'prop-types';

const handleTabPress = (index, multiple, selectedIndex, onTabPress) => {
    if (multiple) {
        onTabPress(index);
    }
    else if (selectedIndex !== index) {
        onTabPress(index);
    }
};

const TabOption = ({
    isTabActive, index, badge, text,
    firstTabStyle, lastTabStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {
    return (
        <TouchableOpacity style={[
            styles.tabStyle,
            tabStyle,
            isTabActive ? [styles.activeTabStyle, activeTabStyle] : {},
            firstTabStyle,
            lastTabStyle]}
            onPress={() => onTabPress(index)}
            activeOpacity={1}>
            <View style={{ flexDirection: "row" }}>
                <Text style={[
                    styles.tabTextStyle,
                    tabTextStyle,
                    isTabActive ? [styles.activeTabTextStyle, activeTabTextStyle] : {}]}
                    numberOfLines={1}
                    ellipsizeMode="tail">
                    {text}
                </Text>
                {
                    badge ?
                        <View style={[
                            styles.tabBadgeContainerStyle,
                            tabBadgeContainerStyle,
                            isTabActive ? [styles.activeTabBadgeContainerStyle, activeTabBadgeContainerStyle] : {}]}>
                            <Text style={[
                                styles.tabBadgeStyle,
                                tabBadgeStyle,
                                isTabActive ? [styles.activeTabBadgeStyle, activeTabBadgeStyle] : {}]}>
                                {badge}
                            </Text>
                        </View> : false
                }
            </View>
        </TouchableOpacity>
    );
}

const SegmentedControlTab = ({
    multiple, selectedIndex, selectedIndices, values,
    badges, borderRadius, tabsContainerStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {

    const firstTabStyle = [{ borderRightWidth: 0, borderTopLeftRadius: borderRadius, borderBottomLeftRadius: borderRadius }]
    const lastTabStyle = [{ borderLeftWidth: 0, borderTopRightRadius: borderRadius, borderBottomRightRadius: borderRadius }]

    return (
        <View
            style={[styles.tabsContainerStyle, tabsContainerStyle]}
            removeClippedSubviews={false}>
            {
                values.map((item, index) => {
                    return (
                        <TabOption
                            key={index}
                            index={index}
                            badge={badges && badges[index] ? badges[index] : false}
                            isTabActive={multiple ? selectedIndices.includes(index) : selectedIndex === index}
                            text={item}
                            onTabPress={(index) => handleTabPress(index, multiple, selectedIndex, onTabPress)}
                            firstTabStyle={index === 0 ? [{ borderRightWidth: 0 }, firstTabStyle] : {}}
                            lastTabStyle={index === values.length - 1 ? [{ borderLeftWidth: 0 }, lastTabStyle] : {}}
                            tabStyle={[tabStyle, index !== 0 && index !== values.length - 1 ? { paddingLeft: -1 } : {}]}
                            activeTabStyle={activeTabStyle}
                            tabTextStyle={tabTextStyle}
                            activeTabTextStyle={activeTabTextStyle}
                            tabBadgeContainerStyle={tabBadgeContainerStyle}
                            activeTabBadgeContainerStyle={activeTabBadgeContainerStyle}
                            tabBadgeStyle={tabBadgeStyle}
                            activeTabBadgeStyle={activeTabBadgeStyle} />
                    );
                })
            }
        </View>
    );
};

SegmentedControlTab.propTypes = {
    values: PropTypes.array,
    badges: PropTypes.array,
    multiple: PropTypes.bool,
    onTabPress: PropTypes.func,
    selectedIndex: PropTypes.number,
    selectedIndices: PropTypes.arrayOf(PropTypes.number),
    tabsContainerStyle: ViewPropTypes.style,
    tabStyle: ViewPropTypes.style,
    activeTabStyle: ViewPropTypes.style,
    tabTextStyle: Text.propTypes.style,
    activeTabTextStyle: Text.propTypes.style,
    tabBadgeContainerStyle: Text.propTypes.style,
    activeTabBadgeContainerStyle: Text.propTypes.style,
    tabBadgeStyle: Text.propTypes.style,
    activeTabBadgeStyle: Text.propTypes.style,
    borderRadius: PropTypes.number
}

SegmentedControlTab.defaultProps = {
    values: ['One', 'Two', 'Three'],
    badges: ['', '', ''],
    multiple: false,
    selectedIndex: 0,
    selectedIndices: [0],
    onTabPress() { },
    tabsContainerStyle: {},
    tabStyle: {},
    activeTabStyle: {},
    tabTextStyle: {},
    activeTabTextStyle: {},
    tabBadgeContainerStyle: {},
    activeTabBadgeContainerStyle: {},
    tabBadgeStyle: {},
    activeTabBadgeStyle: {},
    borderRadius: 5
}

const styles = StyleSheet.create({
    tabsContainerStyle: {
        backgroundColor: 'transparent',
        flexDirection: 'row',
    },
    tabStyle: {
        paddingVertical: 5,
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        borderColor: '#0076FF',
        borderWidth: 1,
        backgroundColor: 'white',
    },
    activeTabStyle: {
        backgroundColor: '#0076FF'
    },
    tabTextStyle: {
        color: '#0076FF'
    },
    activeTabTextStyle: {
        color: 'white'
    },
    tabBadgeContainerStyle: {
        borderRadius: 20,
        backgroundColor: 'red',
        paddingLeft: 5,
        paddingRight: 5,
        marginLeft: 5,
        marginBottom: 3
    },
    activeTabBadgeContainerStyle: {
        backgroundColor: 'white'
    },
    tabBadgeStyle: {
        color: 'white',
        fontSize: 11,
        fontWeight: 'bold'
    },
    activeTabBadgeStyle: {
        color: 'black'
    }
})

export default SegmentedControlTab
<commit_msg>Fix last tab left border<commit_after>import React, { Component } from 'react'
import {
    View,
    ViewPropTypes,
    TouchableOpacity,
    StyleSheet,
    Text
} from 'react-native'
import PropTypes from 'prop-types';

const handleTabPress = (index, multiple, selectedIndex, onTabPress) => {
    if (multiple) {
        onTabPress(index);
    }
    else if (selectedIndex !== index) {
        onTabPress(index);
    }
};

const TabOption = ({
    isTabActive, index, badge, text,
    firstTabStyle, lastTabStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {
    return (
        <TouchableOpacity style={[
            styles.tabStyle,
            tabStyle,
            isTabActive ? [styles.activeTabStyle, activeTabStyle] : {},
            firstTabStyle,
            lastTabStyle]}
            onPress={() => onTabPress(index)}
            activeOpacity={1}>
            <View style={{ flexDirection: "row" }}>
                <Text style={[
                    styles.tabTextStyle,
                    tabTextStyle,
                    isTabActive ? [styles.activeTabTextStyle, activeTabTextStyle] : {}]}
                    numberOfLines={1}
                    ellipsizeMode="tail">
                    {text}
                </Text>
                {
                    badge ?
                        <View style={[
                            styles.tabBadgeContainerStyle,
                            tabBadgeContainerStyle,
                            isTabActive ? [styles.activeTabBadgeContainerStyle, activeTabBadgeContainerStyle] : {}]}>
                            <Text style={[
                                styles.tabBadgeStyle,
                                tabBadgeStyle,
                                isTabActive ? [styles.activeTabBadgeStyle, activeTabBadgeStyle] : {}]}>
                                {badge}
                            </Text>
                        </View> : false
                }
            </View>
        </TouchableOpacity>
    );
}

const SegmentedControlTab = ({
    multiple, selectedIndex, selectedIndices, values,
    badges, borderRadius, tabsContainerStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {

    const firstTabStyle = [{ borderRightWidth: 0, borderTopLeftRadius: borderRadius, borderBottomLeftRadius: borderRadius }]
    const lastTabStyle = [{ borderLeftWidth: 0, borderTopRightRadius: borderRadius, borderBottomRightRadius: borderRadius }]

    return (
        <View
            style={[styles.tabsContainerStyle, tabsContainerStyle]}
            removeClippedSubviews={false}>
            {
                values.map((item, index) => {
                    return (
                        <TabOption
                            key={index}
                            index={index}
                            badge={badges && badges[index] ? badges[index] : false}
                            isTabActive={multiple ? selectedIndices.includes(index) : selectedIndex === index}
                            text={item}
                            onTabPress={(index) => handleTabPress(index, multiple, selectedIndex, onTabPress)}
                            firstTabStyle={index === 0 ? [{ borderRightWidth: 0 }, firstTabStyle] : {}}
                            lastTabStyle={index === values.length - 1 ? [{ borderLeftWidth: 0 }, lastTabStyle] : {}}
                            tabStyle={[tabStyle, index !== 0 && index !== values.length - 1 ? { marginLeft: -1 } : {}]}
                            activeTabStyle={activeTabStyle}
                            tabTextStyle={tabTextStyle}
                            activeTabTextStyle={activeTabTextStyle}
                            tabBadgeContainerStyle={tabBadgeContainerStyle}
                            activeTabBadgeContainerStyle={activeTabBadgeContainerStyle}
                            tabBadgeStyle={tabBadgeStyle}
                            activeTabBadgeStyle={activeTabBadgeStyle} />
                    );
                })
            }
        </View>
    );
};

SegmentedControlTab.propTypes = {
    values: PropTypes.array,
    badges: PropTypes.array,
    multiple: PropTypes.bool,
    onTabPress: PropTypes.func,
    selectedIndex: PropTypes.number,
    selectedIndices: PropTypes.arrayOf(PropTypes.number),
    tabsContainerStyle: ViewPropTypes.style,
    tabStyle: ViewPropTypes.style,
    activeTabStyle: ViewPropTypes.style,
    tabTextStyle: Text.propTypes.style,
    activeTabTextStyle: Text.propTypes.style,
    tabBadgeContainerStyle: Text.propTypes.style,
    activeTabBadgeContainerStyle: Text.propTypes.style,
    tabBadgeStyle: Text.propTypes.style,
    activeTabBadgeStyle: Text.propTypes.style,
    borderRadius: PropTypes.number
}

SegmentedControlTab.defaultProps = {
    values: ['One', 'Two', 'Three'],
    badges: ['', '', ''],
    multiple: false,
    selectedIndex: 0,
    selectedIndices: [0],
    onTabPress() { },
    tabsContainerStyle: {},
    tabStyle: {},
    activeTabStyle: {},
    tabTextStyle: {},
    activeTabTextStyle: {},
    tabBadgeContainerStyle: {},
    activeTabBadgeContainerStyle: {},
    tabBadgeStyle: {},
    activeTabBadgeStyle: {},
    borderRadius: 5
}

const styles = StyleSheet.create({
    tabsContainerStyle: {
        backgroundColor: 'transparent',
        flexDirection: 'row',
    },
    tabStyle: {
        paddingVertical: 5,
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        borderColor: '#0076FF',
        borderWidth: 1,
        backgroundColor: 'white',
    },
    activeTabStyle: {
        backgroundColor: '#0076FF'
    },
    tabTextStyle: {
        color: '#0076FF'
    },
    activeTabTextStyle: {
        color: 'white'
    },
    tabBadgeContainerStyle: {
        borderRadius: 20,
        backgroundColor: 'red',
        paddingLeft: 5,
        paddingRight: 5,
        marginLeft: 5,
        marginBottom: 3
    },
    activeTabBadgeContainerStyle: {
        backgroundColor: 'white'
    },
    tabBadgeStyle: {
        color: 'white',
        fontSize: 11,
        fontWeight: 'bold'
    },
    activeTabBadgeStyle: {
        color: 'black'
    }
})

export default SegmentedControlTab
<|endoftext|>


<commit_before>/**
 * MK is a minimal generator for constructing components from template files
 * powered by inquirer
 */
'use strict';
var fs = require('fs-extra');
var inquirer = require('inquirer');

var questions = [
  {
    type: 'list',
    name: 'template',
    message: 'Which template would you like to use?',
    choices: ['component.js', 'container.js']
  },
  {
    type: 'input',
    name: 'name',
    message: 'Name ❯',
    validate: function (value) {
      var pass = value.match(/^[a-zA-Z0-9_]*$/);
      if (pass) {
        return true;
      }

      return 'Please enter a valid name. i.e. ComponentName';
    }
  },
  {
    type: 'input',
    name: 'description',
    message: 'Describe your component ❯',
  },
  {
    type: 'confirm',
    name: 'styles',
    message: 'Create a styles.scss file?',
    default: false
  },
];

function build(name, description) {
  var dest = 'src/components/' + name + '/';
  var component_src = dest + 'index.js';
  fs.readFile(dest + 'index.js', 'utf8', function (err,data) {
    if (err) {
      return console.log(err);
    }
    var result = data.replace(/COMPONENT_NAME/g, name).replace(/COMPONENT_DESCRIPTION/g, description);

    // another small piece of callback hell that will be resolved
    fs.writeFile(component_src, result, 'utf8', function (err) {
      if (err) return console.log(err);
      console.log('🔮 Converting template into real component');
    });
  });
}

inquirer.prompt(questions).then(function (answers) {
  var dest = 'src/components/' + answers.name;
  var template = 'internals/templates/' + answers.template;

  // only run the given function if there is no error
  const runOrDie = (func, error) => {
    if (error) return console.error(error);
    return func();
  }

  // append an asset filename onto our destination path
  const assetPath = (asset) => `${dest}/${asset}`;

  const stylesCreated = (e) =>
    runOrDie(() => {
      console.log('👠 Created styles.scss');
      build(answers.name, answers.description);
    }, e);

  const copyStyles = () =>
    answers.styles
      ? fs.outputFile(assetPath('styles.scss'), '', stylesCreated)
      : build(answers.name, answers.description);

  const templateCopied = (e) =>
    runOrDie(() => {
      console.log('🔨 Created index.js from component template');
      copyStyles();
    }, e);

  const copyTemplate = () =>
    fs.copy(template, assetPath('index.js'), templateCopied);

  const folderCreated = (e) =>
    runOrDie(() => {
      console.log(`💪 Created ${dest}`);
      copyTemplate();
    }, e);

  const createFolder = () =>
    fs.mkdirs(dest, folderCreated);

  createFolder();
});
<commit_msg>chore(mk): switch var to const<commit_after>/**
 * MK is a minimal generator for constructing components from template files
 * powered by inquirer
 */
'use strict';
const fs = require('fs-extra');
const inquirer = require('inquirer');

const questions = [
  {
    type: 'list',
    name: 'template',
    message: 'Which template would you like to use?',
    choices: ['component.js', 'container.js']
  },
  {
    type: 'input',
    name: 'name',
    message: 'Name ❯',
    validate: function (value) {
      const pass = value.match(/^[a-zA-Z0-9_]*$/);
      if (pass) {
        return true;
      }

      return 'Please enter a valid name. i.e. ComponentName';
    }
  },
  {
    type: 'input',
    name: 'description',
    message: 'Describe your component ❯',
  },
  {
    type: 'confirm',
    name: 'styles',
    message: 'Create a styles.scss file?',
    default: false
  },
];

function build(name, description) {
  const dest = 'src/components/' + name + '/';
  const component_src = dest + 'index.js';
  fs.readFile(dest + 'index.js', 'utf8', function (err,data) {
    if (err) {
      return console.log(err);
    }
    const result = data.replace(/COMPONENT_NAME/g, name).replace(/COMPONENT_DESCRIPTION/g, description);

    // another small piece of callback hell that will be resolved
    fs.writeFile(component_src, result, 'utf8', function (err) {
      if (err) return console.log(err);
      console.log('🔮 Converting template into real component');
    });
  });
}

inquirer.prompt(questions).then(function (answers) {
  const dest = 'src/components/' + answers.name;
  const template = 'internals/templates/' + answers.template;

  // only run the given function if there is no error
  const runOrDie = (func, error) => {
    if (error) return console.error(error);
    return func();
  }

  // append an asset filename onto our destination path
  const assetPath = (asset) => `${dest}/${asset}`;

  const stylesCreated = (e) =>
    runOrDie(() => {
      console.log('👠 Created styles.scss');
      build(answers.name, answers.description);
    }, e);

  const copyStyles = () =>
    answers.styles
      ? fs.outputFile(assetPath('styles.scss'), '', stylesCreated)
      : build(answers.name, answers.description);

  const templateCopied = (e) =>
    runOrDie(() => {
      console.log('🔨 Created index.js from component template');
      copyStyles();
    }, e);

  const copyTemplate = () =>
    fs.copy(template, assetPath('index.js'), templateCopied);

  const folderCreated = (e) =>
    runOrDie(() => {
      console.log(`💪 Created ${dest}`);
      copyTemplate();
    }, e);

  const createFolder = () =>
    fs.mkdirs(dest, folderCreated);

  createFolder();
});
<|endoftext|>


<commit_before>/*---------------------------------------------------------
 * OpenERP web_graph
 *---------------------------------------------------------*/

/* jshint undef: false  */

openerp.web_graph = function (instance) {
'use strict';

var _lt = instance.web._lt;
var _t = instance.web._t;

instance.web.views.add('graph', 'instance.web_graph.GraphView');

instance.web_graph.GraphView = instance.web.View.extend({
    display_name: _lt('Graph'),
    view_type: 'graph',

    // ----------------------------------------------------------------------
    // Init stuff
    // ----------------------------------------------------------------------
    init: function(parent, dataset, view_id, options) {
        this._super(parent, dataset, view_id, options);
        this.dataset = dataset;
        this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true});
        this.search_view = parent.searchview;
    },

    view_loading: function (fields_view_get) {
        var self = this,
            arch = fields_view_get.arch;

        this.widget_config = {
            title: arch.attrs.string,
            stacked : (arch.attrs.stacked === 'True'),
            mode: (arch.attrs.type) ? arch.attrs.type : 'bar',
            measures: [],
            row_groupby: [],
            col_groupby: [],
        };

        _.each(arch.children, function (field) {
            if (_.has(field.attrs, 'type')) {
                switch (field.attrs.type) {
                    case 'row':
                        self.widget_config.row_groupby.push(field.attrs.name);
                        break;
                    case 'col':
                        self.widget_config.col_groupby.push(field.attrs.name);
                        break;
                    case 'measure':
                        self.widget_config.measures.push(field.attrs.name);
                        break;
                }
            } else {  // old style, kept for backward compatibility
                if ('operator' in field.attrs) {
                    self.widget_config.measures.push(field.attrs.name);
                } else {
                    self.widet_config.row_groupby.push(field.attrs.name);
                }
            }
        });
        if (self.widget_config.measures.length === 0) {
            self.widget_config.measures.push('__count');
        }
    },

    do_search: function (domain, context, group_by) {
        var col_group_by = this.get_col_groupbys_from_searchview();

        if (!this.graph_widget) {
            if (group_by.length) {
                this.widget_config.row_groupby = group_by;
            }
            if (col_group_by.length) {
                this.widget_config.col_groupby = group_by;
            }
            this.graph_widget = new openerp.web_graph.Graph(this, this.model, domain, this.widget_config);
            this.graph_widget.appendTo(this.$el);
            this.graph_widget.on('groupby_changed', this, this.proxy('register_groupby'));
            this.graph_widget.on('groupby_swapped', this, this.proxy('swap_groupby'));
            this.ViewManager.on('switch_mode', this, function (e) { if (e === 'graph') this.graph_widget.reload(); });
            return;
        }

        if (this.swapped) {
            this.swapped = false;
            return;
        }

        this.graph_widget.set(domain, group_by, col_group_by);
    },

    get_col_groupbys_from_searchview: function () {
        var facet = this.search_view.query.findWhere({category:'ColGroupBy'}),
            groupby_list = facet ? facet.values.models : [];
        return _.map(groupby_list, function (g) { return g.attributes.value.attrs.context.col_group_by; });
    },

    do_show: function () {
        this.do_push_state({});
        return this._super();
    },

    // ----------------------------------------------------------------------
    // Search view integration
    // ----------------------------------------------------------------------

    // add groupby to the search view
    register_groupby: function() {
        var query = this.search_view.query;

        if (!_.has(this.search_view, '_s_groupby')) { return; }

        // add row groupbys
        var row_groupby = this.graph_widget.get_row_groupby(),
            row_facet = this.make_row_groupby_facets(row_groupby),
            row_search_facet = query.findWhere({category:'GroupBy'});

        if (row_search_facet) {
            row_search_facet.values.reset(row_facet.values);
        } else {
            if (row_groupby.length) {
                query.add(row_facet);
            }
        }
         // add col groupbys
        var col_groupby = this.graph_widget.get_col_groupby(),
            col_facet = this.make_col_groupby_facets(col_groupby),
            col_search_facet = query.findWhere({category:'ColGroupBy'});

        if (col_search_facet) {
            col_search_facet.values.reset(col_facet.values);
        } else {
            if (col_groupby.length) {
                query.add(col_facet);
            }
        }
    },

    swap_groupby: function () {
        this.swap = true;
        this.register_groupby();
    },

    make_row_groupby_facets: function(groupbys) {
        return {
            category:'GroupBy',
            values: this.make_groupby_values(groupbys, 'group_by'),
            icon:'w',
            field: this.search_view._s_groupby
        };
    },

    make_col_groupby_facets: function(groupbys) {
        return {
            category:'ColGroupBy',
            values: this.make_groupby_values(groupbys, 'col_group_by'),
            icon:'f',
            field: this.search_field
        };
    },

    make_groupby_values: function (groupbys, category) {
        return _.map(groupbys, function (groupby) {
            var context = {};
            context[category] = groupby.field;
            return {
                label: groupby.string,
                value: {attrs:{domain: [], context: context}}
            };
        });
    },

    search_field: {
        get_context: function() {},
        get_domain: function () {},
        get_groupby: function () {},
    },

});

};

<commit_msg>[FIX] fixes a typo 'widet_config' => 'widget_config' (it previously crashed the graph view) (addon web_graph)<commit_after>/*---------------------------------------------------------
 * OpenERP web_graph
 *---------------------------------------------------------*/

/* jshint undef: false  */

openerp.web_graph = function (instance) {
'use strict';

var _lt = instance.web._lt;
var _t = instance.web._t;

instance.web.views.add('graph', 'instance.web_graph.GraphView');

instance.web_graph.GraphView = instance.web.View.extend({
    display_name: _lt('Graph'),
    view_type: 'graph',

    // ----------------------------------------------------------------------
    // Init stuff
    // ----------------------------------------------------------------------
    init: function(parent, dataset, view_id, options) {
        this._super(parent, dataset, view_id, options);
        this.dataset = dataset;
        this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true});
        this.search_view = parent.searchview;
    },

    view_loading: function (fields_view_get) {
        var self = this,
            arch = fields_view_get.arch;

        this.widget_config = {
            title: arch.attrs.string,
            stacked : (arch.attrs.stacked === 'True'),
            mode: (arch.attrs.type) ? arch.attrs.type : 'bar',
            measures: [],
            row_groupby: [],
            col_groupby: [],
        };

        _.each(arch.children, function (field) {
            if (_.has(field.attrs, 'type')) {
                switch (field.attrs.type) {
                    case 'row':
                        self.widget_config.row_groupby.push(field.attrs.name);
                        break;
                    case 'col':
                        self.widget_config.col_groupby.push(field.attrs.name);
                        break;
                    case 'measure':
                        self.widget_config.measures.push(field.attrs.name);
                        break;
                }
            } else {  // old style, kept for backward compatibility
                if ('operator' in field.attrs) {
                    self.widget_config.measures.push(field.attrs.name);
                } else {
                    self.widget_config.row_groupby.push(field.attrs.name);
                }
            }
        });
        if (self.widget_config.measures.length === 0) {
            self.widget_config.measures.push('__count');
        }
    },

    do_search: function (domain, context, group_by) {
        var col_group_by = this.get_col_groupbys_from_searchview();

        if (!this.graph_widget) {
            if (group_by.length) {
                this.widget_config.row_groupby = group_by;
            }
            if (col_group_by.length) {
                this.widget_config.col_groupby = group_by;
            }
            this.graph_widget = new openerp.web_graph.Graph(this, this.model, domain, this.widget_config);
            this.graph_widget.appendTo(this.$el);
            this.graph_widget.on('groupby_changed', this, this.proxy('register_groupby'));
            this.graph_widget.on('groupby_swapped', this, this.proxy('swap_groupby'));
            this.ViewManager.on('switch_mode', this, function (e) { if (e === 'graph') this.graph_widget.reload(); });
            return;
        }

        if (this.swapped) {
            this.swapped = false;
            return;
        }

        this.graph_widget.set(domain, group_by, col_group_by);
    },

    get_col_groupbys_from_searchview: function () {
        var facet = this.search_view.query.findWhere({category:'ColGroupBy'}),
            groupby_list = facet ? facet.values.models : [];
        return _.map(groupby_list, function (g) { return g.attributes.value.attrs.context.col_group_by; });
    },

    do_show: function () {
        this.do_push_state({});
        return this._super();
    },

    // ----------------------------------------------------------------------
    // Search view integration
    // ----------------------------------------------------------------------

    // add groupby to the search view
    register_groupby: function() {
        var query = this.search_view.query;

        if (!_.has(this.search_view, '_s_groupby')) { return; }

        // add row groupbys
        var row_groupby = this.graph_widget.get_row_groupby(),
            row_facet = this.make_row_groupby_facets(row_groupby),
            row_search_facet = query.findWhere({category:'GroupBy'});

        if (row_search_facet) {
            row_search_facet.values.reset(row_facet.values);
        } else {
            if (row_groupby.length) {
                query.add(row_facet);
            }
        }
         // add col groupbys
        var col_groupby = this.graph_widget.get_col_groupby(),
            col_facet = this.make_col_groupby_facets(col_groupby),
            col_search_facet = query.findWhere({category:'ColGroupBy'});

        if (col_search_facet) {
            col_search_facet.values.reset(col_facet.values);
        } else {
            if (col_groupby.length) {
                query.add(col_facet);
            }
        }
    },

    swap_groupby: function () {
        this.swap = true;
        this.register_groupby();
    },

    make_row_groupby_facets: function(groupbys) {
        return {
            category:'GroupBy',
            values: this.make_groupby_values(groupbys, 'group_by'),
            icon:'w',
            field: this.search_view._s_groupby
        };
    },

    make_col_groupby_facets: function(groupbys) {
        return {
            category:'ColGroupBy',
            values: this.make_groupby_values(groupbys, 'col_group_by'),
            icon:'f',
            field: this.search_field
        };
    },

    make_groupby_values: function (groupbys, category) {
        return _.map(groupbys, function (groupby) {
            var context = {};
            context[category] = groupby.field;
            return {
                label: groupby.string,
                value: {attrs:{domain: [], context: context}}
            };
        });
    },

    search_field: {
        get_context: function() {},
        get_domain: function () {},
        get_groupby: function () {},
    },

});

};

<|endoftext|>


<commit_before>describe('instantsearch', function () {

  var STATES = [
    'Alabama'
  , 'Alaska'
  , 'American Samoa'
  , 'Arizona'
  , 'Arkansas'
  , 'California'
  , 'Colorado'
  , 'Connecticut'
  , 'Delaware'
  , 'District Of Columbia'
  , 'Federated States Of Micronesia'
  , 'Florida'
  , 'Georgia'
  , 'Guam'
  , 'Hawaii'
  , 'Idaho'
  , 'Illinois'
  , 'Indiana'
  , 'Iowa'
  , 'Kansas'
  , 'Kentucky'
  , 'Louisiana'
  , 'Maine'
  , 'Marshall Islands'
  , 'Maryland'
  , 'Massachusetts'
  , 'Michigan'
  , 'Minnesota'
  , 'Mississippi'
  , 'Missouri'
  , 'Montana'
  , 'Nebraska'
  , 'Nevada'
  , 'New Hampshire'
  , 'New Jersey'
  , 'New Mexico'
  , 'New York'
  , 'North Carolina'
  , 'North Dakota'
  , 'Northern Mariana Islands'
  , 'Ohio'
  , 'Oklahoma'
  , 'Oregon'
  , 'Palau'
  , 'Pennsylvania'
  , 'Puerto Rico'
  , 'Rhode Island'
  , 'South Carolina'
  , 'South Dakota'
  , 'Tennessee'
  , 'Texas'
  , 'Utah'
  , 'Vermont'
  , 'Virgin Islands'
  , 'Virginia'
  , 'Washington'
  , 'West Virginia'
  , 'Wisconsin'
  , 'Wyoming'
  ];

  beforeEach(function () {
    this.$sandbox = $('#sandbox');

    this.$input = $('<input>');
    this.$input.appendTo('#sandbox');
  });

  afterEach(function () {
    this.$input.trigger('instantsearch.destroy');
    this.$sandbox.empty();

    this.$sandbox = null;
    this.$input = null;
  });

  describe('when initialized', function () {

    describe('in wrapping input', function () {

      it('wraps input in a div', function () {
        this.$input.instantSearch();

        this.$input[0].parentNode.className.should.equal('instasearch-wrapper');
      });

      it('adds .instainput class to input', function () {
        this.$input.addClass('test');
        this.$input.instantSearch();

        this.$input[0].className.should.equal('test instainput');
      });

      it('sets input autocomplete to \'off\'', function () {
        this.$input.instantSearch();

        this.$input.attr('autocomplete').should.equal('off');
      });

      it('creates a ghost input', function () {
        this.$input.addClass('test');
        this.$input.instantSearch();

        this.$input.next()[0].className.should.equal('test instaghost');
      });

    });

    describe('in appending results container to body', function () {

      it('initially hides results', function () {
        this.$input.instantSearch();

        $('.instaresults').css('display').should.equal('none');
      });

    });

  });

  describe('when input keydown event fires', function () {

    describe('and key code is', function () {

      var IGNORED_KEY_CODES = {
        'shift': 16
      , 'ctrl': 17
      , 'alt': 18
      , 'pause/break': 19
      , 'caps lock': 20
      , 'page up': 33
      , 'page down': 34
      , 'end': 35
      , 'home': 36
      , 'left': 37
      , 'insert': 45
      , 'windows key/cmd': 91
      , 'select': 93
      , 'f1': 112
      , 'f2': 113
      , 'f3': 114
      , 'f4': 115
      , 'f5': 116
      , 'f6': 117
      , 'f7': 118
      , 'f8': 119
      , 'f9': 120
      , 'f10': 121
      , 'f11': 122
      , 'f12': 123
      , 'f13': 124
      , 'f14': 125
      , 'f15': 126
      };

      $.each(IGNORED_KEY_CODES, function (key, val) {

        var keyCode = IGNORED_KEY_CODES[key];

        describe(key, function () {

          beforeEach(function () {
            sinon.stub($.InstantSearch.prototype, 'navigate');
            sinon.stub($.InstantSearch.prototype, 'complete');
            sinon.stub($.InstantSearch.prototype, 'trigger');
            sinon.stub($.InstantSearch.prototype, 'search');

            this.$input.instantSearch();
            this.$instainput = $('.instainput');
          });

          afterEach(function () {
            $.InstantSearch.prototype.navigate.restore();
            $.InstantSearch.prototype.complete.restore();
            $.InstantSearch.prototype.trigger.restore();
            $.InstantSearch.prototype.search.restore();

            this.$instainput = null;
          });

          it('does not set input value', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            this.$instainput.val().should.be.empty;
          });

          it('does not navigate', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.navigate.should.not.have.been.called;
          });

          it('does not complete', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.complete.should.not.have.been.called;
          });

          it('does not trigger', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.trigger.should.not.have.been.called;
          });

          it('does not search', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.search.should.not.have.been.called;
          });

        });

      });

      describe('up', function () {

        var keyCode = 38;

        describe('and no results are displayed', function () {

          beforeEach(function () {
            this.$input.instantSearch();
          });

          it('does not highlight a result', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $('.instahighlight').length.should.equal(0);
          });

        });

        describe('and results are displayed', function () {

          beforeEach(function (ready) {
            this.$input.instantSearch({
              source: function (req, res) {
                var re = new RegExp(req.term, 'i'),
                    results;

                results = $.grep(STATES, function (state) {
                  return state.match(re);
                });

                res(results, null);
              }
            });

            this.$ghost = this.$input.next();
            this.$results = $('.instaresults');

            this.$input.on('instantsearch.search', function (data) {
              ready();
            });
            /**
             * "c" (67) then "O" (79)
             *
             * Results:
             *    [
             *      'Colorado'
             *    , 'Connecticut'
             *    , 'District Of Columbia'
             *    , 'New Mexico'
             *    , 'Puerto Rico'
             *    , 'Wisconsin'
             *    ]
             */
            this.$input.val('cO');
            this.$input.trigger($.Event('keydown', { keyCode: 79 }));
          });

          afterEach(function () {
            this.$results.remove();

            this.$instainput = null;
            this.$results = null;
          });

          describe('and without highlight', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('highlights last result', function () {
              this.$results.find('.instahighlight').text().should.equal('Wisconsin');
            });

            it('sets input to last result', function () {
              this.$input.val().should.equal('Wisconsin');
            });

            it('sets ghost to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

          describe('and on first result', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: 40 }));
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('does not highlight any results', function () {
              this.$results.find('.instahighlight').length.should.equal(0);
            });

            it('does not change input value', function () {
              this.$input.val().should.equal('cO');
            });

            it('sets ghost value to \'cOlorado\'', function () {
              this.$ghost.val().should.equal('cOlorado');
            });

          });

          describe('and on last result', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('highlights second to last result', function () {
              this.$results.find('.instahighlight').text().should.equal('Puerto Rico');
            });

            it('sets input to second to last result', function () {
              this.$input.val().should.equal('Puerto Rico');
            });

            it('sets ghost to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

        });

        it('prevents default', function (done) {
          this.$input.instantSearch();
          this.$instainput = $('.instainput');
          this.$instainput.on('keydown', function (e) {
            e.isDefaultPrevented().should.be.true;

            done();
          });
          this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));
        });

      });

      describe('right', function () {

        var keyCode = 39;

        describe('and results are displayed', function () {

          beforeEach(function () {
            this.$input.instantSearch({
              source: function (req, res) {
                var re = new RegExp(req.term, 'i'),
                    results;

                results = $.grep(STATES, function (state) {
                  return state.match(re);
                });

                res(results, null);
              }
            });

            this.$ghost = this.$input.next();
            this.$results = $('.instaresults');
          });

          afterEach(function () {
            this.$results.remove();

            this.$instainput = null;
            this.$results = null;
          });

          describe('and beginning of input value matches top result', function () {

            beforeEach(function (ready) {
              var self = this;

              this.$input.on('instantsearch.search', function (data) {
                self.$input.trigger($.Event('keydown', { keyCode: keyCode }));
                ready();
              });
              /**
               * "c" (67) then "A" (65) then "l" (76)
               *
               * Results:
               *    [
               *      'California'
               *    ]
               */
              this.$input.val('cAl');
              this.$input.trigger($.Event('keydown', { keyCode: 76 }));
            });

            it('completes input value', function () {
              this.$input.val().should.equal('California');
            });

            it('sets ghost value to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

          describe('and beginning of input value does not match top result', function () {

            beforeEach(function (ready) {
              var self = this;

              this.$input.on('instantsearch.search', function (data) {
                self.$input.trigger($.Event('keydown', { keyCode: keyCode }));
                ready();
              });
              /**
               * "c" (67) then "A" (65)
               *
               * Results:
               *    [
               *      'American Samoa'
               *    , 'California'
               *    , 'North Carolina'
               *    , 'South Carolina'
               *    ]
               */
              this.$input.val('cA');
              this.$input.trigger($.Event('keydown', { keyCode: 65 }));
            });

            it('does not complete input value', function () {
              this.$input.val().should.equal('cA');
            });

            it('does not change ghost value', function () {
              this.$ghost.val().should.equal('');
            });

          });

        });

      });

      describe('down', function () {
      });

      describe('tab', function () {
      });

      describe('return', function () {
      });

      describe('escape', function () {
      });

    });

  });

  describe('when cut event fires', function () {
  });

  describe('when copy event fires', function () {
  });

  describe('when close on blur option is set to true', function () {
  });

  describe('when close on blur option is set to false', function () {
  });

  describe('when mouse enters search result', function () {
  });

  describe('when mouse leaves search result', function () {
  });

  describe('when mousedown event fires on search results', function () {
  });

  describe('when destroyed', function () {
  });

});
<commit_msg>Add tests for down key.<commit_after>describe('instantsearch', function () {

  var STATES = [
    'Alabama'
  , 'Alaska'
  , 'American Samoa'
  , 'Arizona'
  , 'Arkansas'
  , 'California'
  , 'Colorado'
  , 'Connecticut'
  , 'Delaware'
  , 'District Of Columbia'
  , 'Federated States Of Micronesia'
  , 'Florida'
  , 'Georgia'
  , 'Guam'
  , 'Hawaii'
  , 'Idaho'
  , 'Illinois'
  , 'Indiana'
  , 'Iowa'
  , 'Kansas'
  , 'Kentucky'
  , 'Louisiana'
  , 'Maine'
  , 'Marshall Islands'
  , 'Maryland'
  , 'Massachusetts'
  , 'Michigan'
  , 'Minnesota'
  , 'Mississippi'
  , 'Missouri'
  , 'Montana'
  , 'Nebraska'
  , 'Nevada'
  , 'New Hampshire'
  , 'New Jersey'
  , 'New Mexico'
  , 'New York'
  , 'North Carolina'
  , 'North Dakota'
  , 'Northern Mariana Islands'
  , 'Ohio'
  , 'Oklahoma'
  , 'Oregon'
  , 'Palau'
  , 'Pennsylvania'
  , 'Puerto Rico'
  , 'Rhode Island'
  , 'South Carolina'
  , 'South Dakota'
  , 'Tennessee'
  , 'Texas'
  , 'Utah'
  , 'Vermont'
  , 'Virgin Islands'
  , 'Virginia'
  , 'Washington'
  , 'West Virginia'
  , 'Wisconsin'
  , 'Wyoming'
  ];

  beforeEach(function () {
    this.$sandbox = $('#sandbox');

    this.$input = $('<input>');
    this.$input.appendTo('#sandbox');
  });

  afterEach(function () {
    this.$input.trigger('instantsearch.destroy');
    this.$sandbox.empty();

    this.$sandbox = null;
    this.$input = null;
  });

  describe('when initialized', function () {

    describe('in wrapping input', function () {

      it('wraps input in a div', function () {
        this.$input.instantSearch();

        this.$input[0].parentNode.className.should.equal('instasearch-wrapper');
      });

      it('adds .instainput class to input', function () {
        this.$input.addClass('test');
        this.$input.instantSearch();

        this.$input[0].className.should.equal('test instainput');
      });

      it('sets input autocomplete to \'off\'', function () {
        this.$input.instantSearch();

        this.$input.attr('autocomplete').should.equal('off');
      });

      it('creates a ghost input', function () {
        this.$input.addClass('test');
        this.$input.instantSearch();

        this.$input.next()[0].className.should.equal('test instaghost');
      });

    });

    describe('in appending results container to body', function () {

      it('initially hides results', function () {
        this.$input.instantSearch();

        $('.instaresults').css('display').should.equal('none');
      });

    });

  });

  describe('when input keydown event fires', function () {

    describe('and key code is', function () {

      var IGNORED_KEY_CODES = {
        'shift': 16
      , 'ctrl': 17
      , 'alt': 18
      , 'pause/break': 19
      , 'caps lock': 20
      , 'page up': 33
      , 'page down': 34
      , 'end': 35
      , 'home': 36
      , 'left': 37
      , 'insert': 45
      , 'windows key/cmd': 91
      , 'select': 93
      , 'f1': 112
      , 'f2': 113
      , 'f3': 114
      , 'f4': 115
      , 'f5': 116
      , 'f6': 117
      , 'f7': 118
      , 'f8': 119
      , 'f9': 120
      , 'f10': 121
      , 'f11': 122
      , 'f12': 123
      , 'f13': 124
      , 'f14': 125
      , 'f15': 126
      };

      $.each(IGNORED_KEY_CODES, function (key, val) {

        var keyCode = IGNORED_KEY_CODES[key];

        describe(key, function () {

          beforeEach(function () {
            sinon.stub($.InstantSearch.prototype, 'navigate');
            sinon.stub($.InstantSearch.prototype, 'complete');
            sinon.stub($.InstantSearch.prototype, 'trigger');
            sinon.stub($.InstantSearch.prototype, 'search');

            this.$input.instantSearch();
            this.$instainput = $('.instainput');
          });

          afterEach(function () {
            $.InstantSearch.prototype.navigate.restore();
            $.InstantSearch.prototype.complete.restore();
            $.InstantSearch.prototype.trigger.restore();
            $.InstantSearch.prototype.search.restore();

            this.$instainput = null;
          });

          it('does not set input value', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            this.$instainput.val().should.be.empty;
          });

          it('does not navigate', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.navigate.should.not.have.been.called;
          });

          it('does not complete', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.complete.should.not.have.been.called;
          });

          it('does not trigger', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.trigger.should.not.have.been.called;
          });

          it('does not search', function () {
            this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));

            $.InstantSearch.prototype.search.should.not.have.been.called;
          });

        });

      });

      describe('up', function () {

        var keyCode = 38;

        describe('and results are displayed', function () {

          beforeEach(function (ready) {
            this.$input.instantSearch({
              source: function (req, res) {
                var re = new RegExp(req.term, 'i'),
                    results;

                results = $.grep(STATES, function (state) {
                  return state.match(re);
                });

                res(results, null);
              }
            });

            this.$ghost = this.$input.next();
            this.$results = $('.instaresults');

            this.$input.on('instantsearch.search', function (data) {
              ready();
            });
            /**
             * "c" (67) then "O" (79)
             *
             * Results:
             *    [
             *      'Colorado'
             *    , 'Connecticut'
             *    , 'District Of Columbia'
             *    , 'New Mexico'
             *    , 'Puerto Rico'
             *    , 'Wisconsin'
             *    ]
             */
            this.$input.val('cO');
            this.$input.trigger($.Event('keydown', { keyCode: 79 }));
          });

          afterEach(function () {
            this.$results.remove();

            this.$instainput = null;
            this.$results = null;
          });

          describe('and without highlight', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('highlights last result', function () {
              this.$results.find('.instahighlight').text().should.equal('Wisconsin');
            });

            it('sets input to last result', function () {
              this.$input.val().should.equal('Wisconsin');
            });

            it('sets ghost to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

          describe('and on first result', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: 40 }));
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('does not highlight any results', function () {
              this.$results.find('.instahighlight').length.should.equal(0);
            });

            it('does not change input value', function () {
              this.$input.val().should.equal('cO');
            });

            it('sets ghost value to \'cOlorado\'', function () {
              this.$ghost.val().should.equal('cOlorado');
            });

          });

          describe('and on last result', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('highlights second to last result', function () {
              this.$results.find('.instahighlight').text().should.equal('Puerto Rico');
            });

            it('sets input to second to last result', function () {
              this.$input.val().should.equal('Puerto Rico');
            });

            it('sets ghost to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

        });

        it('prevents default', function (done) {
          this.$input.instantSearch();
          this.$instainput = $('.instainput');
          this.$instainput.on('keydown', function (e) {
            e.isDefaultPrevented().should.be.true;

            done();
          });
          this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));
        });

      });

      describe('right', function () {

        var keyCode = 39;

        describe('and results are displayed', function () {

          beforeEach(function () {
            this.$input.instantSearch({
              source: function (req, res) {
                var re = new RegExp(req.term, 'i'),
                    results;

                results = $.grep(STATES, function (state) {
                  return state.match(re);
                });

                res(results, null);
              }
            });

            this.$ghost = this.$input.next();
            this.$results = $('.instaresults');
          });

          afterEach(function () {
            this.$results.remove();

            this.$instainput = null;
            this.$results = null;
          });

          describe('and beginning of input value matches top result', function () {

            beforeEach(function (ready) {
              var self = this;

              this.$input.on('instantsearch.search', function (data) {
                self.$input.trigger($.Event('keydown', { keyCode: keyCode }));
                ready();
              });
              /**
               * "c" (67) then "A" (65) then "l" (76)
               *
               * Results:
               *    [
               *      'California'
               *    ]
               */
              this.$input.val('cAl');
              this.$input.trigger($.Event('keydown', { keyCode: 76 }));
            });

            it('completes input value', function () {
              this.$input.val().should.equal('California');
            });

            it('sets ghost value to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

          describe('and beginning of input value does not match top result', function () {

            beforeEach(function (ready) {
              var self = this;

              this.$input.on('instantsearch.search', function (data) {
                self.$input.trigger($.Event('keydown', { keyCode: keyCode }));
                ready();
              });
              /**
               * "c" (67) then "A" (65)
               *
               * Results:
               *    [
               *      'American Samoa'
               *    , 'California'
               *    , 'North Carolina'
               *    , 'South Carolina'
               *    ]
               */
              this.$input.val('cA');
              this.$input.trigger($.Event('keydown', { keyCode: 65 }));
            });

            it('does not complete input value', function () {
              this.$input.val().should.equal('cA');
            });

            it('does not change ghost value', function () {
              this.$ghost.val().should.equal('');
            });

          });

        });

      });

      describe('down', function () {

        var keyCode = 40;

        describe('and results are displayed', function () {

          beforeEach(function (ready) {
            this.$input.instantSearch({
              source: function (req, res) {
                var re = new RegExp(req.term, 'i'),
                    results;

                results = $.grep(STATES, function (state) {
                  return state.match(re);
                });

                res(results, null);
              }
            });

            this.$ghost = this.$input.next();
            this.$results = $('.instaresults');

            this.$input.on('instantsearch.search', function (data) {
              ready();
            });
            /**
             * "c" (67) then "O" (79)
             *
             * Results:
             *    [
             *      'Colorado'
             *    , 'Connecticut'
             *    , 'District Of Columbia'
             *    , 'New Mexico'
             *    , 'Puerto Rico'
             *    , 'Wisconsin'
             *    ]
             */
            this.$input.val('cO');
            this.$input.trigger($.Event('keydown', { keyCode: 79 }));
          });

          afterEach(function () {
            this.$results.remove();

            this.$instainput = null;
            this.$results = null;
          });

          describe('and without highlight', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('highlights first result', function () {
              this.$results.find('.instahighlight').text().should.equal('Colorado');
            });

            it('sets input to first result', function () {
              this.$input.val().should.equal('Colorado');
            });

            it('sets ghost to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

          describe('and on first result', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('highlights second result', function () {
              this.$results.find('.instahighlight').text().should.equal('Connecticut');
            });

            it('sets input to second result', function () {
              this.$input.val().should.equal('Connecticut');
            });

            it('sets ghost to empty string', function () {
              this.$ghost.val().should.equal('');
            });

          });

          describe('and on last result', function () {

            beforeEach(function () {
              this.$input.trigger($.Event('keydown', { keyCode: 38 }));
              this.$input.trigger($.Event('keydown', { keyCode: keyCode }));
            });

            it('does not highlight any results', function () {
              this.$results.find('.instahighlight').length.should.equal(0);
            });

            it('does not change input value', function () {
              this.$input.val().should.equal('cO');
            });

            it('sets ghost value to \'cOlorado\'', function () {
              this.$ghost.val().should.equal('cOlorado');
            });

          });

        });

        it('prevents default', function (done) {
          this.$input.instantSearch();
          this.$instainput = $('.instainput');
          this.$instainput.on('keydown', function (e) {
            e.isDefaultPrevented().should.be.true;

            done();
          });
          this.$instainput.trigger($.Event('keydown', { keyCode: keyCode }));
        });

      });

      describe('tab', function () {
      });

      describe('return', function () {
      });

      describe('escape', function () {
      });

    });

  });

  describe('when cut event fires', function () {
  });

  describe('when copy event fires', function () {
  });

  describe('when close on blur option is set to true', function () {
  });

  describe('when close on blur option is set to false', function () {
  });

  describe('when mouse enters search result', function () {
  });

  describe('when mouse leaves search result', function () {
  });

  describe('when mousedown event fires on search results', function () {
  });

  describe('when destroyed', function () {
  });

});
<|endoftext|>