<commit_before>/* eslint-disable no-console */

/**
 * This code allows to create complex form in the twig module
 * You need in place modification
 *
 * In the twig templat eyou will have something like:
 *
 *
 *   And the template will be like:
 *
 *

 *

 *
 *           Kind
 *           Firstname
 *           Lastname
 *           Nationalities
 *

 *

 *
 *               <select data-field='kind'>
 *                   <option value=''></option>
 *                   <option value='author'>Author</option>
 *                   <option value='editor'>Editor</option>
 *              </select>
 *
 *
 *               <input type='text' size=10 data-field='firstname'>
 *
 *
 *               <input type='text' size=10 data-field='lastname'>
 *
 *
 *

 *

 *
 *                           <input placeholder="Nationality" type='text' size=10 data-field=''>
 *
 *

 *

 *
 *

 *

 */

define(['jquery', 'src/util/api', 'modules/modulefactory'], function (
  $,
  API,
  Module
) {
  function AdvancedForm(divID, options = {}) {
    // we will find automatically the variableName
    var moduleId = $(`#${divID}`)
      .closest('[data-module-id]')
      .attr('data-module-id');
    var module = Module.getModules().find(
      (m) => `${m.getId()}` === `${moduleId}`
    );
    var ips = module.vars_in().filter((v) => v.rel === 'form');
    if (ips.length === 0) {
      throw new Error(
        'The twig module does not have variable in of type "form"'
      );
    }
    var variableName = ips[0].name;
    var data;

    if (options.debug) console.log('variableName:', variableName);

    var variable = API.getVar(variableName);
    variable.listen(
      {
        getId() {
          return moduleId + variableName;
        }
      },
      function (newData) {
        newData.currentPromise.then(() => {
          if (options.debug) console.log('receive newData');
          if (!data) {
            if (options.debug) {
              console.log(
                'The variable',
                variableName,
                'does not exist yet. We will load it.'
              );
            }
            data = API.getData(variableName);
            updateTwig();
          }
        });
      }
    );

    // we will initialise the form
    var dom = $(document.getElementById(divID));
    if (options.debug) {
      console.log('Initialize the form');
    }
    // Add the buttons ADD / REMOVE
    dom.find('[data-repeat]').prepend(`

            `);

    if (!data && API.getData(variableName)) {
      data = API.getData(variableName);
      updateTwig();
    }

    // Add the style
    dom.parent().prepend(
      ``
    );

    function handleDataRepeat(index, row) {
      row = $(row);
      var jpath = getJpath(row);
      var variable = data ? data.getChildSync(jpath) : '';
      var table = row.closest('table');
      var length = 0;
      var empty = false;
      if (!variable || variable.length === 0) {
        length = 1;
        empty = true;
      } else if (Array.isArray(variable)) {
        length = variable.length;
      } else {
        console.log('Wrong variable type', variable);
      }
      for (var i = 0; i < length; i++) {
        var currentRow;
        if (i === 0) {
          currentRow = row;
        } else {
          currentRow = row.clone();
          table.append(currentRow);
        }
        currentRow.attr('data-index', i);
        renameRow(currentRow, jpath, i, empty);
      }
      rename(table);
    }

    // need to replicate rows based on the external variable
    function updateTwig() {
      if (options.debug) console.log('Update twig');
      do {
        var elements = dom.find('[data-repeat]:not([data-index])');
        elements.each(handleDataRepeat);
      } while (elements.length > 0);

      // we force the incorporation of the data in the form
      if (data && module.view.formObject) {
        if (options.debug) console.log('FORCE update data');
        module.view.fillForm(true);
      }
    }

    // rename the attributes 'name' or 'name-empty' of one specific row based on the jpath
    function renameRow(row, jpath, rowIndex, empty) {
      row = $(row);
      row
        .children('td:not(:has(table))')
        .find('[data-field]')
        .each(function (index, element) {
          element = $(element);
          var name = jpath.join('.');
          if (name) name += '.';
          name += rowIndex;
          var attr = element.attr('data-field');
          if (attr) name += `.${attr}`;
          if (empty) {
            element.attr('name-empty', name);
          } else {
            element.attr('name', name);
          }
        });
    }

    // get the jpath from one element based on the attributes 'data-repeat' and 'data-index'
    // the jpath is returned as an array
    function getJpath(element) {
      var jpath = [];
      while ($(element).length) {
        if ($(element).attr('data-index')) {
          jpath.unshift($(element).attr('data-index'));
        }
        var repeatName = $(element).attr('data-repeat');
        if (repeatName) jpath.unshift(...repeatName.split('.'));
        element = $(element)
          .parent()
          .closest('[data-repeat]');
      }
      return jpath;
    }

    /*
             Rename the the full table
             */
    function rename(tbody) {
      var base = getBase(tbody).base;
      var search = new RegExp(`${base}.[0-9]+`);
      var rows = tbody.children('tr:has(td)');
      rows.each(function (rowIndex, row) {
        var replace = `${base}.${rowIndex}`;
        for (var attr of ['name', 'name-empty']) {
          $(row)
            .find(`[${attr}]`)
            .each(
              // eslint-disable-next-line no-loop-func
              function (index, element) {
                element = $(element);
                var name = element.attr(attr);
                name = name.replace(search, replace);
                element.attr(attr, name);
              }
            );
        }
      });
    }

    function getBase(element) {
      var names = [];
      element.find('[name]').each(function (index, element) {
        names.push($(element).attr('name'));
      });
      if (names.length === 0) {
        return '';
      }
      names.sort();
      return {
        base: names[0].replace(/(.*)\.([0-9]+).*/, '$1'),
        index: names[0].replace(/(.*)\.([0-9]+).*/, '$2')
      };
    }

    if (options.debug) {
      document
        .getElementById(divID)
        .addEventListener('mouseover', function (event) {
          var target = $(event.target);
          if (target.attr('name')) {
            console.log('Name', target.attr('name'));
          }
        });
      document
        .getElementById(divID)
        .addEventListener('mouseover', function (event) {
          var target = $(event.target);
          if (target.attr('name-empty')) {
            console.log('Empty', target.attr('name'));
          }
        });
    }

    function changeInputFct(event) {
      var target = $(event.target);
      if (target.attr('name-empty')) {
        var empties = target
          .closest('tr')
          .children('td:not(:has(table))')
          .find('[name-empty]');
        empties.each((index, element) => {
          $(element).attr('name', $(element).attr('name-empty'));
          $(element).removeAttr('name-empty');
        });
      }
    }
    // when the value of a row change we should rename property if it was hidden
    document.getElementById(divID).addEventListener('change', changeInputFct);
    document.getElementById(divID).addEventListener('input', changeInputFct);

    document.getElementById(divID).addEventListener('click', function (event) {
      var from = event.target;
      var table = $(from).closest('tbody');
      var tr = $(from).closest('tr');
      switch (from.className) {
        case 'form-button addRow':
        case 'addRow':
          // if we try to add a row we should check if
          // there is already an empty one
          var empties = table
            .children('tr')
            .children('td:not(:has(table))')
            .find('[name-empty]');
          if (empties.length > 0) {
            empties[0].focus();
            return;
          }
          var clone = tr.clone();
          clone.find('select, input, textarea').val('');
          clone.find('tr:not(:first-child)').remove();
          var fields = clone.find('[name]');
          // rename attribute 'name' to 'name-empty'
          fields.each((index, element) => {
            $(element).attr('name-empty', $(element).attr('name'));
            $(element).removeAttr('name');
          });
          table.append(clone);
          clone.find('[name-empty]')[0].focus();
          rename(table);
          break;
        case 'form-button removeRow':
        case 'removeRow':
          var base = getBase(tr);
          if (base) {
            var jpath = base.base.split('.');
            var data = API.getData(variableName);
            var variable = data.getChildSync(jpath);
            variable.splice(base.index, 1);
            data.triggerChange();
          }
          // need to throw an event to remove this entry
          if (table.children('tr:has(td)').length > 1) {
            tr.remove();
          } else {
            tr.find('select, input, textarea').val('');
            tr.find('tr:not(:first-child)').remove();
            tr.find('[name]').each((index, element) => {
              $(element).attr('name-empty', $(element).attr('name'));
              $(element).removeAttr('name');
            });
            tr.find('[name-empty]')[0].focus();
          }
          rename(table);
          break;
        case '':
          break;
        default:
          throw new Error(`Unexpected class name: ${from.className}`);
      }
    });
  }
  return AdvancedForm;
});
<commit_msg>fix problem with twigAdvancedForm<commit_after>/* eslint-disable no-console */

/**
 * This code allows to create complex form in the twig module
 * You need in place modification
 *
 * In the twig templat eyou will have something like:
 *
 *
 *   And the template will be like:
 *
 *

 *

 *
 *           Kind
 *           Firstname
 *           Lastname
 *           Nationalities
 *

 *

 *
 *               <select data-field='kind'>
 *                   <option value=''></option>
 *                   <option value='author'>Author</option>
 *                   <option value='editor'>Editor</option>
 *              </select>
 *
 *
 *               <input type='text' size=10 data-field='firstname'>
 *
 *
 *               <input type='text' size=10 data-field='lastname'>
 *
 *
 *

 *

 *
 *                           <input placeholder="Nationality" type='text' size=10 data-field=''>
 *
 *

 *

 *
 *

 *

 */

define(['jquery', 'src/util/api', 'modules/modulefactory'], function (
  $,
  API,
  Module
) {
  console.log('START');

  function AdvancedForm(divID, options = {}) {
    // we will find automatically the variableName
    if (options.debug) console.log('CREATE ADVANCED FORM');

    var moduleId = $(`#${divID}`)
      .closest('[data-module-id]')
      .attr('data-module-id');
    var module = Module.getModules().find(
      (m) => `${m.getId()}` === `${moduleId}`
    );
    var ips = module.vars_in().filter((v) => v.rel === 'form');
    if (ips.length === 0) {
      throw new Error(
        'The twig module does not have variable in of type "form"'
      );
    }
    var variableName = ips[0].name;
    var data;

    if (options.debug) console.log('variableName:', variableName);

    var variable = API.getVar(variableName);
    variable.listen(
      {
        getId() {
          return moduleId + variableName;
        }
      },
      function (newData) {
        newData.currentPromise.then(() => {
          if (options.debug) console.log('receive newData', newData);
          if (!data) {
            if (options.debug) {
              console.log(
                'The variable',
                variableName,
                'does not exist yet. We will load it.'
              );
            }
            data = API.getData(variableName);
            updateTwig();
          }
        });
      }
    );

    // we will initialise the form
    var dom = $(document.getElementById(divID));
    if (options.debug) {
      console.log('Initialize the form');
    }
    // Add the buttons ADD / REMOVE
    let rows = dom.find('[data-repeat]:not([class="form-button addRow"])');
    if (rows) {
      rows = rows.filter(function (index) {
        return !this.innerHTML.includes('form-button addRow');
      });
      rows.prepend(`

              `);
    }

    if (!data && API.getData(variableName)) {
      data = API.getData(variableName);
      updateTwig();
    }

    // Add the style
    dom.parent().prepend(
      ``
    );

    function handleDataRepeat(index, row) {
      row = $(row);
      var jpath = getJpath(row);
      var variable = data ? data.getChildSync(jpath) : '';
      var table = row.closest('table');
      var length = 0;
      var empty = false;
      if (!variable || variable.length === 0) {
        length = 1;
        empty = true;
      } else if (Array.isArray(variable)) {
        length = variable.length;
      } else {
        console.log('Wrong variable type', variable);
      }
      for (var i = 0; i < length; i++) {
        var currentRow;
        if (i === 0) {
          currentRow = row;
        } else {
          currentRow = row.clone();
          table.append(currentRow);
        }
        currentRow.attr('data-index', i);
        renameRow(currentRow, jpath, i, empty);
      }
      rename(table);
    }

    // need to replicate rows based on the external variable
    function updateTwig() {
      if (options.debug) console.log('Update twig');

      do {
        var elements = dom.find('[data-repeat]:not([data-index])');
        elements.each(handleDataRepeat);
      } while (elements.length > 0);

      // we force the incorporation of the data in the form
      if (data && module.view.formObject) {
        if (options.debug) console.log('FORCE update data');
        module.view.fillForm(true);
      }
    }

    // rename the attributes 'name' or 'name-empty' of one specific row based on the jpath
    function renameRow(row, jpath, rowIndex, empty) {
      row = $(row);
      row
        .children('td:not(:has(table))')
        .find('[data-field]')
        .each(function (index, element) {
          element = $(element);
          var name = jpath.join('.');
          if (name) name += '.';
          name += rowIndex;
          var attr = element.attr('data-field');
          if (attr) name += `.${attr}`;
          if (empty) {
            element.attr('name-empty', name);
          } else {
            element.attr('name', name);
          }
        });
    }

    // get the jpath from one element based on the attributes 'data-repeat' and 'data-index'
    // the jpath is returned as an array
    function getJpath(element) {
      var jpath = [];
      while ($(element).length) {
        if ($(element).attr('data-index')) {
          jpath.unshift($(element).attr('data-index'));
        }
        var repeatName = $(element).attr('data-repeat');
        if (repeatName) jpath.unshift(...repeatName.split('.'));
        element = $(element)
          .parent()
          .closest('[data-repeat]');
      }
      return jpath;
    }

    /*
             Rename the the full table
             */
    function rename(tbody) {
      var base = getBase(tbody).base;
      var search = new RegExp(`${base}.[0-9]+`);
      var rows = tbody.children('tr:has(td)');
      rows.each(function (rowIndex, row) {
        var replace = `${base}.${rowIndex}`;
        for (var attr of ['name', 'name-empty']) {
          $(row)
            .find(`[${attr}]`)
            .each(
              // eslint-disable-next-line no-loop-func
              function (index, element) {
                element = $(element);
                var name = element.attr(attr);
                name = name.replace(search, replace);
                element.attr(attr, name);
              }
            );
        }
      });
    }

    function getBase(element) {
      var names = [];
      element.find('[name]').each(function (index, element) {
        names.push($(element).attr('name'));
      });
      if (names.length === 0) {
        return '';
      }
      names.sort();
      return {
        base: names[0].replace(/(.*)\.([0-9]+).*/, '$1'),
        index: names[0].replace(/(.*)\.([0-9]+).*/, '$2')
      };
    }

    if (options.debug) {
      document
        .getElementById(divID)
        .addEventListener('mouseover', function (event) {
          var target = $(event.target);
          if (target.attr('name')) {
            console.log('Name', target.attr('name'));
          }
        });
      document
        .getElementById(divID)
        .addEventListener('mouseover', function (event) {
          var target = $(event.target);
          if (target.attr('name-empty')) {
            console.log('Empty', target.attr('name'));
          }
        });
    }

    function changeInputFct(event) {
      var target = $(event.target);
      if (target.attr('name-empty')) {
        var empties = target
          .closest('tr')
          .children('td:not(:has(table))')
          .find('[name-empty]');
        empties.each((index, element) => {
          $(element).attr('name', $(element).attr('name-empty'));
          $(element).removeAttr('name-empty');
        });
      }
    }
    // when the value of a row change we should rename property if it was hidden
    document.getElementById(divID).addEventListener('change', changeInputFct);
    document.getElementById(divID).addEventListener('input', changeInputFct);

    document.getElementById(divID).addEventListener('click', function (event) {
      var from = event.target;
      var table = $(from).closest('tbody');
      var tr = $(from).closest('tr');
      switch (from.className) {
        case 'form-button addRow':
        case 'addRow':
          // if we try to add a row we should check if
          // there is already an empty one
          var empties = table
            .children('tr')
            .children('td:not(:has(table))')
            .find('[name-empty]');
          if (empties.length > 0) {
            empties[0].focus();
            return;
          }
          var clone = tr.clone();
          clone.find('select, input, textarea').val('');
          clone.find('tr:not(:first-child)').remove();
          var fields = clone.find('[name]');
          // rename attribute 'name' to 'name-empty'
          fields.each((index, element) => {
            $(element).attr('name-empty', $(element).attr('name'));
            $(element).removeAttr('name');
          });
          table.append(clone);
          clone.find('[name-empty]')[0].focus();
          rename(table);
          break;
        case 'form-button removeRow':
        case 'removeRow':
          var base = getBase(tr);
          if (base) {
            var jpath = base.base.split('.');
            var data = API.getData(variableName);
            var variable = data.getChildSync(jpath);
            variable.splice(base.index, 1);
            data.triggerChange();
          }
          // need to throw an event to remove this entry
          if (table.children('tr:has(td)').length > 1) {
            tr.remove();
          } else {
            tr.find('select, input, textarea').val('');
            tr.find('tr:not(:first-child)').remove();
            tr.find('[name]').each((index, element) => {
              $(element).attr('name-empty', $(element).attr('name'));
              $(element).removeAttr('name');
            });
            tr.find('[name-empty]')[0].focus();
          }
          rename(table);
          break;
        case '':
          break;
        default:
          throw new Error(`Unexpected class name: ${from.className}`);
      }
    });
  }

  return AdvancedForm;
});
<|endoftext|>


<commit_before>define([
         "backbone",
         "handlebars",
         "permission/Permission",
         "user/UserMask"
  ], function(
      Backbone,
      Handlebars,
      Permission,
      UserMask
) {
  var PermissionEditView = Backbone.View.extend(
  /** @lends PermissionEditView.prototype */
  {
    /**
     * @class This is the view of the Permission Model. The Permission is a
     *        textarea that includes a username and a timestamp.
     *
     * @extends Backbone.View
     * @constructs
     */
    initialize : function() {
      if (OPrime.debugMode) OPrime.debug("PERMISSION EDIT VIEW init");
    },

    /**
     * The underlying model of the PermissionEditView is a Permission.
     */
    model : Permission,

    /**
     * Events that the PermissionEditView is listening to and their handlers.
     */
    events : {
      /*
       * TODO test this
       */
      "click .add-user-to-permission-role" : function(e){
        if(e){
          e.preventDefault();
        }
        var user = {};
        var usernameToAdd = $(this.el).find(".choose-add-permission-username").val() || "";
        if(usernameToAdd.trim().length < 1){
          return;
        }
        user.username = usernameToAdd;
        for(u in this.model.get("potentialusers")){
          if(usernameToAdd.indexOf(this.model.get("potentialusers")[u].username) > -1){
            var gravatar = this.model.get("potentialusers")[u].gravatar;
            if(gravatar){
              user.gravatar = gravatar;
            }
//            var g = this.model.get("users");
//            g.add(user);
//            this.model.set("users", g);
            var userAsMask = new UserMask(user);
            userAsMask.set("status","user-permission-pending");
            this.model.get("users").add(userAsMask);
            $(this.el).find(".choose-add-permission-username").val("");
            var permisionviewself = this;
            window.app.get("authentication").addCorpusRoleToUser(
                this.model.get("role"), user,
                /*success */function(returneduser) {
                  //do nothing?
                  userAsMask.set("status","user-permission-confirmed");
                  permisionviewself.render();

                  window.app.addActivity(
                      {
                        verb : "added",
                        verbicon: "icon-plus",
                        directobjecticon : "",
                        directobject : " "+user.username,
                        indirectobject : "as a  "+permisionviewself.model.get("role")+" on this corpus",
                        teamOrPersonal : "team",
                        context : " via Offline App."
                      });

                  window.app.addActivity(
                      {
                        verb : "added",
                        verbicon: "icon-plus",
                        directobjecticon : "icon-group",
                        directobject : " "+user.username,
                        indirectobject : "as a  "+permisionviewself.model.get("role")+" on "+window.app.get("corpus").get('title')+"",
                        teamOrPersonal : "personal",
                        context : " via Offline App."
                      });
                },
                /* failure */ function(errors){
                  window.appView.toastUser("Error adding user "
                      +user.username+" to corpus permissions. "
                      +errors
                      , "alert-warning","Permissions:");
                  permisionviewself.model.get("users").remove(user);
                  permisionviewself.render();
                });
            this.render();
            return;
          }
        }
      }
    },

    /**
     * The Handlebars template rendered as the PermissionEditView.
     */
    template : Handlebars.templates.permissions_edit_embedded,

    /**
     * Renders the DatumFieldView.
     */
    render : function() {
      if (OPrime.debugMode) OPrime.debug("PERMISSION EDIT VIEW render");
      var jsonToRender = this.model.toJSON();
      jsonToRender.users = this.model.get("users").toJSON();
      $(this.el).html(this.template(jsonToRender));
      return this;
    },

    /**
     * Change the model's state.
     */
    updatePermission : function() {
      //TODO
    }
  });

  return PermissionEditView;
});<commit_msg>Fixes #1166 added gravatar url to the gravatar for permissions changes<commit_after>define([
         "backbone",
         "handlebars",
         "permission/Permission",
         "user/UserMask"
  ], function(
      Backbone,
      Handlebars,
      Permission,
      UserMask
) {
  var PermissionEditView = Backbone.View.extend(
  /** @lends PermissionEditView.prototype */
  {
    /**
     * @class This is the view of the Permission Model. The Permission is a
     *        textarea that includes a username and a timestamp.
     *
     * @extends Backbone.View
     * @constructs
     */
    initialize : function() {
      if (OPrime.debugMode) OPrime.debug("PERMISSION EDIT VIEW init");
    },

    /**
     * The underlying model of the PermissionEditView is a Permission.
     */
    model : Permission,

    /**
     * Events that the PermissionEditView is listening to and their handlers.
     */
    events : {
      /*
       * TODO test this
       */
      "click .add-user-to-permission-role" : function(e){
        if(e){
          e.preventDefault();
        }
        var user = {};
        var usernameToAdd = $(this.el).find(".choose-add-permission-username").val() || "";
        if(usernameToAdd.trim().length < 1){
          return;
        }
        user.username = usernameToAdd;
        for(u in this.model.get("potentialusers")){
          if(usernameToAdd.indexOf(this.model.get("potentialusers")[u].username) > -1){
            var gravatar = this.model.get("potentialusers")[u].gravatar;
            if(gravatar){
              user.gravatar = gravatar;
            }
//            var g = this.model.get("users");
//            g.add(user);
//            this.model.set("users", g);
            var userAsMask = new UserMask(user);
            userAsMask.set("status","user-permission-pending");
            this.model.get("users").add(userAsMask);
            $(this.el).find(".choose-add-permission-username").val("");
            var permisionviewself = this;
            window.app.get("authentication").addCorpusRoleToUser(
                this.model.get("role"), user,
                /*success */function(returneduser) {
                  //do nothing?
                  userAsMask.set("status","user-permission-confirmed");
                  permisionviewself.render();

                  window.app.addActivity(
                      {
                        verb : "added",
                        verbicon: "icon-plus",
                        directobjecticon : "",
                        directobject : " "+user.username,
                        indirectobject : "as a  "+permisionviewself.model.get("role")+" on this corpus",
                        teamOrPersonal : "team",
                        context : " via Offline App."
                      });

                  window.app.addActivity(
                      {
                        verb : "added",
                        verbicon: "icon-plus",
                        directobjecticon : "icon-group",
                        directobject : " "+user.username,
                        indirectobject : "as a  "+permisionviewself.model.get("role")+" on "+window.app.get("corpus").get('title')+"",
                        teamOrPersonal : "personal",
                        context : " via Offline App."
                      });
                },
                /* failure */ function(errors){
                  window.appView.toastUser("Error adding user "
                      +user.username+" to corpus permissions. "
                      +errors
                      , "alert-warning","Permissions:");
                  permisionviewself.model.get("users").remove(user);
                  permisionviewself.render();
                });
            this.render();
            return;
          }
        }
      }
    },

    /**
     * The Handlebars template rendered as the PermissionEditView.
     */
    template : Handlebars.templates.permissions_edit_embedded,

    /**
     * Renders the DatumFieldView.
     */
    render : function() {
      if (OPrime.debugMode) OPrime.debug("PERMISSION EDIT VIEW render");
      var jsonToRender = this.model.toJSON();
      jsonToRender.users = this.model.get("users").toJSON();
      $(this.el).html(this.template(jsonToRender));
      return this;
    },

    /**
     * Change the model's state.
     */
    updatePermission : function() {
      //TODO
    }
  });

  return PermissionEditView;
});<|endoftext|>


<commit_before>"use strict";

var P = require('bluebird');
var yaml = require('js-yaml');
var fs = P.promisifyAll(require('fs'));
var util = require('util');
var rbUtil = require('./rbUtil');

var swaggerRouter = require('swagger-router');
var Node = swaggerRouter.Node;
var URI = swaggerRouter.URI;
var SwaggerRouter = swaggerRouter.Router;

function Router (options) {
    this._options = options || {};
    this._nodes = new Map();
    this._modules = new Map();
    this.router = new SwaggerRouter();
}

// Load & parse a yaml spec from disk
Router.prototype._readSpec = function(path) {
    // XXX: make the spec path configurable?
    var fsPath = __dirname + '/../specs/' + path + '.yaml';
    // Let errors bubble up for now. Fail loud & early.
    return fs.readFileAsync(fsPath)
    .then(function(yamlString) {
        return yaml.safeLoad(yamlString);
    });
};

// Extend an existing route tree with a new path by walking the existing tree
// and inserting new subtrees at the desired location.
Router.prototype._buildPath = function route(node, path, value) {
    var params = {};
    for (var i = 0; i < path.length; i++) {
        var segment = path[i];
        var nextNode = node.getChild(segment, params);
        if (!nextNode) {
            nextNode = new Node();
            node.setChild(segment, nextNode);
            if (segment.modifier === '/') {
                // Set the value for each optional path segment ({/foo})
                node.value = value;
            }
            node = nextNode;
        } else {
            node = nextNode;
        }
    }
    return node;
};

/**
 * Load and initialize a module
 */
Router.prototype._loadModule = function (modDef) {
    var cachedModule = this._modules.get(modDef);
    if (cachedModule) {
        // Found a cached instance. Return it.
        return P.resolve(cachedModule);
    }

    var self = this;
    var loadPath;
    // determine the module's load path
    switch (modDef.type) {
        case 'file':
            if (modDef.path && /^\//.test(modDef.path)) {
                // absolute path
                loadPath = modDef.path;
            } else {
                // relative path or missing
                loadPath = __dirname + '/../mods/';
                if (modDef.path) {
                    // the path has been provided, use it
                    loadPath += modDef.path;
                } else {
                    // no path given, so assume the file
                    // name matches the module name
                    loadPath += modDef.name;
                }
            }
            break;
        case 'npm':
            loadPath = modDef.name;
            break;
        default:
            throw new Error('unknown module type ' + modDef.type + ' (for module ' + modDef.name + ').');
    }
    // append the log property to module options, if it is not present
    modDef.options = modDef.options || {};
    if (!modDef.options.log) {
        modDef.options.log = this._options.log || function() {};
    }
    // let the error propagate in case the module cannot be loaded
    var modObj = require(loadPath);
    if (!modObj) {
        return P.reject("Loading module " + loadPath + " failed.");
    }
    // Call if it's a function
    if (modObj instanceof Function) {
        modObj = modObj(modDef.options);
    }
    if (!(modObj instanceof P)) {
        // wrap
        modObj = P.resolve(modObj);
    }
    return modObj.then(function(mod) {
        //console.log(loadPath, mod);
        if (!mod.operations) {
            throw new Error('No operations exported by module ' + loadPath);
        }
        self._modules.set(modDef, mod);
        return mod;
    });
};

function makeRequestTemplate(spec) {
    var uriTemplate = new URI(spec.uri, {}, true);
    return function requestTemplate (restbase, req) {
        // FIXME:
        // - Efficiently template other request parts
        uriTemplate.params = req.params;
        return restbase.request({
            uri: uriTemplate.expand(),
            method: req.method,
            body: req.body,
            headers: req.headers,
            query: req.query
        });
    };
}

/**
 * Process a Swagger path spec object
 */
Router.prototype._handleSwaggerPathSpec = function(node, pathspec,
        operations, specRoot, prefixPath)
{
    var self = this;
    if (!pathspec) {
        return P.resolve();
    }

    // Load sub-specs
    var subSpecs = pathspec['x-subspecs'];
    if (!subSpecs) {
        // Check if there is a single child spec
        var subSpec = pathspec['x-subspec'];
        if (subSpec) {
            var specRootBasePath = specRoot.basePath || '';
            specRoot = rbUtil.extend({}, subSpec);
            specRoot.paths = {};
            specRoot.definitions = {};
            specRoot.basePath = specRootBasePath + prefixPath;
            prefixPath = '';
            // XXX: The basePath is incorrect when shared between domains. Set
            // it dynamically for each request instead?
            // specRoot.basePath = prefixPath;
            var listNode = new Node();
            listNode.value = {
                specRoot: specRoot,
                methods: {},
                path: specRoot.basePath + '/'
            };
            node.setChild('', listNode);
            subSpecs = [subSpec];
        }
    }

    var loaderPromise = P.resolve();
    if (Array.isArray(subSpecs)) {
        // Load sub-specs
        loaderPromise = loaderPromise.then(function() {
            return P.each(subSpecs, function(subSpecOrPath) {
                var subSpecPromise;
                if (subSpecOrPath instanceof Object) {
                    // Inline sub-spec: return directly
                    subSpecPromise = P.resolve(subSpecOrPath);
                } else {
                    subSpecPromise = self._readSpec(subSpecOrPath);
                }
                return subSpecPromise
                .then(function(subSpec) {
                    return self._handleSwaggerSpec(node, subSpec,
                            operations, specRoot, prefixPath);
                });
            });
        });
    }

    // Load modules
    var restBaseModules = pathspec['x-modules'];
    if (Array.isArray(restBaseModules)) {
        loaderPromise = loaderPromise.then(function() {
            return P.each(restBaseModules, function(m) {
                // Share modules
                return self._loadModule(m)
                .then(function(module) {
                    //console.log(module);
                    if (!module) {
                        throw new Error('Null return when loading module ' + m.name);
                    }
                    if (!module.spec) {
                        throw new Error('Module ' + m.name + ' did not export a spec.');
                    }

                    if (module.resources) {
                        node.value.resources = (node.value.resources || [])
                            .concat(module.resources);
                    }
                    return self._handleSwaggerSpec(node, module.spec,
                            module.operations, specRoot, prefixPath);
                });
            });
        });
    }

    return loaderPromise
    // Process HTTP method stanzas ('get', 'put' etc)
    .then(function() {
        // Register the path in the specRoot
        if (specRoot && !specRoot.paths[prefixPath]) {
            specRoot.paths[prefixPath] = {};
        }

        Object.keys(pathspec).forEach(function(methodName) {
            if (/^x-/.test(methodName)) {
                return;
            }
            var method = pathspec[methodName];
            // Insert the method spec into the global merged spec
            if (specRoot.paths[prefixPath]) {
                specRoot.paths[prefixPath][methodName] = method;
            }

            var backendRequest = method && method['x-backend-request'];
            if (backendRequest) {
                // Set up a templated backend request handler
                var templatedReq = makeRequestTemplate(backendRequest);
                node.value.methods[methodName] = function (restbase, req) {
                    //console.log('template', req);
                    return templatedReq(restbase, req);
                };
            } else if (method.operationId) {
                var handler = operations[method.operationId];
                if (handler) {
                    node.value.methods[methodName] = handler;
                    //console.log(method.operationId, node.value);
                } else {
                    throw new Error('No known handler associated with operationId ' + method.operationId);
                }
            }
        });
    });
};

/**
 * Process a Swagger spec
 */
Router.prototype._handleSwaggerSpec = function (rootNode, spec, operations, specRoot, prefixPath) {
    if (!specRoot) {
        specRoot = spec;
        if (!spec.paths) { spec.paths = {}; }
        if (!spec.definitions) { spec.definitions = {}; }
        if (!spec.basePath) { spec.basePath  = prefixPath || ''; }
        prefixPath = '';
    }
    if (spec.definitions) {
        // Merge definitions
        rbUtil.extend(specRoot.definitions, spec.definitions);
    }
    var self = this;
    function handlePaths (paths) {
        if (!paths || !Object.keys(paths).length) {
            // no paths here, nothing to do
            return P.resolve();
        }
        // handle paths
        return P.all(Object.keys(paths).map(function(pathPattern) {
            var pathSpec = paths[pathPattern];
            var pathURI = new URI(pathPattern, {}, true);
            var path = pathURI.path;
            var subPrefixPath = prefixPath + pathURI.toString('simplePattern');

            // Create a value object early, so that _buildPath can set up a reference
            // to it for optional path segments.
            var value = {
                path: undefined,
                methods: {}
            };

            // Expected to return
            // - rootNode for single-element path
            // - a subnode for longer paths
            var branchNode = self._buildPath(rootNode, path.slice(0, path.length - 1), value);

            // Check if we can share the path spec
            var subtree = self._nodes.get(pathSpec);
            var specPromise;
            if (!subtree) {
                //console.log('not shared:', pathPattern);

                var segment = path[path.length - 1];

                // Check if the subtree already exists, which can happen when
                // specs are overlapping. We don't allow this for now to keep
                // specs easy to read & understand.
                subtree = branchNode.getChild(segment, {});
                if (subtree) {
                    throw new Error('Trying to re-define existing subtree ' + pathPattern);
                }

                // Build a new subtree
                subtree = new Node();
                // Set up our specific value object
                subtree.value = value;
                value.path = specRoot.basePath + subPrefixPath;
                value.methods = {};
                // XXX: Set ACLs and other value properties for path
                // subtree.value.acls = ...;

                if (segment.modifier === '+') {
                    // Set up a recursive match and end the traversal
                    subtree.setChild(segment, subtree);
                } else if (segment.modifier === '/') {
                    // Since this path segment is optional, the parent node
                    // has the same value.
                    branchNode.value = value;
                }

                // Assign the node before building the tree, so that sharing
                // opportunities with the same spec are discovered while doing so
                self._nodes.set(pathSpec, subtree);

                // Handle the path spec
                specPromise = self._handleSwaggerPathSpec(subtree, pathSpec,
                        operations, specRoot, subPrefixPath);
            } else {
                //console.log('shared:', pathPattern);
                var origSubtree = subtree;
                subtree = subtree.clone();
                subtree.value = value;
                // Copy over the specRoot
                subtree.value.specRoot = origSubtree.value.specRoot;
                specPromise = P.resolve();
            }
            branchNode.setChild(path[path.length - 1], subtree);
            return specPromise;
        }));
    }

    // TODO: handle global spec settings

    if (spec['x-subspecs'] || spec['x-subspec']) {
        // Allow x-subspecs? at top level spec too. This is useful to avoid
        // introducing an extra level in the spec tree.
        return this._handleSwaggerPathSpec(rootNode, spec, operations, specRoot, prefixPath)
        .then(handlePaths(spec.paths || {}));
    } else {
        return handlePaths(spec.paths || {});
    }
};

/**
 * Set up resources (ex: dynamic storage like tables) by traversing the tree &
 * performing the requests specified in resource stanzas. Default HTTP method
 * is 'put'.
 *
 * Any error during resource creation (status code >= 400) will abort startup
 * after logging the error as a fatal.
 */
Router.prototype.handleResources = function(restbase) {
    var self = this;
    return this.tree.visitAsync(function(value, path) {
        if (value && Array.isArray(value.resources)) {
            return P.resolve(value.resources)
            .each(function(reqTemplate) {
                if (!reqTemplate.uri) {
                    throw new Error("Missing resource URI in spec for "
                            + JSON.stringify(path));
                }
                var req = rbUtil.extend({}, reqTemplate);
                req.uri = new URI(req.uri, {
                    domain: path[0]
                }, true).expand();
                req.method = req.method || 'put';
                //console.log(path, req.uri);
                return restbase.request(req);
            });
        } else {
            return P.resolve();
        }
    });
};

/**
 * Load a new Swagger spec
 *
 * This involves building a tree, initializing modules, merging specs &
 * initializing resources: Basically the entire app startup.
 */
Router.prototype.loadSpec = function(spec, restbase) {
    var self = this;
    var rootNode = new Node();
    var specPromise;
    if (spec && spec.constructor === String) {
        specPromise = this._readSpec(spec);
    } else {
        specPromise = P.resolve(spec);
    }
    return specPromise
    .then(function(spec) {
        return self._handleSwaggerSpec(rootNode, spec, {});
    })
    .then(function() {
        // Only set the tree after loading everything
        self.tree = rootNode;
        //console.log(JSON.stringify(rootNode, null, 2));
        self.router.setTree(rootNode);
        return self.handleResources(restbase);
    })
    .then(function() {
        return self;
    });
};

/**
 * Resolve an URI to a value object
 *
 * Main request routing entry point.
 * @param {URI} uri URI object
 * @return {object} match:
 * - @prop {object} value:
 *   - @prop {object} methods: handlers for methods like get, post etc
 *   - @prop {string} path: path to this tree node
 * - @prop {object} params: Object with path parameters and optionally `_ls`
 *   for URIs ending in `/`.
 */
Router.prototype.route = function (uri) {
    return this.router.lookup(uri);
};

module.exports = Router;
<commit_msg>Break implicit nextTick recursion during resource processing<commit_after>"use strict";

var P = require('bluebird');
var yaml = require('js-yaml');
var fs = P.promisifyAll(require('fs'));
var util = require('util');
var rbUtil = require('./rbUtil');

var swaggerRouter = require('swagger-router');
var Node = swaggerRouter.Node;
var URI = swaggerRouter.URI;
var SwaggerRouter = swaggerRouter.Router;

function Router (options) {
    this._options = options || {};
    this._nodes = new Map();
    this._modules = new Map();
    this.router = new SwaggerRouter();
}

// Load & parse a yaml spec from disk
Router.prototype._readSpec = function(path) {
    // XXX: make the spec path configurable?
    var fsPath = __dirname + '/../specs/' + path + '.yaml';
    // Let errors bubble up for now. Fail loud & early.
    return fs.readFileAsync(fsPath)
    .then(function(yamlString) {
        return yaml.safeLoad(yamlString);
    });
};

// Extend an existing route tree with a new path by walking the existing tree
// and inserting new subtrees at the desired location.
Router.prototype._buildPath = function route(node, path, value) {
    var params = {};
    for (var i = 0; i < path.length; i++) {
        var segment = path[i];
        var nextNode = node.getChild(segment, params);
        if (!nextNode) {
            nextNode = new Node();
            node.setChild(segment, nextNode);
            if (segment.modifier === '/') {
                // Set the value for each optional path segment ({/foo})
                node.value = value;
            }
            node = nextNode;
        } else {
            node = nextNode;
        }
    }
    return node;
};

/**
 * Load and initialize a module
 */
Router.prototype._loadModule = function (modDef) {
    var cachedModule = this._modules.get(modDef);
    if (cachedModule) {
        // Found a cached instance. Return it.
        return P.resolve(cachedModule);
    }

    var self = this;
    var loadPath;
    // determine the module's load path
    switch (modDef.type) {
        case 'file':
            if (modDef.path && /^\//.test(modDef.path)) {
                // absolute path
                loadPath = modDef.path;
            } else {
                // relative path or missing
                loadPath = __dirname + '/../mods/';
                if (modDef.path) {
                    // the path has been provided, use it
                    loadPath += modDef.path;
                } else {
                    // no path given, so assume the file
                    // name matches the module name
                    loadPath += modDef.name;
                }
            }
            break;
        case 'npm':
            loadPath = modDef.name;
            break;
        default:
            throw new Error('unknown module type ' + modDef.type + ' (for module ' + modDef.name + ').');
    }
    // append the log property to module options, if it is not present
    modDef.options = modDef.options || {};
    if (!modDef.options.log) {
        modDef.options.log = this._options.log || function() {};
    }
    // let the error propagate in case the module cannot be loaded
    var modObj = require(loadPath);
    if (!modObj) {
        return P.reject("Loading module " + loadPath + " failed.");
    }
    // Call if it's a function
    if (modObj instanceof Function) {
        modObj = modObj(modDef.options);
    }
    if (!(modObj instanceof P)) {
        // wrap
        modObj = P.resolve(modObj);
    }
    return modObj.then(function(mod) {
        //console.log(loadPath, mod);
        if (!mod.operations) {
            throw new Error('No operations exported by module ' + loadPath);
        }
        self._modules.set(modDef, mod);
        return mod;
    });
};

function makeRequestTemplate(spec) {
    var uriTemplate = new URI(spec.uri, {}, true);
    return function requestTemplate (restbase, req) {
        // FIXME:
        // - Efficiently template other request parts
        uriTemplate.params = req.params;
        return restbase.request({
            uri: uriTemplate.expand(),
            method: req.method,
            body: req.body,
            headers: req.headers,
            query: req.query
        });
    };
}

/**
 * Process a Swagger path spec object
 */
Router.prototype._handleSwaggerPathSpec = function(node, pathspec,
        operations, specRoot, prefixPath)
{
    var self = this;
    if (!pathspec) {
        return P.resolve();
    }

    // Load sub-specs
    var subSpecs = pathspec['x-subspecs'];
    if (!subSpecs) {
        // Check if there is a single child spec
        var subSpec = pathspec['x-subspec'];
        if (subSpec) {
            var specRootBasePath = specRoot.basePath || '';
            specRoot = rbUtil.extend({}, subSpec);
            specRoot.paths = {};
            specRoot.definitions = {};
            specRoot.basePath = specRootBasePath + prefixPath;
            prefixPath = '';
            // XXX: The basePath is incorrect when shared between domains. Set
            // it dynamically for each request instead?
            // specRoot.basePath = prefixPath;
            var listNode = new Node();
            listNode.value = {
                specRoot: specRoot,
                methods: {},
                path: specRoot.basePath + '/'
            };
            node.setChild('', listNode);
            subSpecs = [subSpec];
        }
    }

    var loaderPromise = P.resolve();
    if (Array.isArray(subSpecs)) {
        // Load sub-specs
        loaderPromise = loaderPromise.then(function() {
            return P.each(subSpecs, function(subSpecOrPath) {
                var subSpecPromise;
                if (subSpecOrPath instanceof Object) {
                    // Inline sub-spec: return directly
                    subSpecPromise = P.resolve(subSpecOrPath);
                } else {
                    subSpecPromise = self._readSpec(subSpecOrPath);
                }
                return subSpecPromise
                .then(function(subSpec) {
                    return self._handleSwaggerSpec(node, subSpec,
                            operations, specRoot, prefixPath);
                });
            });
        });
    }

    // Load modules
    var restBaseModules = pathspec['x-modules'];
    if (Array.isArray(restBaseModules)) {
        loaderPromise = loaderPromise.then(function() {
            return P.each(restBaseModules, function(m) {
                // Share modules
                return self._loadModule(m)
                .then(function(module) {
                    //console.log(module);
                    if (!module) {
                        throw new Error('Null return when loading module ' + m.name);
                    }
                    if (!module.spec) {
                        throw new Error('Module ' + m.name + ' did not export a spec.');
                    }

                    if (module.resources) {
                        node.value.resources = (node.value.resources || [])
                            .concat(module.resources);
                    }
                    return self._handleSwaggerSpec(node, module.spec,
                            module.operations, specRoot, prefixPath);
                });
            });
        });
    }

    return loaderPromise
    // Process HTTP method stanzas ('get', 'put' etc)
    .then(function() {
        // Register the path in the specRoot
        if (specRoot && !specRoot.paths[prefixPath]) {
            specRoot.paths[prefixPath] = {};
        }

        Object.keys(pathspec).forEach(function(methodName) {
            if (/^x-/.test(methodName)) {
                return;
            }
            var method = pathspec[methodName];
            // Insert the method spec into the global merged spec
            if (specRoot.paths[prefixPath]) {
                specRoot.paths[prefixPath][methodName] = method;
            }

            var backendRequest = method && method['x-backend-request'];
            if (backendRequest) {
                // Set up a templated backend request handler
                var templatedReq = makeRequestTemplate(backendRequest);
                node.value.methods[methodName] = function (restbase, req) {
                    //console.log('template', req);
                    return templatedReq(restbase, req);
                };
            } else if (method.operationId) {
                var handler = operations[method.operationId];
                if (handler) {
                    node.value.methods[methodName] = handler;
                    //console.log(method.operationId, node.value);
                } else {
                    throw new Error('No known handler associated with operationId ' + method.operationId);
                }
            }
        });
    });
};

/**
 * Process a Swagger spec
 */
Router.prototype._handleSwaggerSpec = function (rootNode, spec, operations, specRoot, prefixPath) {
    if (!specRoot) {
        specRoot = spec;
        if (!spec.paths) { spec.paths = {}; }
        if (!spec.definitions) { spec.definitions = {}; }
        if (!spec.basePath) { spec.basePath  = prefixPath || ''; }
        prefixPath = '';
    }
    if (spec.definitions) {
        // Merge definitions
        rbUtil.extend(specRoot.definitions, spec.definitions);
    }
    var self = this;
    function handlePaths (paths) {
        if (!paths || !Object.keys(paths).length) {
            // no paths here, nothing to do
            return P.resolve();
        }
        // handle paths
        return P.all(Object.keys(paths).map(function(pathPattern) {
            var pathSpec = paths[pathPattern];
            var pathURI = new URI(pathPattern, {}, true);
            var path = pathURI.path;
            var subPrefixPath = prefixPath + pathURI.toString('simplePattern');

            // Create a value object early, so that _buildPath can set up a reference
            // to it for optional path segments.
            var value = {
                path: undefined,
                methods: {}
            };

            // Expected to return
            // - rootNode for single-element path
            // - a subnode for longer paths
            var branchNode = self._buildPath(rootNode, path.slice(0, path.length - 1), value);

            // Check if we can share the path spec
            var subtree = self._nodes.get(pathSpec);
            var specPromise;
            if (!subtree) {
                //console.log('not shared:', pathPattern);

                var segment = path[path.length - 1];

                // Check if the subtree already exists, which can happen when
                // specs are overlapping. We don't allow this for now to keep
                // specs easy to read & understand.
                subtree = branchNode.getChild(segment, {});
                if (subtree) {
                    throw new Error('Trying to re-define existing subtree ' + pathPattern);
                }

                // Build a new subtree
                subtree = new Node();
                // Set up our specific value object
                subtree.value = value;
                value.path = specRoot.basePath + subPrefixPath;
                value.methods = {};
                // XXX: Set ACLs and other value properties for path
                // subtree.value.acls = ...;

                if (segment.modifier === '+') {
                    // Set up a recursive match and end the traversal
                    subtree.setChild(segment, subtree);
                } else if (segment.modifier === '/') {
                    // Since this path segment is optional, the parent node
                    // has the same value.
                    branchNode.value = value;
                }

                // Assign the node before building the tree, so that sharing
                // opportunities with the same spec are discovered while doing so
                self._nodes.set(pathSpec, subtree);

                // Handle the path spec
                specPromise = self._handleSwaggerPathSpec(subtree, pathSpec,
                        operations, specRoot, subPrefixPath);
            } else {
                //console.log('shared:', pathPattern);
                var origSubtree = subtree;
                subtree = subtree.clone();
                subtree.value = value;
                // Copy over the specRoot
                subtree.value.specRoot = origSubtree.value.specRoot;
                specPromise = P.resolve();
            }
            branchNode.setChild(path[path.length - 1], subtree);
            return specPromise;
        }));
    }

    // TODO: handle global spec settings

    if (spec['x-subspecs'] || spec['x-subspec']) {
        // Allow x-subspecs? at top level spec too. This is useful to avoid
        // introducing an extra level in the spec tree.
        return this._handleSwaggerPathSpec(rootNode, spec, operations, specRoot, prefixPath)
        .then(handlePaths(spec.paths || {}));
    } else {
        return handlePaths(spec.paths || {});
    }
};

/**
 * Set up resources (ex: dynamic storage like tables) by traversing the tree &
 * performing the requests specified in resource stanzas. Default HTTP method
 * is 'put'.
 *
 * Any error during resource creation (status code >= 400) will abort startup
 * after logging the error as a fatal.
 */
Router.prototype.handleResources = function(restbase) {
    var self = this;
    return this.tree.visitAsync(function(value, path) {
        if (value && Array.isArray(value.resources)) {
            return P.resolve(value.resources)
            // Workaround (forces setTimeout) to avoid excessive nextTick
            // recursion in bluebird with node 0.10. Otherwise, we see errors
            // like this with >~500 domains:
            // (node) warning: Recursive process.nextTick detected. This will
            // break in the next version of node. Please use setImmediate for
            // recursive deferral.
            .delay(0)
            .each(function(reqTemplate) {
                if (!reqTemplate.uri) {
                    throw new Error("Missing resource URI in spec for "
                            + JSON.stringify(path));
                }
                var req = rbUtil.extend({}, reqTemplate);
                req.uri = new URI(req.uri, {
                    domain: path[0]
                }, true).expand();
                req.method = req.method || 'put';
                //console.log(path, req.uri);
                return restbase.request(req);
            });
        } else {
            return P.resolve();
        }
    });
};

/**
 * Load a new Swagger spec
 *
 * This involves building a tree, initializing modules, merging specs &
 * initializing resources: Basically the entire app startup.
 */
Router.prototype.loadSpec = function(spec, restbase) {
    var self = this;
    var rootNode = new Node();
    var specPromise;
    if (spec && spec.constructor === String) {
        specPromise = this._readSpec(spec);
    } else {
        specPromise = P.resolve(spec);
    }
    return specPromise
    .then(function(spec) {
        return self._handleSwaggerSpec(rootNode, spec, {});
    })
    .then(function() {
        // Only set the tree after loading everything
        self.tree = rootNode;
        //console.log(JSON.stringify(rootNode, null, 2));
        self.router.setTree(rootNode);
        return self.handleResources(restbase);
    })
    .then(function() {
        return self;
    });
};

/**
 * Resolve an URI to a value object
 *
 * Main request routing entry point.
 * @param {URI} uri URI object
 * @return {object} match:
 * - @prop {object} value:
 *   - @prop {object} methods: handlers for methods like get, post etc
 *   - @prop {string} path: path to this tree node
 * - @prop {object} params: Object with path parameters and optionally `_ls`
 *   for URIs ending in `/`.
 */
Router.prototype.route = function (uri) {
    return this.router.lookup(uri);
};

module.exports = Router;
<|endoftext|>


<commit_before>const {fpvsUtils} = ChromeUtils.import("chrome://FolderPaneSwitcher/content/utils.jsm");

var FPVSOptions = {
    mapping: [
        ["FolderPaneSwitcher-arrows-checkbox", "arrows", "bool"],
        ["FolderPaneSwitcher-delay-textbox", "delay", "int"],
        // Currently disabled
        //["FolderPaneSwitcher-drop-delay-textbox", "dropDelay", "int"],
    ],

    menuChangeHandler: function(event) {
        var menu_checkbox = event.target;
        menu_id = menu_checkbox.getAttribute("id");
        arrows_id = menu_id.replace('menu', 'arrows');
        var arrows_checkbox = document.getElementById(arrows_id);
        arrows_checkbox.disabled = ! menu_checkbox.hasAttribute("checked");
    },

    onLoad: function() {
        fpvsUtils.init();
        document.addEventListener("dialogextra1", function(event) {
            FPVSOptions.loadPrefs();
        });
        document.addEventListener("dialogaccept", function(event) {
            if (! FPVSOptions.validatePrefs())
                event.preventDefault();
        });
        mapping = FPVSOptions.mapping;
        var preferences = document.getElementById("fpvs-preferences");
        var rows = document.getElementById("grid-rows");
        var views = fpvsUtils.getViews();
        for (var viewNum in views) {
            var row = document.createXULElement("row");
            var prefName = "views." + viewNum + ".menu_enabled";
            var menu_checkbox = document.createXULElement("checkbox");
            var box_id = viewNum + "_menu_checkbox";
            menu_checkbox.setAttribute("id", box_id);
            mapping.push([box_id, prefName, "bool"]);
            if (views[viewNum]['name'] == "all") {
                // All Folders view can't be completely disabled.
                menu_checkbox.setAttribute("checked", true);
                menu_checkbox.disabled = true;
            }
            prefName = "views." + viewNum + ".arrows_enabled";
            var arrows_checkbox = document.createXULElement("checkbox");
            box_id = viewNum + "_arrows_checkbox";
            arrows_checkbox.setAttribute("id", box_id);
            mapping.push([box_id, prefName, "bool"]);
            fpvsUtils.addEventListener(
                menu_checkbox, "command", FPVSOptions.menuChangeHandler, true);
            row.appendChild(menu_checkbox);
            row.appendChild(arrows_checkbox);
            var label = document.createXULElement("label");
            label.appendChild(document.createTextNode(
                fpvsUtils.getStringPref(
                    fpvsUtils.viewsBranch, viewNum + ".display_name")));
            row.appendChild(label);
            rows.appendChild(row);
        }
        FPVSOptions.loadPrefs();
        mapping.forEach(function(mapping) {
            if (! mapping[0].endsWith("_menu_checkbox")) return;
            FPVSOptions.menuChangeHandler(
                {'target': document.getElementById(mapping[0])});
        });
    },

    loadPrefs: function() {
        mapping.forEach(function(mapping) {
            var elt_id = mapping[0];
            var elt = document.getElementById(elt_id);
            var pref = mapping[1];
            var pref_type = mapping[2];
            var pref_func;
            switch (pref_type) {
            case "int":
                elt.value = fpvsUtils.prefBranch.getIntPref(pref);
                break;
            case "bool":
                elt.checked = fpvsUtils.prefBranch.getBoolPref(pref);
                break;
            case "string":
                elt.value = fpvsUtils.prefBranch.getStringPref(pref);
                break;
            case "char":
                elt.value = fpvsUtils.prefBranch.getCharPref(pref);
                break;
            default:
                throw new Error("Unrecognized pref type: " + pref_type);
            }
        });
    },

    validatePrefs: function(event) {
        FPVSOptions.mapping.forEach(function(mapping) {
            var elt_id = mapping[0];
            var elt = document.getElementById(elt_id);
            var pref = mapping[1];
            var pref_type = mapping[2];
            var pref_func;
            switch (pref_type) {
            case "int":
                fpvsUtils.prefBranch.setIntPref(pref, elt.value);
                break;
            case "bool":
                fpvsUtils.prefBranch.setBoolPref(pref, elt.checked);
                break;
            case "string":
                fpvsUtils.prefBranch.setStringPref(pref, elt.value);
                break;
            case "char":
                fpvsUtils.prefBranch.setCharPref(pref, elt.value);
                break;
            default:
                throw new Error("Unrecognized pref type: " + pref_type);
            }
        });
        return true;
    }
};

fpvsUtils.addEventListener(window, "load", FPVSOptions.onLoad, false);
<commit_msg>Modified the XUL Interaction to the Html-Testing is pending<commit_after>const {fpvsUtils} = ChromeUtils.import("chrome://FolderPaneSwitcher/content/utils.jsm");

var FPVSOptions = {
    mapping: [
        ["FolderPaneSwitcher-arrows-checkbox", "arrows", "bool"],
        ["FolderPaneSwitcher-delay-textbox", "delay", "int"],
        // Currently disabled
        //["FolderPaneSwitcher-drop-delay-textbox", "dropDelay", "int"],
    ],

    menuChangeHandler: function(event) {
        var menu_checkbox = event.target;
        menu_id = menu_checkbox.getAttribute("id");
        arrows_id = menu_id.replace('menu', 'arrows');
        var arrows_checkbox = document.getElementById(arrows_id);
        arrows_checkbox.disabled = ! menu_checkbox.hasAttribute("checked");
    },

    onLoad: function() {
        fpvsUtils.init();
        document.addEventListener("dialogextra1", function(event) {
            FPVSOptions.loadPrefs();
        });
        document.addEventListener("dialogaccept", function(event) {
            if (! FPVSOptions.validatePrefs())
                event.preventDefault();
        });
        mapping = FPVSOptions.mapping;
        var preferences = document.getElementById("fpvs-preferences");

         //Migration TB68-TB78
      //  var rows = document.getElementById("grid-rows");
          var table = document.getElementById("tbl_enbldsblViews");

          var views = fpvsUtils.getViews();
          var row_position=0;
          for (var viewNum in views) {

            //Migration TB68-TB78
            //var row = document.createXULElement("row");

            var row=table.insertRow(row_position);
            var cell1=row.insertCell(0);
            var cell2=row.insertCell(1);
            var cell3=row.insertCell(2);

            var prefName = "views." + viewNum + ".menu_enabled";

            //Migration TB68-TB78
            //var menu_checkbox = document.createXULElement("checkbox");
            var menu_checkbox = document.createElement("input");
            menu_checkbox.setAttribute('type','checkbox');

            var box_id = viewNum + "_menu_checkbox";

            menu_checkbox.setAttribute("id", box_id);
            mapping.push([box_id, prefName, "bool"]);
            if (views[viewNum]['name'] == "all") {
                // All Folders view can't be completely disabled.
                menu_checkbox.setAttribute("checked", true);
                menu_checkbox.disabled = true;
            }
            prefName = "views." + viewNum + ".arrows_enabled";

            //Migration TB68-TB78
           // var arrows_checkbox = document.createXULElement("checkbox");
            var arrows_checkbox = document.createElement("input");
            arrows_checkbox.setAttribute('type','checkbox');

            box_id = viewNum + "_arrows_checkbox";
            arrows_checkbox.setAttribute("id", box_id);
            mapping.push([box_id, prefName, "bool"]);
            fpvsUtils.addEventListener(
                menu_checkbox, "command", FPVSOptions.menuChangeHandler, true);

                cell1.appendChild(menu_checkbox);
                cell2.appendChild(arrows_checkbox);
            //row.appendChild(menu_checkbox);
            //row.appendChild(arrows_checkbox);

            //Migration TB68-TB78
           // var label = document.createXULElement("label");
           var label = document.createElement("label");

            label.appendChild(document.createTextNode(
                fpvsUtils.getStringPref(
                    fpvsUtils.viewsBranch, viewNum + ".display_name")));
            //row.appendChild(label);
            cell3.appendChild(label);
           // rows.appendChild(row);

            row_position +1;
        }
        FPVSOptions.loadPrefs();
        mapping.forEach(function(mapping) {
            if (! mapping[0].endsWith("_menu_checkbox")) return;
            FPVSOptions.menuChangeHandler(
                {'target': document.getElementById(mapping[0])});
        });
    },

    loadPrefs: function() {
        mapping.forEach(function(mapping) {
            var elt_id = mapping[0];
            var elt = document.getElementById(elt_id);
            var pref = mapping[1];
            var pref_type = mapping[2];
            var pref_func;
            switch (pref_type) {
            case "int":
                elt.value = fpvsUtils.prefBranch.getIntPref(pref);
                break;
            case "bool":
                elt.checked = fpvsUtils.prefBranch.getBoolPref(pref);
                break;
            case "string":
                elt.value = fpvsUtils.prefBranch.getStringPref(pref);
                break;
            case "char":
                elt.value = fpvsUtils.prefBranch.getCharPref(pref);
                break;
            default:
                throw new Error("Unrecognized pref type: " + pref_type);
            }
        });
    },

    validatePrefs: function(event) {
        FPVSOptions.mapping.forEach(function(mapping) {
            var elt_id = mapping[0];
            var elt = document.getElementById(elt_id);
            var pref = mapping[1];
            var pref_type = mapping[2];
            var pref_func;
            switch (pref_type) {
            case "int":
                fpvsUtils.prefBranch.setIntPref(pref, elt.value);
                break;
            case "bool":
                fpvsUtils.prefBranch.setBoolPref(pref, elt.checked);
                break;
            case "string":
                fpvsUtils.prefBranch.setStringPref(pref, elt.value);
                break;
            case "char":
                fpvsUtils.prefBranch.setCharPref(pref, elt.value);
                break;
            default:
                throw new Error("Unrecognized pref type: " + pref_type);
            }
        });
        return true;
    }
};

fpvsUtils.addEventListener(window, "load", FPVSOptions.onLoad, false);
<|endoftext|>


<commit_before>var AV = require('leanengine');
var ObjTree = require('objtree');
var request = require('request');
var router = require('express').Router();
var logger = require('log4js').getLogger();

var baiduKey = process.env.baiduKey;
var expressAPI = process.env.expressAPI;
var busAPIOne = process.env.busAPIOne;
var busAPITwo = process.env.busAPITwo;
var busAPIThree = process.env.busAPIThree;
var detailUrl = process.env.busAPIFour;
var dispatchUrl = process.env.busAPISix;

function saveBusName (name) {
  var queryObj = new AV.Query('BusNames');
  queryObj.equalTo('name', name).find().then(function(data) {
    if (data.length) {
      var updateObj = AV.Object.createWithoutData('BusNames', data[0].id);
      updateObj.save().then(function (updateObj) {
        updateObj.increment('count', 1);
        updateObj.fetchWhenSave(true);
        return updateObj.save();
      }).then(function (updateObj) {
        logger.debug(name, '---update bus name success');
      }, function (error) {
        logger.error(JSON.stringify(error), '---update bus name fail');
      });
    } else {
      var BusNames = AV.Object.extend('BusNames');
      var busNames = new BusNames();
      busNames.save({ 'name': name }).then(function(object) {
        logger.debug(name, '---save bus name success');
      });
    }
  });
}

router.get('/weather', function(req, res, next) {
  var option = {
    url: 'http://api.map.baidu.com/telematics/v3/weather',
    qs: {
      ak: baiduKey,
      location: 'shanghai',
      output: 'json'
    }
  }

  request(option, function(error, response, body){
    var resData = JSON.parse(body);
    var tipt = resData.results[0].index[0];
    var weather = resData.results[0].weather_data[0];
    weather.tipt = tipt;
    res.send(weather);
  });
});

router.get('/express/:type/:postId', function(req, res, next){
  var type = req.params.type;
  var postId = req.params.postId;
  var option = {
    url: expressAPI,
    qs: { type: type, postid: postId }
  }

  request(option, function(error, response, body){
    res.send(JSON.parse(body));
  });
});

router.get('/bus/:name', function(req, res, next){
  var result = {};
  result.busLine = {
    "line_name": '请使用 上海Bus 小程序',
    "line_id": '0',
    "start_stop": '请使用 上海Bus 小程序',
    "end_stop": '本程序不再维护',
    "start_earlytime": '本程序不再维护',
    "start_latetime": '请使用 上海Bus 小程序',
    "end_earlytime": '本程序不再维护',
    "end_latetime": '请使用 上海Bus 小程序'
  };
  result.lineResults0 = {
    "direction": true,
    "stops": [
      {
        "zdmc": "通知: 本程序不再维护",
        "id": "1"
      },
      {
        "zdmc": "请搜索使用 上海Bus 小程序",
        "id": "2"
      },
      {
        "zdmc": "全新升级, 稳定实时",
        "id": "3"
      },
    ]
  };

  result.lineResults1 = {
    "direction": false,
    "stops": [
      {
        "zdmc": "请搜索使用 上海Bus 小程序",
        "id": "1"
      },
      {
        "zdmc": "全新升级, 稳定实时",
        "id": "2"
      },
    ]
  };

  res.send(result);
});

router.get('/busstop/:name/:lineid/:stopid/:direction', function(req, res, next){
  var name = req.params.name;
  var lineId = req.params.lineid;
  var stopId = req.params.stopid;
  var direction = req.params.direction;
  res.send({ "cars": [] });

  var option = {
    url: busAPIThree,
    qs: { action: 'Three', name: name, lineid: lineId, stopid: stopId, direction: direction }
  }

  request(option, function(error, response, body){
    if (response && response.statusCode === 200) {
      res.send(JSON.parse(body));
    } else {
      option.url = detailUrl;
      option.direction = option.direction ? true : false;
      request(option, function(error, response, bd){
        if (response && response.statusCode === 200) {
          var xotree = new ObjTree();
          var cars = xotree.parseXML(bd).result.cars.car
          if (cars[0].time !== 'null') {
            res.send({ cars: cars });
          } else {
            res.send({ "cars": [] });
            // request({
            //   url: dispatchUrl,
            //   qs: { direction: direction, lineid: lineId, stopid: stopId }
            // }, function(error, response, resBd){
            //   var xotree = new ObjTree();
            //   var cars = xotree.parseXML(resBd).result.cars.car || [];
            //   res.send({ "cars": cars, noCar: true });
            // });
          }
        } else {
          res.send({ "cars": [] });
        }
      });
    }
  });
});

module.exports = router;
<commit_msg>add query for line detai<commit_after>var AV = require('leanengine');
var ObjTree = require('objtree');
var request = require('request');
var router = require('express').Router();
var logger = require('log4js').getLogger();

var baiduKey = process.env.baiduKey;
var expressAPI = process.env.expressAPI;
var busAPIOne = process.env.busAPIOne;
var busAPITwo = process.env.busAPITwo;
var busAPIThree = process.env.busAPIThree;
var detailUrl = process.env.busAPIFour;
var dispatchUrl = process.env.busAPISix;

function saveBusName (name) {
  var queryObj = new AV.Query('BusNames');
  queryObj.equalTo('name', name).find().then(function(data) {
    if (data.length) {
      var updateObj = AV.Object.createWithoutData('BusNames', data[0].id);
      updateObj.save().then(function (updateObj) {
        updateObj.increment('count', 1);
        updateObj.fetchWhenSave(true);
        return updateObj.save();
      }).then(function (updateObj) {
        logger.debug(name, '---update bus name success');
      }, function (error) {
        logger.error(JSON.stringify(error), '---update bus name fail');
      });
    } else {
      var BusNames = AV.Object.extend('BusNames');
      var busNames = new BusNames();
      busNames.save({ 'name': name }).then(function(object) {
        logger.debug(name, '---save bus name success');
      });
    }
  });
}

router.get('/weather', function(req, res, next) {
  var option = {
    url: 'http://api.map.baidu.com/telematics/v3/weather',
    qs: {
      ak: baiduKey,
      location: 'shanghai',
      output: 'json'
    }
  }

  request(option, function(error, response, body){
    var resData = JSON.parse(body);
    var tipt = resData.results[0].index[0];
    var weather = resData.results[0].weather_data[0];
    weather.tipt = tipt;
    res.send(weather);
  });
});

router.get('/express/:type/:postId', function(req, res, next){
  var type = req.params.type;
  var postId = req.params.postId;
  var option = {
    url: expressAPI,
    qs: { type: type, postid: postId }
  }

  request(option, function(error, response, body){
    res.send(JSON.parse(body));
  });
});

router.get('/bus/:name', function(req, res, next){
  // 本程序部署版本不在支持返回
  // var name = req.params.name;
  // saveBusName(name);
  // var option = {
  //   url: busAPIOne,
  //   qs: { action: 'One', name: name }
  // }
  // var busLine = {};
  // request(option, function(error, response, body){
  //   if (response && response.statusCode === 200) {
  //     body = JSON.parse(body);
  //     for(key in body) {
  //       busLine[key] = body[key].trim();
  //     }
  //     var op = {
  //       url: busAPITwo,
  //       qs: { action: 'Two', name: busLine.line_name, lineid: busLine.line_id }
  //     }
  //     request(op, function(error, response, bd){
  //       bd = JSON.parse(bd);
  //       bd.busLine = busLine;
  //       res.send(bd);
  //     });
  //   } else {
  //     var query = new AV.Query('LinesInfo');
  //     query.equalTo('line_name', name);
  //     query.find().then(function(busData) {
  //       var result = {};
  //       busData = busData[0]._serverData;
  //       result.busLine = {
  //         "line_name": busData.line_name,
  //         "line_id": busData.line_id,
  //         "start_stop": busData.start_stop,
  //         "end_stop": busData.end_stop,
  //         "start_earlytime": busData.start_earlytime,
  //         "start_latetime": busData.start_latetime,
  //         "end_earlytime": busData.end_earlytime,
  //         "end_latetime": busData.end_latetime
  //       };
  //       result.lineResults0 = busData.lineResults0;
  //       result.lineResults1 = busData.lineResults1;
  //       res.send(result);
  //     });
  //   }
  // });

  var result = {};
  result.busLine = {
    "line_name": '请使用 上海Bus 小程序',
    "line_id": '0',
    "start_stop": '请使用 上海Bus 小程序',
    "end_stop": '本程序不再维护',
    "start_earlytime": '本程序不再维护',
    "start_latetime": '请使用 上海Bus 小程序',
    "end_earlytime": '本程序不再维护',
    "end_latetime": '请使用 上海Bus 小程序'
  };
  result.lineResults0 = {
    "direction": true,
    "stops": [
      {
        "zdmc": "通知: 本程序不再维护",
        "id": "1"
      },
      {
        "zdmc": "请搜索使用 上海Bus 小程序",
        "id": "2"
      },
      {
        "zdmc": "全新升级, 稳定实时",
        "id": "3"
      },
    ]
  };

  result.lineResults1 = {
    "direction": false,
    "stops": [
      {
        "zdmc": "请搜索使用 上海Bus 小程序",
        "id": "1"
      },
      {
        "zdmc": "全新升级, 稳定实时",
        "id": "2"
      },
    ]
  };

  res.send(result);
});

router.get('/busstop/:name/:lineid/:stopid/:direction', function(req, res, next){
  var name = req.params.name;
  var lineId = req.params.lineid;
  var stopId = req.params.stopid;
  var direction = req.params.direction;
  res.send({ "cars": [] });

  var option = {
    url: busAPIThree,
    qs: { action: 'Three', name: name, lineid: lineId, stopid: stopId, direction: direction }
  }

  request(option, function(error, response, body){
    if (response && response.statusCode === 200) {
      res.send(JSON.parse(body));
    } else {
      option.url = detailUrl;
      option.direction = option.direction ? true : false;
      request(option, function(error, response, bd){
        if (response && response.statusCode === 200) {
          var xotree = new ObjTree();
          var cars = xotree.parseXML(bd).result.cars.car
          if (cars[0].time !== 'null') {
            res.send({ cars: cars });
          } else {
            res.send({ "cars": [] });
            // request({
            //   url: dispatchUrl,
            //   qs: { direction: direction, lineid: lineId, stopid: stopId }
            // }, function(error, response, resBd){
            //   var xotree = new ObjTree();
            //   var cars = xotree.parseXML(resBd).result.cars.car || [];
            //   res.send({ "cars": cars, noCar: true });
            // });
          }
        } else {
          res.send({ "cars": [] });
        }
      });
    }
  });
});

module.exports = router;
<|endoftext|>


<commit_before>/*
[x] Clear up after transition
[x] Move to events instead of timeouts
[x] Get dimensions from children
[x] Find shared elements based on id
*/

import React from 'react';

const childrenToMap = (children) => {
    const childrenArray = React.Children.toArray(children);
    const keyedArray = childrenArray.map((child) => [child.key, child]);
    return new Map(keyedArray);
};

const compareChildren = (children1, children2) => {
    const keys1 = Array.from(children1.keys());
    const keys2 = Array.from(children2.keys());

    return keys1 === keys2;
};

class SharedElementTransitionGroup extends React.Component {
    constructor(props) {
        super();

        this.state = {
            outgoingShow: true,
            incomingShow: false,
            children: childrenToMap(props.children),
            transitionPending: false,
        };
        this.outgoingSharedElements;
        this.incomingSharedElements;
    }

    getSharedElements = () => {
        if (this.outgoingSharedElements && this.incomingSharedElements) return {outgoingSharedElements: this.outgoingSharedElements, incomingSharedElements: this.incomingSharedElements};

        this.outgoingSharedElements = [];
        this.incomingSharedElements = [];

        const outgoingMarkedEls = this.outgoingRef.querySelectorAll('[id]');
        const incomingMarkedEls = this.incomingRef.querySelectorAll('[id]');

        const outgoingSet = new Set(Array.from(outgoingMarkedEls).map((el) => el.id));
        const incomingSet = new Set(Array.from(incomingMarkedEls).map((el) => el.id));

        for (let element of outgoingMarkedEls) {
            if (incomingSet.has(element.id)) {
                this.outgoingSharedElements.push(element);
            }
        }
        for (let element of incomingMarkedEls) {
            if (outgoingSet.has(element.id)) {
                this.incomingSharedElements.push(element);
            }
        }

        return {outgoingSharedElements: this.outgoingSharedElements, incomingSharedElements: this.incomingSharedElements};
    }

    getSharedDimens = () => {
        const initialDimensArr = this.outgoingSharedElements.map((element) => element.getBoundingClientRect());
        const finalDimensArr = this.incomingSharedElements.map((element) => element.getBoundingClientRect());

        return {initialDimensArr, finalDimensArr};
    };

    componentWillReceiveProps(nextProps) {
        const prevChildren = this.state.children;
        const newChildren = childrenToMap(nextProps.children);

        const allChildren = new Map([...prevChildren, ...newChildren]);

        allChildren.forEach((value, key) => {
            const outgoing = prevChildren.has(key);
            const incoming = newChildren.has(key);

            if (outgoing) {
                allChildren.set(key, React.cloneElement(
                    value,
                    {
                        exit: () => {
                            this.exit(key);
                        },
                        innerRef: (ref) => {
                            this.outgoingRef = ref;
                        },
                        outgoing: true,
                    }
                ));
            } else if (incoming) {
                allChildren.set(key, React.cloneElement(
                    value,
                    {
                        innerRef: (ref) => {
                            this.incomingRef = ref;
                        },
                        incoming: true,
                    }
                ));
            }
        });

        this.setState((prevState) => ({
            ...prevState,
            children: allChildren,
            transitionPending: true,
        }));
    }

    componentDidUpdate(prevProps, prevState) {
        if (!this.state.transitionPending) return;
        if (compareChildren(this.state.children, prevState.children)) return;
        if (!this.incomingRef && !this.outgoingRef) return;

        this.setState((state) => ({
            transitionPending: false,
        }));

        this.getSharedElements();
        const {initialDimensArr, finalDimensArr} = this.getSharedDimens();
        const allChildren = this.state.children;

        const initialDimens = initialDimensArr[0];
        const finalDimens = finalDimensArr[0];

        const sharedEl = document.createElement('img');
        sharedEl.src = 'https://68.media.tumblr.com/4d1f173744a32bb4b35a2d5d0babff74/tumblr_mnh29fxz111st5lhmo1_1280.jpg';
        sharedEl.style.position = 'fixed';
        sharedEl.style.top = initialDimens.top;
        sharedEl.style.left = initialDimens.left;
        sharedEl.style.height = initialDimens.height;
        sharedEl.style.width = initialDimens.width;
        sharedEl.style.transformOrigin = '0 0';
        sharedEl.style.transition = 'transform 300ms ease-in-out';

        sharedEl.addEventListener('transitionend', (event) => {
            this.incomingRef.addEventListener('transitionend', () => {
                event.target.remove();
                for (let [key, value] of allChildren.entries()) {
                    let changedChildren = new Map();
                    let element;
                    if (value.props.incoming) {
                        element = React.cloneElement(value, {ref: undefined, incoming: undefined});
                        changedChildren.set(key, element);
                    }
                    this.outgoingRef = undefined;
                    this.incomingRef = undefined;
                    this.outgoingSharedElements = undefined;
                    this.incomingSharedElements = undefined;
                    this.setState((prevState) => ({
                        ...prevState,
                        children: changedChildren,
                        incomingShow: false,
                        outgoingShow: false,
                    }));
                }
            });
            this.setState((prevState) => ({
                ...prevState,
                incomingShow: true,
            }));
        });
        this.containerRef.appendChild(sharedEl);

        const widthScaleTransform = finalDimens.width / initialDimens.width;
        const heightScaleTransform = finalDimens.height / initialDimens.height;
        const xTranslate = finalDimens.left - initialDimens.left;
        const yTranslate = finalDimens.top - initialDimens.top;

        requestAnimationFrame(() => {
            requestAnimationFrame(() => {
                this.outgoingRef.addEventListener('transitionend', () => {
                    sharedEl.style.transform = `translate(${xTranslate}px, ${yTranslate}px) scale(${widthScaleTransform}, ${heightScaleTransform})`;
                });
                this.setState((prevState) => ({
                    ...prevState,
                    outgoingShow: false,
                }));
            });
        });
    }

    exit = (key) => {
        this.setState((prevState) => ({
            ...prevState,
            children: prevState.children.delete(key),
        }));
    };

    render() {
        const outgoingStyles = {
            position: 'absolute',
            transition: 'opacity 300ms ease-out',
            opacity: this.state.outgoingShow ? 1 : 0,
        };
        const incomingStyles = {
            position: 'absolute',
            transition: 'opacity 300ms ease-out',
            opacity: this.state.incomingShow ? 1 : 0,
        };
        const normalStyles = {
            position: 'absolute',
        };

        return (

 {
                    this.containerRef = ref;
                }}>
                {Array.from(this.state.children.values()).map((child) => {
                    if (child.props.outgoing) {
                        return React.cloneElement(child, {style: outgoingStyles});
                    } else if (child.props.incoming) {
                        return React.cloneElement(child, {style: incomingStyles});
                    } else {
                        return React.cloneElement(child, {style: normalStyles});
                    }
                })}

        );
    }
}

export default SharedElementTransitionGroup;
<commit_msg>Remove hardcoded shared element<commit_after>import React from 'react';

const childrenToMap = (children) => {
    const childrenArray = React.Children.toArray(children);
    const keyedArray = childrenArray.map((child) => [child.key, child]);
    return new Map(keyedArray);
};

const compareChildren = (children1, children2) => {
    const keys1 = Array.from(children1.keys());
    const keys2 = Array.from(children2.keys());

    return keys1 === keys2;
};

class SharedElementTransitionGroup extends React.Component {
    constructor(props) {
        super();

        this.state = {
            outgoingShow: true,
            incomingShow: false,
            children: childrenToMap(props.children),
            transitionPending: false,
        };
        this.outgoingSharedElements;
        this.incomingSharedElements;
    }

    getSharedElements = () => {
        if (this.outgoingSharedElements && this.incomingSharedElements) return {outgoingSharedElements: this.outgoingSharedElements, incomingSharedElements: this.incomingSharedElements};

        this.outgoingSharedElements = [];
        this.incomingSharedElements = [];

        const outgoingMarkedEls = this.outgoingRef.querySelectorAll('[id]');
        const incomingMarkedEls = this.incomingRef.querySelectorAll('[id]');

        const outgoingSet = new Set(Array.from(outgoingMarkedEls).map((el) => el.id));
        const incomingSet = new Set(Array.from(incomingMarkedEls).map((el) => el.id));

        for (let element of outgoingMarkedEls) {
            if (incomingSet.has(element.id)) {
                this.outgoingSharedElements.push(element);
            }
        }
        for (let element of incomingMarkedEls) {
            if (outgoingSet.has(element.id)) {
                this.incomingSharedElements.push(element);
            }
        }

        return {outgoingSharedElements: this.outgoingSharedElements, incomingSharedElements: this.incomingSharedElements};
    }

    getSharedDimens = () => {
        const initialDimensArr = this.outgoingSharedElements.map((element) => element.getBoundingClientRect());
        const finalDimensArr = this.incomingSharedElements.map((element) => element.getBoundingClientRect());

        return {initialDimensArr, finalDimensArr};
    };

    componentWillReceiveProps(nextProps) {
        const prevChildren = this.state.children;
        const newChildren = childrenToMap(nextProps.children);

        const allChildren = new Map([...prevChildren, ...newChildren]);

        allChildren.forEach((value, key) => {
            const outgoing = prevChildren.has(key);
            const incoming = newChildren.has(key);

            if (outgoing) {
                allChildren.set(key, React.cloneElement(
                    value,
                    {
                        exit: () => {
                            this.exit(key);
                        },
                        innerRef: (ref) => {
                            this.outgoingRef = ref;
                        },
                        outgoing: true,
                    }
                ));
            } else if (incoming) {
                allChildren.set(key, React.cloneElement(
                    value,
                    {
                        innerRef: (ref) => {
                            this.incomingRef = ref;
                        },
                        incoming: true,
                    }
                ));
            }
        });

        this.setState((prevState) => ({
            ...prevState,
            children: allChildren,
            transitionPending: true,
        }));
    }

    componentDidUpdate(prevProps, prevState) {
        if (!this.state.transitionPending) return;
        if (compareChildren(this.state.children, prevState.children)) return;
        if (!this.incomingRef && !this.outgoingRef) return;

        this.setState((state) => ({
            transitionPending: false,
        }));

        const {outgoingSharedElements} = this.getSharedElements();
        const {initialDimensArr, finalDimensArr} = this.getSharedDimens();
        const allChildren = this.state.children;

        // Currently assumes only one shared element
        const initialDimens = initialDimensArr[0];
        const finalDimens = finalDimensArr[0];
        const sharedEl = outgoingSharedElements[0].cloneNode();

        sharedEl.style.position = 'fixed';
        sharedEl.style.top = initialDimens.top;
        sharedEl.style.left = initialDimens.left;
        sharedEl.style.height = initialDimens.height;
        sharedEl.style.width = initialDimens.width;
        sharedEl.style.transformOrigin = '0 0';
        sharedEl.style.transition = 'transform 300ms ease-in-out';

        sharedEl.addEventListener('transitionend', (event) => {
            this.incomingRef.addEventListener('transitionend', () => {
                event.target.remove();
                for (let [key, value] of allChildren.entries()) {
                    let changedChildren = new Map();
                    let element;
                    if (value.props.incoming) {
                        element = React.cloneElement(value, {ref: undefined, incoming: undefined});
                        changedChildren.set(key, element);
                    }
                    this.outgoingRef = undefined;
                    this.incomingRef = undefined;
                    this.outgoingSharedElements = undefined;
                    this.incomingSharedElements = undefined;
                    this.setState((prevState) => ({
                        ...prevState,
                        children: changedChildren,
                        incomingShow: false,
                        outgoingShow: false,
                    }));
                }
            });
            this.setState((prevState) => ({
                ...prevState,
                incomingShow: true,
            }));
        });
        this.containerRef.appendChild(sharedEl);

        const widthScaleTransform = finalDimens.width / initialDimens.width;
        const heightScaleTransform = finalDimens.height / initialDimens.height;
        const xTranslate = finalDimens.left - initialDimens.left;
        const yTranslate = finalDimens.top - initialDimens.top;

        requestAnimationFrame(() => {
            requestAnimationFrame(() => {
                this.outgoingRef.addEventListener('transitionend', () => {
                    sharedEl.style.transform = `translate(${xTranslate}px, ${yTranslate}px) scale(${widthScaleTransform}, ${heightScaleTransform})`;
                });
                this.setState((prevState) => ({
                    ...prevState,
                    outgoingShow: false,
                }));
            });
        });
    }

    exit = (key) => {
        this.setState((prevState) => ({
            ...prevState,
            children: prevState.children.delete(key),
        }));
    };

    render() {
        const outgoingStyles = {
            position: 'absolute',
            transition: 'opacity 300ms ease-out',
            opacity: this.state.outgoingShow ? 1 : 0,
        };
        const incomingStyles = {
            position: 'absolute',
            transition: 'opacity 300ms ease-out',
            opacity: this.state.incomingShow ? 1 : 0,
        };
        const normalStyles = {
            position: 'absolute',
        };

        return (

 {
                    this.containerRef = ref;
                }}>
                {Array.from(this.state.children.values()).map((child) => {
                    if (child.props.outgoing) {
                        return React.cloneElement(child, {style: outgoingStyles});
                    } else if (child.props.incoming) {
                        return React.cloneElement(child, {style: incomingStyles});
                    } else {
                        return React.cloneElement(child, {style: normalStyles});
                    }
                })}

        );
    }
}

export default SharedElementTransitionGroup;
<|endoftext|>