<commit_before>import React, { Component } from 'react'
import RFB from '@nraynaud/novnc/lib/rfb'
import { createBackoff } from 'jsonrpc-websocket-client'
import {
  parse as parseUrl,
  resolve as resolveUrl
} from 'url'
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'

import propTypes from './prop-types-decorator'

const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))

const PROTOCOL_ALIASES = {
  'http:': 'ws:',
  'https:': 'wss:'
}
const fixProtocol = url => {
  const protocol = PROTOCOL_ALIASES[url.protocol]
  if (protocol) {
    url.protocol = protocol
  }
}

@propTypes({
  onClipboardChange: propTypes.func,
  url: propTypes.string.isRequired
})
export default class NoVnc extends Component {
  constructor (props) {
    super(props)
    this._rfb = null
    this._retryGen = createBackoff(Infinity)

    this._onUpdateState = (rfb, state) => {
      if (state === 'normal') {
        if (this._retryTimeout) {
          clearTimeout(this._retryTimeout)
          this._retryTimeout = undefined
          this._retryGen = createBackoff(Infinity)
        }
      }

      if (state !== 'disconnected') {
        return
      }

      clearTimeout(this._retryTimeout)
      this._retryTimeout = setTimeout(this._connect, this._retryGen.next().value)
    }
  }

  sendCtrlAltDel () {
    const rfb = this._rfb
    if (rfb) {
      rfb.sendCtrlAltDel()
    }
  }

  setClipboard (text) {
    const rfb = this._rfb
    if (rfb) {
      rfb.clipboardPasteFrom(text)
    }
  }

  _clean () {
    const rfb = this._rfb
    if (rfb) {
      this._rfb = null
      rfb.disconnect()
    }
    enableShortcuts()
  }

  _connect = () => {
    this._clean()

    const { canvas } = this.refs
    if (!canvas) {
      return
    }

    const url = parseRelativeUrl(this.props.url)
    fixProtocol(url)

    const isSecure = url.protocol === 'wss:'

    const { onClipboardChange } = this.props
    const rfb = this._rfb = new RFB({
      encrypt: isSecure,
      target: this.refs.canvas,
      onClipboard: onClipboardChange && ((_, text) => {
        onClipboardChange(text)
      }),
      onUpdateState: this._onUpdateState
    })

    // remove leading slashes from the path
    //
    // a leading slassh will be added by noVNC
    const clippedPath = url.path.replace(/^\/+/, '')

    // a port is required
    //
    // if not available from the URL, use the default ones
    const { port = isSecure ? 443 : 80 } = url

    rfb.connect(url.hostname, port, null, clippedPath)
    disableShortcuts()
  }

  componentDidMount () {
    this._connect()
  }

  componentWillUnmount () {
    this._clean()
  }

  componentWillReceiveProps (props) {
    const rfb = this._rfb
    if (rfb && this.props.scale !== props.scale) {
      rfb.get_display().set_scale(props.scale || 1)
      rfb.get_mouse().set_scale(props.scale || 1)
    }
  }

  _focus = () => {
    const rfb = this._rfb
    if (rfb) {
      const { activeElement } = document
      if (activeElement) {
        activeElement.blur()
      }

      rfb.get_keyboard().grab()
      rfb.get_mouse().grab()

      disableShortcuts()
    }
  }

  _unfocus = () => {
    const rfb = this._rfb
    if (rfb) {
      rfb.get_keyboard().ungrab()
      rfb.get_mouse().ungrab()

      enableShortcuts()
    }
  }

  render () {
    return <canvas
      className='center-block'
      height='480'
      onMouseEnter={this._focus}
      onMouseLeave={this._unfocus}
      ref='canvas'
      width='640'
    />
  }
}
<commit_msg>fix(react-novnc): url.port is not undefined when empty<commit_after>import React, { Component } from 'react'
import RFB from '@nraynaud/novnc/lib/rfb'
import { createBackoff } from 'jsonrpc-websocket-client'
import {
  parse as parseUrl,
  resolve as resolveUrl
} from 'url'
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'

import propTypes from './prop-types-decorator'

const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))

const PROTOCOL_ALIASES = {
  'http:': 'ws:',
  'https:': 'wss:'
}
const fixProtocol = url => {
  const protocol = PROTOCOL_ALIASES[url.protocol]
  if (protocol) {
    url.protocol = protocol
  }
}

@propTypes({
  onClipboardChange: propTypes.func,
  url: propTypes.string.isRequired
})
export default class NoVnc extends Component {
  constructor (props) {
    super(props)
    this._rfb = null
    this._retryGen = createBackoff(Infinity)

    this._onUpdateState = (rfb, state) => {
      if (state === 'normal') {
        if (this._retryTimeout) {
          clearTimeout(this._retryTimeout)
          this._retryTimeout = undefined
          this._retryGen = createBackoff(Infinity)
        }
      }

      if (state !== 'disconnected') {
        return
      }

      clearTimeout(this._retryTimeout)
      this._retryTimeout = setTimeout(this._connect, this._retryGen.next().value)
    }
  }

  sendCtrlAltDel () {
    const rfb = this._rfb
    if (rfb) {
      rfb.sendCtrlAltDel()
    }
  }

  setClipboard (text) {
    const rfb = this._rfb
    if (rfb) {
      rfb.clipboardPasteFrom(text)
    }
  }

  _clean () {
    const rfb = this._rfb
    if (rfb) {
      this._rfb = null
      rfb.disconnect()
    }
    enableShortcuts()
  }

  _connect = () => {
    this._clean()

    const { canvas } = this.refs
    if (!canvas) {
      return
    }

    const url = parseRelativeUrl(this.props.url)
    fixProtocol(url)

    const isSecure = url.protocol === 'wss:'

    const { onClipboardChange } = this.props
    const rfb = this._rfb = new RFB({
      encrypt: isSecure,
      target: this.refs.canvas,
      onClipboard: onClipboardChange && ((_, text) => {
        onClipboardChange(text)
      }),
      onUpdateState: this._onUpdateState
    })

    // remove leading slashes from the path
    //
    // a leading slassh will be added by noVNC
    const clippedPath = url.path.replace(/^\/+/, '')

    // a port is required
    //
    // if not available from the URL, use the default ones
    let port = url.port || isSecure ? 443 : 80

    rfb.connect(url.hostname, port, null, clippedPath)
    disableShortcuts()
  }

  componentDidMount () {
    this._connect()
  }

  componentWillUnmount () {
    this._clean()
  }

  componentWillReceiveProps (props) {
    const rfb = this._rfb
    if (rfb && this.props.scale !== props.scale) {
      rfb.get_display().set_scale(props.scale || 1)
      rfb.get_mouse().set_scale(props.scale || 1)
    }
  }

  _focus = () => {
    const rfb = this._rfb
    if (rfb) {
      const { activeElement } = document
      if (activeElement) {
        activeElement.blur()
      }

      rfb.get_keyboard().grab()
      rfb.get_mouse().grab()

      disableShortcuts()
    }
  }

  _unfocus = () => {
    const rfb = this._rfb
    if (rfb) {
      rfb.get_keyboard().ungrab()
      rfb.get_mouse().ungrab()

      enableShortcuts()
    }
  }

  render () {
    return <canvas
      className='center-block'
      height='480'
      onMouseEnter={this._focus}
      onMouseLeave={this._unfocus}
      ref='canvas'
      width='640'
    />
  }
}
<|endoftext|>


<commit_before>const webpack = require('webpack');

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
// const Visualizer = require('webpack-visualizer-plugin');

const NotifyPlugin = require('notify-webpack-plugin');

const ASSET_NAME_PATTERN = 'static/[name]-[hash:6].[ext]';

const packageJson = require('../package.json');
const nodeModulesDir = path.join(__dirname, '../node_modules');

// https://github.com/dmachat/angular-webpack-cookbook/wiki/Optimizing-Development
var preMinifiedDeps = [
  'moment/min/moment.min.js',
  'underscore/underscore-min.js',
  'indexeddbshim/dist/indexeddbshim.min.js',
  'messageformat/messageformat.min.js',
  'jquery/dist/jquery.min.js'
];

module.exports = (env) => {
  const isDev = env === 'dev';
  let version = packageJson.version.toString();
  if (process.env.TRAVIS_BUILD_NUMBER) {
    version += "." + process.env.TRAVIS_BUILD_NUMBER;
  }

  const config = {
    entry: {
      main: './src/index.js',
      authReturn: './src/authReturn.js'
    },

    output: {
      path: path.resolve('./dist'),
      filename: '[name]-[chunkhash:6].js',
      chunkFilename: 'chunk-[id]-[name]-[chunkhash:6].js'
    },

    devServer: {
      contentBase: path.resolve(__dirname, './src'),
      publicPath: '/',
      https: true,
      host: '0.0.0.0',
      hot: false
    },

    devtool: 'source-map',

    stats: 'errors-only',

    performance: {
      hints: false
    },

    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: [
            'babel-loader'
          ]
        }, {
          test: /\.json$/,
          loader: 'json-loader'
        }, {
          test: /\.html$/,
          use: [
            'raw-loader',
            'extract-loader',
            'html-loader'
          ]
        }, {
          test: /\.(png|eot|svg|ttf|woff(2)?)(\?v=\d+\.\d+\.\d+)?/,
          loader: 'url-loader',
          options: {
            limit: 5 * 1024, // only inline if less than 5kb
            name: ASSET_NAME_PATTERN
          }
        }, {
          test: /\.scss$/,
          loader: ExtractTextPlugin.extract({
            use: [
              'css-loader',
              'postcss-loader',
              'sass-loader'
            ],
            fallback: 'style-loader'
          })
        }
      ],

      noParse: [
        /\/jquery\.slim\.min\.js$/,
        /\/sql\.js$/
      ]
    },

    resolve: {
      extensions: ['.js', '.json'],

      alias: {
        app: path.resolve('./src')
      }
    },

    plugins: [
      new CleanWebpackPlugin(['dist'], {
        root: path.resolve('./')
      }),

      new NotifyPlugin('DIM', !isDev),

      new ExtractTextPlugin('styles-[contenthash:6].css'),

      new HtmlWebpackPlugin({
        inject: false,
        filename: 'index.html',
        template: '!handlebars-loader!src/index.html',
        chunks: ['manifest', 'vendor', 'main']
      }),

      new HtmlWebpackPlugin({
        inject: false,
        filename: 'return.html',
        template: '!handlebars-loader!src/return.html',
        chunks: ['manifest', 'vendor', 'authReturn']
      }),

      new CopyWebpackPlugin([
        { from: './node_modules/zip-js/WebContent/z-worker.js', to: 'static/zipjs' },
        { from: './node_modules/zip-js/WebContent/inflate.js', to: 'static/zipjs' },

        { from: './src/.htaccess' },
        { from: './src/extension-scripts/main.js', to: 'extension-scripts/' },
        { from: './src/manifest.json' },
        { from: './src/manifest-webapp.json' },
        { from: './src/data', to: 'data/' },
        { from: `./icons/${env}/` },
        { from: './src/safari-pinned-tab.svg' },
      ]),

      // Optimize chunk IDs
      new webpack.optimize.OccurrenceOrderPlugin(true),

      // Extract a stable "vendor" chunk
      new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        minChunks: function(module) {
          // this assumes your vendor imports exist in the node_modules directory
          return module.context && module.context.indexOf('node_modules') !== -1;
        }
      }),

      // CommonChunksPlugin will now extract all the common modules
      // from vendor and main bundles.  But since there are no more
      // common modules between them we end up with just the runtime
      // code included in the manifest file
      new webpack.optimize.CommonsChunkPlugin({
        name: 'manifest'
      }),

      new webpack.DefinePlugin({
        $DIM_VERSION: JSON.stringify(version),
        $DIM_FLAVOR: JSON.stringify(env),
        $DIM_CHANGELOG: JSON.stringify(`https://github.com/DestinyItemManager/DIM/blob/${env === 'release' ? 'master' : 'dev'}/CHANGELOG.md${env === 'release' ? '' : '#next'}`),
        // These are set from the Travis repo settings instead of .travis.yml
        $DIM_API_KEY: JSON.stringify(process.env.API_KEY),
        $DIM_AUTH_URL: JSON.stringify(process.env.AUTH_URL),
        // Website and extension have different keys
        $DIM_WEB_API_KEY: JSON.stringify(process.env.WEB_API_KEY),
        $DIM_WEB_AUTH_URL: JSON.stringify(process.env.WEB_AUTH_URL),

        $GOOGLE_DRIVE_CLIENT_ID: JSON.stringify('22022180893-raop2mu1d7gih97t5da9vj26quqva9dc.apps.googleusercontent.com'),

        // Feature flags!

        // Tags are off in release right now
        '$featureFlags.tagsEnabled': JSON.stringify(true),
        '$featureFlags.compareEnabled': JSON.stringify(true),
        '$featureFlags.vendorsEnabled': JSON.stringify(true),
        '$featureFlags.qualityEnabled': JSON.stringify(true),
        // Additional debugging / item info tools
        '$featureFlags.debugMode': JSON.stringify(false),
        // Print debug info to console about item moves
        '$featureFlags.debugMoves': JSON.stringify(false),
        // show changelog toaster
        '$featureFlags.changelogToaster': JSON.stringify(env === 'release'),
        // allow fetching item (weapon) scores and fetching/sending reviews from the Destiny Tracker API
        '$featureFlags.sendingWeaponDataEnabled': JSON.stringify(env === 'dev'),

        '$featureFlags.materialsExchangeEnabled': JSON.stringify(false),
        // allow importing and exporting your DIM data to JSON
        '$featureFlags.importExport': JSON.stringify(env !== 'release'),
        '$featureFlags.reviewsEnabled': JSON.stringify(env !== 'release'),
        // Sync data over gdrive
        '$featureFlags.gdrive': $DIM_FLAVOR !== 'release',
        '$featureFlags.debugSync': false
      }),

      // Enable if you want to debug the size of the chunks
      //new Visualizer(),
    ],

    node: {
      fs: 'empty',
      net: 'empty',
      tls: 'empty'
    }
  };

  // Run through big deps and extract the first part of the path,
  // as that is what you use to require the actual node modules
  // in your code. Then use the complete path to point to the correct
  // file and make sure webpack does not try to parse it
  preMinifiedDeps.forEach(function(dep) {
    var depPath = path.resolve(nodeModulesDir, dep);
    config.resolve.alias[dep.split(path.sep)[0]] = depPath;
    config.module.noParse.push(new RegExp(depPath));
  });

  if (!isDev) {
    // Bail and fail hard on first error
    config.bail = true;
    config.stats = 'verbose';

    // The sql.js library doesnt work at all (reports no tables) when minified,
    // so we exclude it from the regular minification
    // FYI, uglification runs on final chunks rather than individual modules
    config.plugins.push(new webpack.optimize.UglifyJsPlugin({
      exclude: /-sqlLib-/, // ensure the sqlLib chunk doesnt get minifed
      compress: { warnings: false },
      output: { comments: false },
      sourceMap: true
    }));
  }

  return config;
};
<commit_msg>Actually fix it<commit_after>const webpack = require('webpack');

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
// const Visualizer = require('webpack-visualizer-plugin');

const NotifyPlugin = require('notify-webpack-plugin');

const ASSET_NAME_PATTERN = 'static/[name]-[hash:6].[ext]';

const packageJson = require('../package.json');
const nodeModulesDir = path.join(__dirname, '../node_modules');

// https://github.com/dmachat/angular-webpack-cookbook/wiki/Optimizing-Development
var preMinifiedDeps = [
  'moment/min/moment.min.js',
  'underscore/underscore-min.js',
  'indexeddbshim/dist/indexeddbshim.min.js',
  'messageformat/messageformat.min.js',
  'jquery/dist/jquery.min.js'
];

module.exports = (env) => {
  const isDev = env === 'dev';
  let version = packageJson.version.toString();
  if (process.env.TRAVIS_BUILD_NUMBER) {
    version += "." + process.env.TRAVIS_BUILD_NUMBER;
  }

  const config = {
    entry: {
      main: './src/index.js',
      authReturn: './src/authReturn.js'
    },

    output: {
      path: path.resolve('./dist'),
      filename: '[name]-[chunkhash:6].js',
      chunkFilename: 'chunk-[id]-[name]-[chunkhash:6].js'
    },

    devServer: {
      contentBase: path.resolve(__dirname, './src'),
      publicPath: '/',
      https: true,
      host: '0.0.0.0',
      hot: false
    },

    devtool: 'source-map',

    stats: 'errors-only',

    performance: {
      hints: false
    },

    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: [
            'babel-loader'
          ]
        }, {
          test: /\.json$/,
          loader: 'json-loader'
        }, {
          test: /\.html$/,
          use: [
            'raw-loader',
            'extract-loader',
            'html-loader'
          ]
        }, {
          test: /\.(png|eot|svg|ttf|woff(2)?)(\?v=\d+\.\d+\.\d+)?/,
          loader: 'url-loader',
          options: {
            limit: 5 * 1024, // only inline if less than 5kb
            name: ASSET_NAME_PATTERN
          }
        }, {
          test: /\.scss$/,
          loader: ExtractTextPlugin.extract({
            use: [
              'css-loader',
              'postcss-loader',
              'sass-loader'
            ],
            fallback: 'style-loader'
          })
        }
      ],

      noParse: [
        /\/jquery\.slim\.min\.js$/,
        /\/sql\.js$/
      ]
    },

    resolve: {
      extensions: ['.js', '.json'],

      alias: {
        app: path.resolve('./src')
      }
    },

    plugins: [
      new CleanWebpackPlugin(['dist'], {
        root: path.resolve('./')
      }),

      new NotifyPlugin('DIM', !isDev),

      new ExtractTextPlugin('styles-[contenthash:6].css'),

      new HtmlWebpackPlugin({
        inject: false,
        filename: 'index.html',
        template: '!handlebars-loader!src/index.html',
        chunks: ['manifest', 'vendor', 'main']
      }),

      new HtmlWebpackPlugin({
        inject: false,
        filename: 'return.html',
        template: '!handlebars-loader!src/return.html',
        chunks: ['manifest', 'vendor', 'authReturn']
      }),

      new CopyWebpackPlugin([
        { from: './node_modules/zip-js/WebContent/z-worker.js', to: 'static/zipjs' },
        { from: './node_modules/zip-js/WebContent/inflate.js', to: 'static/zipjs' },

        { from: './src/.htaccess' },
        { from: './src/extension-scripts/main.js', to: 'extension-scripts/' },
        { from: './src/manifest.json' },
        { from: './src/manifest-webapp.json' },
        { from: './src/data', to: 'data/' },
        { from: `./icons/${env}/` },
        { from: './src/safari-pinned-tab.svg' },
      ]),

      // Optimize chunk IDs
      new webpack.optimize.OccurrenceOrderPlugin(true),

      // Extract a stable "vendor" chunk
      new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        minChunks: function(module) {
          // this assumes your vendor imports exist in the node_modules directory
          return module.context && module.context.indexOf('node_modules') !== -1;
        }
      }),

      // CommonChunksPlugin will now extract all the common modules
      // from vendor and main bundles.  But since there are no more
      // common modules between them we end up with just the runtime
      // code included in the manifest file
      new webpack.optimize.CommonsChunkPlugin({
        name: 'manifest'
      }),

      new webpack.DefinePlugin({
        $DIM_VERSION: JSON.stringify(version),
        $DIM_FLAVOR: JSON.stringify(env),
        $DIM_CHANGELOG: JSON.stringify(`https://github.com/DestinyItemManager/DIM/blob/${env === 'release' ? 'master' : 'dev'}/CHANGELOG.md${env === 'release' ? '' : '#next'}`),
        // These are set from the Travis repo settings instead of .travis.yml
        $DIM_API_KEY: JSON.stringify(process.env.API_KEY),
        $DIM_AUTH_URL: JSON.stringify(process.env.AUTH_URL),
        // Website and extension have different keys
        $DIM_WEB_API_KEY: JSON.stringify(process.env.WEB_API_KEY),
        $DIM_WEB_AUTH_URL: JSON.stringify(process.env.WEB_AUTH_URL),

        $GOOGLE_DRIVE_CLIENT_ID: JSON.stringify('22022180893-raop2mu1d7gih97t5da9vj26quqva9dc.apps.googleusercontent.com'),

        // Feature flags!

        // Tags are off in release right now
        '$featureFlags.tagsEnabled': JSON.stringify(true),
        '$featureFlags.compareEnabled': JSON.stringify(true),
        '$featureFlags.vendorsEnabled': JSON.stringify(true),
        '$featureFlags.qualityEnabled': JSON.stringify(true),
        // Additional debugging / item info tools
        '$featureFlags.debugMode': JSON.stringify(false),
        // Print debug info to console about item moves
        '$featureFlags.debugMoves': JSON.stringify(false),
        // show changelog toaster
        '$featureFlags.changelogToaster': JSON.stringify(env === 'release'),
        // allow fetching item (weapon) scores and fetching/sending reviews from the Destiny Tracker API
        '$featureFlags.sendingWeaponDataEnabled': JSON.stringify(env === 'dev'),

        '$featureFlags.materialsExchangeEnabled': JSON.stringify(false),
        // allow importing and exporting your DIM data to JSON
        '$featureFlags.importExport': JSON.stringify(env !== 'release'),
        '$featureFlags.reviewsEnabled': JSON.stringify(env !== 'release'),
        // Sync data over gdrive
        '$featureFlags.gdrive': JSON.stringify(env !== 'release'),
        '$featureFlags.debugSync': false
      }),

      // Enable if you want to debug the size of the chunks
      //new Visualizer(),
    ],

    node: {
      fs: 'empty',
      net: 'empty',
      tls: 'empty'
    }
  };

  // Run through big deps and extract the first part of the path,
  // as that is what you use to require the actual node modules
  // in your code. Then use the complete path to point to the correct
  // file and make sure webpack does not try to parse it
  preMinifiedDeps.forEach(function(dep) {
    var depPath = path.resolve(nodeModulesDir, dep);
    config.resolve.alias[dep.split(path.sep)[0]] = depPath;
    config.module.noParse.push(new RegExp(depPath));
  });

  if (!isDev) {
    // Bail and fail hard on first error
    config.bail = true;
    config.stats = 'verbose';

    // The sql.js library doesnt work at all (reports no tables) when minified,
    // so we exclude it from the regular minification
    // FYI, uglification runs on final chunks rather than individual modules
    config.plugins.push(new webpack.optimize.UglifyJsPlugin({
      exclude: /-sqlLib-/, // ensure the sqlLib chunk doesnt get minifed
      compress: { warnings: false },
      output: { comments: false },
      sourceMap: true
    }));
  }

  return config;
};
<|endoftext|>


<commit_before>(function() {

  var IMGUR_CLIENT_ID = "cc9df57988494ca";
  var BUTTON_INSERT_BEFORE_SELECTOR = ".composer-submit";
  var SELFIE_BUTTON_SELECTOR = "#totallyAwesomeSelfieButton";
  var VIDEO_SELECTOR = "#selfieVideo";
  var CANVAS_SELECTOR = "#selfieCanvas";
  var BODY_SELECTOR = "#pull_request_body";

  var selfieButton = '<button id="totallyAwesomeSelfieButton" type="button" class="button" onclick="return false;" style="position: absolute; right: 10px; bottom: 55px; width: 158px; left: 10px;">Add a Selfie</button>';
  var videoContainer = '<video autoplay id="selfieVideo" style="width: 125px; height: 125px; padding-bottom: 35px;"></video>';
  var canvasContainer = '<canvas id="selfieCanvas" style="display: none;"></canvas>';

  var setupSelfieStream = function() {

    if ($(BUTTON_INSERT_BEFORE_SELECTOR).length === 0) {
      setTimeout(setupSelfieStream, 250);
      return;
    }

    navigator.webkitGetUserMedia({video: true}, function(stream) {
      $(selfieButton).insertBefore(BUTTON_INSERT_BEFORE_SELECTOR);
      $(canvasContainer).insertBefore(SELFIE_BUTTON_SELECTOR);
      $(videoContainer).insertBefore(SELFIE_BUTTON_SELECTOR);
      $(SELFIE_BUTTON_SELECTOR).on('click', addSelfie);
      $(VIDEO_SELECTOR).attr('src', window.URL.createObjectURL(stream));
    });
  };

  var resizeCanvasElement = function() {
    var video = document.querySelector(VIDEO_SELECTOR);
    $(CANVAS_SELECTOR).attr('height', video.videoHeight);
    $(CANVAS_SELECTOR).attr('width', video.videoWidth);
  };

  var addSelfie = function() {
    var imageData = snapSelfie();
    var success = function(data) { addToBody(data['data']['link']); };
    uploadSelfie(imageData, success, notifyFail);
  };

  var snapSelfie = function() {
    resizeCanvasElement();
    var video = document.querySelector(VIDEO_SELECTOR);
    var canvas = document.querySelector(CANVAS_SELECTOR);
    var ctx = canvas.getContext('2d');
    ctx.drawImage(video, 0, 0);
    return canvas.toDataURL('/image/jpeg', 1).split(',')[1];
  };

  var uploadSelfie = function(imageData, successCb, errorCb) {
    $.ajax({
      url: 'https://api.imgur.com/3/upload',
      type: 'POST',
      beforeSend: function (xhr) {
        xhr.setRequestHeader('Authorization', 'Client-ID ' + IMGUR_CLIENT_ID);
      },
      data: {
        type: 'base64',
        image: imageData
      },
      dataType: 'json',
      success: successCb,
      error: errorCb
    });
  };

  var addToBody = function(link) {
    if ($(BODY_SELECTOR).val() !== "") {
      $(BODY_SELECTOR).val($(BODY_SELECTOR).val() + "\n");
    }
    $(BODY_SELECTOR).val($(BODY_SELECTOR).val() + "![selfie](" + link + ")\n");
  };

  var notifyFail = function() {
  };

  setTimeout(setupSelfieStream, 250);

})();
<commit_msg>Hide on preview<commit_after>(function() {

  var IMGUR_CLIENT_ID = "cc9df57988494ca";
  var BUTTON_INSERT_BEFORE_SELECTOR = ".composer-submit";
  var SELFIE_BUTTON_SELECTOR = "#totallyAwesomeSelfieButton";
  var VIDEO_SELECTOR = "#selfieVideo";
  var CANVAS_SELECTOR = "#selfieCanvas";
  var BODY_SELECTOR = "#pull_request_body";

  var selfieButton = '<button id="totallyAwesomeSelfieButton" type="button" class="button" onclick="return false;" style="position: absolute; right: 10px; bottom: 55px; width: 158px; left: 10px;">Add a Selfie</button>';
  var videoContainer = '<video autoplay id="selfieVideo" style="width: 125px; height: 125px; padding-bottom: 35px;"></video>';
  var canvasContainer = '<canvas id="selfieCanvas" style="display: none;"></canvas>';

  var setupSelfieStream = function() {

    if ($(BUTTON_INSERT_BEFORE_SELECTOR).length === 0) {
      setTimeout(setupSelfieStream, 250);
      return;
    }

    navigator.webkitGetUserMedia({video: true}, function(stream) {
      $(selfieButton).insertBefore(BUTTON_INSERT_BEFORE_SELECTOR);
      $(canvasContainer).insertBefore(SELFIE_BUTTON_SELECTOR);
      $(videoContainer).insertBefore(SELFIE_BUTTON_SELECTOR);
      $(SELFIE_BUTTON_SELECTOR).on('click', addSelfie);
      $(VIDEO_SELECTOR).attr('src', window.URL.createObjectURL(stream));
    });
  };

  var resizeCanvasElement = function() {
    var video = document.querySelector(VIDEO_SELECTOR);
    $(CANVAS_SELECTOR).attr('height', video.videoHeight);
    $(CANVAS_SELECTOR).attr('width', video.videoWidth);
  };

  var addSelfie = function() {
    var imageData = snapSelfie();
    var success = function(data) { addToBody(data['data']['link']); };
    uploadSelfie(imageData, success, notifyFail);
  };

  var snapSelfie = function() {
    resizeCanvasElement();
    var video = document.querySelector(VIDEO_SELECTOR);
    var canvas = document.querySelector(CANVAS_SELECTOR);
    var ctx = canvas.getContext('2d');
    ctx.drawImage(video, 0, 0);
    return canvas.toDataURL('/image/jpeg', 1).split(',')[1];
  };

  var uploadSelfie = function(imageData, successCb, errorCb) {
    $.ajax({
      url: 'https://api.imgur.com/3/upload',
      type: 'POST',
      beforeSend: function (xhr) {
        xhr.setRequestHeader('Authorization', 'Client-ID ' + IMGUR_CLIENT_ID);
      },
      data: {
        type: 'base64',
        image: imageData
      },
      dataType: 'json',
      success: successCb,
      error: errorCb
    });
  };

  var addToBody = function(link) {
    if ($(BODY_SELECTOR).val() !== "") {
      $(BODY_SELECTOR).val($(BODY_SELECTOR).val() + "\n");
    }
    $(BODY_SELECTOR).val($(BODY_SELECTOR).val() + "![selfie](" + link + ")\n");
  }

  var notifyFail = function() {
  };

  var hideOrShowElements = function() {
    if ($('.write-tab.selected').length > 0 ) {
      $(VIDEO_SELECTOR).css('display', 'inline-block');
      $(SELFIE_BUTTON_SELECTOR).css('display', 'inline-block');
    } else if ($('.preview-tab.selected').length > 0) {
      $(VIDEO_SELECTOR).css('display', 'none');
      $(SELFIE_BUTTON_SELECTOR).css('display', 'none');
    }
  };

  setTimeout(setupSelfieStream, 250);
  setInterval(hideOrShowElements, 100);

})();
<|endoftext|>


<commit_before>/*!
 * jQuery UI Droppable @VERSION
 * http://jqueryui.com
 *
 * Copyright 2012 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 *
 * http://api.jqueryui.com/droppable/
 *
 * Depends:
 *	jquery.ui.core.js
 *	jquery.ui.widget.js
 *	jquery.ui.mouse.js
 *	jquery.ui.draggable.js
 */
(function( $, undefined ) {

function isOverAxis( x, reference, size ) {
	return ( x > reference ) && ( x < ( reference + size ) );
}

$.widget("ui.droppable", {
	version: "@VERSION",
	widgetEventPrefix: "drop",
	options: {
		accept: '*',
		activeClass: false,
		addClasses: true,
		greedy: false,
		hoverClass: false,
		scope: 'default',
		tolerance: 'intersect'
	},
	_create: function() {

		var o = this.options,
			accept = o.accept;

		this.isover = false;
		this.isout = true;

		this.accept = $.isFunction(accept) ? accept : function(d) {
			return d.is(accept);
		};

		//Store the droppable's proportions
		this.proportions = { width: this.element[0].offsetWidth, height: this.element[0].offsetHeight };

		// Add the reference and positions to the manager
		$.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || [];
		$.ui.ddmanager.droppables[o.scope].push(this);

		(o.addClasses && this.element.addClass("ui-droppable"));

	},

	_destroy: function() {
		var i = 0,
			drop = $.ui.ddmanager.droppables[this.options.scope];

		for ( ; i < drop.length; i++ ) {
			if ( drop[i] === this ) {
				drop.splice(i, 1);
			}
		}

		this.element.removeClass("ui-droppable ui-droppable-disabled");
	},

	_setOption: function(key, value) {

		if(key === 'accept') {
			this.accept = $.isFunction(value) ? value : function(d) {
				return d.is(value);
			};
		}
		$.Widget.prototype._setOption.apply(this, arguments);
	},

	_activate: function(event) {
		var draggable = $.ui.ddmanager.current;
		if(this.options.activeClass) {
			this.element.addClass(this.options.activeClass);
		}
		if(draggable){
			this._trigger('activate', event, this.ui(draggable));
		}
	},

	_deactivate: function(event) {
		var draggable = $.ui.ddmanager.current;
		if(this.options.activeClass) {
			this.element.removeClass(this.options.activeClass);
		}
		if(draggable){
			this._trigger('deactivate', event, this.ui(draggable));
		}
	},

	_over: function(event) {

		var draggable = $.ui.ddmanager.current;

		// Bail if draggable and droppable are same element
		if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) {
			return;
		}

		if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			if(this.options.hoverClass) {
				this.element.addClass(this.options.hoverClass);
			}
			this._trigger('over', event, this.ui(draggable));
		}

	},

	_out: function(event) {

		var draggable = $.ui.ddmanager.current;

		// Bail if draggable and droppable are same element
		if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) {
			return;
		}

		if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			if(this.options.hoverClass) {
				this.element.removeClass(this.options.hoverClass);
			}
			this._trigger('out', event, this.ui(draggable));
		}

	},

	_drop: function(event,custom) {

		var draggable = custom || $.ui.ddmanager.current,
			childrenIntersection = false;

		// Bail if draggable and droppable are same element
		if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) {
			return false;
		}

		this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() {
			var inst = $.data(this, 'ui-droppable');
			if(
				inst.options.greedy &&
				!inst.options.disabled &&
				inst.options.scope === draggable.options.scope &&
				inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) &&
				$.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance)
			) { childrenIntersection = true; return false; }
		});
		if(childrenIntersection) {
			return false;
		}

		if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			if(this.options.activeClass) {
				this.element.removeClass(this.options.activeClass);
			}
			if(this.options.hoverClass) {
				this.element.removeClass(this.options.hoverClass);
			}
			this._trigger('drop', event, this.ui(draggable));
			return this.element;
		}

		return false;

	},

	ui: function(c) {
		return {
			draggable: (c.currentItem || c.element),
			helper: c.helper,
			position: c.position,
			offset: c.positionAbs
		};
	}

});

$.ui.intersect = function(draggable, droppable, toleranceMode) {

	if (!droppable.offset) {
		return false;
	}

	var draggableLeft, draggableTop,
		x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width,
		y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height,
		l = droppable.offset.left, r = l + droppable.proportions.width,
		t = droppable.offset.top, b = t + droppable.proportions.height;

	switch (toleranceMode) {
		case 'fit':
			return (l <= x1 && x2 <= r && t <= y1 && y2 <= b);
		case 'intersect':
			return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half
				x2 - (draggable.helperProportions.width / 2) < r && // Left Half
				t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half
				y2 - (draggable.helperProportions.height / 2) < b ); // Top Half
		case 'pointer':
			draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left);
			draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top);
			return isOverAxis( draggableTop, t, droppable.proportions.height ) && isOverAxis( draggableLeft, l, droppable.proportions.width )
		case 'touch':
			return (
				(y1 >= t && y1 <= b) ||	// Top edge touching
				(y2 >= t && y2 <= b) ||	// Bottom edge touching
				(y1 < t && y2 > b)		// Surrounded vertically
			) && (
				(x1 >= l && x1 <= r) ||	// Left edge touching
				(x2 >= l && x2 <= r) ||	// Right edge touching
				(x1 < l && x2 > r)		// Surrounded horizontally
			);
		default:
			return false;
		}

};

/*
	This manager tracks offsets of draggables and droppables
*/
$.ui.ddmanager = {
	current: null,
	droppables: { 'default': [] },
	prepareOffsets: function(t, event) {

		var i, j,
			m = $.ui.ddmanager.droppables[t.options.scope] || [],
			type = event ? event.type : null, // workaround for #2317
			list = (t.currentItem || t.element).find(":data(ui-droppable)").andSelf();

		droppablesLoop: for (i = 0; i < m.length; i++) {

			//No disabled and non-accepted
			if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) {
				continue;
			}

			// Filter out elements in the current dragged item
			for (j=0; j < list.length; j++) {
				if(list[j] === m[i].element[0]) {
					m[i].proportions.height = 0;
					continue droppablesLoop;
				}
			}

			m[i].visible = m[i].element.css("display") !== "none";
			if(!m[i].visible) {
				continue;
			}

			//Activate the droppable if used directly from draggables
			if(type === "mousedown") {
				m[i]._activate.call(m[i], event);
			}

			m[i].offset = m[i].element.offset();
			m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight };

		}

	},
	drop: function(draggable, event) {

		var dropped = false;
		$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

			if(!this.options) {
				return;
			}
			if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) {
				dropped = this._drop.call(this, event) || dropped;
			}

			if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
				this.isout = true;
				this.isover = false;
				this._deactivate.call(this, event);
			}

		});
		return dropped;

	},
	dragStart: function( draggable, event ) {
		//Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003)
		draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() {
			if( !draggable.options.refreshPositions ) {
				$.ui.ddmanager.prepareOffsets( draggable, event );
			}
		});
	},
	drag: function(draggable, event) {

		//If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse.
		if(draggable.options.refreshPositions) {
			$.ui.ddmanager.prepareOffsets(draggable, event);
		}

		//Run through all droppables and check their positions based on specific tolerance options
		$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

			if(this.options.disabled || this.greedyChild || !this.visible) {
				return;
			}

			var parentInstance, scope, parent,
				intersects = $.ui.intersect(draggable, this, this.options.tolerance),
				c = !intersects && this.isover ? 'isout' : (intersects && !this.isover ? 'isover' : null);
			if(!c) {
				return;
			}

			if (this.options.greedy) {
				// find droppable parents with same scope
				scope = this.options.scope;
				parent = this.element.parents(':data(ui-droppable)').filter(function () {
					return $.data(this, 'ui-droppable').options.scope === scope;
				});

				if (parent.length) {
					parentInstance = $.data(parent[0], 'ui-droppable');
					parentInstance.greedyChild = (c === 'isover');
				}
			}

			// we just moved into a greedy child
			if (parentInstance && c === 'isover') {
				parentInstance.isover = false;
				parentInstance.isout = true;
				parentInstance._out.call(parentInstance, event);
			}

			this[c] = true;
			this[c === 'isout' ? 'isover' : 'isout'] = false;
			this[c === "isover" ? "_over" : "_out"].call(this, event);

			// we just moved out of a greedy child
			if (parentInstance && c === 'isout') {
				parentInstance.isout = false;
				parentInstance.isover = true;
				parentInstance._over.call(parentInstance, event);
			}
		});

	},
	dragStop: function( draggable, event ) {
		draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" );
		//Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003)
		if( !draggable.options.refreshPositions ) {
			$.ui.ddmanager.prepareOffsets( draggable, event );
		}
	}
};

})(jQuery);
<commit_msg>Droppable: Added missing semicolon.<commit_after>/*!
 * jQuery UI Droppable @VERSION
 * http://jqueryui.com
 *
 * Copyright 2012 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 *
 * http://api.jqueryui.com/droppable/
 *
 * Depends:
 *	jquery.ui.core.js
 *	jquery.ui.widget.js
 *	jquery.ui.mouse.js
 *	jquery.ui.draggable.js
 */
(function( $, undefined ) {

function isOverAxis( x, reference, size ) {
	return ( x > reference ) && ( x < ( reference + size ) );
}

$.widget("ui.droppable", {
	version: "@VERSION",
	widgetEventPrefix: "drop",
	options: {
		accept: '*',
		activeClass: false,
		addClasses: true,
		greedy: false,
		hoverClass: false,
		scope: 'default',
		tolerance: 'intersect'
	},
	_create: function() {

		var o = this.options,
			accept = o.accept;

		this.isover = false;
		this.isout = true;

		this.accept = $.isFunction(accept) ? accept : function(d) {
			return d.is(accept);
		};

		//Store the droppable's proportions
		this.proportions = { width: this.element[0].offsetWidth, height: this.element[0].offsetHeight };

		// Add the reference and positions to the manager
		$.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || [];
		$.ui.ddmanager.droppables[o.scope].push(this);

		(o.addClasses && this.element.addClass("ui-droppable"));

	},

	_destroy: function() {
		var i = 0,
			drop = $.ui.ddmanager.droppables[this.options.scope];

		for ( ; i < drop.length; i++ ) {
			if ( drop[i] === this ) {
				drop.splice(i, 1);
			}
		}

		this.element.removeClass("ui-droppable ui-droppable-disabled");
	},

	_setOption: function(key, value) {

		if(key === 'accept') {
			this.accept = $.isFunction(value) ? value : function(d) {
				return d.is(value);
			};
		}
		$.Widget.prototype._setOption.apply(this, arguments);
	},

	_activate: function(event) {
		var draggable = $.ui.ddmanager.current;
		if(this.options.activeClass) {
			this.element.addClass(this.options.activeClass);
		}
		if(draggable){
			this._trigger('activate', event, this.ui(draggable));
		}
	},

	_deactivate: function(event) {
		var draggable = $.ui.ddmanager.current;
		if(this.options.activeClass) {
			this.element.removeClass(this.options.activeClass);
		}
		if(draggable){
			this._trigger('deactivate', event, this.ui(draggable));
		}
	},

	_over: function(event) {

		var draggable = $.ui.ddmanager.current;

		// Bail if draggable and droppable are same element
		if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) {
			return;
		}

		if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			if(this.options.hoverClass) {
				this.element.addClass(this.options.hoverClass);
			}
			this._trigger('over', event, this.ui(draggable));
		}

	},

	_out: function(event) {

		var draggable = $.ui.ddmanager.current;

		// Bail if draggable and droppable are same element
		if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) {
			return;
		}

		if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			if(this.options.hoverClass) {
				this.element.removeClass(this.options.hoverClass);
			}
			this._trigger('out', event, this.ui(draggable));
		}

	},

	_drop: function(event,custom) {

		var draggable = custom || $.ui.ddmanager.current,
			childrenIntersection = false;

		// Bail if draggable and droppable are same element
		if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) {
			return false;
		}

		this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() {
			var inst = $.data(this, 'ui-droppable');
			if(
				inst.options.greedy &&
				!inst.options.disabled &&
				inst.options.scope === draggable.options.scope &&
				inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) &&
				$.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance)
			) { childrenIntersection = true; return false; }
		});
		if(childrenIntersection) {
			return false;
		}

		if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			if(this.options.activeClass) {
				this.element.removeClass(this.options.activeClass);
			}
			if(this.options.hoverClass) {
				this.element.removeClass(this.options.hoverClass);
			}
			this._trigger('drop', event, this.ui(draggable));
			return this.element;
		}

		return false;

	},

	ui: function(c) {
		return {
			draggable: (c.currentItem || c.element),
			helper: c.helper,
			position: c.position,
			offset: c.positionAbs
		};
	}

});

$.ui.intersect = function(draggable, droppable, toleranceMode) {

	if (!droppable.offset) {
		return false;
	}

	var draggableLeft, draggableTop,
		x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width,
		y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height,
		l = droppable.offset.left, r = l + droppable.proportions.width,
		t = droppable.offset.top, b = t + droppable.proportions.height;

	switch (toleranceMode) {
		case 'fit':
			return (l <= x1 && x2 <= r && t <= y1 && y2 <= b);
		case 'intersect':
			return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half
				x2 - (draggable.helperProportions.width / 2) < r && // Left Half
				t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half
				y2 - (draggable.helperProportions.height / 2) < b ); // Top Half
		case 'pointer':
			draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left);
			draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top);
			return isOverAxis( draggableTop, t, droppable.proportions.height ) && isOverAxis( draggableLeft, l, droppable.proportions.width );
		case 'touch':
			return (
				(y1 >= t && y1 <= b) ||	// Top edge touching
				(y2 >= t && y2 <= b) ||	// Bottom edge touching
				(y1 < t && y2 > b)		// Surrounded vertically
			) && (
				(x1 >= l && x1 <= r) ||	// Left edge touching
				(x2 >= l && x2 <= r) ||	// Right edge touching
				(x1 < l && x2 > r)		// Surrounded horizontally
			);
		default:
			return false;
		}

};

/*
	This manager tracks offsets of draggables and droppables
*/
$.ui.ddmanager = {
	current: null,
	droppables: { 'default': [] },
	prepareOffsets: function(t, event) {

		var i, j,
			m = $.ui.ddmanager.droppables[t.options.scope] || [],
			type = event ? event.type : null, // workaround for #2317
			list = (t.currentItem || t.element).find(":data(ui-droppable)").andSelf();

		droppablesLoop: for (i = 0; i < m.length; i++) {

			//No disabled and non-accepted
			if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) {
				continue;
			}

			// Filter out elements in the current dragged item
			for (j=0; j < list.length; j++) {
				if(list[j] === m[i].element[0]) {
					m[i].proportions.height = 0;
					continue droppablesLoop;
				}
			}

			m[i].visible = m[i].element.css("display") !== "none";
			if(!m[i].visible) {
				continue;
			}

			//Activate the droppable if used directly from draggables
			if(type === "mousedown") {
				m[i]._activate.call(m[i], event);
			}

			m[i].offset = m[i].element.offset();
			m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight };

		}

	},
	drop: function(draggable, event) {

		var dropped = false;
		$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

			if(!this.options) {
				return;
			}
			if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) {
				dropped = this._drop.call(this, event) || dropped;
			}

			if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
				this.isout = true;
				this.isover = false;
				this._deactivate.call(this, event);
			}

		});
		return dropped;

	},
	dragStart: function( draggable, event ) {
		//Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003)
		draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() {
			if( !draggable.options.refreshPositions ) {
				$.ui.ddmanager.prepareOffsets( draggable, event );
			}
		});
	},
	drag: function(draggable, event) {

		//If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse.
		if(draggable.options.refreshPositions) {
			$.ui.ddmanager.prepareOffsets(draggable, event);
		}

		//Run through all droppables and check their positions based on specific tolerance options
		$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

			if(this.options.disabled || this.greedyChild || !this.visible) {
				return;
			}

			var parentInstance, scope, parent,
				intersects = $.ui.intersect(draggable, this, this.options.tolerance),
				c = !intersects && this.isover ? 'isout' : (intersects && !this.isover ? 'isover' : null);
			if(!c) {
				return;
			}

			if (this.options.greedy) {
				// find droppable parents with same scope
				scope = this.options.scope;
				parent = this.element.parents(':data(ui-droppable)').filter(function () {
					return $.data(this, 'ui-droppable').options.scope === scope;
				});

				if (parent.length) {
					parentInstance = $.data(parent[0], 'ui-droppable');
					parentInstance.greedyChild = (c === 'isover');
				}
			}

			// we just moved into a greedy child
			if (parentInstance && c === 'isover') {
				parentInstance.isover = false;
				parentInstance.isout = true;
				parentInstance._out.call(parentInstance, event);
			}

			this[c] = true;
			this[c === 'isout' ? 'isover' : 'isout'] = false;
			this[c === "isover" ? "_over" : "_out"].call(this, event);

			// we just moved out of a greedy child
			if (parentInstance && c === 'isout') {
				parentInstance.isout = false;
				parentInstance.isover = true;
				parentInstance._over.call(parentInstance, event);
			}
		});

	},
	dragStop: function( draggable, event ) {
		draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" );
		//Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003)
		if( !draggable.options.refreshPositions ) {
			$.ui.ddmanager.prepareOffsets( draggable, event );
		}
	}
};

})(jQuery);
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'

import { msg } from '_/intl'
import { isNumber } from '_/utils'
import { createDiskFormatList, createStorageDomainList, isDiskNameValid } from '_/components/utils'

import {
  Button,
  Col,
  ControlLabel,
  FieldLevelHelp,
  Form,
  FormControl,
  FormGroup,
  Modal,
  HelpBlock,
  Checkbox,
} from 'patternfly-react'
import SelectBox from '_/components/SelectBox'
import style from './style.css'
import OverlayTooltip from '_/components/OverlayTooltip'

const DISK_DEFAULTS = {
  active: true,
  iface: 'virtio_scsi', // virtio | virtio_scsi

  id: undefined,
  name: '',
  type: 'image',
  format: 'cow', // cow | raw
  bootable: false,
  sparse: true,

  provisionedSize: 1 * 1024 ** 3,
}

const DISK_FORMATS = createDiskFormatList()

const LabelCol = ({ children, ...props }) => {
  return <Col componentClass={ControlLabel} {...props}>
    { children }
  </Col>
}
LabelCol.propTypes = {
  children: PropTypes.node.isRequired,
}

/**
 * Collect required information required to create a new or edit an existing Disk Image
 * for a VM.
 *
 * Note: Only deal with Disk Images.  Does not support iSCSI/LUN or Cinder disks.
 *
 * Note 2: Ultimately will use the DiskAttachments Services to create and DiskAttachment
 *         service to edit and remove disk images.
 *
 *    http://ovirt.github.io/ovirt-engine-api-model/master/#services/disk_attachments/methods/add
 *    http://ovirt.github.io/ovirt-engine-api-model/master/#services/disk_attachment/methods/update
 *    http://ovirt.github.io/ovirt-engine-api-model/master/#services/disk_attachment/methods/remove
 *
 * Needed for Create:
 *   - Alias (aka Name) (defaulting to a suggested name)
 *   - Size in GiB
 *   - Storage Domain (defaulted to a suggested storage domain)
 *   - Disk Image Format (Allocation Policy ... Thin Provision vs Preallocated)
 *   - (auto-selected and never on the UI): Interface
 *
 * May be changed via Edit for a Disk Image (type == 'image'):
 *   - Alias
 *   - Size, but only allow extension: "Extend size by (GiB)"
 *
 * May be changed via Edit for a Direct LUN (type == 'lun'):
 *   - Alias
 */
class DiskImageEditor extends Component {
  constructor (props) {
    super(props)
    this.state = {
      showModal: false,
      storageDomainSelectList: createStorageDomainList(props.storageDomainList, props.dataCenterId, true),

      id: undefined,
      values: {
        alias: props.suggestedName || '',
        size: DISK_DEFAULTS.provisionedSize / 1024 ** 3,
        storageDomain: props.suggestedStorageDomain || '',
        bootable: DISK_DEFAULTS.bootable,
        format: DISK_DEFAULTS.format, // cow vs raw .. Allocation Policy (thin sparse vs preallocated !sparse)?
      },
      errors: {},
    }
    this.changesMade = false

    this.open = this.open.bind(this)
    this.close = this.close.bind(this)
    this.composeDisk = this.composeDisk.bind(this)
    this.isFormValid = this.isFormValid.bind(this)
    this.validateField = this.validateField.bind(this)
    this.handleSave = this.handleSave.bind(this)

    this.changeAlias = this.changeAlias.bind(this)
    this.changeSize = this.changeSize.bind(this)
    this.changeStorageDomain = this.changeStorageDomain.bind(this)
    this.changeFormat = this.changeFormat.bind(this)
    this.changeBootable = this.changeBootable.bind(this)
  }

  open (e) {
    e.preventDefault()
    const { disk, suggestedName, suggestedStorageDomain } = this.props
    const { storageDomainSelectList } = this.state
    const diskInfo = disk
      ? { // edit
        id: disk.get('id'),
        values: {
          alias: disk.get('name'),
          bootable: disk.get('bootable'),
          size: 0,
          storageDomain: disk.get('storageDomainId'),
          format: disk.get('format'),
        },
      }
      : { // new
        id: undefined,
        values: {
          alias: suggestedName,
          bootable: this.props.vm.get('disks').size > 0 ? DISK_DEFAULTS.bootable : true,
          size: DISK_DEFAULTS.provisionedSize / 1024 ** 3,
          storageDomain:
            (/\w+/.test(suggestedStorageDomain) && suggestedStorageDomain) ||
            (storageDomainSelectList.length > 0 && storageDomainSelectList[0].id) ||
            '',
          format: DISK_DEFAULTS.format,
        },
        errors: {},
      }

    this.setState({
      showModal: true,
      storageDomainSelectList: createStorageDomainList(this.props.storageDomainList, this.props.dataCenterId, true),
      ...diskInfo,
    })
    this.changesMade = false
  }

  close () {
    this.setState({ showModal: false })
  }

  composeDisk () {
    const { vm, disk } = this.props
    const vmDiskInSameStorageDomain =
      vm.get('disks') &&
      vm.get('disks').find(disk => disk.get('storageDomainId') === this.state.values.storageDomain)

    const iface =
      (disk && disk.get('iface')) ||
      (vmDiskInSameStorageDomain && vmDiskInSameStorageDomain.get('iface')) ||
      'virtio_scsi'

    const provisionedSize = disk
      ? disk.get('provisionedSize') + this.state.values.size * 1024 ** 3
      : this.state.values.size * 1024 ** 3
    const bootable = this.state.values.bootable
    const newDisk = {
      ...DISK_DEFAULTS,
      attachmentId: disk && disk.get('attachmentId'),

      iface,
      id: this.state.id,
      name: this.state.values.alias,
      provisionedSize,
      storageDomainId: this.state.values.storageDomain,
      format: this.state.values.format,
      bootable,
      sparse: this.state.values.format === 'cow',
    }

    if (disk && disk.get('type') !== 'image') {
      newDisk.type = disk.get('type')
      newDisk.provisionedSize = undefined
    }
    return newDisk
  }

  validateField (field = '') {
    return () => {
      const errors = {}
      let isErrors = false
      switch (field) {
        case 'alias':
          if (!isDiskNameValid(this.state.values.alias)) {
            errors['alias'] = msg.pleaseEnterValidDiskName()
            isErrors = true
          }
          break
      }
      this.setState((state) => ({ errors: { ...state.errors, ...errors } }))
      return isErrors
    }
  }

  handleSave () {
    if (this.validateField('alias')()) {
      return
    }
    if (!this.props.disk || this.changesMade) {
      const newDisk = this.composeDisk()
      this.props.onSave(this.props.vm.get('id'), newDisk)
    }
    this.close()
  }

  changeAlias ({ target: { value } }) {
    this.setState((state) => ({ values: { ...state.values, alias: value }, errors: { ...state.errors, 'alias': '' } }))
    this.changesMade = true
  }

  changeSize ({ target: { value } }) {
    if (isNumber(value) && value >= 0) {
      this.setState((state) => ({ values: { ...state.values, size: value } }))
      this.changesMade = true
    }
  }

  changeBootable (event) {
    const target = event.target
    const bootable = target.type === 'checkbox' ? target.checked : target.value
    this.setState((state) => ({ values: { ...state.values, bootable } }))
    this.changesMade = true
  }

  changeStorageDomain (value) {
    this.setState((state) => ({ values: { ...state.values, storageDomain: value } }))
    this.changesMade = true
  }

  changeFormat (value) {
    this.setState((state) => ({ values: { ...state.values, format: value } }))
    this.changesMade = true
  }

  isFormValid () {
    const isErrors = Object.values(this.state.errors).reduce((acc, value) => (value !== undefined) ? true : acc, false)
    if (isErrors) {
      return false
    }

    // make sure that required fields have data
    const { values } = this.state
    return this.props.disk
      // edit mode needs: a change, an alias and a disk size
      ? this.changesMade && !!values.alias // add size + bootable or leave like that
      // create mode needs: name, size, storage domain, disk type
      : values.alias && values.size > 0 && values.storageDomain && values.format
  }

  render () {
    const { idPrefix, disk, trigger } = this.props

    const createMode = !disk
    const isImage = disk && disk.get('type') === 'image'
    const isDirectLUN = disk && disk.get('type') === 'lun'

    const diskSize = disk && (disk.get('lunSize') ? disk.get('lunSize') : disk.get('provisionedSize'))
    const vmIsDown = this.props.vm.get('status') === 'down'

    return <React.Fragment>
      { trigger({ onClick: this.open }) }

      <Modal
        dialogClassName={style['editor-modal']}
        id={`${idPrefix}-modal`}
        show={this.state.showModal}
        onHide={this.close}
      >
        <Modal.Header>
          <Modal.CloseButton id={`${idPrefix}-modal-close`} onClick={this.close} />
          <Modal.Title>{createMode ? msg.createNewDisk() : msg.editDisk()}</Modal.Title>
        </Modal.Header>
        <Modal.Body>

          <Form
            horizontal
            onSubmit={e => { e.preventDefault() }}
            id={`${idPrefix}-modal-form`}
          >
            {/* Alias */}
            <FormGroup controlId={`${idPrefix}-alias`} validationState={this.state.errors['alias'] && 'error'}>
              <LabelCol sm={3}>
                { msg.diskEditorAliasLabel() }
              </LabelCol>
              <Col sm={9}>
                <FormControl
                  type='text'
                  defaultValue={this.state.values.alias}
                  onChange={this.changeAlias}
                  onBlur={this.validateField('alias')}
                />
                {this.state.errors['alias'] && <HelpBlock>{this.state.errors['alias']}</HelpBlock>}
              </Col>
            </FormGroup>

            {/* Size Display (for edit mode) */}
            { !createMode &&
            <FormGroup controlId={`${idPrefix}-size`}>
              <LabelCol sm={3}>
                { msg.diskEditorSizeEditLabel() }
                { !isImage &&
                  <FieldLevelHelp
                    inline
                    content={msg.diskEditorSizeCantChangeHelp()}
                    buttonClass={style['editor-field-help']}
                  />
                }
              </LabelCol>
              <Col sm={9}>
                <div id={`${idPrefix}-size`} className={style['editor-field-read-only']}>
                  { diskSize / 1024 ** 3 }

              </Col>
            </FormGroup>
            }

            {/* Size Editor (initial size for create, expand by size for edit) */}
            { (createMode || isImage) &&
            <FormGroup controlId={`${idPrefix}-size-edit`}>
              <LabelCol sm={3}>
                { createMode &&
                  <React.Fragment>
                    {msg.diskEditorSizeLabel()}
                    <FieldLevelHelp
                      inline
                      content={msg.diskEditorSizeCreateHelp()}
                      buttonClass={style['editor-field-help']}
                    />
                  </React.Fragment>
                }
                { !createMode && msg.diskEditorResizeLabel() }
              </LabelCol>
              <Col sm={2}>
                <OverlayTooltip id={`${idPrefix}-form-tooltip`} tooltip={msg.diskEditorResizeNote()} placement='right'>
                  <FormControl
                    type='number'
                    value={this.state.values.size}
                    onChange={this.changeSize}
                  />
                </OverlayTooltip>
              </Col>
            </FormGroup>
            }

            {/* Storage Domain */}
            <FormGroup controlId={`${idPrefix}-storage-domain`}>
              <LabelCol sm={3}>
                { msg.diskEditorStorageDomainLabel() }
                <FieldLevelHelp
                  inline
                  content={
                    createMode
                      ? msg.diskEditorStorageDomainCreateHelp()
                      : msg.diskEditorStorageDomainCantChangeHelp()
                  }
                  buttonClass={style['editor-field-help']}
                />
              </LabelCol>
              <Col sm={9}>
                { createMode &&
                  <SelectBox
                    id={`${idPrefix}-storage-domain`}
                    items={this.state.storageDomainSelectList}
                    selected={this.state.values.storageDomain}
                    onChange={this.changeStorageDomain}
                  />
                }
                { !createMode && !isDirectLUN &&
                  <div id={`${idPrefix}-storage-domain`} className={style['editor-field-read-only']}>
                    {
                      this.props.storageDomains.getIn([this.state.values.storageDomain, 'name']) ||
                      msg.diskEditorStorageDomainNotAvailable()
                    }

                }
                { isDirectLUN &&
                  <div id={`${idPrefix}-storage-domain`} className={style['editor-field-read-only']}>
                    { msg.diskEditorStorageDomainNotAvailable() }

                }
              </Col>
            </FormGroup>

            {/* Disk Format */}
            <FormGroup controlId={`${idPrefix}-format`}>
              <LabelCol sm={3}>
                { msg.diskEditorFormatLabel() }
                <FieldLevelHelp
                  inline
                  content={
                    createMode
                      ? msg.diskEditorFormatCreateHelp()
                      : msg.diskEditorFormatCantChangeHelp()
                  }
                  buttonClass={style['editor-field-help']}
                />
              </LabelCol>
              <Col sm={9}>
                { createMode &&
                  <SelectBox
                    id={`${idPrefix}-format`}
                    items={DISK_FORMATS}
                    selected={this.state.format}
                    onChange={this.changeFormat}
                  />
                }
                { !createMode && !isDirectLUN &&
                  <div id={`${idPrefix}-format`} className={style['editor-field-read-only']}>
                    { this.state.values.format === 'raw' && msg.diskEditorFormatOptionRaw() }
                    { this.state.values.format === 'cow' && msg.diskEditorFormatOptionCow() }

                }
                { isDirectLUN &&
                  <div id={`${idPrefix}-format`} className={style['editor-field-read-only']}>
                    { msg.diskEditorFormatNotAvailable() }

                }
              </Col>
            </FormGroup>
            {/* Disk Bootable */}
            <FormGroup controlId={`${idPrefix}-bootable`}>
              <LabelCol sm={3}>
                { msg.diskEditorBootableLabel() }
                {!vmIsDown &&
                  <FieldLevelHelp
                    inline
                    content={msg.bootableEditTooltip()}
                    buttonClass={style['editor-field-help']}
                  />
                }
              </LabelCol>
              <Col sm={9}>
                <Checkbox
                  checked={this.state.values.bootable}
                  onChange={this.changeBootable}
                  id={`${idPrefix}-bootable`}
                  disabled={!vmIsDown}
                />
              </Col>
            </FormGroup>
          </Form>
        </Modal.Body>
        <Modal.Footer>
          <Button
            id={`${idPrefix}-modal-cancel`}
            bsStyle='default'
            className='btn-cancel'
            onClick={this.close}
          >
            { msg.cancel() }
          </Button>
          <Button
            id={`${idPrefix}-modal-ok`}
            bsStyle='primary'
            onClick={this.handleSave}
            disabled={!this.isFormValid()}
          >
            { msg.ok() }
          </Button>
        </Modal.Footer>
      </Modal>
    </React.Fragment>
  }
}
DiskImageEditor.propTypes = {
  idPrefix: PropTypes.string.isRequired,
  vm: PropTypes.object.isRequired,

  disk: PropTypes.object, // if !!disk then edit mode
  suggestedName: PropTypes.string, // if suggestedName && !disk then create mode
  suggestedStorageDomain: PropTypes.string,

  storageDomainList: PropTypes.object.isRequired,
  trigger: PropTypes.func.isRequired,
  onSave: PropTypes.func.isRequired,

  storageDomains: PropTypes.object.isRequired,
  dataCenterId: PropTypes.string.isRequired,
}

export default connect(
  (state, { vm }) => ({
    storageDomains: state.storageDomains,
    dataCenterId: state.clusters.getIn([ vm.getIn([ 'cluster', 'id' ]), 'dataCenterId' ]),
  })
)(DiskImageEditor)
<commit_msg>Fix form validation for `DiskImageEditor`<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'

import { msg } from '_/intl'
import { isNumber } from '_/utils'
import { createDiskFormatList, createStorageDomainList, isDiskNameValid } from '_/components/utils'

import {
  Button,
  Col,
  ControlLabel,
  FieldLevelHelp,
  Form,
  FormControl,
  FormGroup,
  Modal,
  HelpBlock,
  Checkbox,
} from 'patternfly-react'
import SelectBox from '_/components/SelectBox'
import style from './style.css'
import OverlayTooltip from '_/components/OverlayTooltip'

const DISK_DEFAULTS = {
  active: true,
  iface: 'virtio_scsi', // virtio | virtio_scsi

  id: undefined,
  name: '',
  type: 'image',
  format: 'cow', // cow | raw
  bootable: false,
  sparse: true,

  provisionedSize: 1 * 1024 ** 3,
}

const DISK_FORMATS = createDiskFormatList()

const LabelCol = ({ children, ...props }) => {
  return <Col componentClass={ControlLabel} {...props}>
    { children }
  </Col>
}
LabelCol.propTypes = {
  children: PropTypes.node.isRequired,
}

/**
 * Collect required information required to create a new or edit an existing Disk Image
 * for a VM.
 *
 * Note: Only deal with Disk Images.  Does not support iSCSI/LUN or Cinder disks.
 *
 * Note 2: Ultimately will use the DiskAttachments Services to create and DiskAttachment
 *         service to edit and remove disk images.
 *
 *    http://ovirt.github.io/ovirt-engine-api-model/master/#services/disk_attachments/methods/add
 *    http://ovirt.github.io/ovirt-engine-api-model/master/#services/disk_attachment/methods/update
 *    http://ovirt.github.io/ovirt-engine-api-model/master/#services/disk_attachment/methods/remove
 *
 * Needed for Create:
 *   - Alias (aka Name) (defaulting to a suggested name)
 *   - Size in GiB
 *   - Storage Domain (defaulted to a suggested storage domain)
 *   - Disk Image Format (Allocation Policy ... Thin Provision vs Preallocated)
 *   - (auto-selected and never on the UI): Interface
 *
 * May be changed via Edit for a Disk Image (type == 'image'):
 *   - Alias
 *   - Size, but only allow extension: "Extend size by (GiB)"
 *
 * May be changed via Edit for a Direct LUN (type == 'lun'):
 *   - Alias
 */
class DiskImageEditor extends Component {
  constructor (props) {
    super(props)
    this.state = {
      showModal: false,
      storageDomainSelectList: createStorageDomainList(props.storageDomainList, props.dataCenterId, true),

      id: undefined,
      values: {
        alias: props.suggestedName || '',
        size: DISK_DEFAULTS.provisionedSize / 1024 ** 3,
        storageDomain: props.suggestedStorageDomain || '',
        bootable: DISK_DEFAULTS.bootable,
        format: DISK_DEFAULTS.format, // cow vs raw .. Allocation Policy (thin sparse vs preallocated !sparse)?
      },
      errors: {},
    }
    this.changesMade = false

    this.open = this.open.bind(this)
    this.close = this.close.bind(this)
    this.composeDisk = this.composeDisk.bind(this)
    this.isFormValid = this.isFormValid.bind(this)
    this.validateField = this.validateField.bind(this)
    this.handleSave = this.handleSave.bind(this)

    this.changeAlias = this.changeAlias.bind(this)
    this.changeSize = this.changeSize.bind(this)
    this.changeStorageDomain = this.changeStorageDomain.bind(this)
    this.changeFormat = this.changeFormat.bind(this)
    this.changeBootable = this.changeBootable.bind(this)
  }

  open (e) {
    e.preventDefault()
    const { disk, suggestedName, suggestedStorageDomain } = this.props
    const { storageDomainSelectList } = this.state
    const diskInfo = disk
      ? { // edit
        id: disk.get('id'),
        values: {
          alias: disk.get('name'),
          bootable: disk.get('bootable'),
          size: 0,
          storageDomain: disk.get('storageDomainId'),
          format: disk.get('format'),
        },
      }
      : { // new
        id: undefined,
        values: {
          alias: suggestedName,
          bootable: this.props.vm.get('disks').size > 0 ? DISK_DEFAULTS.bootable : true,
          size: DISK_DEFAULTS.provisionedSize / 1024 ** 3,
          storageDomain:
            (/\w+/.test(suggestedStorageDomain) && suggestedStorageDomain) ||
            (storageDomainSelectList.length > 0 && storageDomainSelectList[0].id) ||
            '',
          format: DISK_DEFAULTS.format,
        },
        errors: {},
      }

    this.setState({
      showModal: true,
      storageDomainSelectList: createStorageDomainList(this.props.storageDomainList, this.props.dataCenterId, true),
      ...diskInfo,
    })
    this.changesMade = false
  }

  close () {
    this.setState({ showModal: false })
  }

  composeDisk () {
    const { vm, disk } = this.props
    const vmDiskInSameStorageDomain =
      vm.get('disks') &&
      vm.get('disks').find(disk => disk.get('storageDomainId') === this.state.values.storageDomain)

    const iface =
      (disk && disk.get('iface')) ||
      (vmDiskInSameStorageDomain && vmDiskInSameStorageDomain.get('iface')) ||
      'virtio_scsi'

    const provisionedSize = disk
      ? disk.get('provisionedSize') + this.state.values.size * 1024 ** 3
      : this.state.values.size * 1024 ** 3
    const bootable = this.state.values.bootable
    const newDisk = {
      ...DISK_DEFAULTS,
      attachmentId: disk && disk.get('attachmentId'),

      iface,
      id: this.state.id,
      name: this.state.values.alias,
      provisionedSize,
      storageDomainId: this.state.values.storageDomain,
      format: this.state.values.format,
      bootable,
      sparse: this.state.values.format === 'cow',
    }

    if (disk && disk.get('type') !== 'image') {
      newDisk.type = disk.get('type')
      newDisk.provisionedSize = undefined
    }
    return newDisk
  }

  validateField (field = '') {
    const errors = this.state.errors
    let isErrorOnField = false

    switch (field) {
      case 'alias':
        if (!isDiskNameValid(this.state.values.alias)) {
          errors['alias'] = msg.pleaseEnterValidDiskName()
          isErrorOnField = true
        } else {
          delete errors['alias']
        }
        break
    }

    this.setState((state) => ({ errors }))
    return isErrorOnField
  }

  handleSave () {
    if (!this.isFormValid()) {
      return
    }
    if (!this.props.disk || this.changesMade) {
      const newDisk = this.composeDisk()
      this.props.onSave(this.props.vm.get('id'), newDisk)
    }
    this.close()
  }

  changeAlias ({ target: { value } }) {
    this.setState(
      (state) => ({ values: { ...state.values, alias: value }, errors: { ...state.errors, 'alias': '' } }),
      () => {
        this.validateField('alias')
      })
    this.changesMade = true
  }

  changeSize ({ target: { value } }) {
    if (isNumber(value) && value >= 0) {
      this.setState((state) => ({ values: { ...state.values, size: value } }))
      this.changesMade = true
    }
  }

  changeBootable (event) {
    const target = event.target
    const bootable = target.type === 'checkbox' ? target.checked : target.value
    this.setState((state) => ({ values: { ...state.values, bootable } }))
    this.changesMade = true
  }

  changeStorageDomain (value) {
    this.setState((state) => ({ values: { ...state.values, storageDomain: value } }))
    this.changesMade = true
  }

  changeFormat (value) {
    this.setState((state) => ({ values: { ...state.values, format: value } }))
    this.changesMade = true
  }

  isFormValid () {
    const isErrors = Object.values(this.state.errors).reduce((acc, value) => (value !== undefined) ? true : acc, false)
    if (isErrors) {
      return false
    }

    // make sure that required fields have data
    const { values } = this.state
    return !!(this.props.disk
      // edit mode needs: a change, an alias and a disk size
      ? this.changesMade && !!values.alias // add size + bootable or leave like that
      // create mode needs: name, size, storage domain, disk type
      : values.alias && values.size > 0 && values.storageDomain && values.format
    )
  }

  render () {
    const { idPrefix, disk, trigger } = this.props

    const createMode = !disk
    const isImage = disk && disk.get('type') === 'image'
    const isDirectLUN = disk && disk.get('type') === 'lun'

    const diskSize = disk && (disk.get('lunSize') ? disk.get('lunSize') : disk.get('provisionedSize'))
    const vmIsDown = this.props.vm.get('status') === 'down'

    return <React.Fragment>
      { trigger({ onClick: this.open }) }

      <Modal
        dialogClassName={style['editor-modal']}
        id={`${idPrefix}-modal`}
        show={this.state.showModal}
        onHide={this.close}
      >
        <Modal.Header>
          <Modal.CloseButton id={`${idPrefix}-modal-close`} onClick={this.close} />
          <Modal.Title>{createMode ? msg.createNewDisk() : msg.editDisk()}</Modal.Title>
        </Modal.Header>
        <Modal.Body>

          <Form
            horizontal
            onSubmit={e => { e.preventDefault() }}
            id={`${idPrefix}-modal-form`}
          >
            {/* Alias */}
            <FormGroup controlId={`${idPrefix}-alias`} validationState={this.state.errors['alias'] ? 'error' : null}>
              <LabelCol sm={3}>
                { msg.diskEditorAliasLabel() }
              </LabelCol>
              <Col sm={9}>
                <FormControl
                  type='text'
                  defaultValue={this.state.values.alias}
                  onChange={this.changeAlias}
                />
                {this.state.errors['alias'] && <HelpBlock>{this.state.errors['alias']}</HelpBlock>}
              </Col>
            </FormGroup>

            {/* Size Display (for edit mode) */}
            { !createMode &&
            <FormGroup controlId={`${idPrefix}-size`}>
              <LabelCol sm={3}>
                { msg.diskEditorSizeEditLabel() }
                { !isImage &&
                  <FieldLevelHelp
                    inline
                    content={msg.diskEditorSizeCantChangeHelp()}
                    buttonClass={style['editor-field-help']}
                  />
                }
              </LabelCol>
              <Col sm={9}>
                <div id={`${idPrefix}-size`} className={style['editor-field-read-only']}>
                  { diskSize / 1024 ** 3 }

              </Col>
            </FormGroup>
            }

            {/* Size Editor (initial size for create, expand by size for edit) */}
            { (createMode || isImage) &&
            <FormGroup controlId={`${idPrefix}-size-edit`}>
              <LabelCol sm={3}>
                { createMode &&
                  <React.Fragment>
                    {msg.diskEditorSizeLabel()}
                    <FieldLevelHelp
                      inline
                      content={msg.diskEditorSizeCreateHelp()}
                      buttonClass={style['editor-field-help']}
                    />
                  </React.Fragment>
                }
                { !createMode && msg.diskEditorResizeLabel() }
              </LabelCol>
              <Col sm={2}>
                <OverlayTooltip id={`${idPrefix}-form-tooltip`} tooltip={msg.diskEditorResizeNote()} placement='right'>
                  <FormControl
                    type='number'
                    value={this.state.values.size}
                    onChange={this.changeSize}
                  />
                </OverlayTooltip>
              </Col>
            </FormGroup>
            }

            {/* Storage Domain */}
            <FormGroup controlId={`${idPrefix}-storage-domain`}>
              <LabelCol sm={3}>
                { msg.diskEditorStorageDomainLabel() }
                <FieldLevelHelp
                  inline
                  content={
                    createMode
                      ? msg.diskEditorStorageDomainCreateHelp()
                      : msg.diskEditorStorageDomainCantChangeHelp()
                  }
                  buttonClass={style['editor-field-help']}
                />
              </LabelCol>
              <Col sm={9}>
                { createMode &&
                  <SelectBox
                    id={`${idPrefix}-storage-domain`}
                    items={this.state.storageDomainSelectList}
                    selected={this.state.values.storageDomain}
                    onChange={this.changeStorageDomain}
                  />
                }
                { !createMode && !isDirectLUN &&
                  <div id={`${idPrefix}-storage-domain`} className={style['editor-field-read-only']}>
                    {
                      this.props.storageDomains.getIn([this.state.values.storageDomain, 'name']) ||
                      msg.diskEditorStorageDomainNotAvailable()
                    }

                }
                { isDirectLUN &&
                  <div id={`${idPrefix}-storage-domain`} className={style['editor-field-read-only']}>
                    { msg.diskEditorStorageDomainNotAvailable() }

                }
              </Col>
            </FormGroup>

            {/* Disk Format */}
            <FormGroup controlId={`${idPrefix}-format`}>
              <LabelCol sm={3}>
                { msg.diskEditorFormatLabel() }
                <FieldLevelHelp
                  inline
                  content={
                    createMode
                      ? msg.diskEditorFormatCreateHelp()
                      : msg.diskEditorFormatCantChangeHelp()
                  }
                  buttonClass={style['editor-field-help']}
                />
              </LabelCol>
              <Col sm={9}>
                { createMode &&
                  <SelectBox
                    id={`${idPrefix}-format`}
                    items={DISK_FORMATS}
                    selected={this.state.format}
                    onChange={this.changeFormat}
                  />
                }
                { !createMode && !isDirectLUN &&
                  <div id={`${idPrefix}-format`} className={style['editor-field-read-only']}>
                    { this.state.values.format === 'raw' && msg.diskEditorFormatOptionRaw() }
                    { this.state.values.format === 'cow' && msg.diskEditorFormatOptionCow() }

                }
                { isDirectLUN &&
                  <div id={`${idPrefix}-format`} className={style['editor-field-read-only']}>
                    { msg.diskEditorFormatNotAvailable() }

                }
              </Col>
            </FormGroup>
            {/* Disk Bootable */}
            <FormGroup controlId={`${idPrefix}-bootable`}>
              <LabelCol sm={3}>
                { msg.diskEditorBootableLabel() }
                {!vmIsDown &&
                  <FieldLevelHelp
                    inline
                    content={msg.bootableEditTooltip()}
                    buttonClass={style['editor-field-help']}
                  />
                }
              </LabelCol>
              <Col sm={9}>
                <Checkbox
                  checked={this.state.values.bootable}
                  onChange={this.changeBootable}
                  id={`${idPrefix}-bootable`}
                  disabled={!vmIsDown}
                />
              </Col>
            </FormGroup>
          </Form>
        </Modal.Body>
        <Modal.Footer>
          <Button
            id={`${idPrefix}-modal-cancel`}
            bsStyle='default'
            className='btn-cancel'
            onClick={this.close}
          >
            { msg.cancel() }
          </Button>
          <Button
            id={`${idPrefix}-modal-ok`}
            bsStyle='primary'
            onClick={this.handleSave}
            disabled={!this.isFormValid()}
          >
            { msg.ok() }
          </Button>
        </Modal.Footer>
      </Modal>
    </React.Fragment>
  }
}
DiskImageEditor.propTypes = {
  idPrefix: PropTypes.string.isRequired,
  vm: PropTypes.object.isRequired,

  disk: PropTypes.object, // if !!disk then edit mode
  suggestedName: PropTypes.string, // if suggestedName && !disk then create mode
  suggestedStorageDomain: PropTypes.string,

  storageDomainList: PropTypes.object.isRequired,
  trigger: PropTypes.func.isRequired,
  onSave: PropTypes.func.isRequired,

  storageDomains: PropTypes.object.isRequired,
  dataCenterId: PropTypes.string.isRequired,
}

export default connect(
  (state, { vm }) => ({
    storageDomains: state.storageDomains,
    dataCenterId: state.clusters.getIn([ vm.getIn([ 'cluster', 'id' ]), 'dataCenterId' ]),
  })
)(DiskImageEditor)
<|endoftext|>