<commit_before>'use strict';

/**
 *  client
 **/

/**
 *  client.common
 **/

/**
 *  client.common.history
 **/

/*global $, _, nodeca, window, document*/

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

/**
 *  client.common.history.init()
 *
 *  Assigns all necessary event listeners and handlers.
 *
 *
 *  ##### Example
 *
 *      nodeca.client.common.history.init();
 **/
module.exports.init = function () {
  if (!History.enabled) {
    // do not do anything if History.js is not available
    return;
  }

  // ## WARNING ############################################################# //
  //                                                                          //
  // History.js works poorly with URLs containing hashes:                     //
  //                                                                          //
  //    https://github.com/balupton/history.js/issues/111                     //
  //    https://github.com/balupton/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                                                 //
  //                                                                          //
  // ######################################################################## //

  // An API object with show/hide methods
  //
  var notification = (function () {
    var timeout, msg = nodeca.runtime.t('common.notice.loading');

    return {
      show: function () {
        clearTimeout(timeout); // make sure previous timeout was cleared
        timeout = setTimeout(function () {
          nodeca.client.common.notice.show(msg);
        }, 500);
      },
      hide: function () {
        clearTimeout(timeout);
        nodeca.client.common.notice.hide();
      }
    };
  }());

  // Tries to find match data from the router
  //
  function find_match_data(url) {
    var parts   = String(url).split('#'),
        href    = String(parts[0]).replace(/^[^:]+:\/\//, '//'),
        anchor  = String(parts[1]),
        match   = nodeca.runtime.router.match(href);

    if (!match && /^\/\//.test(href)) {
      // try relative URL if full didn;t match
      //
      //    `//example.com/foo/bar` -> `/foo/bar`
      href  = href.replace(/^\/\/[^\/]+\//, '/');
      match = nodeca.runtime.router.match(href);
    }

    return match ? [match, href, anchor] : null;
  }

  // Executes api3 method from given `data` (an array of `match`, `href` and
  // `anchor` as returned by find_match_data);
  //
  function exec_api3_call(data, callback) {
    var match = data[0], href = data[1], anchor = data[2];

    // schedule "loading..." notification
    notification.show();

    nodeca.io.apiTree(match.meta, match.params, function (err, msg) {
      if (err && err.statusCode && (301 === err.statusCode || 307 === err.statusCode)) {
        // handle rediect via RPC
        exec_api3_call(find_match_data(err.headers.Location), callback);
        return;
      }

      if (err) {
        // can't deal via rpc - try http
        window.location = href;
        return;
      }

      loadAssets((msg.view || match.meta).split('.').shift(), function () {
        callback({
          view:   msg.view || match.meta,
          layout: msg.layout,
          locals: msg.data,
          route:  msg.data.head.route || match.meta,
          anchor: anchor
        }, msg.data.head.title, href);
      });
    });
  }

  //
  // Global semaphore that allows/dissalows ScrollTo animation.
  // Animation is allowed every time we handle user click.
  //

  var allowScrollTo = false;

  //
  // Bind @statechange handler
  //

  History.Adapter.bind(window, 'statechange', function (event) {
    var data = History.getState().data, $el;

    if (!data || History.isEmptyObject(data)) {
      if (History.getStateByIndex(0).id === History.getState().id) {
        // First time got back to initial state - get necessary data
        var match = find_match_data(History.getState().url);

        // if router was able to find apropriate data - make a call,
        // otherwise should never happen
        if (match) {
          exec_api3_call(match, History.replaceState);
          return;
        }
      }

      // FIXME: handle this unexpected situation?
      return;
    }

    // make contnet semi-opque before rendering
    $('#content').stop().fadeTo('fast', 0.3, function () {
      var html;

      try {
        nodeca.client.common.navbar_menu.activate(data.route);

        html = nodeca.client.common.render(data.view, data.locals, data.layout);
        $('#content').html(html);

        nodeca.client.common.stats.inject(data.locals);
      } catch (err) {
        // FIXME: redirect on error? or at least propose user to click
        //        a link to reload to the requested page
        nodeca.logger.error('Failed render view <' + data.view +
                            '> with layout <' + data.layout + '>', err);
      }

      // scroll to element only when we handle user click
      if (allowScrollTo) {
        // if anchor is given try to find matching element
        if (data.anchor) {
          $el = $('#' + data.anchor);
        }

        // if there were no anchor or thre were no matching element
        // use `top` element instead
        if (!$el || !$el.length) {
          $el = $(document.body);
        }

        $("html:not(:animated)" + (!$.browser.opera ? ",body:not(:animated)" : ""))
          .animate({scrollTop: $el.position().top}, 300);

        // disable scrollTo
        allowScrollTo = false;
      }

      // restore opacity
      $('#content').stop().fadeTo('fast', 1, function () {
        nodeca.client.common.floatbar.init();
      });

      // remove "loading..." notification
      notification.hide();
    });
  });

  //
  // Bind global a.click handlers
  //

  $(function () {
    $('body').on('click', 'a', function (event) {
      var $this = $(this), match;

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

      match = find_match_data($this.attr('href'));

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

      if (match) {
        allowScrollTo = true;
        exec_api3_call(match, History.pushState);
        event.preventDefault();
        return false;
      }
    });
  });
};
<commit_msg>Add history.updateStateURL()<commit_after>'use strict';

/**
 *  client
 **/

/**
 *  client.common
 **/

/**
 *  client.common.history
 **/

/*global $, _, nodeca, window, document*/

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

/**
 *  client.common.history.updateStateURL(url) -> Void
 *
 *  Proxy to History.js that replaces current State with new URL.
 **/
module.exports.updateStateURL = $.noop;

/**
 *  client.common.history.init()
 *
 *  Assigns all necessary event listeners and handlers.
 *
 *
 *  ##### Example
 *
 *      nodeca.client.common.history.init();
 **/
module.exports.init = function () {
  if (!History.enabled) {
    // do not do anything if History.js is not available
    return;
  }

  // ## WARNING ############################################################# //
  //                                                                          //
  // History.js works poorly with URLs containing hashes:                     //
  //                                                                          //
  //    https://github.com/balupton/history.js/issues/111                     //
  //    https://github.com/balupton/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                                                 //
  //                                                                          //
  // ######################################################################## //

  // An API object with show/hide methods
  //
  var notification = (function () {
    var timeout, msg = nodeca.runtime.t('common.notice.loading');

    return {
      show: function () {
        clearTimeout(timeout); // make sure previous timeout was cleared
        timeout = setTimeout(function () {
          nodeca.client.common.notice.show(msg);
        }, 500);
      },
      hide: function () {
        clearTimeout(timeout);
        nodeca.client.common.notice.hide();
      }
    };
  }());

  // Tries to find match data from the router
  //
  function find_match_data(url) {
    var parts   = String(url).split('#'),
        href    = String(parts[0]).replace(/^[^:]+:\/\//, '//'),
        anchor  = String(parts[1]),
        match   = nodeca.runtime.router.match(href);

    if (!match && /^\/\//.test(href)) {
      // try relative URL if full didn;t match
      //
      //    `//example.com/foo/bar` -> `/foo/bar`
      href  = href.replace(/^\/\/[^\/]+\//, '/');
      match = nodeca.runtime.router.match(href);
    }

    return match ? [match, href, anchor] : null;
  }

  // Executes api3 method from given `data` (an array of `match`, `href` and
  // `anchor` as returned by find_match_data);
  //
  function exec_api3_call(data, callback) {
    var match = data[0], href = data[1], anchor = data[2];

    // schedule "loading..." notification
    notification.show();

    nodeca.io.apiTree(match.meta, match.params, function (err, msg) {
      if (err && err.statusCode && (301 === err.statusCode || 307 === err.statusCode)) {
        // handle rediect via RPC
        exec_api3_call(find_match_data(err.headers.Location), callback);
        return;
      }

      if (err) {
        // can't deal via rpc - try http
        window.location = href;
        return;
      }

      loadAssets((msg.view || match.meta).split('.').shift(), function () {
        callback({
          view:   msg.view || match.meta,
          layout: msg.layout,
          locals: msg.data,
          route:  msg.data.head.route || match.meta,
          anchor: anchor
        }, msg.data.head.title, href);
      });
    });
  }

  //
  // Global semaphore that allows/dissalows ScrollTo animation.
  // Animation is allowed every time we handle user click.
  //

  var allowScrollTo   = false;
  var skipStateChange = false;

  //
  // Bind @statechange handler
  //

  History.Adapter.bind(window, 'statechange', function (event) {
    var data = History.getState().data, $el;

    if (skipStateChange) {
      skipStateChange = false;
      return;
    }

    if (!data || History.isEmptyObject(data)) {
      // First time got back to initial state - get necessary data
      var match = find_match_data(History.getState().url);

      // if router was able to find apropriate data - make a call,
      // otherwise should never happen
      if (match) {
        exec_api3_call(match, History.replaceState);
        return;
      }
    }

    // make contnet semi-opque before rendering
    $('#content').stop().fadeTo('fast', 0.3, function () {
      var html;

      try {
        nodeca.client.common.navbar_menu.activate(data.route);

        html = nodeca.client.common.render(data.view, data.locals, data.layout);
        $('#content').html(html);

        nodeca.client.common.stats.inject(data.locals);
      } catch (err) {
        // FIXME: redirect on error? or at least propose user to click
        //        a link to reload to the requested page
        nodeca.logger.error('Failed render view <' + data.view +
                            '> with layout <' + data.layout + '>', err);
      }

      // scroll to element only when we handle user click
      if (allowScrollTo) {
        // if anchor is given try to find matching element
        if (data.anchor) {
          $el = $('#' + data.anchor);
        }

        // if there were no anchor or thre were no matching element
        // use `top` element instead
        if (!$el || !$el.length) {
          $el = $(document.body);
        }

        $("html:not(:animated)" + (!$.browser.opera ? ",body:not(:animated)" : ""))
          .animate({scrollTop: $el.position().top}, 300);

        // disable scrollTo
        allowScrollTo = false;
      }

      // restore opacity
      $('#content').stop().fadeTo('fast', 1, function () {
        nodeca.client.common.floatbar.init();
      });

      // remove "loading..." notification
      notification.hide();
    });
  });

  //
  // Bind global a.click handlers
  //

  $(function () {
    $('body').on('click', 'a', function (event) {
      var $this = $(this), match;

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

      match = find_match_data($this.attr('href'));

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

      if (match) {
        allowScrollTo = true;
        exec_api3_call(match, History.pushState);
        event.preventDefault();
        return false;
      }
    });
  });

  //
  // Provide real updateStateURL()
  //

  module.exports.updateStateURL = function updateStateURL(url) {
    skipStateChange = true;
    History.replaceState({}, History.getState().title, url);
  };
};
<|endoftext|>


<commit_before>import {login} from '../api/sessionsApi'
import {updateSources} from './sourcesActions'
import Auth from '../auth/authenticator.js'

export function loginUser(userDetails, history) {
  return function(dispatch) {
    dispatch({type: 'LOGGING_IN'})
    return login(userDetails).then(response => {
        if (response.token) {
          dispatch(
            {
              type: 'LOGGED_IN',
              payload: response.user.id
            }
          )
          dispatch(updateSources(response.sources, history));
          sessionStorage.setItem('jwt', response.token);
        } else {
          dispatch(
            {
              type: 'LOGIN_ERROR',
              payload: response.errors
            }
          )
        }
      }).catch(error => {
        throw(error);
      });
  }
}

export function logOutUser() {
  return function(dispatch) {
    Auth.logOut()
    dispatch(
      { type: 'LOG_OUT'}
    )
  }
}
<commit_msg>Refactor dispatch with functions<commit_after>import {login} from '../api/sessionsApi'
import {updateSources} from './sourcesActions'

export const loggingIn = () => {
  return {type: 'LOGGING_IN'}
}

export const loggedIn = (response) => {
  return {
    type: 'LOGGED_IN',
    payload: response.user.id
  }
}

export const loginError = (response) => {
  return {
    type: 'LOGIN_ERROR',
    payload: response.errors
  }
}

export const logOut = () => {
   return {type: 'LOG_OUT'}
}

export function loginUser(userDetails, history) {
  return function(dispatch) {
    dispatch(loggingIn())
    return login(userDetails).then(response => {
        if (response.token) {
          dispatch(loggedIn(response));
          dispatch(updateSources(response.sources, history));
          sessionStorage.setItem('jwt', response.token);
        } else {
          dispatch(loginError(response));
        }
      }).catch(error => {
        throw(error);
      });
  }
}

export function logOutUser() {
  return function(dispatch) {
    sessionStorage.removeItem('jwt')
    dispatch(logOut());
  }
}
<|endoftext|>


<commit_before>function centerWindow() {
    "use strict";
    var centeredWidth = $('.centered').width() / 2,
        centeredHeight = $('.centered').height() / 2;
    $('.centered').css({ 'margin-left' : -centeredWidth , 'margin-top' : -centeredHeight });
}

function requestData() {
    "use strict";
    var req = new XMLHttpRequest();
    req.open("GET", "/games", true);
    req.setRequestHeader("accept", "application/json");

    req.onload = function() {
        if (req.status >= 200 && req.status < 400){
            var gamesjson = req.responseText;
            var gamesData = JSON.parse(gamesjson);
            var gamesToHtml = "";
            var key, x = 0;

            for (key in gamesData.games) {
                gamesToHtml += "
";
                var gameurl = "/#" + gamesData.games[x].id;
                var threadurl = gamesData.games[x].url;
                gamesToHtml +=  '' + gamesData.games[x].name + "	";
                gamesToHtml += '' + threadurl + "	";
 //             gamesToHtml += "" + gamesData.games[x].start_date + "	";
                gamesToHtml += "
";
                x++;
            }

            document.getElementById("games").innerHTML = gamesToHtml;
        } else {
            console.log("Error. Status code " + req.status);
        }
    };

    req.send(null);
    locationHashChanged();
}

function locationHashChanged() {
    "use strict";
    if (window.location.hash != "") {
        var id = "" + window.location.hash;
        id = id.slice(1, id.length);
        showGame(id);
    }
}

function showCreateGame() {
    "use strict";
    var createGame = document.getElementById("creategame");
    document.getElementById("index").className += ' hidden';
    createGame.className = createGame.className.replace( /(?:^|\s)hidden(?!\S)/g , '' );

    centerWindow();
}

function hideCreateGame() {
    "use strict";
    document.getElementById("creategame").className += ' hidden';
    document.getElementById("index").className =
    document.getElementById("index").className.replace
      ( /(?:^|\s)hidden(?!\S)/g , '' );
    window.location.hash = "";
}

function showGame(id) {
    "use strict";
    var req = new XMLHttpRequest();
    var address = "/game/" + id;
    req.open("GET", address, true);
    req.setRequestHeader("accept", "application/json");

    req.onload = function() {
        if (req.status >= 200 && req.status < 400){
            var gamejson = req.responseText;
            var gameData = JSON.parse(gamejson);
            var gameToHtml = "";

            document.getElementById("gameheader").innerHTML = gameData.game.name;

            gameToHtml += '' + gameData.game.url + "

";

            gameToHtml += "Hosts";

            for (var i = 0; i < gameData.hosts.length; i++) {
                gameToHtml += "
" + gameData.hosts[i] + "
";
            }

            gameToHtml += "
Players";

            for (var i = 0; i < gameData.players.length; i++) {
                gameToHtml += "
" + gameData.players[i].name + "
";
            }

            var currentDay = 0;

            if (gameData.votes.length > 0) {
                currentDay = gameData.votes[gameData.votes.length - 1].day;

                gameToHtml += "

Current Day Votecount

";

                var targets = [];
                var nonvoters = gameData.players;

                for (var i = 0; i < gameData.votes.length; i++) {
                    if (currentDay == gameData.votes[i].day) {
                        if (targets.indexOf(gameData.votes[i].target.toLowerCase()) == -1) {
                            // push every new unique vote target to the targets array
                            targets.push(gameData.votes[i].target.toLowerCase());
                        }
                            for (var j = 0; j < nonvoters; j++) {
                                if (nonvoters[j] == gameData.votes[j].voter) {
                                    nonvoters.splice(j,1);
                                }
                            }
                      }
                }

                for (var i = 0; i < targets.length; i++) {
                    if (targets[i] == "") {
                        // don't show the null (unvote) target in the targets listing
                        continue;
                    }
                    var voters = [];
                    for (var j = 0; j < gameData.votes.length; j++) {
                        if (currentDay == gameData.votes[j].day) {
                            for (var k = 0; k < targets.length; k++) {
                                for (var z = 0; z < voters.length; z++) {
                                    if (voters[z] == gameData.votes[j].voter) {
                                        // every time a new vote is given, mark every previous vote by the same voter as "overwritten"
                                        voters[z] = "<s>" + voters[z] + "</s>";
                                    }
                                }
                            }
                            if (gameData.votes[j].target != "" && nonvoters.indexOf(gameData.votes[j].voter) == -1) {
                                nonvoters.push(gameData.votes[j].voter);
                            }
                        }
                        if (currentDay == gameData.votes[j].day && gameData.votes[j].target.toLowerCase() == targets[i] && gameData.votes[j].target != "") {
                            // add a new voter to an existing vote target
                            voters.push(gameData.votes[j].voter);
                            nonvoters.splice(j,1);
                        }
                    }
                    var voterscount = 0;
                    for (var j = 0; j < voters.length; j++) {
                        if (voters[j].indexOf("<s>") == -1) {
                            // count the number of voters for this target that haven't been unvoted
                            voterscount++;
                        }
                    }
                    gameToHtml += "
" + targets[i] + " (" + voterscount + "): " + voters.join(', ') + "
";
            }
                gameToHtml += 'Not Voting (' + nonvoters.length + '):' + nonvoters.join(', ');
        }

            var firstNewDayVote = false;
            currentDay = 0;

            for (var i = 0; i < gameData.votes.length; i++) {
                var post = gameData.votes[i].index;
                var page = Math.floor(post/20) + 1;
                var address = gameData.game.url + "?page=" + page + "#" + post;

                if (currentDay != gameData.votes[i].day) {
                    firstNewDayVote = true;
                    currentDay = gameData.votes[i].day;
                }
                if (firstNewDayVote === true) {
                    gameToHtml += "

Day " + gameData.votes[i].day + "

";
                    firstNewDayVote = false;
                }
                if (gameData.votes[i].target == "") {
                    gameToHtml += '
' + gameData.votes[i].voter + " unvoted
";
                } else if (gameData.votes[i].target == null) {
                    gameToHtml += '
' + gameData.votes[i].voter + " gave an invalid vote!
";
                } else {
                    gameToHtml += '
' + gameData.votes[i].voter +
                        " voted for " + gameData.votes[i].target + "
";
                }
            }

            if (gameData.votes.length > 0) {
                gameToHtml += '
<textarea id="reply_area" cols=80 rows=25 name="bericht" style="font:10pt Arial; margin-bottom: 4px; width:732px">';
                currentDay = gameData.votes[gameData.votes.length - 1].day;

                gameToHtml += "[blue][b][u][big]Day " + currentDay + " Votecount[/big][/u][/b][/blue]\r\n\r\n";

                var targets = [];
                var nonvoters = gameData.players;

                for (var i = 0; i < gameData.votes.length; i++) {
                    if (currentDay == gameData.votes[i].day) {
                        if (targets.indexOf(gameData.votes[i].target.toLowerCase()) == -1) {
                            // push every new unique vote target to the targets array
                            targets.push(gameData.votes[i].target.toLowerCase());
                        }
                        for (var j = 0; j < nonvoters; j++) {
                                if (nonvoters[j] == gameData.votes[j].voter) {
                                    nonvoters.splice(j,1);
                                }
                            }
                      }
                }

                for (var i = 0; i < targets.length; i++) {
                    if (targets[i] == "") {
                        // don't show the null (unvote) target in the targets listing
                        continue;
                    }
                    var voters = [];
                    for (var j = 0; j < gameData.votes.length; j++) {
                        if (currentDay == gameData.votes[j].day) {
                            for (var k = 0; k < targets.length; k++) {
                                for (var z = 0; z < voters.length; z++) {
                                    if (voters[z] == gameData.votes[j].voter) {
                                        // every time a new vote is given, mark every previous vote by the same voter as "overwritten"
                                        voters[z] = "[s]" + voters[z] + "[/s]";
                                    }
                                }
                            }
                            if (gameData.votes[j].target != "" && nonvoters.indexOf(gameData.votes[j].voter) == -1) {
                                nonvoters.push(gameData.votes[j].voter);
                            }
                        }
                        if (currentDay == gameData.votes[j].day && gameData.votes[j].target.toLowerCase() == targets[i] && gameData.votes[j].target != "") {
                            // add a new voter to an existing vote target
                            voters.push(gameData.votes[j].voter);
                        }
                    }
                    var voterscount = 0;
                    for (var j = 0; j < voters.length; j++) {
                        if (voters[j].indexOf("[s]") == -1) {
                            // count the number of voters for this target that haven't been unvoted
                            voterscount++;
                        }
                    }
                    gameToHtml += "[b]" + targets[i] + " (" + voterscount + "):[/b] " + voters.join(', ') + "\r\n";
            }
                gameToHtml += '[b]Not Voting (' + nonvoters.length + '):[/b]' + nonvoters.join(', ');
                gameToHtml += '</textarea>';
        }

            if (gameData.votes.length === 0) {
                gameToHtml += "
No votes have been extracted from the given thread yet (this may take more time or there may have been an error while reading the thread)
";
            }

            document.getElementById("gamedata").innerHTML = gameToHtml;
        } else {
            console.log("Error. Status code " + req.status);
        }
    };

    req.send(null);
    document.getElementById("index").className += ' hidden';
    document.getElementById("gameview").className =
    document.getElementById("gameview").className.replace
      ( /(?:^|\s)hidden(?!\S)/g , '' );
}

function hideGame() {
    "use strict";
    document.getElementById("gameview").className += ' hidden';
    document.getElementById("index").className =
    document.getElementById("index").className.replace
      ( /(?:^|\s)hidden(?!\S)/g , '' );
    window.location.hash = "";
}

requestData();
<commit_msg>fixed more bugs with nonvoters list<commit_after>function centerWindow() {
    "use strict";
    var centeredWidth = $('.centered').width() / 2,
        centeredHeight = $('.centered').height() / 2;
    $('.centered').css({ 'margin-left' : -centeredWidth , 'margin-top' : -centeredHeight });
}

function requestData() {
    "use strict";
    var req = new XMLHttpRequest();
    req.open("GET", "/games", true);
    req.setRequestHeader("accept", "application/json");

    req.onload = function() {
        if (req.status >= 200 && req.status < 400){
            var gamesjson = req.responseText;
            var gamesData = JSON.parse(gamesjson);
            var gamesToHtml = "";
            var key, x = 0;

            for (key in gamesData.games) {
                gamesToHtml += "
";
                var gameurl = "/#" + gamesData.games[x].id;
                var threadurl = gamesData.games[x].url;
                gamesToHtml +=  '' + gamesData.games[x].name + "	";
                gamesToHtml += '' + threadurl + "	";
 //             gamesToHtml += "" + gamesData.games[x].start_date + "	";
                gamesToHtml += "
";
                x++;
            }

            document.getElementById("games").innerHTML = gamesToHtml;
        } else {
            console.log("Error. Status code " + req.status);
        }
    };

    req.send(null);
    locationHashChanged();
}

function locationHashChanged() {
    "use strict";
    if (window.location.hash != "") {
        var id = "" + window.location.hash;
        id = id.slice(1, id.length);
        showGame(id);
    }
}

function showCreateGame() {
    "use strict";
    var createGame = document.getElementById("creategame");
    document.getElementById("index").className += ' hidden';
    createGame.className = createGame.className.replace( /(?:^|\s)hidden(?!\S)/g , '' );

    centerWindow();
}

function hideCreateGame() {
    "use strict";
    document.getElementById("creategame").className += ' hidden';
    document.getElementById("index").className =
    document.getElementById("index").className.replace
      ( /(?:^|\s)hidden(?!\S)/g , '' );
    window.location.hash = "";
}

function showGame(id) {
    "use strict";
    var req = new XMLHttpRequest();
    var address = "/game/" + id;
    req.open("GET", address, true);
    req.setRequestHeader("accept", "application/json");

    req.onload = function() {
        if (req.status >= 200 && req.status < 400){
            var gamejson = req.responseText;
            var gameData = JSON.parse(gamejson);
            var gameToHtml = "";

            document.getElementById("gameheader").innerHTML = gameData.game.name;

            gameToHtml += '' + gameData.game.url + "

";

            gameToHtml += "Hosts";

            for (var i = 0; i < gameData.hosts.length; i++) {
                gameToHtml += "
" + gameData.hosts[i] + "
";
            }

            gameToHtml += "
Players";

            for (var i = 0; i < gameData.players.length; i++) {
                gameToHtml += "
" + gameData.players[i].name + "
";
            }

            var currentDay = 0;

            if (gameData.votes.length > 0) {
                currentDay = gameData.votes[gameData.votes.length - 1].day;

                gameToHtml += "

Current Day Votecount

";

                var targets = [];
                var nonvoters = gameData.players;

                for (var i = 0; i < gameData.votes.length; i++) {
                    if (currentDay == gameData.votes[i].day) {
                        if (targets.indexOf(gameData.votes[i].target.toLowerCase()) == -1) {
                            // push every new unique vote target to the targets array
                            targets.push(gameData.votes[i].target.toLowerCase());
                        }
                            for (var j = 0; j < nonvoters; j++) {
                                if (nonvoters[j] == gameData.votes[j].voter) {
                                    nonvoters.splice(j,1);
                                }
                            }
                      }
                }

                for (var i = 0; i < targets.length; i++) {
                    if (targets[i] == "") {
                        // don't show the null (unvote) target in the targets listing
                        continue;
                    }
                    var voters = [];
                    for (var j = 0; j < gameData.votes.length; j++) {
                        if (currentDay == gameData.votes[j].day) {
                            for (var k = 0; k < targets.length; k++) {
                                for (var z = 0; z < voters.length; z++) {
                                    if (voters[z] == gameData.votes[j].voter) {
                                        // every time a new vote is given, mark every previous vote by the same voter as "overwritten"
                                        voters[z] = "<s>" + voters[z] + "</s>";
                                    }
                                }
                            }
                            if (gameData.votes[j].target == "" && nonvoters.indexOf(gameData.votes[j].voter) == -1) {
                                nonvoters.push(gameData.votes[j].voter);
                            }
                        }
                        if (currentDay == gameData.votes[j].day && gameData.votes[j].target.toLowerCase() == targets[i] && gameData.votes[j].target != "") {
                            // add a new voter to an existing vote target
                            voters.push(gameData.votes[j].voter);
                            nonvoters.splice(j,1);
                        }
                    }
                    var voterscount = 0;
                    for (var j = 0; j < voters.length; j++) {
                        if (voters[j].indexOf("<s>") == -1) {
                            // count the number of voters for this target that haven't been unvoted
                            voterscount++;
                        }
                    }
                    gameToHtml += "
" + targets[i] + " (" + voterscount + "): " + voters.join(', ') + "
";
            }
                gameToHtml += '
Not Voting (' + nonvoters.length + '): ' + nonvoters.join(', ');
        }

            var firstNewDayVote = false;
            currentDay = 0;

            for (var i = 0; i < gameData.votes.length; i++) {
                var post = gameData.votes[i].index;
                var page = Math.floor(post/20) + 1;
                var address = gameData.game.url + "?page=" + page + "#" + post;

                if (currentDay != gameData.votes[i].day) {
                    firstNewDayVote = true;
                    currentDay = gameData.votes[i].day;
                }
                if (firstNewDayVote === true) {
                    gameToHtml += "

Day " + gameData.votes[i].day + "

";
                    firstNewDayVote = false;
                }
                if (gameData.votes[i].target == "") {
                    gameToHtml += '
' + gameData.votes[i].voter + " unvoted
";
                } else if (gameData.votes[i].target == null) {
                    gameToHtml += '
' + gameData.votes[i].voter + " gave an invalid vote!
";
                } else {
                    gameToHtml += '
' + gameData.votes[i].voter +
                        " voted for " + gameData.votes[i].target + "
";
                }
            }

            if (gameData.votes.length > 0) {
                gameToHtml += '
<textarea id="reply_area" cols=80 rows=25 name="bericht" style="font:10pt Arial; margin-bottom: 4px; width:732px">';
                currentDay = gameData.votes[gameData.votes.length - 1].day;

                gameToHtml += "[blue][b][u][big]Day " + currentDay + " Votecount[/big][/u][/b][/blue]\r\n\r\n";

                var targets = [];
                var nonvoters = gameData.players;

                for (var i = 0; i < gameData.votes.length; i++) {
                    if (currentDay == gameData.votes[i].day) {
                        if (targets.indexOf(gameData.votes[i].target.toLowerCase()) == -1) {
                            // push every new unique vote target to the targets array
                            targets.push(gameData.votes[i].target.toLowerCase());
                        }
                        for (var j = 0; j < nonvoters; j++) {
                                if (nonvoters[j] == gameData.votes[j].voter) {
                                    nonvoters.splice(j,1);
                                }
                            }
                      }
                }

                for (var i = 0; i < targets.length; i++) {
                    if (targets[i] == "") {
                        // don't show the null (unvote) target in the targets listing
                        continue;
                    }
                    var voters = [];
                    for (var j = 0; j < gameData.votes.length; j++) {
                        if (currentDay == gameData.votes[j].day) {
                            for (var k = 0; k < targets.length; k++) {
                                for (var z = 0; z < voters.length; z++) {
                                    if (voters[z] == gameData.votes[j].voter) {
                                        // every time a new vote is given, mark every previous vote by the same voter as "overwritten"
                                        voters[z] = "[s]" + voters[z] + "[/s]";
                                    }
                                }
                            }
                            if (gameData.votes[j].target == "" && nonvoters.indexOf(gameData.votes[j].voter) == -1) {
                                nonvoters.push(gameData.votes[j].voter);
                            }
                        }
                        if (currentDay == gameData.votes[j].day && gameData.votes[j].target.toLowerCase() == targets[i] && gameData.votes[j].target != "") {
                            // add a new voter to an existing vote target
                            voters.push(gameData.votes[j].voter);
                        }
                    }
                    var voterscount = 0;
                    for (var j = 0; j < voters.length; j++) {
                        if (voters[j].indexOf("[s]") == -1) {
                            // count the number of voters for this target that haven't been unvoted
                            voterscount++;
                        }
                    }
                    gameToHtml += "[b]" + targets[i] + " (" + voterscount + "):[/b] " + voters.join(', ') + "\r\n";
            }
                gameToHtml += '\r\n' + '[b]Not Voting (' + nonvoters.length + '):[/b] ' + nonvoters.join(', ');
                gameToHtml += '</textarea>';
        }

            if (gameData.votes.length === 0) {
                gameToHtml += "
No votes have been extracted from the given thread yet (this may take more time or there may have been an error while reading the thread)
";
            }

            document.getElementById("gamedata").innerHTML = gameToHtml;
        } else {
            console.log("Error. Status code " + req.status);
        }
    };

    req.send(null);
    document.getElementById("index").className += ' hidden';
    document.getElementById("gameview").className =
    document.getElementById("gameview").className.replace
      ( /(?:^|\s)hidden(?!\S)/g , '' );
}

function hideGame() {
    "use strict";
    document.getElementById("gameview").className += ' hidden';
    document.getElementById("index").className =
    document.getElementById("index").className.replace
      ( /(?:^|\s)hidden(?!\S)/g , '' );
    window.location.hash = "";
}

requestData();
<|endoftext|>


<commit_before>"use strict";
const express = require('express'),
    app = module.exports = express(),
    account = require('../mongodb/account');
console.log("2222222222");
app.get('/:email/:name', function (req, res) {
    let email = req.params.email,
        name = req.params.name;
    console.log("333333");
    account.find(email, user => {
        console.log("444444444");
        if (user){
            res.json(user);
        } else {
            account.save({name: name, email: email, credits: 100}, userData => {
                res.send(userData);
            })
        }
    });
});

app.put('/:email', function (req, res) {
    let email = req.params.email,
        credits = req.body.credits;

    account.topUp(email, credits, () => {
        account.find(email, user => {
            res.json(user);
        });
    });
});<commit_msg>heroku try<commit_after>"use strict";
const express = require('express'),
    app = module.exports = express(),
    account = require('../mongodb/account');
console.log("2222222222");
app.get('/:email/:name', function (req, res) {
    let email = req.params.email,
        name = req.params.name;
    console.log("333333");
    account.find(email, user => {
        console.log("444444444");
        if (user){
            res.json(user);
        } else {
            account.save({name: name, email: email, credits: 100}, userData => {
                res.send(userData);
            })
        }
    });
});

app.put('/:email', function (req, res) {
    let email = req.params.email,
        credits = req.body.credits;

    account.topUp(email, credits, () => {
        account.find(email, user => {
            res.json(user);
        });
    });
});

app.all('*', (req, res) => {
    console.log("0000000000000",req, res);
});<|endoftext|>


<commit_before>window.$ = window.jQuery = require('jquery')
require('jquery-ui-bundle')
require('es6-shim')
require('patternfly/dist/css/patternfly.css')
require('patternfly/dist/css/patternfly-additions.css')
let initRemoteConsole = require('./common')

/* global WMKS */

$(function () {
  const settings = initRemoteConsole()
  $('#console-type').html('WebMKS')
  // WebMKS cannot be a part of the asset pipeline, therefore, it has to be loaded with this hack
  var WEBMKS_JS_PATH = '/webmks/wmks.min.js'
  // Test if the file exists under the given path
  fetch(WEBMKS_JS_PATH, {
    method: 'HEAD',
    cache: 'no-cache'
  }).then(function (response) {
    return new Promise(function (resolve, reject) {
      if (!response.ok) {
        // Reject the promise if there's any error with the request, i.e. the file doesn't exist
        reject(response)
      } else {
        // Create a <link> tag that loads the CSS file and append it to the page
        var link = document.createElement('link')
        link.rel = 'stylesheet'
        link.type = 'text/css'
        link.href = '/webmks/css/wmks-all.css'
        document.head.appendChild(link)

        // Create a <script> tag that loads the JS file
        var script = document.createElement('script')
        script.src = WEBMKS_JS_PATH
        // Set an onload function that resolves the promise
        script.onload = function () {
          resolve()
        }
        // Append the <script> tag to the page
        document.head.appendChild(script)
      }
    })
  }).then(function () {
    // Set up the remote console after the JS is loaded
    var host = window.location.hostname
    var encrypt = window.location.protocol === 'https:'
    var port = encrypt ? 443 : 80
    if (window.location.port) {
      port = window.location.port
    }
    var proto = encrypt ? 'wss' : 'ws'

    var options = {}
    // vCloud requires some extra attributes
    if (settings.isvcloud) {
      options.VCDProxyHandshakeVmxPath = settings.vmx
      options.enableUint8Utf8 = true
    }

    var wmks = WMKS.createWMKS('remote-console', options).register(WMKS.CONST.Events.CONNECTION_STATE_CHANGE, function (event, data) {
      if (data.state === WMKS.CONST.ConnectionState.CONNECTED) {
        $('#connection-status').text(__('Connected'))
      }
    })

    wmks.connect(proto + '://' + host + ':' + port + '/' + settings.path)

    $('#ctrlaltdel').on('click', function () {
      wmks.sendCAD()
    })

    $('#keymap').on('change', function () {
      wmks.setOption('keyboardLayoutId', this.value)
    })
  }).catch(function () {
    $('#remote-console').html(__('The appliance has no access to the assets required to run the WebMKS console. For more info please see the documentation.'))
  })
})
<commit_msg>webmks - comment about Promise<commit_after>window.$ = window.jQuery = require('jquery')
require('jquery-ui-bundle')
require('es6-shim')
require('patternfly/dist/css/patternfly.css')
require('patternfly/dist/css/patternfly-additions.css')
let initRemoteConsole = require('./common')

/* global WMKS */

$(function () {
  const settings = initRemoteConsole()
  $('#console-type').html('WebMKS')
  // WebMKS cannot be a part of the asset pipeline, therefore, it has to be loaded with this hack
  var WEBMKS_JS_PATH = '/webmks/wmks.min.js'
  // Test if the file exists under the given path
  fetch(WEBMKS_JS_PATH, {
    method: 'HEAD',
    cache: 'no-cache'
  }).then(function (response) {
    return new Promise(function (resolve, reject) { // valid, outside angular
      if (!response.ok) {
        // Reject the promise if there's any error with the request, i.e. the file doesn't exist
        reject(response)
      } else {
        // Create a <link> tag that loads the CSS file and append it to the page
        var link = document.createElement('link')
        link.rel = 'stylesheet'
        link.type = 'text/css'
        link.href = '/webmks/css/wmks-all.css'
        document.head.appendChild(link)

        // Create a <script> tag that loads the JS file
        var script = document.createElement('script')
        script.src = WEBMKS_JS_PATH
        // Set an onload function that resolves the promise
        script.onload = function () {
          resolve()
        }
        // Append the <script> tag to the page
        document.head.appendChild(script)
      }
    })
  }).then(function () {
    // Set up the remote console after the JS is loaded
    var host = window.location.hostname
    var encrypt = window.location.protocol === 'https:'
    var port = encrypt ? 443 : 80
    if (window.location.port) {
      port = window.location.port
    }
    var proto = encrypt ? 'wss' : 'ws'

    var options = {}
    // vCloud requires some extra attributes
    if (settings.isvcloud) {
      options.VCDProxyHandshakeVmxPath = settings.vmx
      options.enableUint8Utf8 = true
    }

    var wmks = WMKS.createWMKS('remote-console', options).register(WMKS.CONST.Events.CONNECTION_STATE_CHANGE, function (event, data) {
      if (data.state === WMKS.CONST.ConnectionState.CONNECTED) {
        $('#connection-status').text(__('Connected'))
      }
    })

    wmks.connect(proto + '://' + host + ':' + port + '/' + settings.path)

    $('#ctrlaltdel').on('click', function () {
      wmks.sendCAD()
    })

    $('#keymap').on('change', function () {
      wmks.setOption('keyboardLayoutId', this.value)
    })
  }).catch(function () {
    $('#remote-console').html(__('The appliance has no access to the assets required to run the WebMKS console. For more info please see the documentation.'))
  })
})
<|endoftext|>


<commit_before>// DO NOT EDIT THIS FILE!
// THIS FILE IS JUST A SAMPLE; TO APPLY, YOU MUST
//   CREATE A NEW FILE config.js AND ADD CONTENTS
//   SUCH AS SHOWN BELOW INTO THAT FILE.

/*globals svgEditor*/
/*
The config.js file is intended for the setting of configuration or
  preferences which must run early on; if this is not needed, it is
  recommended that you create an extension instead (for greater
  reusability and modularity).
*/

// CONFIG AND EXTENSION SETTING
/*
See defaultConfig and defaultExtensions in svg-editor.js for a list
  of possible configuration settings.

See svg-editor.js for documentation on using setConfig().
*/

// URL OVERRIDE CONFIG
svgEditor.setConfig({
	/**
	To override the ability for URLs to set URL-based SVG content,
	    uncomment the following:
	*/
	// preventURLContentLoading: true,
	/**
	To override the ability for URLs to set other configuration (including
	    extension config), uncomment the following:
	*/
	// preventAllURLConfig: true,
	/**
	To override the ability for URLs to set their own extensions,
	  uncomment the following (note that if setConfig() is used in
	  extension code, it will still be additive to extensions,
	  however):
	*/
	// lockExtensions: true,
});

svgEditor.setConfig({
	/*
	Provide default values here which differ from that of the editor but
		which the URL can override
	*/
}, {allowInitialUserOverride: true});

// EXTENSION CONFIG
svgEditor.setConfig({
	extensions: [
		// 'ext-overview_window.js', 'ext-markers.js', 'ext-connector.js', 'ext-eyedropper.js', 'ext-shapes.js', 'ext-imagelib.js', 'ext-grid.js', 'ext-polygon.js', 'ext-star.js', 'ext-panning.js', 'ext-storage.js'
	]
	// , noDefaultExtensions: false, // noDefaultExtensions can only be meaningfully used in config.js or in the URL
});

// OTHER CONFIG
svgEditor.setConfig({
	// canvasName: 'default',
	// canvas_expansion: 3,
	// initFill: {
		// color: 'FF0000', // solid red
		// opacity: 1
	// },
	// initStroke: {
		// width: 5,
		// color: '000000', // solid black
		// opacity: 1
	// },
	// initOpacity: 1,
	// colorPickerCSS: null,
	// initTool: 'select',
	// wireframe: false,
	// showlayers: false,
	// no_save_warning: false,
	// PATH CONFIGURATION
	// imgPath: 'images/',
	// langPath: 'locale/',
	// extPath: 'extensions/',
	// jGraduatePath: 'jgraduate/images/',
	/*
	Uncomment the following to allow at least same domain (embedded) access,
	including file:// access.
	Setting as `['*']` would allow any domain to access but would be unsafe to
	data privacy and integrity.
	*/
	// allowedOrigins: [window.location.origin || 'null'], // May be 'null' (as a string) when used as a file:// URL
	// DOCUMENT PROPERTIES
	// dimensions: [640, 480],
	// EDITOR OPTIONS
	// gridSnapping: false,
	// gridColor: '#000',
	// baseUnit: 'px',
	// snappingStep: 10,
	// showRulers: true,
	// EXTENSION-RELATED (GRID)
	// showGrid: false, // Set by ext-grid.js
	// EXTENSION-RELATED (STORAGE)
	// noStorageOnLoad: false, // Some interaction with ext-storage.js; prevent even the loading of previously saved local storage
	// forceStorage: false, // Some interaction with ext-storage.js; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not
	// emptyStorageOnDecline: true, // Used by ext-storage.js; empty any prior storage if the user declines to store
});

// PREF CHANGES
/**
setConfig() can also be used to set preferences in addition to
  configuration (see defaultPrefs in svg-editor.js for a list of
  possible settings), but at least if you are using ext-storage.js
  to store preferences, it will probably be better to let your
  users control these.
As with configuration, one may use allowInitialUserOverride, but
  in the case of preferences, any previously stored preferences
  will also thereby be enabled to override this setting (and at a
  higher priority than any URL preference setting overrides).
  Failing to use allowInitialUserOverride will ensure preferences
  are hard-coded here regardless of URL or prior user storage setting.
*/
svgEditor.setConfig(
	{
		// lang: '', // Set dynamically within locale.js if not previously set
		// iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise
		/**
		* When showing the preferences dialog, svg-editor.js currently relies
		* on curPrefs instead of $.pref, so allowing an override for bkgd_color
		* means that this value won't have priority over block auto-detection as
		* far as determining which color shows initially in the preferences
		* dialog (though it can be changed and saved).
		*/
		// bkgd_color: '#FFF',
		// bkgd_url: '',
		// img_save: 'embed',
		// Only shows in UI as far as alert notices
		// save_notice_done: false,
		// export_notice_done: false
	}
);
svgEditor.setConfig(
	{
		// Indicate pref settings here if you wish to allow user storage or URL settings
		//   to be able to override your default preferences (unless other config options
		//   have already explicitly prevented one or the other)
	},
	{allowInitialUserOverride: true}
);
<commit_msg>Indicate new config for exportWindowType<commit_after>// DO NOT EDIT THIS FILE!
// THIS FILE IS JUST A SAMPLE; TO APPLY, YOU MUST
//   CREATE A NEW FILE config.js AND ADD CONTENTS
//   SUCH AS SHOWN BELOW INTO THAT FILE.

/*globals svgEditor*/
/*
The config.js file is intended for the setting of configuration or
  preferences which must run early on; if this is not needed, it is
  recommended that you create an extension instead (for greater
  reusability and modularity).
*/

// CONFIG AND EXTENSION SETTING
/*
See defaultConfig and defaultExtensions in svg-editor.js for a list
  of possible configuration settings.

See svg-editor.js for documentation on using setConfig().
*/

// URL OVERRIDE CONFIG
svgEditor.setConfig({
	/**
	To override the ability for URLs to set URL-based SVG content,
	    uncomment the following:
	*/
	// preventURLContentLoading: true,
	/**
	To override the ability for URLs to set other configuration (including
	    extension config), uncomment the following:
	*/
	// preventAllURLConfig: true,
	/**
	To override the ability for URLs to set their own extensions,
	  uncomment the following (note that if setConfig() is used in
	  extension code, it will still be additive to extensions,
	  however):
	*/
	// lockExtensions: true,
});

svgEditor.setConfig({
	/*
	Provide default values here which differ from that of the editor but
		which the URL can override
	*/
}, {allowInitialUserOverride: true});

// EXTENSION CONFIG
svgEditor.setConfig({
	extensions: [
		// 'ext-overview_window.js', 'ext-markers.js', 'ext-connector.js', 'ext-eyedropper.js', 'ext-shapes.js', 'ext-imagelib.js', 'ext-grid.js', 'ext-polygon.js', 'ext-star.js', 'ext-panning.js', 'ext-storage.js'
	]
	// , noDefaultExtensions: false, // noDefaultExtensions can only be meaningfully used in config.js or in the URL
});

// OTHER CONFIG
svgEditor.setConfig({
	// canvasName: 'default',
	// canvas_expansion: 3,
	// initFill: {
		// color: 'FF0000', // solid red
		// opacity: 1
	// },
	// initStroke: {
		// width: 5,
		// color: '000000', // solid black
		// opacity: 1
	// },
	// initOpacity: 1,
	// colorPickerCSS: null,
	// initTool: 'select',
	// exportWindowType: 'new', // 'same'
	// wireframe: false,
	// showlayers: false,
	// no_save_warning: false,
	// PATH CONFIGURATION
	// imgPath: 'images/',
	// langPath: 'locale/',
	// extPath: 'extensions/',
	// jGraduatePath: 'jgraduate/images/',
	/*
	Uncomment the following to allow at least same domain (embedded) access,
	including file:// access.
	Setting as `['*']` would allow any domain to access but would be unsafe to
	data privacy and integrity.
	*/
	// allowedOrigins: [window.location.origin || 'null'], // May be 'null' (as a string) when used as a file:// URL
	// DOCUMENT PROPERTIES
	// dimensions: [640, 480],
	// EDITOR OPTIONS
	// gridSnapping: false,
	// gridColor: '#000',
	// baseUnit: 'px',
	// snappingStep: 10,
	// showRulers: true,
	// EXTENSION-RELATED (GRID)
	// showGrid: false, // Set by ext-grid.js
	// EXTENSION-RELATED (STORAGE)
	// noStorageOnLoad: false, // Some interaction with ext-storage.js; prevent even the loading of previously saved local storage
	// forceStorage: false, // Some interaction with ext-storage.js; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not
	// emptyStorageOnDecline: true, // Used by ext-storage.js; empty any prior storage if the user declines to store
});

// PREF CHANGES
/**
setConfig() can also be used to set preferences in addition to
  configuration (see defaultPrefs in svg-editor.js for a list of
  possible settings), but at least if you are using ext-storage.js
  to store preferences, it will probably be better to let your
  users control these.
As with configuration, one may use allowInitialUserOverride, but
  in the case of preferences, any previously stored preferences
  will also thereby be enabled to override this setting (and at a
  higher priority than any URL preference setting overrides).
  Failing to use allowInitialUserOverride will ensure preferences
  are hard-coded here regardless of URL or prior user storage setting.
*/
svgEditor.setConfig(
	{
		// lang: '', // Set dynamically within locale.js if not previously set
		// iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise
		/**
		* When showing the preferences dialog, svg-editor.js currently relies
		* on curPrefs instead of $.pref, so allowing an override for bkgd_color
		* means that this value won't have priority over block auto-detection as
		* far as determining which color shows initially in the preferences
		* dialog (though it can be changed and saved).
		*/
		// bkgd_color: '#FFF',
		// bkgd_url: '',
		// img_save: 'embed',
		// Only shows in UI as far as alert notices
		// save_notice_done: false,
		// export_notice_done: false
	}
);
svgEditor.setConfig(
	{
		// Indicate pref settings here if you wish to allow user storage or URL settings
		//   to be able to override your default preferences (unless other config options
		//   have already explicitly prevented one or the other)
	},
	{allowInitialUserOverride: true}
);
<|endoftext|>


<commit_before>exports = module.exports = function() {

    ////

    // Vektorumwandlung -  Winkel, Betrag -> Koordinaten
    var angleAbs2vector = function(a, s) {

        var x = Math.cos((a-90) * 0.0174532) * s;
        var y = Math.sin((a-90) * 0.0174532) * s;

        return {x:x, y:y};
    };

    // Vektorumwandlung - Koordinaten -> Winkel, Betrag
    var vector2angleAbs = function(x, y) {
        var s = Math.sqrt(x*x + y*y);

        var a = 0;
        if ( x >= 0) {
            a = Math.atan(y / x) / 0.0174532 + 90;
        } else {
            a = Math.atan(y / x) / 0.0174532 - 90;
        }

        return { s: s, a: angleInBoundaries(a) };
    };

    // Winkel in Grenzbereich bringen (0-359)
    var angleInBoundaries = function(a) {
        if (a >= 360) {
            a = a - 360;
        }
        if (a < 0) {
            a = a + 360;
        }
        return a;
    };

    // Winkel für Linien, die keine Richtung haben (0-179)
    var directionlessAngle = function(a) {

        a = angleInBoundaries(a);
        if(a >= 180 ) {
            a = a - 180;
        }

        return a;
    };

    // Abstand eines Punkts von einer Linie errechnen
    var distancePointFromPath = function(px, py, x1, y1, x2, y2) {
        var dx = x2 - x1; //j
        var dy = y2 - y1; //k

        var a = ( dx*(px-x1) + dy*(py-y1) ) / ( dx*dx + dy*dy );

        //Schnittpunkt
        var sx = x1 + a * dx;
        var sy = y1 + a * dy;

        //Abstand
        return { d: Math.sqrt( Math.pow(sx-px, 2) + Math.pow(sy-py, 2) ), sx: sx, sy };
    };

    // Weltobjekt updaten
    var updateObj = function(obj, statics, secselapsed) {

        if(obj) {

            var breakfaktor = 0; // Faktor um den durch eine Kollision gebremst wird
            var collided = false;

            // Kollision mit static finden
            if (obj.cr) {
                // dieses Object hat cr => kann kollidieren

                _.each(statics, function(static) {

                    if (static.cp) {
                        // dieser static hat cp => kann kollidieren

                        if(obj.x + obj.cr > static.x &&
                           obj.x - obj.cr < static.x + static.w &&
                           obj.y + obj.cr > static.y &&
                           obj.y - obj.cr < static.y + static.h) {

                            // in Reichweite dieses statics, checke ob pfade kollidieren
                            var clplist = [];
                            _.each(static.cp, function(cp) {

                                var dpp = distancePointFromPath(obj.x, obj.y, cp.x1 + static.x, cp.y1 + static.y, cp.x2 + static.x, cp.y2 + static.y);

                                if (dpp.d < obj.cr) {

                                    clplist.push({
                                        dpp: dpp,
                                        cp: cp
                                    });
                                }
                            });

                            if (clplist) {

                                // bei mehreren Kollisionen den mit dem grössten Winkelunterschied nehmen
                                var clp = _.max(clplist, function(clp) {
                                    var cp = clp.cp;
                                    var a = directionlessAngle(obj.ma) - cp.a;
                                    var b = cp.a - directionlessAngle(obj.ma);

                                    if (a < b) {
                                        return b;
                                    } else {
                                        return a;
                                    }
                                });

                                if (clp) {
                                    // !Kollision!

                                    // Einfallswinkel gleich Ausfallswinkel
                                    obj.ma = angleInBoundaries(2 * clp.a - obj.ma);

                                    //todo: Idee: Obj erst im RechtenWinkel verschieben

                                    breakfaktor = static.bf; //todo: checken ob notwendig
                                    collided = true; //todo: checken
                                }

                            }

                        }

                    }

                });
            }

            // Vektor aus aktueller Geschwindigkeit bestimmen
            var m = angleAbs2vector(obj.ma, obj.s);

            if (obj.type == "player") {

                var v = {x:0,y:0};

                var playervelocity = 5; //pro sek
                var playerrotationspeed = 3;
                var playermaxspeed = 200;

                if (obj.rturning) {
                    obj.va = angleInBoundaries(obj.va + playerrotationspeed);
                }
                if (obj.lturning) {
                    obj.va = angleInBoundaries(obj.va - playerrotationspeed);
                }

                if (!collided) { //nur wenn gerade nicht kollidiert wurde (tunneling verhindern)
                    if (obj.thrusting) {
                        v = angleAbs2vector(obj.va, playervelocity);
                    }
                    if (obj.breaking) {
                        v = angleAbs2vector( angleInBoundaries(obj.va-180), playervelocity);
                    }

                    if (obj.thrusting || obj.breaking) {
                        // Beschleunigungsvektor addieren (falls vorhanden)
                        m.x += v.x;
                        m.y += v.y;

                        // neue Werte im Objekt vermerken
                        var as = vector2angleAbs(m.x, m.y);

                        obj.s = as.s;
                        obj.ma = as.a;

                        if (obj.s > playermaxspeed) {
                            obj.s = playermaxspeed;
                        }
                    }
                }
            }

            // Änderung pro Sekunde
            obj.x += m.x * secselapsed;
            obj.y += m.y * secselapsed;

            // Geschwindigkeit durch Kollision verringern (nachdem das letzte Frame berechnet wurde)
            if (breakfaktor) {
                obj.s *= breakfaktor;
            }
        }
    };

    ////
    return {
        updateObj: updateObj,
        angleAbs2vector: angleAbs2vector,
        vector2angleAbs: vector2angleAbs,
        angleInBoundaries: angleInBoundaries,
        distancePointFromPath: distancePointFromPath,
        directionlessAngle: directionlessAngle
    };
};<commit_msg>fixes<commit_after>exports = module.exports = function() {

    ////

    // Vektorumwandlung -  Winkel, Betrag -> Koordinaten
    var angleAbs2vector = function(a, s) {

        var x = Math.cos((a-90) * 0.0174532) * s;
        var y = Math.sin((a-90) * 0.0174532) * s;

        return {x:x, y:y};
    };

    // Vektorumwandlung - Koordinaten -> Winkel, Betrag
    var vector2angleAbs = function(x, y) {
        var s = Math.sqrt(x*x + y*y);

        var a = 0;
        if ( x >= 0) {
            a = Math.atan(y / x) / 0.0174532 + 90;
        } else {
            a = Math.atan(y / x) / 0.0174532 - 90;
        }

        return { s: s, a: angleInBoundaries(a) };
    };

    // Winkel in Grenzbereich bringen (0-359)
    var angleInBoundaries = function(a) {
        if (a >= 360) {
            a = a - 360;
        }
        if (a < 0) {
            a = a + 360;
        }
        return a;
    };

    // Winkel für Linien, die keine Richtung haben (0-179)
    var directionlessAngle = function(a) {

        a = angleInBoundaries(a);
        if(a >= 180 ) {
            a = a - 180;
        }

        return a;
    };

    // Abstand eines Punkts von einer Linie errechnen
    var distancePointFromPath = function(px, py, x1, y1, x2, y2) {
        var dx = x2 - x1; //j
        var dy = y2 - y1; //k

        var a = ( dx*(px-x1) + dy*(py-y1) ) / ( dx*dx + dy*dy );

        //Schnittpunkt
        var sx = x1 + a * dx;
        var sy = y1 + a * dy;

        //Abstand
        var d = Math.sqrt( Math.pow(sx-px, 2) + Math.pow(sy-py, 2) );
        return { d: d, sx: sx, sy:sy };
    };

    // Weltobjekt updaten
    var updateObj = function(obj, statics, secselapsed) {

        if(obj) {

            var breakfaktor = 0; // Faktor um den durch eine Kollision gebremst wird
            var collided = false;

            // Kollision mit static finden
            if (obj.cr) {
                // dieses Object hat cr => kann kollidieren

                _.each(statics, function(static) {

                    if (static.cp) {
                        // dieser static hat cp => kann kollidieren

                        if(obj.x + obj.cr > static.x &&
                           obj.x - obj.cr < static.x + static.w &&
                           obj.y + obj.cr > static.y &&
                           obj.y - obj.cr < static.y + static.h) {

                            // in Reichweite dieses statics, checke ob pfade kollidieren
                            var clplist = [];
                            _.each(static.cp, function(cp) {

                                var dpp = distancePointFromPath(obj.x, obj.y, cp.x1 + static.x, cp.y1 + static.y, cp.x2 + static.x, cp.y2 + static.y);

                                if (dpp.d < obj.cr) {

                                    clplist.push({
                                        dpp: dpp,
                                        cp: cp
                                    });
                                }
                            });

                            if (clplist) {

                                // bei mehreren Kollisionen den mit dem grössten Winkelunterschied nehmen
                                var clp = _.max(clplist, function(clp) {
                                    var cp = clp.cp;
                                    var a = directionlessAngle(obj.ma) - cp.a;
                                    var b = cp.a - directionlessAngle(obj.ma);

                                    if (a < b) {
                                        return b;
                                    } else {
                                        return a;
                                    }
                                });

                                if (clp) {
                                    // !Kollision!

                                    // Einfallswinkel gleich Ausfallswinkel
                                    obj.ma = angleInBoundaries(2 * clp.cp.a - obj.ma);

                                    //todo: Idee: Obj erst im RechtenWinkel verschieben

                                    breakfaktor = static.bf; //todo: checken ob notwendig
                                    collided = true; //todo: checken
                                }

                            }

                        }

                    }

                });
            }

            // Vektor aus aktueller Geschwindigkeit bestimmen
            var m = angleAbs2vector(obj.ma, obj.s);

            if (obj.type == "player") {

                var v = {x:0,y:0};

                var playervelocity = 5; //pro sek
                var playerrotationspeed = 3;
                var playermaxspeed = 200;

                if (obj.rturning) {
                    obj.va = angleInBoundaries(obj.va + playerrotationspeed);
                }
                if (obj.lturning) {
                    obj.va = angleInBoundaries(obj.va - playerrotationspeed);
                }

                if (!collided) { //nur wenn gerade nicht kollidiert wurde (tunneling verhindern)
                    if (obj.thrusting) {
                        v = angleAbs2vector(obj.va, playervelocity);
                    }
                    if (obj.breaking) {
                        v = angleAbs2vector( angleInBoundaries(obj.va-180), playervelocity);
                    }

                    if (obj.thrusting || obj.breaking) {
                        // Beschleunigungsvektor addieren (falls vorhanden)
                        m.x += v.x;
                        m.y += v.y;

                        // neue Werte im Objekt vermerken
                        var as = vector2angleAbs(m.x, m.y);

                        obj.s = as.s;
                        obj.ma = as.a;

                        if (obj.s > playermaxspeed) {
                            obj.s = playermaxspeed;
                        }
                    }
                }
            }

            // Änderung pro Sekunde
            obj.x += m.x * secselapsed;
            obj.y += m.y * secselapsed;

            // Geschwindigkeit durch Kollision verringern (nachdem das letzte Frame berechnet wurde)
            if (breakfaktor) {
                obj.s *= breakfaktor;
            }
        }
    };

    ////
    return {
        updateObj: updateObj,
        angleAbs2vector: angleAbs2vector,
        vector2angleAbs: vector2angleAbs,
        angleInBoundaries: angleInBoundaries,
        distancePointFromPath: distancePointFromPath,
        directionlessAngle: directionlessAngle
    };
};<|endoftext|>


<commit_before>import { Settings } from "api"
import CustomDateInput from "components/CustomDateInput"
import * as FieldHelper from "components/FieldHelper"
import Fieldset from "components/Fieldset"
import LikertScale from "components/graphs/LikertScale"
import Model, {
  CUSTOM_FIELD_TYPE,
  createYupObjectShape
} from "components/Model"
import { Field, FieldArray } from "formik"
import { JSONPath } from "jsonpath-plus"
import _cloneDeep from "lodash/cloneDeep"
import _isEmpty from "lodash/isEmpty"
import _isEqualWith from "lodash/isEqualWith"
import _set from "lodash/set"
import _upperFirst from "lodash/upperFirst"
import moment from "moment"
import PropTypes from "prop-types"
import React, { useEffect, useMemo, useRef, useState } from "react"
import { Button, HelpBlock } from "react-bootstrap"
import REMOVE_ICON from "resources/delete.png"
import utils from "utils"

const WIDGETS = {
  likertScale: LikertScale
}
const RENDERERS = {}

const DEFAULT_CUSTOM_FIELDS_PREFIX = "formCustomFields"

const SpecialField = fieldProps => {
  const { widget, ...otherFieldProps } = fieldProps
  const SpecialFieldWidget = WIDGETS[widget]
  return (
    <Field
      component={FieldHelper.renderSpecialField}
      widget={<SpecialFieldWidget />}
      {...otherFieldProps}
    />
  )
}

const ReadonlySpecialField = fieldProps =>
  SpecialField({ ...fieldProps, readonly: true })

const TextField = fieldProps => {
  const { onChange, onBlur, ...otherFieldProps } = fieldProps
  return <Field component={FieldHelper.renderInputField} {...otherFieldProps} />
}

const ReadonlyTextField = fieldProps => {
  const { name, label } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
    />
  )
}

const DateField = fieldProps => {
  const { withTime, ...otherFieldProps } = fieldProps
  return (
    <Field
      component={FieldHelper.renderSpecialField}
      widget={<CustomDateInput withTime={withTime} />}
      {...otherFieldProps}
    />
  )
}

const ReadonlyDateField = fieldProps => {
  const { name, label, withTime } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
      humanValue={fieldVal =>
        fieldVal &&
        moment(fieldVal).format(
          withTime
            ? Settings.dateFormats.forms.displayShort.withTime
            : Settings.dateFormats.forms.displayShort.date
        )}
    />
  )
}

const DateTimeField = props => <DateField {...props} withTime />

const ReadonlyDateTimeField = props => <ReadonlyDateField {...props} withTime />

const EnumField = fieldProps => {
  const { choices, renderer, ...otherFieldProps } = fieldProps
  return (
    <Field
      buttons={FieldHelper.customEnumButtons(choices)}
      component={
        RENDERERS[renderer] || FieldHelper.renderRadioButtonToggleGroup
      }
      {...otherFieldProps}
    />
  )
}

const enumHumanValue = (choices, fieldVal) => {
  if (Array.isArray(fieldVal)) {
    return fieldVal && fieldVal.map(k => choices[k].label).join(", ")
  } else {
    return fieldVal && choices[fieldVal].label
  }
}

const ReadonlyEnumField = fieldProps => {
  const { name, label, choices } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
      humanValue={fieldVal => enumHumanValue(choices, fieldVal)}
    />
  )
}

const EnumSetField = fieldProps => {
  const { choices, renderer, ...otherFieldProps } = fieldProps
  return (
    <Field
      buttons={FieldHelper.customEnumButtons(choices)}
      component={
        RENDERERS[renderer] || FieldHelper.renderCheckboxButtonToggleGroup
      }
      {...otherFieldProps}
    />
  )
}

const ReadonlyEnumSetField = fieldProps => {
  const { name, label, choices } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
      humanValue={fieldVal => enumHumanValue(choices, fieldVal)}
    />
  )
}

const ArrayOfObjectsField = fieldProps => {
  const {
    name,
    fieldConfig,
    formikProps,
    invisibleFields,
    updateInvisibleFields
  } = fieldProps
  const nameKeys = name.split(".")
  const value = nameKeys.reduce(
    (v, key) => (v && v[key] ? v[key] : []),
    formikProps.values
  )
  const fieldsetTitle = fieldConfig.label || ""
  const addButtonLabel = fieldConfig.addButtonLabel || "Add a new item"
  const objSchema = createYupObjectShape(fieldConfig.objectFields)
  const objDefault = {}
  Model.fillObject(objDefault, objSchema)

  return (
    <Fieldset title={fieldsetTitle}>
      <FieldArray
        name={name}
        render={arrayHelpers => (

            <Button
              className="pull-right"
              onClick={() => addObject(objDefault, arrayHelpers)}
              bsStyle="primary"
              id="addObjectButton"
            >
              {addButtonLabel}
            </Button>
            {value.map((obj, index) =>
              renderArrayObject(
                name,
                fieldConfig,
                formikProps,
                invisibleFields,
                updateInvisibleFields,
                arrayHelpers,
                obj,
                index
              )
            )}

        )}
      />
    </Fieldset>
  )
}

const renderArrayObject = (
  fieldName,
  fieldConfig,
  formikProps,
  invisibleFields,
  updateInvisibleFields,
  arrayHelpers,
  obj,
  index
) => {
  const objLabel = _upperFirst(fieldConfig.objectLabel || "item")
  return (
    <Fieldset title={`${objLabel} ${index + 1}`} key={index}>
      <Button
        className="pull-right"
        title={`Remove this ${objLabel}`}
        onClick={() => arrayHelpers.remove(index)}
      >
        <img src={REMOVE_ICON} height={14} alt={`Remove this ${objLabel}`} />
      </Button>
      <CustomFields
        fieldsConfig={fieldConfig.objectFields}
        formikProps={formikProps}
        invisibleFields={invisibleFields}
        updateInvisibleFields={updateInvisibleFields}
        fieldNamePrefix={`${fieldName}.${index}`}
      />
    </Fieldset>
  )
}

const addObject = (objDefault, arrayHelpers) => {
  arrayHelpers.push(objDefault)
}

const ReadonlyArrayOfObjectsField = fieldProps => {
  const { name, fieldConfig, formikProps } = fieldProps
  const nameKeys = name.split(".")
  const value = nameKeys.reduce(
    (v, key) => (v && v[key] ? v[key] : []),
    formikProps.values
  )
  const fieldsetTitle = fieldConfig.label || ""
  return (
    <Fieldset title={fieldsetTitle}>
      <FieldArray
        name={name}
        render={arrayHelpers => (

            {value.map((obj, index) =>
              renderReadonlyArrayObject(name, fieldConfig, formikProps, index)
            )}

        )}
      />
    </Fieldset>
  )
}

const renderReadonlyArrayObject = (
  fieldName,
  fieldConfig,
  formikProps,
  index
) => {
  const objLabel = _upperFirst(fieldConfig.objectLabel || "item")
  return (
    <Fieldset title={`${objLabel} ${index + 1}`} key={index}>
      <ReadonlyCustomFields
        fieldsConfig={fieldConfig.objectFields}
        formikProps={formikProps}
        fieldNamePrefix={`${fieldName}.${index}`}
      />
    </Fieldset>
  )
}

const FIELD_COMPONENTS = {
  [CUSTOM_FIELD_TYPE.TEXT]: TextField,
  [CUSTOM_FIELD_TYPE.NUMBER]: TextField,
  [CUSTOM_FIELD_TYPE.DATE]: DateField,
  [CUSTOM_FIELD_TYPE.DATETIME]: DateTimeField,
  [CUSTOM_FIELD_TYPE.ENUM]: EnumField,
  [CUSTOM_FIELD_TYPE.ENUMSET]: EnumSetField,
  [CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS]: ArrayOfObjectsField,
  [CUSTOM_FIELD_TYPE.SPECIAL_FIELD]: SpecialField
}

function getInvisibleFields(
  invisibleFields,
  fieldsConfig,
  fieldNamePrefix,
  formikValues
) {
  const prevInvisibleFields = _cloneDeep(invisibleFields)
  const turnedInvisible = []
  const turnedVisible = []
  let curInvisibleFields = []
  Object.keys(fieldsConfig).forEach(key => {
    const fieldConfig = fieldsConfig[key]
    const fieldName = `${fieldNamePrefix}.${key}`
    const isVisible =
      !fieldConfig.visibleWhen ||
      (fieldConfig.visibleWhen &&
        !_isEmpty(JSONPath(fieldConfig.visibleWhen, formikValues)))
    if (!isVisible && !prevInvisibleFields.includes(fieldName)) {
      turnedInvisible.push(fieldName)
    } else if (isVisible && prevInvisibleFields.includes(fieldName)) {
      turnedVisible.push(fieldName)
    }
  })
  if (turnedVisible.length || turnedInvisible.length) {
    curInvisibleFields = prevInvisibleFields.filter(
      x => !turnedVisible.includes(x)
    )
    turnedInvisible.forEach(x => curInvisibleFields.push(x))
    return curInvisibleFields
  }
  return invisibleFields
}

export const CustomFieldsContainer = props => {
  const { fieldNamePrefix, formikProps } = props
  const [invisibleFields, setInvisibleFields] = useState([])
  const { setFieldValue } = formikProps
  const invisibleFieldsFieldName = `${fieldNamePrefix}.invisibleCustomFields`
  useEffect(() => {
    setFieldValue(invisibleFieldsFieldName, invisibleFields)
  }, [invisibleFieldsFieldName, invisibleFields, setFieldValue])

  return (
    <>
      <Field
        type="text"
        value=""
        name={invisibleFieldsFieldName}
        className="hidden"
      />
      <CustomFields
        invisibleFields={invisibleFields}
        updateInvisibleFields={setInvisibleFields}
        {...props}
      />
    </>
  )
}
CustomFieldsContainer.propTypes = {
  fieldsConfig: PropTypes.object,
  formikProps: PropTypes.object,
  fieldNamePrefix: PropTypes.string.isRequired
}
CustomFieldsContainer.defaultProps = {
  fieldNamePrefix: DEFAULT_CUSTOM_FIELDS_PREFIX
}

const CustomField = ({
  fieldConfig,
  fieldName,
  formikProps,
  invisibleFields,
  updateInvisibleFields
}) => {
  const {
    type,
    helpText,
    validations,
    visibleWhen,
    ...fieldProps
  } = fieldConfig
  const { setFieldValue } = formikProps
  const handleChange = useMemo(() => value => setFieldValue(fieldName, value), [
    setFieldValue,
    fieldName
  ])
  const FieldComponent = FIELD_COMPONENTS[type]
  const isVisible = !invisibleFields.includes(fieldName)
  let extraProps = {}
  if (type === CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS) {
    extraProps = {
      fieldConfig,
      formikProps,
      invisibleFields,
      updateInvisibleFields
    }
  }
  return (
    isVisible && (
      <FieldComponent
        name={fieldName}
        onChange={handleChange}
        {...fieldProps}
        {...extraProps}
      >
        {helpText && (
          <HelpBlock>
            {helpText}
          </HelpBlock>
        )}
      </FieldComponent>
    )
  )
}

const CustomFields = ({
  fieldsConfig,
  formikProps,
  fieldNamePrefix,
  invisibleFields,
  updateInvisibleFields
}) => {
  const formikValues = formikProps.values

  const latestInvisibleFieldsProp = useRef(invisibleFields)
  const invisibleFieldsPropUnchanged = _isEqualWith(
    latestInvisibleFieldsProp.current,
    invisibleFields,
    utils.treatFunctionsAsEqual
  )

  const curInvisibleFields = useMemo(
    () =>
      getInvisibleFields(
        invisibleFields,
        fieldsConfig,
        fieldNamePrefix,
        formikValues
      ),
    [invisibleFields, fieldsConfig, fieldNamePrefix, formikValues]
  )
  const invisibleFieldsUnchanged = _isEqualWith(
    latestInvisibleFieldsProp.current,
    curInvisibleFields,
    utils.treatFunctionsAsEqual
  )

  useEffect(() => {
    if (!invisibleFieldsPropUnchanged) {
      latestInvisibleFieldsProp.current = invisibleFields
    }
  }, [invisibleFieldsPropUnchanged, invisibleFields])

  useEffect(() => {
    if (!invisibleFieldsUnchanged) {
      updateInvisibleFields(curInvisibleFields)
    }
  }, [invisibleFieldsUnchanged, curInvisibleFields, updateInvisibleFields])

  return (
    <>
      {Object.keys(fieldsConfig).map(key => {
        const fieldConfig = fieldsConfig[key]
        return (
          <CustomField
            key={key}
            fieldConfig={fieldConfig}
            fieldName={`${fieldNamePrefix}.${key}`}
            formikProps={formikProps}
            invisibleFields={invisibleFields}
            updateInvisibleFields={updateInvisibleFields}
          />
        )
      })}
    </>
  )
}
CustomFields.propTypes = {
  fieldsConfig: PropTypes.object,
  formikProps: PropTypes.object,
  fieldNamePrefix: PropTypes.string.isRequired,
  invisibleFields: PropTypes.array,
  updateInvisibleFields: PropTypes.func
}
CustomFields.defaultProps = {
  fieldNamePrefix: DEFAULT_CUSTOM_FIELDS_PREFIX
}

const READONLY_FIELD_COMPONENTS = {
  [CUSTOM_FIELD_TYPE.TEXT]: ReadonlyTextField,
  [CUSTOM_FIELD_TYPE.NUMBER]: ReadonlyTextField,
  [CUSTOM_FIELD_TYPE.DATE]: ReadonlyDateField,
  [CUSTOM_FIELD_TYPE.DATETIME]: ReadonlyDateTimeField,
  [CUSTOM_FIELD_TYPE.ENUM]: ReadonlyEnumField,
  [CUSTOM_FIELD_TYPE.ENUMSET]: ReadonlyEnumSetField,
  [CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS]: ReadonlyArrayOfObjectsField,
  [CUSTOM_FIELD_TYPE.SPECIAL_FIELD]: ReadonlySpecialField
}

export const ReadonlyCustomFields = ({
  fieldsConfig,
  formikProps,
  fieldNamePrefix
}) => {
  return (
    <>
      {Object.keys(fieldsConfig).map(key => {
        const fieldConfig = fieldsConfig[key]
        const {
          type,
          placeholder,
          helpText,
          validations,
          visibleWhen,
          objectFields,
          ...fieldProps
        } = fieldConfig
        let extraProps = {}
        if (type === CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS) {
          extraProps = {
            fieldConfig: fieldConfig,
            formikProps: formikProps
          }
        }
        const FieldComponent = READONLY_FIELD_COMPONENTS[type]
        return (
          <FieldComponent
            key={key}
            name={`${fieldNamePrefix}.${key}`}
            {...fieldProps}
            {...extraProps}
          />
        )
      })}
    </>
  )
}
ReadonlyCustomFields.propTypes = {
  fieldsConfig: PropTypes.object,
  formikProps: PropTypes.object,
  fieldNamePrefix: PropTypes.string.isRequired
}
ReadonlyCustomFields.defaultProps = {
  fieldNamePrefix: DEFAULT_CUSTOM_FIELDS_PREFIX
}

// customFields should contain the JSON of all the visible custom fields
export const customFieldsJSONString = values => {
  const clonedValues = _cloneDeep(values)
  if (values.formCustomFields.invisibleCustomFields) {
    clonedValues.formCustomFields.invisibleCustomFields.forEach(f => {
      _set(clonedValues, f.split("."), undefined)
    })
  }
  return JSON.stringify(clonedValues.formCustomFields)
}
<commit_msg>NCI-Agency/anet#2352: Fix warning about date component not having an id<commit_after>import { Settings } from "api"
import CustomDateInput from "components/CustomDateInput"
import * as FieldHelper from "components/FieldHelper"
import Fieldset from "components/Fieldset"
import LikertScale from "components/graphs/LikertScale"
import Model, {
  CUSTOM_FIELD_TYPE,
  createYupObjectShape
} from "components/Model"
import { Field, FieldArray } from "formik"
import { JSONPath } from "jsonpath-plus"
import _cloneDeep from "lodash/cloneDeep"
import _isEmpty from "lodash/isEmpty"
import _isEqualWith from "lodash/isEqualWith"
import _set from "lodash/set"
import _upperFirst from "lodash/upperFirst"
import moment from "moment"
import PropTypes from "prop-types"
import React, { useEffect, useMemo, useRef, useState } from "react"
import { Button, HelpBlock } from "react-bootstrap"
import REMOVE_ICON from "resources/delete.png"
import utils from "utils"

const WIDGETS = {
  likertScale: LikertScale
}
const RENDERERS = {}

const DEFAULT_CUSTOM_FIELDS_PREFIX = "formCustomFields"

const SpecialField = fieldProps => {
  const { widget, ...otherFieldProps } = fieldProps
  const SpecialFieldWidget = WIDGETS[widget]
  return (
    <Field
      component={FieldHelper.renderSpecialField}
      widget={<SpecialFieldWidget />}
      {...otherFieldProps}
    />
  )
}

const ReadonlySpecialField = fieldProps =>
  SpecialField({ ...fieldProps, readonly: true })

const TextField = fieldProps => {
  const { onChange, onBlur, ...otherFieldProps } = fieldProps
  return <Field component={FieldHelper.renderInputField} {...otherFieldProps} />
}

const ReadonlyTextField = fieldProps => {
  const { name, label } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
    />
  )
}

const DateField = fieldProps => {
  const { name, withTime, ...otherFieldProps } = fieldProps
  return (
    <Field
      name={name}
      component={FieldHelper.renderSpecialField}
      widget={<CustomDateInput id={name} withTime={withTime} />}
      {...otherFieldProps}
    />
  )
}

const ReadonlyDateField = fieldProps => {
  const { name, label, withTime } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
      humanValue={fieldVal =>
        fieldVal &&
        moment(fieldVal).format(
          withTime
            ? Settings.dateFormats.forms.displayShort.withTime
            : Settings.dateFormats.forms.displayShort.date
        )}
    />
  )
}

const DateTimeField = props => <DateField {...props} withTime />

const ReadonlyDateTimeField = props => <ReadonlyDateField {...props} withTime />

const EnumField = fieldProps => {
  const { choices, renderer, ...otherFieldProps } = fieldProps
  return (
    <Field
      buttons={FieldHelper.customEnumButtons(choices)}
      component={
        RENDERERS[renderer] || FieldHelper.renderRadioButtonToggleGroup
      }
      {...otherFieldProps}
    />
  )
}

const enumHumanValue = (choices, fieldVal) => {
  if (Array.isArray(fieldVal)) {
    return fieldVal && fieldVal.map(k => choices[k].label).join(", ")
  } else {
    return fieldVal && choices[fieldVal].label
  }
}

const ReadonlyEnumField = fieldProps => {
  const { name, label, choices } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
      humanValue={fieldVal => enumHumanValue(choices, fieldVal)}
    />
  )
}

const EnumSetField = fieldProps => {
  const { choices, renderer, ...otherFieldProps } = fieldProps
  return (
    <Field
      buttons={FieldHelper.customEnumButtons(choices)}
      component={
        RENDERERS[renderer] || FieldHelper.renderCheckboxButtonToggleGroup
      }
      {...otherFieldProps}
    />
  )
}

const ReadonlyEnumSetField = fieldProps => {
  const { name, label, choices } = fieldProps
  return (
    <Field
      name={name}
      label={label}
      component={FieldHelper.renderReadonlyField}
      humanValue={fieldVal => enumHumanValue(choices, fieldVal)}
    />
  )
}

const ArrayOfObjectsField = fieldProps => {
  const {
    name,
    fieldConfig,
    formikProps,
    invisibleFields,
    updateInvisibleFields
  } = fieldProps
  const nameKeys = name.split(".")
  const value = nameKeys.reduce(
    (v, key) => (v && v[key] ? v[key] : []),
    formikProps.values
  )
  const fieldsetTitle = fieldConfig.label || ""
  const addButtonLabel = fieldConfig.addButtonLabel || "Add a new item"
  const objSchema = createYupObjectShape(fieldConfig.objectFields)
  const objDefault = {}
  Model.fillObject(objDefault, objSchema)

  return (
    <Fieldset title={fieldsetTitle}>
      <FieldArray
        name={name}
        render={arrayHelpers => (

            <Button
              className="pull-right"
              onClick={() => addObject(objDefault, arrayHelpers)}
              bsStyle="primary"
              id="addObjectButton"
            >
              {addButtonLabel}
            </Button>
            {value.map((obj, index) =>
              renderArrayObject(
                name,
                fieldConfig,
                formikProps,
                invisibleFields,
                updateInvisibleFields,
                arrayHelpers,
                obj,
                index
              )
            )}

        )}
      />
    </Fieldset>
  )
}

const renderArrayObject = (
  fieldName,
  fieldConfig,
  formikProps,
  invisibleFields,
  updateInvisibleFields,
  arrayHelpers,
  obj,
  index
) => {
  const objLabel = _upperFirst(fieldConfig.objectLabel || "item")
  return (
    <Fieldset title={`${objLabel} ${index + 1}`} key={index}>
      <Button
        className="pull-right"
        title={`Remove this ${objLabel}`}
        onClick={() => arrayHelpers.remove(index)}
      >
        <img src={REMOVE_ICON} height={14} alt={`Remove this ${objLabel}`} />
      </Button>
      <CustomFields
        fieldsConfig={fieldConfig.objectFields}
        formikProps={formikProps}
        invisibleFields={invisibleFields}
        updateInvisibleFields={updateInvisibleFields}
        fieldNamePrefix={`${fieldName}.${index}`}
      />
    </Fieldset>
  )
}

const addObject = (objDefault, arrayHelpers) => {
  arrayHelpers.push(objDefault)
}

const ReadonlyArrayOfObjectsField = fieldProps => {
  const { name, fieldConfig, formikProps } = fieldProps
  const nameKeys = name.split(".")
  const value = nameKeys.reduce(
    (v, key) => (v && v[key] ? v[key] : []),
    formikProps.values
  )
  const fieldsetTitle = fieldConfig.label || ""
  return (
    <Fieldset title={fieldsetTitle}>
      <FieldArray
        name={name}
        render={arrayHelpers => (

            {value.map((obj, index) =>
              renderReadonlyArrayObject(name, fieldConfig, formikProps, index)
            )}

        )}
      />
    </Fieldset>
  )
}

const renderReadonlyArrayObject = (
  fieldName,
  fieldConfig,
  formikProps,
  index
) => {
  const objLabel = _upperFirst(fieldConfig.objectLabel || "item")
  return (
    <Fieldset title={`${objLabel} ${index + 1}`} key={index}>
      <ReadonlyCustomFields
        fieldsConfig={fieldConfig.objectFields}
        formikProps={formikProps}
        fieldNamePrefix={`${fieldName}.${index}`}
      />
    </Fieldset>
  )
}

const FIELD_COMPONENTS = {
  [CUSTOM_FIELD_TYPE.TEXT]: TextField,
  [CUSTOM_FIELD_TYPE.NUMBER]: TextField,
  [CUSTOM_FIELD_TYPE.DATE]: DateField,
  [CUSTOM_FIELD_TYPE.DATETIME]: DateTimeField,
  [CUSTOM_FIELD_TYPE.ENUM]: EnumField,
  [CUSTOM_FIELD_TYPE.ENUMSET]: EnumSetField,
  [CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS]: ArrayOfObjectsField,
  [CUSTOM_FIELD_TYPE.SPECIAL_FIELD]: SpecialField
}

function getInvisibleFields(
  invisibleFields,
  fieldsConfig,
  fieldNamePrefix,
  formikValues
) {
  const prevInvisibleFields = _cloneDeep(invisibleFields)
  const turnedInvisible = []
  const turnedVisible = []
  let curInvisibleFields = []
  Object.keys(fieldsConfig).forEach(key => {
    const fieldConfig = fieldsConfig[key]
    const fieldName = `${fieldNamePrefix}.${key}`
    const isVisible =
      !fieldConfig.visibleWhen ||
      (fieldConfig.visibleWhen &&
        !_isEmpty(JSONPath(fieldConfig.visibleWhen, formikValues)))
    if (!isVisible && !prevInvisibleFields.includes(fieldName)) {
      turnedInvisible.push(fieldName)
    } else if (isVisible && prevInvisibleFields.includes(fieldName)) {
      turnedVisible.push(fieldName)
    }
  })
  if (turnedVisible.length || turnedInvisible.length) {
    curInvisibleFields = prevInvisibleFields.filter(
      x => !turnedVisible.includes(x)
    )
    turnedInvisible.forEach(x => curInvisibleFields.push(x))
    return curInvisibleFields
  }
  return invisibleFields
}

export const CustomFieldsContainer = props => {
  const { fieldNamePrefix, formikProps } = props
  const [invisibleFields, setInvisibleFields] = useState([])
  const { setFieldValue } = formikProps
  const invisibleFieldsFieldName = `${fieldNamePrefix}.invisibleCustomFields`
  useEffect(() => {
    setFieldValue(invisibleFieldsFieldName, invisibleFields)
  }, [invisibleFieldsFieldName, invisibleFields, setFieldValue])

  return (
    <>
      <Field
        type="text"
        value=""
        name={invisibleFieldsFieldName}
        className="hidden"
      />
      <CustomFields
        invisibleFields={invisibleFields}
        updateInvisibleFields={setInvisibleFields}
        {...props}
      />
    </>
  )
}
CustomFieldsContainer.propTypes = {
  fieldsConfig: PropTypes.object,
  formikProps: PropTypes.object,
  fieldNamePrefix: PropTypes.string.isRequired
}
CustomFieldsContainer.defaultProps = {
  fieldNamePrefix: DEFAULT_CUSTOM_FIELDS_PREFIX
}

const CustomField = ({
  fieldConfig,
  fieldName,
  formikProps,
  invisibleFields,
  updateInvisibleFields
}) => {
  const {
    type,
    helpText,
    validations,
    visibleWhen,
    ...fieldProps
  } = fieldConfig
  const { setFieldValue } = formikProps
  const handleChange = useMemo(() => value => setFieldValue(fieldName, value), [
    setFieldValue,
    fieldName
  ])
  const FieldComponent = FIELD_COMPONENTS[type]
  const isVisible = !invisibleFields.includes(fieldName)
  let extraProps = {}
  if (type === CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS) {
    extraProps = {
      fieldConfig,
      formikProps,
      invisibleFields,
      updateInvisibleFields
    }
  }
  return (
    isVisible && (
      <FieldComponent
        name={fieldName}
        onChange={handleChange}
        {...fieldProps}
        {...extraProps}
      >
        {helpText && (
          <HelpBlock>
            {helpText}
          </HelpBlock>
        )}
      </FieldComponent>
    )
  )
}

const CustomFields = ({
  fieldsConfig,
  formikProps,
  fieldNamePrefix,
  invisibleFields,
  updateInvisibleFields
}) => {
  const formikValues = formikProps.values

  const latestInvisibleFieldsProp = useRef(invisibleFields)
  const invisibleFieldsPropUnchanged = _isEqualWith(
    latestInvisibleFieldsProp.current,
    invisibleFields,
    utils.treatFunctionsAsEqual
  )

  const curInvisibleFields = useMemo(
    () =>
      getInvisibleFields(
        invisibleFields,
        fieldsConfig,
        fieldNamePrefix,
        formikValues
      ),
    [invisibleFields, fieldsConfig, fieldNamePrefix, formikValues]
  )
  const invisibleFieldsUnchanged = _isEqualWith(
    latestInvisibleFieldsProp.current,
    curInvisibleFields,
    utils.treatFunctionsAsEqual
  )

  useEffect(() => {
    if (!invisibleFieldsPropUnchanged) {
      latestInvisibleFieldsProp.current = invisibleFields
    }
  }, [invisibleFieldsPropUnchanged, invisibleFields])

  useEffect(() => {
    if (!invisibleFieldsUnchanged) {
      updateInvisibleFields(curInvisibleFields)
    }
  }, [invisibleFieldsUnchanged, curInvisibleFields, updateInvisibleFields])

  return (
    <>
      {Object.keys(fieldsConfig).map(key => {
        const fieldConfig = fieldsConfig[key]
        return (
          <CustomField
            key={key}
            fieldConfig={fieldConfig}
            fieldName={`${fieldNamePrefix}.${key}`}
            formikProps={formikProps}
            invisibleFields={invisibleFields}
            updateInvisibleFields={updateInvisibleFields}
          />
        )
      })}
    </>
  )
}
CustomFields.propTypes = {
  fieldsConfig: PropTypes.object,
  formikProps: PropTypes.object,
  fieldNamePrefix: PropTypes.string.isRequired,
  invisibleFields: PropTypes.array,
  updateInvisibleFields: PropTypes.func
}
CustomFields.defaultProps = {
  fieldNamePrefix: DEFAULT_CUSTOM_FIELDS_PREFIX
}

const READONLY_FIELD_COMPONENTS = {
  [CUSTOM_FIELD_TYPE.TEXT]: ReadonlyTextField,
  [CUSTOM_FIELD_TYPE.NUMBER]: ReadonlyTextField,
  [CUSTOM_FIELD_TYPE.DATE]: ReadonlyDateField,
  [CUSTOM_FIELD_TYPE.DATETIME]: ReadonlyDateTimeField,
  [CUSTOM_FIELD_TYPE.ENUM]: ReadonlyEnumField,
  [CUSTOM_FIELD_TYPE.ENUMSET]: ReadonlyEnumSetField,
  [CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS]: ReadonlyArrayOfObjectsField,
  [CUSTOM_FIELD_TYPE.SPECIAL_FIELD]: ReadonlySpecialField
}

export const ReadonlyCustomFields = ({
  fieldsConfig,
  formikProps,
  fieldNamePrefix
}) => {
  return (
    <>
      {Object.keys(fieldsConfig).map(key => {
        const fieldConfig = fieldsConfig[key]
        const {
          type,
          placeholder,
          helpText,
          validations,
          visibleWhen,
          objectFields,
          ...fieldProps
        } = fieldConfig
        let extraProps = {}
        if (type === CUSTOM_FIELD_TYPE.ARRAY_OF_OBJECTS) {
          extraProps = {
            fieldConfig: fieldConfig,
            formikProps: formikProps
          }
        }
        const FieldComponent = READONLY_FIELD_COMPONENTS[type]
        return (
          <FieldComponent
            key={key}
            name={`${fieldNamePrefix}.${key}`}
            {...fieldProps}
            {...extraProps}
          />
        )
      })}
    </>
  )
}
ReadonlyCustomFields.propTypes = {
  fieldsConfig: PropTypes.object,
  formikProps: PropTypes.object,
  fieldNamePrefix: PropTypes.string.isRequired
}
ReadonlyCustomFields.defaultProps = {
  fieldNamePrefix: DEFAULT_CUSTOM_FIELDS_PREFIX
}

// customFields should contain the JSON of all the visible custom fields
export const customFieldsJSONString = values => {
  const clonedValues = _cloneDeep(values)
  if (values.formCustomFields.invisibleCustomFields) {
    clonedValues.formCustomFields.invisibleCustomFields.forEach(f => {
      _set(clonedValues, f.split("."), undefined)
    })
  }
  return JSON.stringify(clonedValues.formCustomFields)
}
<|endoftext|>