<commit_before>'use strict';

// ## NOTE ################################################################ //
//                                                                          //
// History.js works poorly with URLs containing hashes:                     //
//                                                                          //
//    https://github.com/browserstate/history.js/issues/111                 //
//    https://github.com/browserstate/history.js/issues/173                 //
//                                                                          //
// So upon clicks on `/foo#bar` we treat URL and push it to the state as    //
// `/foo` and saving `bar` in the state data, so we could scroll to desired //
// element upon statechange                                                 //
//                                                                          //
// ######################################################################## //

var _ = require('lodash');

var History = window.History; // History.js

// Takes a params hash (can be nested) and tries to parse each string-value as
// number or boolean. Returns a new hash with parsed values.
//
function castParamTypes(inputValue) {
  var parsedValue;

  if (_.isArray(inputValue)) {
    return _.map(inputValue, castParamTypes);

  } else if (_.isObject(inputValue)) {
    parsedValue = {};

    _.forEach(inputValue, function (value, key) {
      parsedValue[key] = castParamTypes(value);
    });

    return parsedValue;

  } else if ('true' === inputValue) {
    return true;

  } else if ('false' === inputValue) {
    return false;

  } else if (/^[0-9\.\-]+$/.test(inputValue)) {
    parsedValue = Number(inputValue);
    return String(parsedValue) === inputValue ? parsedValue : inputValue;

  } else {
    return inputValue;
  }
}

// Returns a normalized URL:
//
//  http://example.com/foo.html  => http://example.com/foo.html
//  /foo.html                    => http://example.com/foo.html
//  //example.com/foo.html       => http://example.com/foo.html
//
// NOTE: History.JS does not plays well with full URLs but without protocols.
//
function normalizeURL(url) {
  var a = document.createElement('a');
  a.href = url;
  return a.href.toString();
}

// Default renderer for `navigate.to` event.
// Used to render content when user clicks a link.
//
function renderNewContent(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    var offset = data.anchor ? $(data.anchor).offset() : null;

    $(this).replaceWith(content);
    content.fadeIn('fast');

    // To scroll window:
    // - WebKit-based browsers and the quirks mode use `body` element.
    // - Other browsers use `html` element.
    $('html:not(:animated), body:not(:animated)').animate({
      scrollTop:  offset ? offset.top  : 0
    , scrollLeft: offset ? offset.left : 0
    }, 300);

    callback();
  });
}

// Used to render content when user presses Back/Forward buttons.
//
function renderFromHistory(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    $(this).replaceWith(content);
    content.fadeIn('fast');
    callback();
  });
}

// Reference to a function to be used on next fire of history 'statechange' event
// to perform content injection/replacement.
//
// NOTE: The event handler *always* resets this variable to `renderFromHistory`
// after each call.
var __renderCallback__ = renderFromHistory;

// Reference to a function to be used by history 'statechange' handler after
// content rendering is done.
//
// NOTE: The event handler *always* resets this variable to null after each call.
var __completeCallback__ = null;

// API path of current page. Updated via `navigate.done` event.
var __currentApiPath__ = null;

// Performs RPC navigation to the specified page. Allowed options:
//
//    options.href
//    options.apiPath
//    options.params
//    options.render       - optional function; default is `renderNewContent`
//    options.replaceState - `true` to use `History.replaceState` instead of
//                           `History.pushState`
//
// `href` and `apiPath` parameters are calculated from each other.
// So they are mutually exclusive.
//
N.wire.on('navigate.to', function navigate_to(options, callback) {
  var match, href, anchor, apiPath, params, errorReport;

  if ('string' === typeof options) {
    options = { href: options };
  }

  if (options.href) {
    href   = normalizeURL(options.href).split('#')[0];
    anchor = normalizeURL(options.href).slice(href.length) || '';

    match = _.find(N.runtime.router.matchAll(href), function (match) {
      return _.has(match.meta.methods, 'get');
    });

    // It's an external link or 404 error if route is not matched. So perform
    // regular page requesting via HTTP.
    if (!match) {
      window.location = href + anchor;
      callback();
      return;
    }

    apiPath = match.meta.methods.get;
    params = castParamTypes(match.params || {});

  } else if (options.apiPath) {
    apiPath = options.apiPath;
    params  = options.params || {};
    href    = normalizeURL(N.runtime.router.linkTo(apiPath, params));
    anchor  = options.anchor || '';

    if (!href) {
      errorReport = 'Invalid parameters passed to `navigate.to` event: ' +
                    JSON.stringify(options);

      window.alert(errorReport);
      callback(new Error(errorReport));
      return;
    }

  } else {
    errorReport = 'Not enough parameters for `navigate.to` event. ' +
                  'Need `href` or `apiPath` at least: ' +
                  JSON.stringify(options);

    window.alert(errorReport);
    callback(new Error(errorReport));
    return;
  }

  // Add anchor hash-prefix if not exists.
  if (anchor && '#' !== anchor.charAt(0)) {
    anchor = '#' + anchor;
  }

  // Stop here if base URL (all except anchor) haven't changed.
  if (href === (location.protocol + '//' + location.host + location.pathname)) {

    // Update anchor if it's changed.
    if (location.hash !== anchor) {
      location.hash = anchor;
    }

    callback();
    return;
  }

  // Fallback for old browsers.
  if (!History.enabled) {
    window.location = href + anchor;
    callback();
    return;
  }

  // History is enabled - try RPC navigation.
  N.io.rpc(apiPath, params, function (err, response) {
    if (err && N.io.REDIRECT === err.code) {
      var redirectUrl = document.createElement('a');

      // Tricky way to parse URL.
      redirectUrl.href = err.head.Location;

      // Note, that we try to keep anchor, if exists.
      // That's important for moved threads and last pages redirects.
      redirectUrl.hash = anchor || window.location.hash;

      // If protocol is changed, we must completely reload the page to keep
      // Same-origin policy for RPC.
      // - port check not required, because port depends on protocol.
      // - domain check not required, because RPC is available on all domains
      //   (it uses relative path)
      if (redirectUrl.protocol !== location.protocol) {
        window.location = redirectUrl.href;
        callback();
      } else {
        N.wire.emit('navigate.to', {
          href:    redirectUrl.href
        , render:  options.render
        , history: options.history
        }, callback);
      }
      return;
    }

    if (err) {
      // Can't deal via RPC - try HTTP. This might be:
      //
      // - Either a generic error, e.g. authorization / bad params / fuckup
      //   so we redirect user to show him an error page.
      //
      // - Version mismatch, so we call action by HTTP to update client.
      window.location = href + anchor;
      callback();
      return;
    }

    /*
    if (response.layout !== N.runtime.layout) {
      // Layout was changed - perform normal page loading.
      //
      // TODO: Prevent double page requesting. The server should not perform
      // database queries on RPC when the client is not intending to use the
      // response data. Like in this situation.
      window.location = href + anchor;
      callback();
      return;
    }
    */

    N.loader.loadAssets((response.view || apiPath).split('.')[0], function () {
      var state = {
        apiPath: apiPath
      , anchor:  anchor
      , view:    response.view   || apiPath
    //, layout:  response.layout || null
      , locals:  response.data   || {}
      };

      // Set one-use callbacks for history 'statechange' handler.
      // The handler will reset these to defaults (`renderFromHistory` and null).
      __renderCallback__   = options.render || renderNewContent;
      __completeCallback__ = callback;

      if (options.replaceState) {
        History.replaceState(state, response.data.head.title, href);
      } else {
        History.pushState(state, response.data.head.title, href);
      }
    });
  });
});

//
// Bind History's statechange handler. It fires when:
//
// - User presses `Back` or `Forward` button in his browser.
// - User clicks a link.
// - User clicks "More threads/posts/etc" button.
//

if (History.enabled) {
  History.Adapter.bind(window, 'statechange', function () {
    var state = History.getState();

    // We have no state data for the initial page (received via HTTP responder).
    // So request that data via RPC and place into History.
    if (_.isEmpty(state.data)) {
      var match = _.find(N.runtime.router.matchAll(state.url), function (match) {
        return _.has(match.meta.methods, 'get');
      });

      // Can't match initial URL by some reason - just reload the page.
      // This is internal error. Must not happen on normal Nodeca workflow.
      if (!match) {
        window.location = state.url;
        return;
      }

      // Retrieve data.
      N.io.rpc(match.meta.methods.get, castParamTypes(match.params || {}), function (err, response) {
        var a, data, url;

        // Simple way to parse URL in browser.
        a = document.createElement('a');
        a.href = state.url;

        // Result state data.
        data = {
          apiPath: match.meta.methods.get
        , anchor:  a.hash
        , view:    response.view   || match.meta.methods.get
      //, layout:  response.layout || null
        , locals:  response.data   || {}
        };

        // State URL without anchor. (due to a problem in History.js; see above)
        url = a.protocol + '//' + a.host + a.pathname;

        // We terminate here, but History.replaceState will trigger 'statechange'
        // again, and we will continue with next part of code.
        History.replaceState(data, state.title, url);
      });
      return;
    }

    var render   = __renderCallback__
      , complete = __completeCallback__;

    // Restore callbacks to defaults. It's needed to ensure using right renderer
    // on regular history state changes - when user clicks back/forward buttons
    // in his browser.
    __renderCallback__   = renderFromHistory;
    __completeCallback__ = null;

    var exitEventData = { apiPath: __currentApiPath__, url: state.url }
      , doneEventData = { apiPath: state.data.apiPath, url: state.url };

    // Invoke exit handlers.
    N.wire.emit(['navigate.exit:' + __currentApiPath__, 'navigate.exit'], exitEventData, function (err) {
      if (err) {
        N.logger.error('%s', err); // Log error, but not stop.
      }

      // Clear old raw response data. It's collected by view templates.
      N.runtime.page_data = {};

      render(state.data, function () {
        // Invoke done-handlers.
        N.wire.emit(['navigate.done', 'navigate.done:' + state.data.apiPath], doneEventData, function (err) {
          if (err) {
            N.logger.error('%s', err); // Log error, but not stop.
          }

          if (complete) {
            complete();
          }
        });
      });
    });
  });
}

//
// __currentApiPath__ updater.
//

N.wire.on('navigate.done', { priority: -999 }, function apipath_set(data) {
  __currentApiPath__ = data.apiPath;
});

//
// Bind global a.click handler.
//
// NOTE: This handler must have *the lowest* priority. So:
//
// - It must be binded to `document`, since root DOM node has the lowest
//   priotity on event bubbling.
//
// - External libraries like Bootstrap must be placed *before* Nodeca client
//   code to ensure right order of handlers.
//

N.wire.once('navigate.done', { priority: 999 }, function navigate_click_handler() {
  $(document).on('click', 'a', function (event) {
    var $this = $(this);
    console.log($this.attr('href'));

    if ($this.attr('target') || event.isDefaultPrevented()) {
      // skip links that have `target` attribute specified
      // and clicks that were already handled
      return;
    }

    // Continue as normal for cmd clicks etc
    if (2 === event.which || event.metaKey) {
      return;
    }

    if ('#' === $this.attr('href')) {
      // Prevent clicks on special "button"-links.
      event.preventDefault();
      return;
    }

    N.wire.emit('navigate.to', $this.attr('href'), function (err) {
      if (err) {
        N.logger.error('%s', err);
      }
    });

    event.preventDefault();
  });
});
<commit_msg>Cleanup.<commit_after>'use strict';

// ## NOTE ################################################################ //
//                                                                          //
// History.js works poorly with URLs containing hashes:                     //
//                                                                          //
//    https://github.com/browserstate/history.js/issues/111                 //
//    https://github.com/browserstate/history.js/issues/173                 //
//                                                                          //
// So upon clicks on `/foo#bar` we treat URL and push it to the state as    //
// `/foo` and saving `bar` in the state data, so we could scroll to desired //
// element upon statechange                                                 //
//                                                                          //
// ######################################################################## //

var _ = require('lodash');

var History = window.History; // History.js

// Takes a params hash (can be nested) and tries to parse each string-value as
// number or boolean. Returns a new hash with parsed values.
//
function castParamTypes(inputValue) {
  var parsedValue;

  if (_.isArray(inputValue)) {
    return _.map(inputValue, castParamTypes);

  } else if (_.isObject(inputValue)) {
    parsedValue = {};

    _.forEach(inputValue, function (value, key) {
      parsedValue[key] = castParamTypes(value);
    });

    return parsedValue;

  } else if ('true' === inputValue) {
    return true;

  } else if ('false' === inputValue) {
    return false;

  } else if (/^[0-9\.\-]+$/.test(inputValue)) {
    parsedValue = Number(inputValue);
    return String(parsedValue) === inputValue ? parsedValue : inputValue;

  } else {
    return inputValue;
  }
}

// Returns a normalized URL:
//
//  http://example.com/foo.html  => http://example.com/foo.html
//  /foo.html                    => http://example.com/foo.html
//  //example.com/foo.html       => http://example.com/foo.html
//
// NOTE: History.JS does not plays well with full URLs but without protocols.
//
function normalizeURL(url) {
  var a = document.createElement('a');
  a.href = url;
  return a.href.toString();
}

// Default renderer for `navigate.to` event.
// Used to render content when user clicks a link.
//
function renderNewContent(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    var offset = data.anchor ? $(data.anchor).offset() : null;

    $(this).replaceWith(content);
    content.fadeIn('fast');

    // To scroll window:
    // - WebKit-based browsers and the quirks mode use `body` element.
    // - Other browsers use `html` element.
    $('html:not(:animated), body:not(:animated)').animate({
      scrollTop:  offset ? offset.top  : 0
    , scrollLeft: offset ? offset.left : 0
    }, 300);

    callback();
  });
}

// Used to render content when user presses Back/Forward buttons.
//
function renderFromHistory(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    $(this).replaceWith(content);
    content.fadeIn('fast');
    callback();
  });
}

// Reference to a function to be used on next fire of history 'statechange' event
// to perform content injection/replacement.
//
// NOTE: The event handler *always* resets this variable to `renderFromHistory`
// after each call.
var __renderCallback__ = renderFromHistory;

// Reference to a function to be used by history 'statechange' handler after
// content rendering is done.
//
// NOTE: The event handler *always* resets this variable to null after each call.
var __completeCallback__ = null;

// API path of current page. Updated via `navigate.done` event.
var __currentApiPath__ = null;

// Performs RPC navigation to the specified page. Allowed options:
//
//    options.href
//    options.apiPath
//    options.params
//    options.render       - optional function; default is `renderNewContent`
//    options.replaceState - `true` to use `History.replaceState` instead of
//                           `History.pushState`
//
// `href` and `apiPath` parameters are calculated from each other.
// So they are mutually exclusive.
//
N.wire.on('navigate.to', function navigate_to(options, callback) {
  var match, href, anchor, apiPath, params, errorReport;

  if ('string' === typeof options) {
    options = { href: options };
  }

  if (options.href) {
    href   = normalizeURL(options.href).split('#')[0];
    anchor = normalizeURL(options.href).slice(href.length) || '';

    match = _.find(N.runtime.router.matchAll(href), function (match) {
      return _.has(match.meta.methods, 'get');
    });

    // It's an external link or 404 error if route is not matched. So perform
    // regular page requesting via HTTP.
    if (!match) {
      window.location = href + anchor;
      callback();
      return;
    }

    apiPath = match.meta.methods.get;
    params = castParamTypes(match.params || {});

  } else if (options.apiPath) {
    apiPath = options.apiPath;
    params  = options.params || {};
    href    = normalizeURL(N.runtime.router.linkTo(apiPath, params));
    anchor  = options.anchor || '';

    if (!href) {
      errorReport = 'Invalid parameters passed to `navigate.to` event: ' +
                    JSON.stringify(options);

      window.alert(errorReport);
      callback(new Error(errorReport));
      return;
    }

  } else {
    errorReport = 'Not enough parameters for `navigate.to` event. ' +
                  'Need `href` or `apiPath` at least: ' +
                  JSON.stringify(options);

    window.alert(errorReport);
    callback(new Error(errorReport));
    return;
  }

  // Add anchor hash-prefix if not exists.
  if (anchor && '#' !== anchor.charAt(0)) {
    anchor = '#' + anchor;
  }

  // Stop here if base URL (all except anchor) haven't changed.
  if (href === (location.protocol + '//' + location.host + location.pathname)) {

    // Update anchor if it's changed.
    if (location.hash !== anchor) {
      location.hash = anchor;
    }

    callback();
    return;
  }

  // Fallback for old browsers.
  if (!History.enabled) {
    window.location = href + anchor;
    callback();
    return;
  }

  // History is enabled - try RPC navigation.
  N.io.rpc(apiPath, params, function (err, response) {
    if (err && N.io.REDIRECT === err.code) {
      var redirectUrl = document.createElement('a');

      // Tricky way to parse URL.
      redirectUrl.href = err.head.Location;

      // Note, that we try to keep anchor, if exists.
      // That's important for moved threads and last pages redirects.
      redirectUrl.hash = anchor || window.location.hash;

      // If protocol is changed, we must completely reload the page to keep
      // Same-origin policy for RPC.
      // - port check not required, because port depends on protocol.
      // - domain check not required, because RPC is available on all domains
      //   (it uses relative path)
      if (redirectUrl.protocol !== location.protocol) {
        window.location = redirectUrl.href;
        callback();
      } else {
        N.wire.emit('navigate.to', {
          href:    redirectUrl.href
        , render:  options.render
        , history: options.history
        }, callback);
      }
      return;
    }

    if (err) {
      // Can't deal via RPC - try HTTP. This might be:
      //
      // - Either a generic error, e.g. authorization / bad params / fuckup
      //   so we redirect user to show him an error page.
      //
      // - Version mismatch, so we call action by HTTP to update client.
      window.location = href + anchor;
      callback();
      return;
    }

    /*
    if (response.layout !== N.runtime.layout) {
      // Layout was changed - perform normal page loading.
      //
      // TODO: Prevent double page requesting. The server should not perform
      // database queries on RPC when the client is not intending to use the
      // response data. Like in this situation.
      window.location = href + anchor;
      callback();
      return;
    }
    */

    N.loader.loadAssets((response.view || apiPath).split('.')[0], function () {
      var state = {
        apiPath: apiPath
      , anchor:  anchor
      , view:    response.view   || apiPath
    //, layout:  response.layout || null
      , locals:  response.data   || {}
      };

      // Set one-use callbacks for history 'statechange' handler.
      // The handler will reset these to defaults (`renderFromHistory` and null).
      __renderCallback__   = options.render || renderNewContent;
      __completeCallback__ = callback;

      if (options.replaceState) {
        History.replaceState(state, response.data.head.title, href);
      } else {
        History.pushState(state, response.data.head.title, href);
      }
    });
  });
});

//
// Bind History's statechange handler. It fires when:
//
// - User presses `Back` or `Forward` button in his browser.
// - User clicks a link.
// - User clicks "More threads/posts/etc" button.
//

if (History.enabled) {
  History.Adapter.bind(window, 'statechange', function () {
    var state = History.getState();

    // We have no state data for the initial page (received via HTTP responder).
    // So request that data via RPC and place into History.
    if (_.isEmpty(state.data)) {
      var match = _.find(N.runtime.router.matchAll(state.url), function (match) {
        return _.has(match.meta.methods, 'get');
      });

      // Can't match initial URL by some reason - just reload the page.
      // This is internal error. Must not happen on normal Nodeca workflow.
      if (!match) {
        window.location = state.url;
        return;
      }

      // Retrieve data.
      N.io.rpc(match.meta.methods.get, castParamTypes(match.params || {}), function (err, response) {
        var a, data, url;

        // Simple way to parse URL in browser.
        a = document.createElement('a');
        a.href = state.url;

        // Result state data.
        data = {
          apiPath: match.meta.methods.get
        , anchor:  a.hash
        , view:    response.view   || match.meta.methods.get
      //, layout:  response.layout || null
        , locals:  response.data   || {}
        };

        // State URL without anchor. (due to a problem in History.js; see above)
        url = a.protocol + '//' + a.host + a.pathname;

        // We terminate here, but History.replaceState will trigger 'statechange'
        // again, and we will continue with next part of code.
        History.replaceState(data, state.title, url);
      });
      return;
    }

    var render   = __renderCallback__
      , complete = __completeCallback__;

    // Restore callbacks to defaults. It's needed to ensure using right renderer
    // on regular history state changes - when user clicks back/forward buttons
    // in his browser.
    __renderCallback__   = renderFromHistory;
    __completeCallback__ = null;

    var exitEventData = { apiPath: __currentApiPath__, url: state.url }
      , doneEventData = { apiPath: state.data.apiPath, url: state.url };

    // Invoke exit handlers.
    N.wire.emit(['navigate.exit:' + __currentApiPath__, 'navigate.exit'], exitEventData, function (err) {
      if (err) {
        N.logger.error('%s', err); // Log error, but not stop.
      }

      // Clear old raw response data. It's collected by view templates.
      N.runtime.page_data = {};

      render(state.data, function () {
        // Invoke done-handlers.
        N.wire.emit(['navigate.done', 'navigate.done:' + state.data.apiPath], doneEventData, function (err) {
          if (err) {
            N.logger.error('%s', err); // Log error, but not stop.
          }

          if (complete) {
            complete();
          }
        });
      });
    });
  });
}

//
// __currentApiPath__ updater.
//

N.wire.on('navigate.done', { priority: -999 }, function apipath_set(data) {
  __currentApiPath__ = data.apiPath;
});

//
// Bind global a.click handler.
//
// NOTE: This handler must have *the lowest* priority. So:
//
// - It must be binded to `document`, since root DOM node has the lowest
//   priotity on event bubbling.
//
// - External libraries like Bootstrap must be placed *before* Nodeca client
//   code to ensure right order of handlers.
//

N.wire.once('navigate.done', { priority: 999 }, function navigate_click_handler() {
  $(document).on('click', 'a', function (event) {
    var $this = $(this);

    if ($this.attr('target') || event.isDefaultPrevented()) {
      // skip links that have `target` attribute specified
      // and clicks that were already handled
      return;
    }

    // Continue as normal for cmd clicks etc
    if (2 === event.which || event.metaKey) {
      return;
    }

    if ('#' === $this.attr('href')) {
      // Prevent clicks on special "button"-links.
      event.preventDefault();
      return;
    }

    N.wire.emit('navigate.to', $this.attr('href'), function (err) {
      if (err) {
        N.logger.error('%s', err);
      }
    });

    event.preventDefault();
  });
});
<|endoftext|>


<commit_before>"use strict";

let React = require("react/addons");
let SearchField = require("./search_field");
let ListDisplay = require("./list_display");
let ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

const PAGE_SIZE = 40;

/**
 * A generic component for controlling a master detail type view
 */
let MasterDetail = React.createClass({
    propTypes: {
        route: React.PropTypes.object,
        params: React.PropTypes.object,
        children: React.PropTypes.node
    },

    /**
     * Returns the initial state for the master/detail
     */
    getInitialState: function() {
        return {
            value: "",
            skip: 0,
            total: 0,
            data: null,
            currentRequest: null,
            hasQueried: false,
            loading: false
        };
    },

    /**
     * If our props are changed restart this beast
     */
    componentWillReceiveProps: function(nextProps) {
        // This is necessary cause React reuses components if it can
        if (this.props.route &&
            nextProps.route &&
            this.props.route.path !== nextProps.route.path) {
            this.setState(this.getInitialState());
        }
    },

    componentWillUnmount: function () {
        this._canclePossibleRequest();
    },

    /**
     * Gets a prop by a given name from either the master detail config or from props
     */
    getProp(name, otherProps) {
        let route = (otherProps ? otherProps : this.props.route) || {};
        let config = route.masterDetailConfig || {};

        // On load, set the props to the ones retrieved from the route definition (if any)
        return config[name] || this.props[name];
    },

    /*
     * Cancel any possible ongoing request
     */
    _canclePossibleRequest: function () {
        if (this.state.currentRequest) {
            this.state.currentRequest.cancel();
        }
    },

    /*
     * Fetch data from the store.
     * @param {String} query Search query
     * @param {Number} skip The offset amount
     */
    _performQuery: function (query, skip) {
        this._canclePossibleRequest();

        let req = this.getProp("store").listByName(query, skip, PAGE_SIZE).then((res) => {
            if (res.data !== null) {
                let data = this.state.data;
                data = data ? data.concat(res.data) : res.data;
                this.setState({data: data, loading: false, currentRequest: null, hasQueried: data.length > 0 || this.state.hasQueried, total: res.total });
            } else {
                this.setState({data: null, loading: false, currentRequest: null});
            }
        });

        this.setState({currentRequest: req, skip: skip, loading: true});
    },

    /*
     * Handle a user initiated search
     */
    _handleSearch: function (query) {
        this.setState({ value: query });
        if (query) {
            this.setState({data: null});
            this._performQuery(query, 0);
        } else {
            this.setState({loading: false});
        }
    },

    /*
     * Handle a request to 'infinitely' load more data.
     */
    _handleInfiniteLoad: function () {
        let nextSkip = Math.min(this.state.skip + PAGE_SIZE, this.state.total);
        if (!this.state.loading && nextSkip < this.state.total) {
            this._performQuery(this.state.value, nextSkip);
        }
    },

    /*
     * Create the classNames for the root drugs element.
     */
    _getClassNames: function () {
        let classes = [this.getProp("itemName") || "generic", "master-detail"];
        if (this.props.params.detailId) {
            classes.push("master-detail--populated");
        }
        if (this.state.hasQueried) {
            classes.push("master-detail--has-queried");
        }
        return classes.join(" ");
    },

    /*
     * Conditionally render the list based on the state of the data.
     */
    _renderList: function () {
        let list = null;
        if (this.state.data && this.state.data.length > 0) {
            list = (

                    {this.state.total ?

{this.state.total} results found

                        : null}
                    <ListDisplay containerHeight={250}
                            itemName={this.getProp("itemName")}
                            itemComponent={this.getProp("listItem")}
                            itemHeight={this.getProp("itemHeight")}
                            onInfiniteLoad={this._handleInfiniteLoad}
                            isInfiniteLoading={this.state.loading} key={this.state.value} data={this.state.data}/>

            );
        }
        return list;
    },

    _renderHelpText: function () {
        if (!this.state.hasQueried && !this.state.data && !this.props.params.detailId && !this.state.loading) {
            return (

                    Hi! Welcome to DrugFax.

                    { this.getProp("masterSearchPlaceholder") || "Search" }.

            );
        }
    },

    /**
     * Renders the empty state for the list view
     */
    _renderEmptyState: function () {
        if (this.state.data && this.state.data.length === 0) {
            return (

                    No results for "{this.state.value}"

            );
        }
    },

    render: function() {
        return (

                    <SearchField key={this.getProp("itemName")} onSearch={this._handleSearch} loading={this.state.loading} placeholder={this.getProp("masterSearchPlaceholder") || "Search"}/>
                    <ReactCSSTransitionGroup transitionName="transition" transitionAppear={true}>
                        {this._renderList()}
                    </ReactCSSTransitionGroup>
                    {this._renderHelpText()}
                    {this._renderEmptyState()}

                    {this.props.children}

        );
    }
});

module.exports = MasterDetail;
<commit_msg>#170 - Allow for clearing a search<commit_after>"use strict";

let React = require("react/addons");
let SearchField = require("./search_field");
let ListDisplay = require("./list_display");
let ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

const PAGE_SIZE = 40;

/**
 * A generic component for controlling a master detail type view
 */
let MasterDetail = React.createClass({
    propTypes: {
        route: React.PropTypes.object,
        params: React.PropTypes.object,
        children: React.PropTypes.node
    },

    /**
     * Returns the initial state for the master/detail
     */
    getInitialState: function() {
        return {
            value: "",
            skip: 0,
            total: 0,
            data: null,
            currentRequest: null,
            hasQueried: false,
            loading: false
        };
    },

    /**
     * If our props are changed restart this beast
     */
    componentWillReceiveProps: function(nextProps) {
        // This is necessary cause React reuses components if it can
        if (this.props.route &&
            nextProps.route &&
            this.props.route.path !== nextProps.route.path) {
            this.setState(this.getInitialState());
        }
    },

    componentWillUnmount: function () {
        this._canclePossibleRequest();
    },

    /**
     * Gets a prop by a given name from either the master detail config or from props
     */
    getProp(name, otherProps) {
        let route = (otherProps ? otherProps : this.props.route) || {};
        let config = route.masterDetailConfig || {};

        // On load, set the props to the ones retrieved from the route definition (if any)
        return config[name] || this.props[name];
    },

    /*
     * Cancel any possible ongoing request
     */
    _canclePossibleRequest: function () {
        if (this.state.currentRequest) {
            this.state.currentRequest.cancel();
        }
    },

    /*
     * Fetch data from the store.
     * @param {String} query Search query
     * @param {Number} skip The offset amount
     */
    _performQuery: function (query, skip) {
        this._canclePossibleRequest();

        let req = this.getProp("store").listByName(query, skip, PAGE_SIZE).then((res) => {
            if (res.data !== null) {
                let data = this.state.data;
                data = data ? data.concat(res.data) : res.data;
                this.setState({data: data, loading: false, currentRequest: null, hasQueried: data.length > 0 || this.state.hasQueried, total: res.total });
            } else {
                this.setState({data: null, loading: false, currentRequest: null});
            }
        });

        this.setState({currentRequest: req, skip: skip, loading: true});
    },

    /*
     * Handle a user initiated search
     */
    _handleSearch: function (query) {
        if (query !== this.state.value) {
            this.setState({ data: null, value: query });
            if (query ) {
                this._performQuery(query, 0);
            }
        }
    },

    /*
     * Handle a request to 'infinitely' load more data.
     */
    _handleInfiniteLoad: function () {
        let nextSkip = Math.min(this.state.skip + PAGE_SIZE, this.state.total);
        if (!this.state.loading && nextSkip < this.state.total) {
            this._performQuery(this.state.value, nextSkip);
        }
    },

    /*
     * Create the classNames for the root drugs element.
     */
    _getClassNames: function () {
        let classes = [this.getProp("itemName") || "generic", "master-detail"];
        if (this.props.params.detailId) {
            classes.push("master-detail--populated");
        }
        if (this.state.hasQueried) {
            classes.push("master-detail--has-queried");
        }
        return classes.join(" ");
    },

    /*
     * Conditionally render the list based on the state of the data.
     */
    _renderList: function () {
        let list = null;
        if (this.state.data && this.state.data.length > 0) {
            list = (

                    {this.state.total ?

{this.state.total} results found

                        : null}
                    <ListDisplay containerHeight={250}
                            itemName={this.getProp("itemName")}
                            itemComponent={this.getProp("listItem")}
                            itemHeight={this.getProp("itemHeight")}
                            onInfiniteLoad={this._handleInfiniteLoad}
                            isInfiniteLoading={this.state.loading} key={this.state.value} data={this.state.data}/>

            );
        }
        return list;
    },

    _renderHelpText: function () {
        if (!this.state.hasQueried && !this.state.data && !this.props.params.detailId && !this.state.loading) {
            return (

                    Hi! Welcome to DrugFax.

                    { this.getProp("masterSearchPlaceholder") || "Search" }.

            );
        }
    },

    /**
     * Renders the empty state for the list view
     */
    _renderEmptyState: function () {
        if (this.state.data && this.state.data.length === 0) {
            return (

                    No results for "{this.state.value}"

            );
        }
    },

    render: function() {
        return (

                    <SearchField key={this.getProp("itemName")} onSearch={this._handleSearch} loading={this.state.loading} placeholder={this.getProp("masterSearchPlaceholder") || "Search"}/>
                    <ReactCSSTransitionGroup transitionName="transition" transitionAppear={true}>
                        {this._renderList()}
                    </ReactCSSTransitionGroup>
                    {this._renderHelpText()}
                    {this._renderEmptyState()}

                    {this.props.children}

        );
    }
});

module.exports = MasterDetail;
<|endoftext|>


<commit_before>import {
  AnchorButton,
  Popover,
  PopoverInteractionKind,
  Position,
  Tooltip
} from "@blueprintjs/core"
import * as FieldHelper from "components/FieldHelper"
import { Field } from "formik"
import {
  convertLatLngToMGRS,
  convertMGRSToLatLng,
  parseCoordinate
} from "geoUtils"
import PropTypes from "prop-types"
import React, { useEffect, useState } from "react"
import { Col, ControlLabel, FormGroup, Table } from "react-bootstrap"
import Settings from "settings"

export const GEO_LOCATION_DISPLAY_TYPE = {
  FORM_FIELD: "FORM_FIELD",
  GENERIC: "GENERIC"
}

const MGRS_LABEL = "MGRS Coordinate"
const LAT_LON_LABEL = "Latitude, Longitude"

const GeoLocation = ({
  lat,
  lng,
  editable,
  setFieldValue,
  setFieldTouched,
  isSubmitting,
  displayType
}) => {
  let label = LAT_LON_LABEL
  let CoordinatesFormField = LatLonFormField
  if (Settings?.fields?.location?.format === "MGRS") {
    label = MGRS_LABEL
    CoordinatesFormField = MGRSFormField
  }

  if (!editable) {
    const humanValue = (

        <CoordinatesFormField lat={lat} lng={lng} />
        <AllFormatsInfo lat={lat} lng={lng} />

    )

    if (displayType === GEO_LOCATION_DISPLAY_TYPE.FORM_FIELD) {
      return (
        <Field
          name="location"
          label={label}
          component={FieldHelper.ReadonlyField}
          humanValue={humanValue}
          style={{ paddingTop: "2px" }}
        />
      )
    }

    return humanValue
  }

  return (
    <CoordinatesFormField
      lat={lat}
      lng={lng}
      editable
      setFieldValue={setFieldValue}
      setFieldTouched={setFieldTouched}
      isSubmitting={isSubmitting}
    />
  )
}

function fnRequiredWhenEditable(props, propName, componentName) {
  if (props.editable === true && typeof props[propName] !== "function") {
    return new Error(
      `Invalid prop '${propName}' is supplied to ${componentName}. '${propName}' isRequired when ${componentName} is editable and '${propName}' must be a function!`
    )
  }
}

GeoLocation.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  editable: PropTypes.bool,
  setFieldValue: fnRequiredWhenEditable,
  setFieldTouched: fnRequiredWhenEditable,
  isSubmitting: PropTypes.bool,
  displayType: PropTypes.oneOf([
    GEO_LOCATION_DISPLAY_TYPE.FORM_FIELD,
    GEO_LOCATION_DISPLAY_TYPE.GENERIC
  ])
}

GeoLocation.defaultProps = {
  lat: null,
  lng: null,
  editable: false,
  isSubmitting: false,
  displayType: GEO_LOCATION_DISPLAY_TYPE.GENERIC
}

export default GeoLocation

/* =========================== MGRSFormField ================================ */

const MGRSFormField = ({
  lat,
  lng,
  editable,
  setFieldValue,
  setFieldTouched,
  isSubmitting
}) => {
  const [mgrs, setMgrs] = useState("")

  useEffect(() => {
    if (!editable && lat === null && lng === null) {
      setMgrs("")
    } else {
      const mgrsValue = convertLatLngToMGRS(lat, lng)
      if (mgrsValue) {
        setMgrs(mgrsValue)
        if (editable) {
          setFieldValue("displayedCoordinate", mgrsValue)
          setFieldValue("lat", lat, false)
          setFieldValue("lng", lng, false)
        }
      }
    }
  }, [editable, lat, lng, setFieldValue])

  if (!editable) {
    return {mgrs || "?"}
  }

  return (
    <FormGroup style={{ marginBottom: 0 }}>
      <Col sm={2} componentClass={ControlLabel} htmlFor="displayedCoordinate">
        {MGRS_LABEL}
      </Col>

      <Col sm={7}>
        <Col sm={4}>
          <Field
            name="displayedCoordinate"
            component={FieldHelper.InputFieldNoLabel}
            value={mgrs}
            onChange={e => setMgrs(e.target.value)}
            onBlur={e => {
              const newLatLng = convertMGRSToLatLng(mgrs)
              setFieldValue("displayedCoordinate", e.target.value)
              setFieldValue("lat", newLatLng[0], false)
              setFieldValue("lng", newLatLng[1], false)
              setFieldTouched("displayedCoordinate", true, false)
            }}
          />
        </Col>
        <CoordinateActionButtons
          lat={lat}
          lng={lng}
          isSubmitting={isSubmitting}
          disabled={!mgrs}
          onClear={() => {
            setFieldValue("displayedCoordinate", null)
            setFieldValue("lat", null)
            setFieldValue("lng", null)
            setMgrs("")
          }}
        />
      </Col>
    </FormGroup>
  )
}

MGRSFormField.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  editable: PropTypes.bool,
  setFieldValue: fnRequiredWhenEditable,
  setFieldTouched: fnRequiredWhenEditable,
  isSubmitting: PropTypes.bool
}

MGRSFormField.defaultProps = {
  lat: null,
  lng: null,
  editable: false,
  isSubmitting: false
}

/* ========================= LatLonFormField ================================ */

const LatLonFormField = ({
  lat,
  lng,
  editable,
  setFieldValue,
  setFieldTouched,
  isSubmitting
}) => {
  if (!editable) {
    const lt = parseCoordinate(lat)
    const ln = parseCoordinate(lng)
    return (
      <>
        {lt || lt === 0 ? lt : "?"}
        ,
        {ln || ln === 0 ? ln : "?"}
      </>
    )
  }
  const setParsedLatLng = (_lat, _lng) => {
    setFieldTouched("lat", true, false)
    setFieldTouched("lng", true, false)
    setFieldValue("lat", parseCoordinate(_lat))
    setFieldValue("lng", parseCoordinate(_lng))
  }
  return (
    <FormGroup style={{ marginBottom: 0 }}>
      <Col sm={2} componentClass={ControlLabel} htmlFor="lat">
        {LAT_LON_LABEL}
      </Col>

      <Col sm={7}>
        <Col sm={3} style={{ marginRight: "8px" }}>
          <Field
            name="lat"
            component={FieldHelper.InputFieldNoLabel}
            onBlur={() => {
              setParsedLatLng(lat, lng)
            }}
          />
        </Col>
        <Col sm={3}>
          <Field
            name="lng"
            component={FieldHelper.InputFieldNoLabel}
            onBlur={() => {
              setParsedLatLng(lat, lng)
            }}
          />
        </Col>
        <CoordinateActionButtons
          lat={lat}
          lng={lng}
          isSubmitting={isSubmitting}
          disabled={!lat && lat !== 0 && !lng && lng !== 0}
          onClear={() => {
            // setting second param to false prevents validation since lat, lng can be null together
            setFieldTouched("lat", false, false)
            setFieldTouched("lng", false, false)
            setFieldValue("lat", null)
            setFieldValue("lng", null)
          }}
        />
      </Col>
    </FormGroup>
  )
}

LatLonFormField.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  editable: PropTypes.bool,
  setFieldValue: fnRequiredWhenEditable,
  setFieldTouched: fnRequiredWhenEditable,
  isSubmitting: PropTypes.bool
}

LatLonFormField.defaultProps = {
  lat: null,
  lng: null,
  editable: false,
  isSubmitting: false
}

/* ======================= CoordinateActionButtons ============================ */

const CoordinateActionButtons = ({
  lat,
  lng,
  onClear,
  isSubmitting,
  disabled
}) => {
  return (
    <Col sm={2} style={{ padding: "4px 8px" }}>
      <Tooltip content="Clear coordinates">
        <AnchorButton
          minimal
          icon="delete"
          outlined
          intent="danger"
          onClick={onClear}
          disabled={isSubmitting || disabled}
        />
      </Tooltip>
      <AllFormatsInfo lat={lat} lng={lng} inForm />
    </Col>
  )
}

CoordinateActionButtons.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  onClear: PropTypes.func.isRequired,
  isSubmitting: PropTypes.bool.isRequired,
  disabled: PropTypes.bool
}

CoordinateActionButtons.defaultProps = {
  lat: null,
  lng: null,
  disabled: true
}

/* ======================= AllFormatsInfo ============================ */

const AllFormatsInfo = ({ lat, lng, inForm }) => {
  if (!inForm && ((!lat && lat !== 0) || (!lng && lng !== 0))) {
    return null
  }
  return (
    <Popover
      content={

                  All Coordinate Formats

                {LAT_LON_LABEL}

                  <LatLonFormField lat={lat} lng={lng} />

                {MGRS_LABEL}

                  <MGRSFormField lat={lat} lng={lng} />

      }
      target={
        <Tooltip content="Display all coordinate formats">
          <AnchorButton
            style={{ marginLeft: "8px" }}
            id="gloc-info-btn"
            minimal
            icon="info-sign"
            intent="primary"
            outlined={inForm}
          />
        </Tooltip>
      }
      position={Position.RIGHT}
      interactionKind={PopoverInteractionKind.CLICK_TARGET_ONLY}
      usePortal={false}
    />
  )
}

AllFormatsInfo.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  inForm: PropTypes.bool
}

AllFormatsInfo.defaultProps = {
  lat: null,
  lng: null,
  inForm: false
}
<commit_msg>Add location format prop to GeoLocation<commit_after>import {
  AnchorButton,
  Popover,
  PopoverInteractionKind,
  Position,
  Tooltip
} from "@blueprintjs/core"
import * as FieldHelper from "components/FieldHelper"
import { Field } from "formik"
import {
  convertLatLngToMGRS,
  convertMGRSToLatLng,
  parseCoordinate
} from "geoUtils"
import PropTypes from "prop-types"
import React, { useEffect, useState } from "react"
import { Col, ControlLabel, FormGroup, Table } from "react-bootstrap"
import Settings from "settings"

export const GEO_LOCATION_DISPLAY_TYPE = {
  FORM_FIELD: "FORM_FIELD",
  GENERIC: "GENERIC"
}

const MGRS_LABEL = "MGRS Coordinate"
const LAT_LON_LABEL = "Latitude, Longitude"

const GeoLocation = ({
  lat,
  lng,
  editable,
  setFieldValue,
  setFieldTouched,
  isSubmitting,
  displayType,
  locationFormat
}) => {
  let label = LAT_LON_LABEL
  let CoordinatesFormField = LatLonFormField
  if (locationFormat === "MGRS") {
    label = MGRS_LABEL
    CoordinatesFormField = MGRSFormField
  }

  if (!editable) {
    const humanValue = (

        <CoordinatesFormField lat={lat} lng={lng} />
        <AllFormatsInfo lat={lat} lng={lng} />

    )

    if (displayType === GEO_LOCATION_DISPLAY_TYPE.FORM_FIELD) {
      return (
        <Field
          name="location"
          label={label}
          component={FieldHelper.ReadonlyField}
          humanValue={humanValue}
          style={{ paddingTop: "2px" }}
        />
      )
    }

    return humanValue
  }

  return (
    <CoordinatesFormField
      lat={lat}
      lng={lng}
      editable
      setFieldValue={setFieldValue}
      setFieldTouched={setFieldTouched}
      isSubmitting={isSubmitting}
    />
  )
}

function fnRequiredWhenEditable(props, propName, componentName) {
  if (props.editable === true && typeof props[propName] !== "function") {
    return new Error(
      `Invalid prop '${propName}' is supplied to ${componentName}. '${propName}' isRequired when ${componentName} is editable and '${propName}' must be a function!`
    )
  }
}

GeoLocation.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  editable: PropTypes.bool,
  setFieldValue: fnRequiredWhenEditable,
  setFieldTouched: fnRequiredWhenEditable,
  isSubmitting: PropTypes.bool,
  displayType: PropTypes.oneOf([
    GEO_LOCATION_DISPLAY_TYPE.FORM_FIELD,
    GEO_LOCATION_DISPLAY_TYPE.GENERIC
  ]),
  locationFormat: PropTypes.string
}

GeoLocation.defaultProps = {
  lat: null,
  lng: null,
  editable: false,
  isSubmitting: false,
  displayType: GEO_LOCATION_DISPLAY_TYPE.GENERIC,
  locationFormat: Settings?.fields?.location?.format
}

export default GeoLocation

/* =========================== MGRSFormField ================================ */

const MGRSFormField = ({
  lat,
  lng,
  editable,
  setFieldValue,
  setFieldTouched,
  isSubmitting
}) => {
  const [mgrs, setMgrs] = useState("")

  // Coordinate should be sync between input field and map interaction
  useEffect(
    function syncMapEdit() {
      if (!editable && lat === null && lng === null) {
        setMgrs("")
      } else {
        const mgrsValue = convertLatLngToMGRS(lat, lng)
        if (mgrsValue) {
          setMgrs(mgrsValue)
          if (editable) {
            setFieldValue("displayedCoordinate", mgrsValue)
            setFieldValue("lat", lat, false)
            setFieldValue("lng", lng, false)
          }
        }
      }
    },
    [editable, lat, lng, setFieldValue]
  )

  if (!editable) {
    return {mgrs || "?"}
  }

  return (
    <FormGroup style={{ marginBottom: 0 }}>
      <Col sm={2} componentClass={ControlLabel} htmlFor="displayedCoordinate">
        {MGRS_LABEL}
      </Col>

      <Col sm={7}>
        <Col sm={4}>
          <Field
            name="displayedCoordinate"
            component={FieldHelper.InputFieldNoLabel}
            value={mgrs}
            onChange={e => setMgrs(e.target.value)}
            onBlur={e => {
              setFieldTouched("displayedCoordinate", true, false)
              const newLatLng = convertMGRSToLatLng(mgrs)
              setFieldValue("displayedCoordinate", e.target.value)
              setFieldValue("lat", newLatLng[0], false)
              setFieldValue("lng", newLatLng[1], false)
            }}
          />
        </Col>
        <CoordinateActionButtons
          lat={lat}
          lng={lng}
          isSubmitting={isSubmitting}
          disabled={!mgrs}
          onClear={() => {
            setFieldValue("displayedCoordinate", null)
            setFieldValue("lat", null, false)
            setFieldValue("lng", null, false)
            setMgrs("")
          }}
        />
      </Col>
    </FormGroup>
  )
}

MGRSFormField.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  editable: PropTypes.bool,
  setFieldValue: fnRequiredWhenEditable,
  setFieldTouched: fnRequiredWhenEditable,
  isSubmitting: PropTypes.bool
}

MGRSFormField.defaultProps = {
  lat: null,
  lng: null,
  editable: false,
  isSubmitting: false
}

/* ========================= LatLonFormField ================================ */

const LatLonFormField = ({
  lat,
  lng,
  editable,
  setFieldValue,
  setFieldTouched,
  isSubmitting
}) => {
  if (!editable) {
    const lt = parseCoordinate(lat)
    const ln = parseCoordinate(lng)
    return (
      <>
        {lt || lt === 0 ? lt : "?"}
        ,
        {ln || ln === 0 ? ln : "?"}
      </>
    )
  }
  const setParsedLatLng = (_lat, _lng) => {
    setFieldTouched("lat", true, false)
    setFieldTouched("lng", true, false)
    setFieldValue("lat", parseCoordinate(_lat))
    setFieldValue("lng", parseCoordinate(_lng))
  }
  return (
    <FormGroup style={{ marginBottom: 0 }}>
      <Col sm={2} componentClass={ControlLabel} htmlFor="lat">
        {LAT_LON_LABEL}
      </Col>

      <Col sm={7}>
        <Col sm={3} style={{ marginRight: "8px" }}>
          <Field
            name="lat"
            component={FieldHelper.InputFieldNoLabel}
            onBlur={() => {
              setParsedLatLng(lat, lng)
            }}
          />
        </Col>
        <Col sm={3}>
          <Field
            name="lng"
            component={FieldHelper.InputFieldNoLabel}
            onBlur={() => {
              setParsedLatLng(lat, lng)
            }}
          />
        </Col>
        <CoordinateActionButtons
          lat={lat}
          lng={lng}
          isSubmitting={isSubmitting}
          disabled={!lat && lat !== 0 && !lng && lng !== 0}
          onClear={() => {
            // setting second param to false prevents validation since lat, lng can be null together
            setFieldTouched("lat", false, false)
            setFieldTouched("lng", false, false)
            setFieldValue("lat", null)
            setFieldValue("lng", null)
          }}
        />
      </Col>
    </FormGroup>
  )
}

LatLonFormField.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  editable: PropTypes.bool,
  setFieldValue: fnRequiredWhenEditable,
  setFieldTouched: fnRequiredWhenEditable,
  isSubmitting: PropTypes.bool
}

LatLonFormField.defaultProps = {
  lat: null,
  lng: null,
  editable: false,
  isSubmitting: false
}

/* ======================= CoordinateActionButtons ============================ */

const CoordinateActionButtons = ({
  lat,
  lng,
  onClear,
  isSubmitting,
  disabled
}) => {
  return (
    <Col sm={2} style={{ padding: "4px 8px" }}>
      <Tooltip content="Clear coordinates">
        <AnchorButton
          minimal
          icon="delete"
          outlined
          intent="danger"
          onClick={onClear}
          disabled={isSubmitting || disabled}
        />
      </Tooltip>
      <AllFormatsInfo lat={lat} lng={lng} inForm />
    </Col>
  )
}

CoordinateActionButtons.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  onClear: PropTypes.func.isRequired,
  isSubmitting: PropTypes.bool.isRequired,
  disabled: PropTypes.bool
}

CoordinateActionButtons.defaultProps = {
  lat: null,
  lng: null,
  disabled: true
}

/* ======================= AllFormatsInfo ============================ */

const AllFormatsInfo = ({ lat, lng, inForm }) => {
  if (!inForm && ((!lat && lat !== 0) || (!lng && lng !== 0))) {
    return null
  }
  return (
    <Popover
      content={

                  All Coordinate Formats

                {LAT_LON_LABEL}

                  <LatLonFormField lat={lat} lng={lng} />

                {MGRS_LABEL}

                  <MGRSFormField lat={lat} lng={lng} />

      }
      target={
        <Tooltip content="Display all coordinate formats">
          <AnchorButton
            style={{ marginLeft: "8px" }}
            id="gloc-info-btn"
            minimal
            icon="info-sign"
            intent="primary"
            outlined={inForm}
          />
        </Tooltip>
      }
      position={Position.RIGHT}
      interactionKind={PopoverInteractionKind.CLICK_TARGET_ONLY}
      usePortal={false}
    />
  )
}

AllFormatsInfo.propTypes = {
  lat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  lng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  inForm: PropTypes.bool
}

AllFormatsInfo.defaultProps = {
  lat: null,
  lng: null,
  inForm: false
}
<|endoftext|>


<commit_before>export class OrderDialogController {
    constructor($mdDialog, ToastsService,
            QuotesService, OrdersService, AccountsService) {
        this.$mdDialog = $mdDialog;
        this.ToastsService = ToastsService;
        this.QuotesService = QuotesService;
        this.OrdersService = OrdersService;
        this.AccountsService = AccountsService;
    }

    $onInit() {
        const account = this.AccountsService.getAccount();

        this.pips = account.pips;

        this.type = "MARKET";
        this.side = this.params.side;
        this.instruments = this.params.instruments;
        this.selectedInstrument = this.params.selectedInstrument;
        this.changeMarket(this.selectedInstrument);
        this.expires = [
            { label: "1 Hour", value: 60 * 60 * 1000 },
            { label: "2 Hours", value: 2 * 60 * 60 * 1000 },
            { label: "3 Hours", value: 3 * 60 * 60 * 1000 },
            { label: "4 Hours", value: 4 * 60 * 60 * 1000 },
            { label: "5 Hours", value: 5 * 60 * 60 * 1000 },
            { label: "6 Hours", value: 6 * 60 * 60 * 1000 },
            { label: "8 Hours", value: 8 * 60 * 60 * 1000 },
            { label: "12 Hours", value: 12 * 60 * 60 * 1000 },
            { label: "18 Hours", value: 18 * 60 * 60 * 1000 },
            { label: "1 Day", value: 60 * 60 * 24 * 1000 },
            { label: "2 Days", value: 2 * 60 * 60 * 24 * 1000 },
            { label: "1 Week", value: 7 * 60 * 60 * 24 * 1000 },
            { label: "1 Month", value: 30 * 60 * 60 * 24 * 1000 },
            { label: "2 Months", value: 60 * 60 * 60 * 24 * 1000 },
            { label: "3 Months", value: 90 * 60 * 60 * 24 * 1000 }
        ];
        this.selectedExpire = 604800000; // 1 week
        this.measure = "price";
        this.isLowerBound = false;
        this.isUpperBound = false;
        this.isTakeProfit = false;
        this.isStopLoss = false;
        this.isTrailingStop = false;
    }

    changeMarket(instrument) {
        if (!this.pips) {
            return;
        }

        const price = this.QuotesService.getQuotes()[instrument],
            fixed = ((this.pips[this.selectedInstrument].toString())
                .match(/0/g) || []).length;

        this.measure = "price";
        this.step = parseFloat(this.pips[this.selectedInstrument]);
        if (this.side === "buy") {
            this.quote = parseFloat(price && price.ask);
            this.takeProfit = parseFloat((this.quote + this.step * 10)
                .toFixed(fixed));
            this.stopLoss = parseFloat((this.quote - this.step * 10)
                .toFixed(fixed));
        } else {
            this.quote = parseFloat(price && price.bid);
            this.takeProfit = parseFloat((this.quote - this.step * 10)
                .toFixed(fixed));
            this.stopLoss = parseFloat((this.quote + this.step * 10)
                .toFixed(fixed));
        }
        this.lowerBound = parseFloat((this.quote - this.step).toFixed(fixed));
        this.upperBound = parseFloat((this.quote + this.step).toFixed(fixed));
        this.trailingStop = 25;
    }

    changeMeasure(measure) {
        if (measure === "price") {
            this.changeMarket(this.selectedInstrument);
        } else {
            this.lowerBound = 1;
            this.upperBound = 1;
            this.takeProfit = 10;
            this.stopLoss = 10;
            this.trailingStop = 25;
            this.step = 1;
        }
    }

    hide() {
        this.$mdDialog.hide();
    }

    cancel() {
        this.$mdDialog.cancel();
    }

    answer(action) {
        const order = {},
            isBuy = this.side === "buy",
            isMeasurePips = this.measure === "pips";

        this.$mdDialog.hide(action);

        this.step = parseFloat(this.pips[this.selectedInstrument]);

        order.instrument = this.selectedInstrument;
        order.units = this.units;
        if (this.units && !isBuy) {
            order.units = `-${order.units}`;
        }

        order.side = this.side;
        order.type = this.type;

        if (order.type === "LIMIT") {
            order.price = this.quote && this.quote.toString();
            order.gtdTime = new Date(Date.now() + this.selectedExpire);
        }

        if (isMeasurePips) {
            if (this.isLowerBound) {
                order.priceBound =
                    parseFloat(this.quote - this.step * this.lowerBound)
                        .toString();
            }
            if (this.isUpperBound) {
                order.priceBound =
                    parseFloat(this.quote + this.step * this.upperBound)
                        .toString();
            }
            if (isBuy) {
                if (this.isTakeProfit) {
                    order.takeProfitOnFill = {};
                    order.takeProfitOnFill.price =
                        parseFloat(this.quote + this.step * this.takeProfit)
                            .toString();
                }
                if (this.isStopLoss) {
                    order.stopLossOnFill = {};
                    order.order.takeProfitOnFill.price =
                        parseFloat(this.quote - this.step * this.stopLoss)
                            .toString();
                }
            } else {
                if (this.isTakeProfit) {
                    order.takeProfitOnFill = {};
                    order.takeProfitOnFill.price =
                        parseFloat(this.quote - this.step * this.takeProfit)
                            .toString();
                }
                if (this.isStopLoss) {
                    order.stopLossOnFill = {};
                    order.order.takeProfitOnFill.price =
                        parseFloat(this.quote + this.step * this.stopLoss)
                            .toString();
                }
            }
        } else {
            if (this.isLowerBound) {
                order.priceBound = this.lowerBound.toString();
            }
            if (this.isUpperBound) {
                order.priceBound = this.upperBound.toString();
            }
            if (this.isTakeProfit) {
                order.takeProfitOnFill = {};
                order.takeProfitOnFill.price = this.takeProfit.toString();
            }
            if (this.isStopLoss) {
                order.stopLossOnFill = {};
                order.stopLossOnFill.price = this.stopLoss.toString();
            }
        }
        if (this.isTrailingStop) {
            order.trailingStopLossOnFill = {};
            order.trailingStopLossOnFill.distance =
                (this.step * this.trailingStop).toString();
        }

        if (action === "submit") {
            this.OrdersService.putOrder(order).then(transaction => {
                let opened,
                    canceled,
                    side,
                    message;

                if (transaction.code && transaction.message) {
                    message = "ERROR " +
                        `${transaction.code} ${transaction.message}`;

                    this.ToastsService.addToast(message);
                } else if (transaction.errorMessage) {
                    message = `ERROR ${transaction.errorMessage}`;

                    this.ToastsService.addToast(message);
                } else if (transaction.orderCancelTransaction) {
                    canceled = transaction.orderCancelTransaction;

                    message = `ERROR ${canceled.reason}`;

                    this.ToastsService.addToast(message);
                } else {
                    opened = transaction.orderFillTransaction ||
                        transaction.orderFillTransaction ||
                        transaction.orderCreateTransaction;

                    side = opened.units > 0 ? "buy" : "sell";
                    message = `${side} ` +
                        `${opened.instrument} ` +
                        `#${opened.id} ` +
                        `@${opened.price} ` +
                        `for ${opened.units}`;

                    this.ToastsService.addToast(message);
                }
            });
        }
    }
}
OrderDialogController.$inject = [
    "$mdDialog", "ToastsService",
    "QuotesService", "OrdersService", "AccountsService"
];
<commit_msg>fix(order-dialog): fix message error<commit_after>export class OrderDialogController {
    constructor($mdDialog, ToastsService,
            QuotesService, OrdersService, AccountsService) {
        this.$mdDialog = $mdDialog;
        this.ToastsService = ToastsService;
        this.QuotesService = QuotesService;
        this.OrdersService = OrdersService;
        this.AccountsService = AccountsService;
    }

    $onInit() {
        const account = this.AccountsService.getAccount();

        this.pips = account.pips;

        this.type = "MARKET";
        this.side = this.params.side;
        this.instruments = this.params.instruments;
        this.selectedInstrument = this.params.selectedInstrument;
        this.changeMarket(this.selectedInstrument);
        this.expires = [
            { label: "1 Hour", value: 60 * 60 * 1000 },
            { label: "2 Hours", value: 2 * 60 * 60 * 1000 },
            { label: "3 Hours", value: 3 * 60 * 60 * 1000 },
            { label: "4 Hours", value: 4 * 60 * 60 * 1000 },
            { label: "5 Hours", value: 5 * 60 * 60 * 1000 },
            { label: "6 Hours", value: 6 * 60 * 60 * 1000 },
            { label: "8 Hours", value: 8 * 60 * 60 * 1000 },
            { label: "12 Hours", value: 12 * 60 * 60 * 1000 },
            { label: "18 Hours", value: 18 * 60 * 60 * 1000 },
            { label: "1 Day", value: 60 * 60 * 24 * 1000 },
            { label: "2 Days", value: 2 * 60 * 60 * 24 * 1000 },
            { label: "1 Week", value: 7 * 60 * 60 * 24 * 1000 },
            { label: "1 Month", value: 30 * 60 * 60 * 24 * 1000 },
            { label: "2 Months", value: 60 * 60 * 60 * 24 * 1000 },
            { label: "3 Months", value: 90 * 60 * 60 * 24 * 1000 }
        ];
        this.selectedExpire = 604800000; // 1 week
        this.measure = "price";
        this.isLowerBound = false;
        this.isUpperBound = false;
        this.isTakeProfit = false;
        this.isStopLoss = false;
        this.isTrailingStop = false;
    }

    changeMarket(instrument) {
        if (!this.pips) {
            return;
        }

        const price = this.QuotesService.getQuotes()[instrument],
            fixed = ((this.pips[this.selectedInstrument].toString())
                .match(/0/g) || []).length;

        this.measure = "price";
        this.step = parseFloat(this.pips[this.selectedInstrument]);
        if (this.side === "buy") {
            this.quote = parseFloat(price && price.ask);
            this.takeProfit = parseFloat((this.quote + this.step * 10)
                .toFixed(fixed));
            this.stopLoss = parseFloat((this.quote - this.step * 10)
                .toFixed(fixed));
        } else {
            this.quote = parseFloat(price && price.bid);
            this.takeProfit = parseFloat((this.quote - this.step * 10)
                .toFixed(fixed));
            this.stopLoss = parseFloat((this.quote + this.step * 10)
                .toFixed(fixed));
        }
        this.lowerBound = parseFloat((this.quote - this.step).toFixed(fixed));
        this.upperBound = parseFloat((this.quote + this.step).toFixed(fixed));
        this.trailingStop = 25;
    }

    changeMeasure(measure) {
        if (measure === "price") {
            this.changeMarket(this.selectedInstrument);
        } else {
            this.lowerBound = 1;
            this.upperBound = 1;
            this.takeProfit = 10;
            this.stopLoss = 10;
            this.trailingStop = 25;
            this.step = 1;
        }
    }

    hide() {
        this.$mdDialog.hide();
    }

    cancel() {
        this.$mdDialog.cancel();
    }

    answer(action) {
        const order = {},
            isBuy = this.side === "buy",
            isMeasurePips = this.measure === "pips";

        this.$mdDialog.hide(action);

        this.step = parseFloat(this.pips[this.selectedInstrument]);

        order.instrument = this.selectedInstrument;
        order.units = this.units;
        if (this.units && !isBuy) {
            order.units = `-${order.units}`;
        }

        order.side = this.side;
        order.type = this.type;

        if (order.type === "LIMIT") {
            order.price = this.quote && this.quote.toString();
            order.gtdTime = new Date(Date.now() + this.selectedExpire);
        }

        if (isMeasurePips) {
            if (this.isLowerBound) {
                order.priceBound =
                    parseFloat(this.quote - this.step * this.lowerBound)
                        .toString();
            }
            if (this.isUpperBound) {
                order.priceBound =
                    parseFloat(this.quote + this.step * this.upperBound)
                        .toString();
            }
            if (isBuy) {
                if (this.isTakeProfit) {
                    order.takeProfitOnFill = {};
                    order.takeProfitOnFill.price =
                        parseFloat(this.quote + this.step * this.takeProfit)
                            .toString();
                }
                if (this.isStopLoss) {
                    order.stopLossOnFill = {};
                    order.order.takeProfitOnFill.price =
                        parseFloat(this.quote - this.step * this.stopLoss)
                            .toString();
                }
            } else {
                if (this.isTakeProfit) {
                    order.takeProfitOnFill = {};
                    order.takeProfitOnFill.price =
                        parseFloat(this.quote - this.step * this.takeProfit)
                            .toString();
                }
                if (this.isStopLoss) {
                    order.stopLossOnFill = {};
                    order.order.takeProfitOnFill.price =
                        parseFloat(this.quote + this.step * this.stopLoss)
                            .toString();
                }
            }
        } else {
            if (this.isLowerBound) {
                order.priceBound = this.lowerBound.toString();
            }
            if (this.isUpperBound) {
                order.priceBound = this.upperBound.toString();
            }
            if (this.isTakeProfit) {
                order.takeProfitOnFill = {};
                order.takeProfitOnFill.price = this.takeProfit.toString();
            }
            if (this.isStopLoss) {
                order.stopLossOnFill = {};
                order.stopLossOnFill.price = this.stopLoss.toString();
            }
        }
        if (this.isTrailingStop) {
            order.trailingStopLossOnFill = {};
            order.trailingStopLossOnFill.distance =
                (this.step * this.trailingStop).toString();
        }

        if (action === "submit") {
            this.OrdersService.putOrder(order).then(transaction => {
                let opened,
                    canceled,
                    side,
                    message;

                if (transaction.message) {
                    message = `ERROR ${transaction.message}`;

                    this.ToastsService.addToast(message);
                } else if (transaction.errorMessage) {
                    message = `ERROR ${transaction.errorMessage}`;

                    this.ToastsService.addToast(message);
                } else if (transaction.orderCancelTransaction) {
                    canceled = transaction.orderCancelTransaction;

                    message = `ERROR ${canceled.reason}`;

                    this.ToastsService.addToast(message);
                } else {
                    opened = transaction.orderFillTransaction ||
                        transaction.orderFillTransaction ||
                        transaction.orderCreateTransaction;

                    side = opened.units > 0 ? "buy" : "sell";
                    message = `${side} ` +
                        `${opened.instrument} ` +
                        `#${opened.id} ` +
                        `@${opened.price} ` +
                        `for ${opened.units}`;

                    this.ToastsService.addToast(message);
                }
            });
        }
    }
}
OrderDialogController.$inject = [
    "$mdDialog", "ToastsService",
    "QuotesService", "OrdersService", "AccountsService"
];
<|endoftext|>


<commit_before>/*
 * Auto Expanding Text Area (1.2.2)
 * by Chrys Bader (www.chrysbader.com)
 * chrysb@gmail.com
 *
 * Special thanks to:
 * Jake Chapa - jake@hybridstudio.com
 * John Resig - jeresig@gmail.com
 *
 * Copyright (c) 2008 Chrys Bader (www.chrysbader.com)
 * Licensed under the GPL (GPL-LICENSE.txt) license.
 *
 *
 * NOTE: This script requires jQuery to work.  Download jQuery at www.jquery.com
 *
 */

(function(jQuery) {

	var self = null;

	jQuery.fn.autogrow = function(o)
	{
		return this.each(function() {
			new jQuery.autogrow(this, o);
		});
	};

    /**
     * The autogrow object.
     *
     * @constructor
     * @name jQuery.autogrow
     * @param Object e The textarea to create the autogrow for.
     * @param Hash o A set of key/value pairs to set as configuration properties.
     * @cat Plugins/autogrow
     */

	jQuery.autogrow = function (e, o)
	{
		this.options		  	= o || {};
		this.dummy			  	= null;
		this.interval	 	  	= null;
		this.line_height	  	= this.options.lineHeight || parseInt(jQuery(e).css('line-height'));
		this.min_height		  	= this.options.minHeight || parseInt(jQuery(e).css('min-height'));
		this.max_height		  	= this.options.maxHeight || parseInt(jQuery(e).css('max-height'));;
		this.textarea		  	= jQuery(e);

		if(this.line_height == NaN)
		  this.line_height = 0;

		// Only one textarea activated at a time, the one being used
		this.init();
	};

	jQuery.autogrow.fn = jQuery.autogrow.prototype = {
    autogrow: '1.2.2'
  };

 	jQuery.autogrow.fn.extend = jQuery.autogrow.extend = jQuery.extend;

	jQuery.autogrow.fn.extend({

		init: function() {
			var self = this;
			this.textarea.css({overflow: 'hidden', display: 'block'});
			this.textarea.bind('focus', function() { self.startExpand() } ).bind('blur', function() { self.stopExpand() });
			this.checkExpand();
		},

		startExpand: function() {
		  var self = this;
			this.interval = window.setInterval(function() {self.checkExpand()}, 400);
		},

		stopExpand: function() {
			clearInterval(this.interval);
		},

		checkExpand: function() {

			if (this.dummy == null)
			{
				this.dummy = jQuery('

');
				this.dummy.css({
												'font-size'  : this.textarea.css('font-size'),
												'font-family': this.textarea.css('font-family'),
												'width'      : this.textarea.css('width'),
												'padding'    : this.textarea.css('padding'),
												'line-height': this.line_height + 'px',
												'overflow-x' : 'hidden',
												'position'   : 'absolute',
												'top'        : 0,
												'left'		 : -9999
												}).appendTo('body');
			}

			// Strip HTML tags
			var html = this.textarea.val().replace(/(<|>)/g, '');

			// IE is different, as per usual
			if ($.browser.msie)
			{
				html = html.replace(/\n/g, '
new');
			}
			else
			{
				html = html.replace(/\n/g, '
new');
			}

			if (this.dummy.html() != html)
			{
				this.dummy.html(html);

				if (this.max_height > 0 && (this.dummy.height() + this.line_height > this.max_height))
				{
					this.textarea.css('overflow-y', 'auto');
				}
				else
				{
					this.textarea.css('overflow-y', 'hidden');
					if (this.textarea.height() < this.dummy.height() + this.line_height || (this.dummy.height() < this.textarea.height()))
					{
						this.textarea.animate({height: (this.dummy.height() + this.line_height) + 'px'}, 100);
					}
				}
			}
		}

	 });
})(jQuery);<commit_msg>js: fix a bug due to $.brower is not supported after jquery 1.9<commit_after>/*
 * Auto Expanding Text Area (1.2.2)
 * by Chrys Bader (www.chrysbader.com)
 * chrysb@gmail.com
 *
 * Special thanks to:
 * Jake Chapa - jake@hybridstudio.com
 * John Resig - jeresig@gmail.com
 *
 * Copyright (c) 2008 Chrys Bader (www.chrysbader.com)
 * Licensed under the GPL (GPL-LICENSE.txt) license.
 *
 *
 * NOTE: This script requires jQuery to work.  Download jQuery at www.jquery.com
 *
 */

(function(jQuery) {

	var self = null;

	jQuery.fn.autogrow = function(o)
	{
		return this.each(function() {
			new jQuery.autogrow(this, o);
		});
	};

    /**
     * The autogrow object.
     *
     * @constructor
     * @name jQuery.autogrow
     * @param Object e The textarea to create the autogrow for.
     * @param Hash o A set of key/value pairs to set as configuration properties.
     * @cat Plugins/autogrow
     */

	jQuery.autogrow = function (e, o)
	{
		this.options		  	= o || {};
		this.dummy			  	= null;
		this.interval	 	  	= null;
		this.line_height	  	= this.options.lineHeight || parseInt(jQuery(e).css('line-height'));
		this.min_height		  	= this.options.minHeight || parseInt(jQuery(e).css('min-height'));
		this.max_height		  	= this.options.maxHeight || parseInt(jQuery(e).css('max-height'));;
		this.textarea		  	= jQuery(e);

		if(this.line_height == NaN)
		  this.line_height = 0;

		// Only one textarea activated at a time, the one being used
		this.init();
	};

	jQuery.autogrow.fn = jQuery.autogrow.prototype = {
    autogrow: '1.2.2'
  };

 	jQuery.autogrow.fn.extend = jQuery.autogrow.extend = jQuery.extend;

	jQuery.autogrow.fn.extend({

		init: function() {
			var self = this;
			this.textarea.css({overflow: 'hidden', display: 'block'});
			this.textarea.bind('focus', function() { self.startExpand() } ).bind('blur', function() { self.stopExpand() });
			this.checkExpand();
		},

		startExpand: function() {
		  var self = this;
			this.interval = window.setInterval(function() {self.checkExpand()}, 400);
		},

		stopExpand: function() {
			clearInterval(this.interval);
		},

		checkExpand: function() {

			if (this.dummy == null)
			{
				this.dummy = jQuery('

');
				this.dummy.css({
												'font-size'  : this.textarea.css('font-size'),
												'font-family': this.textarea.css('font-family'),
												'width'      : this.textarea.css('width'),
												'padding'    : this.textarea.css('padding'),
												'line-height': this.line_height + 'px',
												'overflow-x' : 'hidden',
												'position'   : 'absolute',
												'top'        : 0,
												'left'		 : -9999
												}).appendTo('body');
			}

			// Strip HTML tags
			var html = this.textarea.val().replace(/(<|>)/g, '');

			// IE is different, as per usual
			if (msieversion())
            {
				html = html.replace(/\n/g, '
new');
			}
			else
			{
				html = html.replace(/\n/g, '
new');
			}

			if (this.dummy.html() != html)
			{
				this.dummy.html(html);

				if (this.max_height > 0 && (this.dummy.height() + this.line_height > this.max_height))
				{
					this.textarea.css('overflow-y', 'auto');
				}
				else
				{
					this.textarea.css('overflow-y', 'hidden');
					if (this.textarea.height() < this.dummy.height() + this.line_height || (this.dummy.height() < this.textarea.height()))
					{
						this.textarea.animate({height: (this.dummy.height() + this.line_height) + 'px'}, 100);
					}
				}
			}
		}

	 });
})(jQuery);

function msieversion() {
    var ua = window.navigator.userAgent;
    var msie = ua.indexOf("MSIE ");

    if (msie > 0)      // If Internet Explorer, return version number
        //alert(parseInt(ua.substring(msie + 5, ua.indexOf(".", msie))));
        return true;
    else                 // If another browser, return 0
        //alert('otherbrowser');
        return false;
}

<|endoftext|>


<commit_before>/**
 * Devilry page header.
 */
Ext.define('devilry_header.Header', {
    extend: 'Ext.container.Container',
    alias: 'widget.devilryheader',
    cls: 'devilryheader',
    margins: '0 0 0 0',
    height: 30,

    requires: [
        'devilry_header.FlatButton',
        'devilry_header.HoverMenu',
        'devilry_header.Roles',
        'devilry_authenticateduserinfo.UserInfo'
    ],

    navclass_to_rolename: {
        'student': gettext('Student'),
        'examiner': gettext('Examiner'),
        'administrator': gettext('Administrator'),
        'superuser': gettext('Superuser')
    },

    /**
     * @cfg {string} [navclass]
     * The css class to style the header buttons with.
     */

    /**
     * @cfg {Object} [breadcrumbs=undefined]
     * The object to use for breadcrumbs. You can also set this after load with #setBreadcrumbComponent.
     */

    constructor: function(config) {
        if(!this.navclass_to_rolename[config.navclass]) {
            throw "Invalid navclass: " + config.navclass;
        }
        devilry_authenticateduserinfo.UserInfo.load(); // Load the userinfo as soon as possible.
        this.callParent(arguments);
    },

    initComponent: function() {
        var breadcrumbareaItems = [];
        if(this.breadcrumbs) {
            breadcrumbareaItems = [this.breadcrumbs];
        }
        Ext.apply(this, {
            layout: {
                type: 'hbox',
                align: 'stretch'
            },
            items: [{
                xtype: 'box',
                //width: 110,
                cls: 'devilrylogo',
                tpl: '{text}',
                data: {
                    text: 'Devilry'
                }
            }, {
                xtype: 'devilryheader_flatbutton',
                itemId: 'currentRoleButton',
                cls: 'flatbutton currentrolebutton',
                listeners: {
                    scope: this,
                    render: this._onRenderCurrentRoleButton,
                    click: this._onClickCurrentRoleButton
                }
            }, {
                xtype: 'container',
                itemId: 'breadcrumbarea',
                cls: 'breadcrumbarea',
                items: breadcrumbareaItems,
                flex: 1
            }, {
                xtype: 'devilryheader_flatbutton',
                itemId: 'userButton',
                enableToggle: true,
                width: 100,
                listeners: {
                    scope: this,
                    render: this._onRenderUserButton,
                    toggle: this._onToggleUserButton
                }
            }, {
                // NOTE: This component is floating, so it is not really part of the layout
                xtype: 'devilryheader_hovermenu',
                listeners: {
                    scope: this,
                    show: this._onShowHovermenu,
                    hide: this._onHideHovermenu
                }
            }]
        });
        this.callParent(arguments);
    },

    _getCurrentRoleButton: function() {
        return this.down('#currentRoleButton');
    },
    _getHoverMenu: function() {
        return this.down('devilryheader_hovermenu');
    },
    _getUserButton: function() {
        return this.down('#userButton');
    },
    _getBreadcrumbArea: function() {
        return this.down('#breadcrumbarea');
    },

    setBreadcrumbComponent: function(config) {
        this._getBreadcrumbArea().removeAll();
        this._getBreadcrumbArea().add(config);
    },

    _onRenderCurrentRoleButton: function() {
        this._getCurrentRoleButton().setText(this.navclass_to_rolename[this.navclass]);
        this._getCurrentRoleButton().addExtraClass(this.navclass);
    },
    _onRenderUserButton: function() {
        devilry_authenticateduserinfo.UserInfo.load(function(userInfoRecord) {
            this._getHoverMenu().setUserInfoRecord(userInfoRecord);
            this._getUserButton().addExtraClass(this.navclass);
            this._getUserButton().setText(userInfoRecord.getDisplayName());
        }, this);
    },

    _onClickCurrentRoleButton: function() {
        this._getUserButton().toggle();
    },

    _onToggleUserButton: function(button) {
        var hovermenu = this._getHoverMenu();
        if(button.pressed) {
            hovermenu.show();
        } else {
            hovermenu.hide();
        }
    },

    _onShowHovermenu: function() {
        this._getCurrentRoleButton().setPressedCls();
        this._getUserButton().setPressedCls();
    },
    _onHideHovermenu: function() {
        this._getCurrentRoleButton().setNotPressedCls();
        this._getUserButton().setNotPressedCls();
    },
});
<commit_msg>devilry_header...Header: Add default breadcrumbs widget.<commit_after>/**
 * Devilry page header.
 */
Ext.define('devilry_header.Header', {
    extend: 'Ext.container.Container',
    alias: 'widget.devilryheader',
    cls: 'devilryheader',
    margins: '0 0 0 0',
    height: 30,

    requires: [
        'devilry_header.FlatButton',
        'devilry_header.HoverMenu',
        'devilry_header.Roles',
        'devilry_authenticateduserinfo.UserInfo',
        'devilry_extjsextras.Breadcrumbs'
    ],

    navclass_to_rolename: {
        'student': gettext('Student'),
        'examiner': gettext('Examiner'),
        'administrator': gettext('Administrator'),
        'superuser': gettext('Superuser')
    },

    /**
     * @cfg {string} [navclass]
     * The css class to style the header buttons with.
     */

    /**
     * @cfg {Object} [breadcrumbs=devilry_extjsextras.Breadcrumbs]
     * The object to use for breadcrumbs. You can also set this after load with #setBreadcrumbComponent.
     * Defaults to an instance of devilry_extjsextras.Breadcrumbs.
     */

    constructor: function(config) {
        if(!this.navclass_to_rolename[config.navclass]) {
            throw "Invalid navclass: " + config.navclass;
        }
        devilry_authenticateduserinfo.UserInfo.load(); // Load the userinfo as soon as possible.
        this.callParent(arguments);
    },

    initComponent: function() {
        var breadcrumbareaItem;
        if(this.breadcrumbs) {
            breadcrumbareaItem = this.breadcrumbs;
        } else {
            breadcrumbareaItem = Ext.widget('breadcrumbs');
        }
        Ext.apply(this, {
            layout: {
                type: 'hbox',
                align: 'stretch'
            },
            items: [{
                xtype: 'box',
                //width: 110,
                cls: 'devilrylogo',
                tpl: '{text}',
                data: {
                    text: 'Devilry'
                }
            }, {
                xtype: 'devilryheader_flatbutton',
                itemId: 'currentRoleButton',
                cls: 'flatbutton currentrolebutton',
                listeners: {
                    scope: this,
                    render: this._onRenderCurrentRoleButton,
                    click: this._onClickCurrentRoleButton
                }
            }, {
                xtype: 'container',
                itemId: 'breadcrumbarea',
                cls: 'breadcrumbarea',
                items: [breadcrumbareaItem],
                flex: 1
            }, {
                xtype: 'devilryheader_flatbutton',
                itemId: 'userButton',
                enableToggle: true,
                width: 100,
                listeners: {
                    scope: this,
                    render: this._onRenderUserButton,
                    toggle: this._onToggleUserButton
                }
            }, {
                // NOTE: This component is floating, so it is not really part of the layout
                xtype: 'devilryheader_hovermenu',
                listeners: {
                    scope: this,
                    show: this._onShowHovermenu,
                    hide: this._onHideHovermenu
                }
            }]
        });
        this.callParent(arguments);
    },

    _getCurrentRoleButton: function() {
        return this.down('#currentRoleButton');
    },
    _getHoverMenu: function() {
        return this.down('devilryheader_hovermenu');
    },
    _getUserButton: function() {
        return this.down('#userButton');
    },
    _getBreadcrumbArea: function() {
        return this.down('#breadcrumbarea');
    },

    setBreadcrumbComponent: function(config) {
        this._getBreadcrumbArea().removeAll();
        this._getBreadcrumbArea().add(config);
    },

    _onRenderCurrentRoleButton: function() {
        this._getCurrentRoleButton().setText(this.navclass_to_rolename[this.navclass]);
        this._getCurrentRoleButton().addExtraClass(this.navclass);
    },
    _onRenderUserButton: function() {
        devilry_authenticateduserinfo.UserInfo.load(function(userInfoRecord) {
            this._getHoverMenu().setUserInfoRecord(userInfoRecord);
            this._getUserButton().addExtraClass(this.navclass);
            this._getUserButton().setText(userInfoRecord.getDisplayName());
        }, this);
    },

    _onClickCurrentRoleButton: function() {
        this._getUserButton().toggle();
    },

    _onToggleUserButton: function(button) {
        var hovermenu = this._getHoverMenu();
        if(button.pressed) {
            hovermenu.show();
        } else {
            hovermenu.hide();
        }
    },

    _onShowHovermenu: function() {
        this._getCurrentRoleButton().setPressedCls();
        this._getUserButton().setPressedCls();
    },
    _onHideHovermenu: function() {
        this._getCurrentRoleButton().setNotPressedCls();
        this._getUserButton().setNotPressedCls();
    },
});
<|endoftext|>