<commit_before>(function (Handsontable) {
  'use strict';

  function ContextMenu(instance, customOptions){
    this.instance = instance;
    var contextMenu = this;

    this.menu = createMenu();
    this.enabled = true;

    this.bindMouseEvents();

    this.instance.addHook('afterDestroy', function () {
       contextMenu.destroy();
    });

    this.defaultOptions = {
      items: {
        'row_above': {
          name: 'Insert row above',
          callback: function(key, selection){
            this.alter("insert_row", selection.start.row());
          },
          disabled: function () {
            return this.countRows() >= this.getSettings().maxRows;
          }
        },
        'row_below': {
          name: 'Insert row below',
          callback: function(key, selection){
            this.alter("insert_row", selection.end.row() + 1);
          },
          disabled: function () {
            return this.countRows() >= this.getSettings().maxRows;
          }
        },
        "hsep1": ContextMenu.SEPARATOR,
        'col_left': {
          name: 'Insert column on the left',
          callback: function(key, selection){
            this.alter("insert_col", selection.start.col());
          },
          disabled: function () {
            return this.countCols() >= this.getSettings().maxCols;
          }
        },
        'col_right': {
          name: 'Insert column on the right',
          callback: function(key, selection){
            this.alter("insert_col", selection.end.col() + 1);
          },
          disabled: function () {
            return this.countCols() >= this.getSettings().maxCols;
          }
        },
        "hsep2": ContextMenu.SEPARATOR,
        'remove_row': {
          name: 'Remove row',
          callback: function(key, selection){
            var amount = selection.end.row() - selection.start.row() + 1;
            this.alter("remove_row", selection.start.row(), amount);
          }
        },
        'remove_col': {
          name: 'Remove column',
          callback: function(key, selection){
            var amount = selection.end.col() - selection.start.col() + 1;
            this.alter("remove_col", selection.start.col(), amount);
          }
        },
        "hsep3": ContextMenu.SEPARATOR,
        'undo': {
          name: 'Undo',
          callback: function(){
            this.undo();
          },
          disabled: function () {
            return this.undoRedo && !this.undoRedo.isUndoAvailable();
          }
        },
        'redo': {
          name: 'Redo',
          callback: function(){
            this.redo();
          },
          disabled: function () {
            return this.undoRedo && !this.undoRedo.isRedoAvailable();
          }
        }

      }
    };

    this.options = {};
    Handsontable.helper.extend(this.options, this.defaultOptions);

    this.updateOptions(customOptions);

    function createMenu(){
      var menu = document.createElement('DIV');
      Handsontable.Dom.addClass(menu, 'htContextMenu');
      instance.rootElement[0].appendChild(menu);

      return menu;
    }
  }

  ContextMenu.prototype.bindMouseEvents = function (){

    function contextMenuOpenListener(event){

      event.preventDefault();

      if(event.target.nodeName != 'TD' && !(Handsontable.Dom.hasClass(event.target, 'current') && Handsontable.Dom.hasClass(event.target, 'wtBorder'))){
        return;
      }

      this.show(event.pageY, event.pageX);

      $(document).on('mousedown.htContextMenu', Handsontable.helper.proxy(contextMenuCloseListener, this));
    }

    function contextMenuCloseListener(event){
      this.hide();
      $(document).off('mousedown.htContextMenu');
    }

    this.instance.rootElement.on('contextmenu.htContextMenu', Handsontable.helper.proxy(contextMenuOpenListener, this));

    $(this.menu).on('mousedown', Handsontable.helper.proxy(this.performAction, this));
  };

  ContextMenu.prototype.performAction = function (){

    var hot = $(this.menu).handsontable('getInstance')
    var selectedItemIndex = hot.getSelected()[0];
    var selectedItem = hot.getData()[selectedItemIndex];

    if (selectedItem.disabled === true || (typeof selectedItem.disabled == 'function' && selectedItem.disabled.call(this.instance) === true)){
      return;
    }

    if(typeof selectedItem.callback != 'function'){
      return;
    }

    var corners = this.instance.getSelected();
    var normalizedSelection = ContextMenu.utils.normalizeSelection(corners);

    selectedItem.callback.call(this.instance, selectedItem.key, normalizedSelection);

  };

  ContextMenu.prototype.unbindMouseEvents = function () {
    this.instance.rootElement.off('contextmenu.htContextMenu');
    $(document).off('mousedown.htContextMenu');
  };

  ContextMenu.prototype.show = function(top, left){

    this.menu.style.display = 'block';

    var parentInstance = this.instance;
    var contextMenu = this;

    $(this.menu).handsontable({
      data: ContextMenu.utils.convertItemsToArray(this.getItems()),
      colHeaders: false,
      colWidths: [160],
      readOnly: true,
      columns: [
        {
          data: 'name',
          renderer: function(instance, TD, row, col, prop, value, cellProperties){

            var item = instance.getData()[row];

            if(RegExp(ContextMenu.SEPARATOR, 'i').test(value)){
              Handsontable.Dom.addClass(TD, 'htSeparator');
            } else {
              Handsontable.TextRenderer.apply(this, arguments);
            }

            if (item.disabled === true || (typeof item.disabled == 'function' && item.disabled.call(parentInstance) === true)){
              Handsontable.Dom.addClass(TD, 'htDisabled');
            } else {
              Handsontable.Dom.removeClass(TD, 'htDisabled');
            }
          }
        }
      ],
      beforeKeyDown: function (event) {
        var instance = this;
        var selection = instance.getSelected();

        switch(event.keyCode){
          case Handsontable.helper.keyCode.ARROW_DOWN:
            if(!selection){

              instance.selectCell(0, 0);
              event.preventDefault();
              event.stopImmediatePropagation();

            } else {

              var nextCell = selection[0] + 1 < this.coutRows() ? this.getCell(selection[0] + 1, selection[1]) : null;

              if(nextCell && ContextMenu.utils.isSeparator(nextCell)){
                if(selection[0] + 2 < this.countRows()){
                  this.selectCell(selection[0] + 2, selection[1]);
                }

                event.preventDefault();
                event.stopImmediatePropagation();

              }

            }

            break;

          case Handsontable.helper.keyCode.ARROW_UP:
            if(!selection){
              instance.selectCell(0, 0);
              event.preventDefault();
              event.stopImmediatePropagation();
            }  else {

              var prevCell = selection[0] - 1 >= 0 ? this.getCell(selection[0] - 1, selection[1]) : null;

              if (!prevCell) {
                event.preventDefault();
                event.stopImmediatePropagation();
              } else if(ContextMenu.utils.isSeparator(prevCell)){
                if(selection[0] - 2 >= 0){
                  this.selectCell(selection[0] - 2, selection[1]);
                }

                event.preventDefault();
                event.stopImmediatePropagation();

              }

            }
            break;

          case Handsontable.helper.keyCode.ENTER:
            if(instance.getSelected()){
              contextMenu.performAction();
              contextMenu.hide();
            }
            break;
        }

      }
    });

    this.setMenuPosition(top, left);

    $(this.menu).handsontable('listen');

  };

  ContextMenu.prototype.getItems = function () {
    var items = {};
    function Item(rawItem){
      if(typeof rawItem == 'string'){
        this.name = rawItem;
      } else {
        Handsontable.helper.extend(this, rawItem);
      }
    }
    Item.prototype = this.options;

    for(var itemName in this.options.items){
      if(this.options.items.hasOwnProperty(itemName) && (!this.itemsFilter || this.itemsFilter.indexOf(itemName) != -1)){
        items[itemName] = new Item(this.options.items[itemName]);
      }
    }

    return items;

  };

  ContextMenu.prototype.updateOptions = function(newOptions){
    newOptions = newOptions || {};

    if(newOptions.items){
      for(var itemName in newOptions.items){
        var item = {};

        if(newOptions.items.hasOwnProperty(itemName)
          && this.defaultOptions.items.hasOwnProperty(itemName)
          && Handsontable.helper.isObject(newOptions.items[itemName])){
          Handsontable.helper.extend(item, this.defaultOptions.items[itemName]);
          Handsontable.helper.extend(item, newOptions.items[itemName]);
          newOptions.items[itemName] = item;
        }

      }
    }

    Handsontable.helper.extend(this.options, newOptions);
  };

  ContextMenu.prototype.setMenuPosition = function (cursorY, cursorX) {

    var cursor = {
      top: cursorY,
      left: cursorX
    };

    if(this.menuFitsBelowCursor(cursor)){
      this.positionMenuBelowCursor(cursor);
    } else {
      this.positionMenuAboveCursor(cursor);
    }

    if(this.menuFitsOnRightOfCursor(cursor)){
      this.positionMenuOnRightOfCursor(cursor);
    } else {
      this.positionMenuOnLeftOfCursor(cursor);
    }

  };

  ContextMenu.prototype.menuFitsBelowCursor = function (cursor) {
    return cursor.top + this.menu.offsetHeight <= document.documentElement.scrollTop + document.documentElement.clientHeight;
  };

  ContextMenu.prototype.menuFitsOnRightOfCursor = function (cursor) {
    return cursor.left + this.menu.offsetWidth <= document.documentElement.scrollLeft + document.documentElement.clientWidth;
  };

  ContextMenu.prototype.positionMenuBelowCursor = function (cursor) {
    this.menu.style.top = this.getCursorRelativeToContainer(cursor).top + 'px';
  };

  ContextMenu.prototype.positionMenuAboveCursor = function (cursor) {
    this.menu.style.top = (this.getCursorRelativeToContainer(cursor).top - this.menu.offsetHeight) + 'px';
  };

  ContextMenu.prototype.positionMenuOnRightOfCursor = function (cursor) {
    this.menu.style.left = this.getCursorRelativeToContainer(cursor).left + 'px';
  };

  ContextMenu.prototype.positionMenuOnLeftOfCursor = function (cursor) {
    this.menu.style.left = (this.getCursorRelativeToContainer(cursor).left - this.menu.offsetWidth) + 'px';
  };

  ContextMenu.prototype.getCursorRelativeToContainer = function (cursor) {
    var containerOffset = Handsontable.Dom.offset(this.instance.rootElement[0]);

    return {
      left: cursor.left - containerOffset.left,
      top: cursor.top - containerOffset.top
    }
  };

  ContextMenu.utils = {};
  ContextMenu.utils.convertItemsToArray = function (items) {
    var itemArray = [];
    var item;
    for(var itemName in items){
      if(items.hasOwnProperty(itemName)){
        if(typeof items[itemName] == 'string'){
          item = {name: items[item]};
        } else if (items[itemName].visible !== false) {
          item = items[itemName];
        } else {
          continue;
        }

        item.key = itemName;
        itemArray.push(item);
      }
    }

    return itemArray;
  };

  ContextMenu.utils.normalizeSelection = function(corners){
    var selection = {
      start: new Handsontable.SelectionPoint(),
      end: new Handsontable.SelectionPoint()
    };

    selection.start.row(Math.min(corners[0], corners[2]));
    selection.start.col(Math.min(corners[1], corners[3]));

    selection.end.row(Math.max(corners[0], corners[2]));
    selection.end.col(Math.max(corners[1], corners[3]));

    return selection;
  };

  ContextMenu.utils.isSeparator = function (cell) {
    return Handsontable.Dom.hasClass(cell, 'htSeparator');
  };

  ContextMenu.prototype.hide = function(){
    this.menu.style.display = 'none';
    $(this.menu).handsontable('destroy');
  };

  ContextMenu.prototype.enable = function () {
    if(!this.enabled){
      this.enabled = true;
      this.bindMouseEvents();
    }
  };

  ContextMenu.prototype.disable = function () {
    if(this.enabled){
      this.enabled = false;
      this.hide();
      this.unbindMouseEvents();
    }
  };

  ContextMenu.prototype.destroy = function () {
    this.hide();
    this.unbindMouseEvents();

    if(this.menu.parentNode){
      this.menu.parentNode.removeChild(this.menu);
    }
  };

  ContextMenu.prototype.filterItems = function(itemsToLeave){
    this.itemsFilter = itemsToLeave;
  };

  ContextMenu.SEPARATOR = "---------";

  function init(){
    var instance = this;
    var contextMenuSetting = instance.getSettings().contextMenu;
    var customOptions = Handsontable.helper.isObject(contextMenuSetting) ? contextMenuSetting : {};

    if(contextMenuSetting){
      if(!instance.contextMenu){
        instance.contextMenu = new ContextMenu(instance, customOptions);
      }

      instance.contextMenu.enable();

      if(Handsontable.helper.isArray(contextMenuSetting)){
        instance.contextMenu.filterItems(contextMenuSetting);
      }

    }  else if(instance.contextMenu){
      instance.contextMenu.destroy();
      delete instance.contextMenu;
    }

  }

  Handsontable.PluginHooks.add('afterInit', init);
  Handsontable.PluginHooks.add('afterUpdateSettings', init);

  Handsontable.ContextMenu = ContextMenu;

})(Handsontable);
<commit_msg>Fixed menu positioning when page is scrolled<commit_after>(function (Handsontable) {
  'use strict';

  function ContextMenu(instance, customOptions){
    this.instance = instance;
    var contextMenu = this;

    this.menu = createMenu();
    this.enabled = true;

    this.bindMouseEvents();

    this.instance.addHook('afterDestroy', function () {
       contextMenu.destroy();
    });

    this.defaultOptions = {
      items: {
        'row_above': {
          name: 'Insert row above',
          callback: function(key, selection){
            this.alter("insert_row", selection.start.row());
          },
          disabled: function () {
            return this.countRows() >= this.getSettings().maxRows;
          }
        },
        'row_below': {
          name: 'Insert row below',
          callback: function(key, selection){
            this.alter("insert_row", selection.end.row() + 1);
          },
          disabled: function () {
            return this.countRows() >= this.getSettings().maxRows;
          }
        },
        "hsep1": ContextMenu.SEPARATOR,
        'col_left': {
          name: 'Insert column on the left',
          callback: function(key, selection){
            this.alter("insert_col", selection.start.col());
          },
          disabled: function () {
            return this.countCols() >= this.getSettings().maxCols;
          }
        },
        'col_right': {
          name: 'Insert column on the right',
          callback: function(key, selection){
            this.alter("insert_col", selection.end.col() + 1);
          },
          disabled: function () {
            return this.countCols() >= this.getSettings().maxCols;
          }
        },
        "hsep2": ContextMenu.SEPARATOR,
        'remove_row': {
          name: 'Remove row',
          callback: function(key, selection){
            var amount = selection.end.row() - selection.start.row() + 1;
            this.alter("remove_row", selection.start.row(), amount);
          }
        },
        'remove_col': {
          name: 'Remove column',
          callback: function(key, selection){
            var amount = selection.end.col() - selection.start.col() + 1;
            this.alter("remove_col", selection.start.col(), amount);
          }
        },
        "hsep3": ContextMenu.SEPARATOR,
        'undo': {
          name: 'Undo',
          callback: function(){
            this.undo();
          },
          disabled: function () {
            return this.undoRedo && !this.undoRedo.isUndoAvailable();
          }
        },
        'redo': {
          name: 'Redo',
          callback: function(){
            this.redo();
          },
          disabled: function () {
            return this.undoRedo && !this.undoRedo.isRedoAvailable();
          }
        }

      }
    };

    this.options = {};
    Handsontable.helper.extend(this.options, this.defaultOptions);

    this.updateOptions(customOptions);

    function createMenu(){
      var menu = document.createElement('DIV');
      Handsontable.Dom.addClass(menu, 'htContextMenu');
      instance.rootElement[0].appendChild(menu);

      return menu;
    }
  }

  ContextMenu.prototype.bindMouseEvents = function (){

    function contextMenuOpenListener(event){

      event.preventDefault();

      if(event.target.nodeName != 'TD' && !(Handsontable.Dom.hasClass(event.target, 'current') && Handsontable.Dom.hasClass(event.target, 'wtBorder'))){
        return;
      }

      this.show(event.pageY, event.pageX);

      $(document).on('mousedown.htContextMenu', Handsontable.helper.proxy(contextMenuCloseListener, this));
    }

    function contextMenuCloseListener(event){
      this.hide();
      $(document).off('mousedown.htContextMenu');
    }

    this.instance.rootElement.on('contextmenu.htContextMenu', Handsontable.helper.proxy(contextMenuOpenListener, this));

    $(this.menu).on('mousedown', Handsontable.helper.proxy(this.performAction, this));
  };

  ContextMenu.prototype.performAction = function (){

    var hot = $(this.menu).handsontable('getInstance')
    var selectedItemIndex = hot.getSelected()[0];
    var selectedItem = hot.getData()[selectedItemIndex];

    if (selectedItem.disabled === true || (typeof selectedItem.disabled == 'function' && selectedItem.disabled.call(this.instance) === true)){
      return;
    }

    if(typeof selectedItem.callback != 'function'){
      return;
    }

    var corners = this.instance.getSelected();
    var normalizedSelection = ContextMenu.utils.normalizeSelection(corners);

    selectedItem.callback.call(this.instance, selectedItem.key, normalizedSelection);

  };

  ContextMenu.prototype.unbindMouseEvents = function () {
    this.instance.rootElement.off('contextmenu.htContextMenu');
    $(document).off('mousedown.htContextMenu');
  };

  ContextMenu.prototype.show = function(top, left){

    this.menu.style.display = 'block';

    var parentInstance = this.instance;
    var contextMenu = this;

    $(this.menu).handsontable({
      data: ContextMenu.utils.convertItemsToArray(this.getItems()),
      colHeaders: false,
      colWidths: [160],
      readOnly: true,
      columns: [
        {
          data: 'name',
          renderer: function(instance, TD, row, col, prop, value, cellProperties){

            var item = instance.getData()[row];

            if(RegExp(ContextMenu.SEPARATOR, 'i').test(value)){
              Handsontable.Dom.addClass(TD, 'htSeparator');
            } else {
              Handsontable.TextRenderer.apply(this, arguments);
            }

            if (item.disabled === true || (typeof item.disabled == 'function' && item.disabled.call(parentInstance) === true)){
              Handsontable.Dom.addClass(TD, 'htDisabled');
            } else {
              Handsontable.Dom.removeClass(TD, 'htDisabled');
            }
          }
        }
      ],
      beforeKeyDown: function (event) {
        var instance = this;
        var selection = instance.getSelected();

        switch(event.keyCode){
          case Handsontable.helper.keyCode.ARROW_DOWN:
            if(!selection){

              instance.selectCell(0, 0);
              event.preventDefault();
              event.stopImmediatePropagation();

            } else {

              var nextCell = selection[0] + 1 < this.coutRows() ? this.getCell(selection[0] + 1, selection[1]) : null;

              if(nextCell && ContextMenu.utils.isSeparator(nextCell)){
                if(selection[0] + 2 < this.countRows()){
                  this.selectCell(selection[0] + 2, selection[1]);
                }

                event.preventDefault();
                event.stopImmediatePropagation();

              }

            }

            break;

          case Handsontable.helper.keyCode.ARROW_UP:
            if(!selection){
              instance.selectCell(0, 0);
              event.preventDefault();
              event.stopImmediatePropagation();
            }  else {

              var prevCell = selection[0] - 1 >= 0 ? this.getCell(selection[0] - 1, selection[1]) : null;

              if (!prevCell) {
                event.preventDefault();
                event.stopImmediatePropagation();
              } else if(ContextMenu.utils.isSeparator(prevCell)){
                if(selection[0] - 2 >= 0){
                  this.selectCell(selection[0] - 2, selection[1]);
                }

                event.preventDefault();
                event.stopImmediatePropagation();

              }

            }
            break;

          case Handsontable.helper.keyCode.ENTER:
            if(instance.getSelected()){
              contextMenu.performAction();
              contextMenu.hide();
            }
            break;
        }

      }
    });

    this.setMenuPosition(top, left);

    $(this.menu).handsontable('listen');

  };

  ContextMenu.prototype.getItems = function () {
    var items = {};
    function Item(rawItem){
      if(typeof rawItem == 'string'){
        this.name = rawItem;
      } else {
        Handsontable.helper.extend(this, rawItem);
      }
    }
    Item.prototype = this.options;

    for(var itemName in this.options.items){
      if(this.options.items.hasOwnProperty(itemName) && (!this.itemsFilter || this.itemsFilter.indexOf(itemName) != -1)){
        items[itemName] = new Item(this.options.items[itemName]);
      }
    }

    return items;

  };

  ContextMenu.prototype.updateOptions = function(newOptions){
    newOptions = newOptions || {};

    if(newOptions.items){
      for(var itemName in newOptions.items){
        var item = {};

        if(newOptions.items.hasOwnProperty(itemName)
          && this.defaultOptions.items.hasOwnProperty(itemName)
          && Handsontable.helper.isObject(newOptions.items[itemName])){
          Handsontable.helper.extend(item, this.defaultOptions.items[itemName]);
          Handsontable.helper.extend(item, newOptions.items[itemName]);
          newOptions.items[itemName] = item;
        }

      }
    }

    Handsontable.helper.extend(this.options, newOptions);
  };

  ContextMenu.prototype.setMenuPosition = function (cursorY, cursorX) {

    var cursor = {
      top:  cursorY,
      topRelative: cursorY - window.pageYOffset,
      left: cursorX,
      leftRelative:cursorX - window.pageXOffset
    };

    if(this.menuFitsBelowCursor(cursor)){
      this.positionMenuBelowCursor(cursor);
    } else {
      this.positionMenuAboveCursor(cursor);
    }

    if(this.menuFitsOnRightOfCursor(cursor)){
      this.positionMenuOnRightOfCursor(cursor);
    } else {
      this.positionMenuOnLeftOfCursor(cursor);
    }

  };

  ContextMenu.prototype.menuFitsBelowCursor = function (cursor) {
    return cursor.topRelative + this.menu.offsetHeight <= document.documentElement.scrollTop + document.documentElement.clientHeight;
  };

  ContextMenu.prototype.menuFitsOnRightOfCursor = function (cursor) {
    return cursor.leftRelative + this.menu.offsetWidth <= document.documentElement.scrollLeft + document.documentElement.clientWidth;
  };

  ContextMenu.prototype.positionMenuBelowCursor = function (cursor) {
    this.menu.style.top = this.getCursorRelativeToContainer(cursor).top + 'px';
  };

  ContextMenu.prototype.positionMenuAboveCursor = function (cursor) {
    this.menu.style.top = (this.getCursorRelativeToContainer(cursor).top - this.menu.offsetHeight) + 'px';
  };

  ContextMenu.prototype.positionMenuOnRightOfCursor = function (cursor) {
    this.menu.style.left = this.getCursorRelativeToContainer(cursor).left + 'px';
  };

  ContextMenu.prototype.positionMenuOnLeftOfCursor = function (cursor) {
    this.menu.style.left = (this.getCursorRelativeToContainer(cursor).left - this.menu.offsetWidth) + 'px';
  };

  ContextMenu.prototype.getCursorRelativeToContainer = function (cursor) {
    var containerOffset = Handsontable.Dom.offset(this.instance.rootElement[0]);

    return {
      left: cursor.left - containerOffset.left,
      top: cursor.top - containerOffset.top
    }
  };

  ContextMenu.utils = {};
  ContextMenu.utils.convertItemsToArray = function (items) {
    var itemArray = [];
    var item;
    for(var itemName in items){
      if(items.hasOwnProperty(itemName)){
        if(typeof items[itemName] == 'string'){
          item = {name: items[item]};
        } else if (items[itemName].visible !== false) {
          item = items[itemName];
        } else {
          continue;
        }

        item.key = itemName;
        itemArray.push(item);
      }
    }

    return itemArray;
  };

  ContextMenu.utils.normalizeSelection = function(corners){
    var selection = {
      start: new Handsontable.SelectionPoint(),
      end: new Handsontable.SelectionPoint()
    };

    selection.start.row(Math.min(corners[0], corners[2]));
    selection.start.col(Math.min(corners[1], corners[3]));

    selection.end.row(Math.max(corners[0], corners[2]));
    selection.end.col(Math.max(corners[1], corners[3]));

    return selection;
  };

  ContextMenu.utils.isSeparator = function (cell) {
    return Handsontable.Dom.hasClass(cell, 'htSeparator');
  };

  ContextMenu.prototype.hide = function(){
    this.menu.style.display = 'none';
    $(this.menu).handsontable('destroy');
  };

  ContextMenu.prototype.enable = function () {
    if(!this.enabled){
      this.enabled = true;
      this.bindMouseEvents();
    }
  };

  ContextMenu.prototype.disable = function () {
    if(this.enabled){
      this.enabled = false;
      this.hide();
      this.unbindMouseEvents();
    }
  };

  ContextMenu.prototype.destroy = function () {
    this.hide();
    this.unbindMouseEvents();

    if(this.menu.parentNode){
      this.menu.parentNode.removeChild(this.menu);
    }
  };

  ContextMenu.prototype.filterItems = function(itemsToLeave){
    this.itemsFilter = itemsToLeave;
  };

  ContextMenu.SEPARATOR = "---------";

  function init(){
    var instance = this;
    var contextMenuSetting = instance.getSettings().contextMenu;
    var customOptions = Handsontable.helper.isObject(contextMenuSetting) ? contextMenuSetting : {};

    if(contextMenuSetting){
      if(!instance.contextMenu){
        instance.contextMenu = new ContextMenu(instance, customOptions);
      }

      instance.contextMenu.enable();

      if(Handsontable.helper.isArray(contextMenuSetting)){
        instance.contextMenu.filterItems(contextMenuSetting);
      }

    }  else if(instance.contextMenu){
      instance.contextMenu.destroy();
      delete instance.contextMenu;
    }

  }

  Handsontable.PluginHooks.add('afterInit', init);
  Handsontable.PluginHooks.add('afterUpdateSettings', init);

  Handsontable.ContextMenu = ContextMenu;

})(Handsontable);
<|endoftext|>


<commit_before>import commonCoreStrings from '../mixins/commonCoreStrings';
import taskStrings from '../mixins/taskStrings';
import bytesForHumans from '../utils/bytesForHumans';

export const TaskTypes = {
  REMOTECHANNELIMPORT: 'kolibri.core.content.tasks.remotechannelimport',
  REMOTECONTENTIMPORT: 'kolibri.core.content.tasks.remotecontentimport',
  REMOTEIMPORT: 'kolibri.core.content.tasks.remoteimport',
  DISKCHANNELIMPORT: 'kolibri.core.content.tasks.diskchannelimport',
  DISKCONTENTIMPORT: 'kolibri.core.content.tasks.diskcontentimport',
  DISKIMPORT: 'kolibri.core.content.tasks.diskimport',
  DISKCONTENTEXPORT: 'kolibri.core.content.tasks.diskcontentexport',
  DISKEXPORT: 'kolibri.core.content.tasks.diskexport',
  DELETECHANNEL: 'kolibri.core.content.tasks.deletechannel',
  UPDATECHANNEL: 'kolibri.core.content.tasks.updatechannel',
  REMOTECHANNELDIFFSTATS: 'kolibri.core.content.tasks.remotechanneldiffstats',
  LOCALCHANNELDIFFSTATS: 'kolibri.core.content.tasks.localchanneldiffstats',
  SYNCDATAPORTAL: 'kolibri.core.auth.tasks.dataportalsync',
  SYNCPEERFULL: 'kolibri.core.auth.tasks.peerfacilitysync',
  SYNCPEERPULL: 'kolibri.core.auth.tasks.peerfacilityimport',
  DELETEFACILITY: 'kolibri.core.auth.tasks.deletefacility',
};

// identical to facility constants.js
export const TaskStatuses = Object.freeze({
  IN_PROGRESS: 'INPROGRESS',
  COMPLETED: 'COMPLETED',
  FAILED: 'FAILED',
  PENDING: 'PENDING',
  RUNNING: 'RUNNING',
  QUEUED: 'QUEUED',
  SCHEDULED: 'SCHEDULED',
  CANCELED: 'CANCELED',
  CANCELING: 'CANCELING',
});

export const TransferTypes = {
  LOCALEXPORT: 'localexport',
  LOCALIMPORT: 'localimport',
  PEERIMPORT: 'peerimport',
  REMOTEIMPORT: 'remoteimport',
};

export const SyncTaskStatuses = {
  SESSION_CREATION: 'SESSION_CREATION',
  REMOTE_QUEUING: 'REMOTE_QUEUING',
  PULLING: 'PULLING',
  LOCAL_DEQUEUING: 'LOCAL_DEQUEUING',
  LOCAL_QUEUING: 'LOCAL_QUEUING',
  PUSHING: 'PUSHING',
  REMOTE_DEQUEUING: 'REMOTE_DEQUEUING',
  REMOVING_FACILITY: 'REMOVING_FACILITY',
  PENDING: 'PENDING',
  COMPLETED: 'COMPLETED',
  CANCELLED: 'CANCELLED',
  FAILED: 'FAILED',
};

const { getTaskString } = taskStrings.methods;
const { coreString } = commonCoreStrings.methods;

const syncTaskStatusToStepMap = {
  [SyncTaskStatuses.SESSION_CREATION]: 1,
  [SyncTaskStatuses.REMOTE_QUEUING]: 2,
  [SyncTaskStatuses.PULLING]: 3,
  [SyncTaskStatuses.LOCAL_DEQUEUING]: 4,
  [SyncTaskStatuses.LOCAL_QUEUING]: 5,
  [SyncTaskStatuses.PUSHING]: 6,
  [SyncTaskStatuses.REMOTE_DEQUEUING]: 7,
};

// getTaskString is wrapped in an arrow func to avoid evaluation before i18n is ready
const genericStatusToDescriptionMap = {
  [TaskStatuses.PENDING]: () => getTaskString('taskWaitingStatus'),
  [TaskStatuses.QUEUED]: () => getTaskString('taskWaitingStatus'),
  [TaskStatuses.COMPLETED]: () => getTaskString('taskFinishedStatus'),
  [TaskStatuses.CANCELED]: () => getTaskString('taskCanceledStatus'),
  [TaskStatuses.CANCELING]: () => getTaskString('taskCancelingStatus'),
  [TaskStatuses.FAILED]: () => getTaskString('taskFailedStatus'),
};

export const syncStatusToDescriptionMap = {
  ...genericStatusToDescriptionMap,
  [SyncTaskStatuses.SESSION_CREATION]: () => getTaskString('establishingConnectionStatus'),
  [SyncTaskStatuses.REMOTE_QUEUING]: () => getTaskString('remotelyPreparingDataStatus'),
  [SyncTaskStatuses.PULLING]: () => getTaskString('receivingDataStatus'),
  [SyncTaskStatuses.LOCAL_DEQUEUING]: () => getTaskString('locallyIntegratingDataStatus'),
  [SyncTaskStatuses.LOCAL_QUEUING]: () => getTaskString('locallyPreparingDataStatus'),
  [SyncTaskStatuses.PUSHING]: () => getTaskString('sendingDataStatus'),
  [SyncTaskStatuses.REMOTE_DEQUEUING]: () => getTaskString('remotelyIntegratingDataStatus'),
};

function formatNameWithId(name, id) {
  return coreString('nameWithIdInParens', { name, id: id.slice(0, 4) });
}

const PUSHPULLSTEPS = 7;
const PULLSTEPS = 4;

// Consolidates logic on how Sync-Facility Tasks should be displayed
export function syncFacilityTaskDisplayInfo(task) {
  let statusMsg;
  let bytesTransferredMsg = '';
  let deviceNameMsg = '';
  let headingMsg = '';

  const facilityName = formatNameWithId(task.extra_metadata.facility_name, task.facility_id);

  if (task.type === TaskTypes.SYNCPEERPULL) {
    headingMsg = getTaskString('importFacilityTaskLabel', { facilityName });
  } else {
    headingMsg = getTaskString('syncFacilityTaskLabel', { facilityName });
  }
  // Device info isn't shown on the Setup Wizard version of panel
  if (task.type === TaskTypes.SYNCDATAPORTAL) {
    deviceNameMsg = 'Kolibri Data Portal';
  } else if (task.extra_metadata.device_name) {
    deviceNameMsg = formatNameWithId(
      task.extra_metadata.device_name,
      task.extra_metadata.device_id
    );
  }
  const syncStep = syncTaskStatusToStepMap[task.extra_metadata.sync_state];
  const statusDescription =
    syncStatusToDescriptionMap[task.extra_metadata.sync_state]() ||
    syncStatusToDescriptionMap[task.status]() ||
    getTaskString('taskUnknownStatus');

  if (task.status === TaskStatuses.COMPLETED) {
    statusMsg = getTaskString('taskFinishedStatus');
  } else if (syncStep) {
    statusMsg = getTaskString('syncStepAndDescription', {
      step: syncStep,
      total: task.type === TaskTypes.SYNCPEERPULL ? PULLSTEPS : PUSHPULLSTEPS,
      description: statusDescription(),
    });
  } else {
    if (task.type === TaskTypes.SYNCLOD && task.status === TaskStatuses.FAILED)
      statusMsg = `${statusDescription()}: ${task.exception}`;
    else statusMsg = statusDescription();
  }

  if (task.status === TaskStatuses.COMPLETED) {
    bytesTransferredMsg = getTaskString('syncBytesSentAndReceived', {
      bytesReceived: bytesForHumans(task.extra_metadata.bytes_received),
      bytesSent: bytesForHumans(task.extra_metadata.bytes_sent),
    });
  }

  const canClear = task.clearable;

  return {
    headingMsg,
    statusMsg,
    startedByMsg: getTaskString('taskStartedByLabel', {
      username: task.extra_metadata.started_by_username,
    }),
    bytesTransferredMsg,
    deviceNameMsg,
    isRunning: Boolean(syncStep) && !canClear,
    canClear,
    canCancel: !canClear && task.cancellable,
    canRetry: task.status === TaskStatuses.FAILED,
  };
}

export const removeStatusToDescriptionMap = {
  ...genericStatusToDescriptionMap,
  [TaskStatuses.RUNNING]: () => getTaskString('removingFacilityStatus'),
};

// Consolidates logic on how Remove-Facility Tasks should be displayed
export function removeFacilityTaskDisplayInfo(task) {
  const facilityName = formatNameWithId(
    task.extra_metadata.facility_name,
    task.extra_metadata.facility
  );
  const statusDescription =
    removeStatusToDescriptionMap[task.status]() || getTaskString('taskUnknownStatus');

  return {
    headingMsg: getTaskString('removeFacilityTaskLabel', { facilityName }),
    statusMsg: statusDescription,
    startedByMsg: getTaskString('taskStartedByLabel', {
      username: task.extra_metadata.started_by_username,
    }),
    isRunning: task.status === TaskStatuses.RUNNING,
    canClear: task.clearable,
    canCancel: !task.clearable && task.status !== TaskStatuses.RUNNING,
    canRetry: task.status === TaskStatuses.FAILED,
  };
}

// For the SetupWizard Import Task
export function importFacilityTaskDisplayInfo(task) {
  // Basically takes the sync output and removes things
  const info = syncFacilityTaskDisplayInfo(task);
  info.bytesTransferredMsg = '';
  info.headingMsg = '';

  if (task.status === TaskStatuses.FAILED) {
    info.deviceNameMsg = getTaskString('importFailedStatus', {
      facilityName: task.extra_metadata.facility_name,
    });
    info.statusMsg = getTaskString('taskFailedStatus');
    info.isRunning = false;
  } else if (task.status === TaskStatuses.COMPLETED) {
    info.deviceNameMsg = getTaskString('importSuccessStatus', {
      facilityName: task.extra_metadata.facility_name,
    });
    info.statusMsg = getTaskString('taskFinishedStatus');
    info.isRunning = false;
  } else {
    info.deviceNameMsg = '';
  }
  info.canRetry = false;
  info.canClear = false;
  return info;
}
<commit_msg>use the new getTaskString export<commit_after>import commonCoreStrings from '../mixins/commonCoreStrings';
import { getTaskString } from '../mixins/taskStrings';
import bytesForHumans from '../utils/bytesForHumans';

export const TaskTypes = {
  REMOTECHANNELIMPORT: 'kolibri.core.content.tasks.remotechannelimport',
  REMOTECONTENTIMPORT: 'kolibri.core.content.tasks.remotecontentimport',
  REMOTEIMPORT: 'kolibri.core.content.tasks.remoteimport',
  DISKCHANNELIMPORT: 'kolibri.core.content.tasks.diskchannelimport',
  DISKCONTENTIMPORT: 'kolibri.core.content.tasks.diskcontentimport',
  DISKIMPORT: 'kolibri.core.content.tasks.diskimport',
  DISKCONTENTEXPORT: 'kolibri.core.content.tasks.diskcontentexport',
  DISKEXPORT: 'kolibri.core.content.tasks.diskexport',
  DELETECHANNEL: 'kolibri.core.content.tasks.deletechannel',
  UPDATECHANNEL: 'kolibri.core.content.tasks.updatechannel',
  REMOTECHANNELDIFFSTATS: 'kolibri.core.content.tasks.remotechanneldiffstats',
  LOCALCHANNELDIFFSTATS: 'kolibri.core.content.tasks.localchanneldiffstats',
  SYNCDATAPORTAL: 'kolibri.core.auth.tasks.dataportalsync',
  SYNCPEERFULL: 'kolibri.core.auth.tasks.peerfacilitysync',
  SYNCPEERPULL: 'kolibri.core.auth.tasks.peerfacilityimport',
  DELETEFACILITY: 'kolibri.core.auth.tasks.deletefacility',
};

// identical to facility constants.js
export const TaskStatuses = Object.freeze({
  IN_PROGRESS: 'INPROGRESS',
  COMPLETED: 'COMPLETED',
  FAILED: 'FAILED',
  PENDING: 'PENDING',
  RUNNING: 'RUNNING',
  QUEUED: 'QUEUED',
  SCHEDULED: 'SCHEDULED',
  CANCELED: 'CANCELED',
  CANCELING: 'CANCELING',
});

export const TransferTypes = {
  LOCALEXPORT: 'localexport',
  LOCALIMPORT: 'localimport',
  PEERIMPORT: 'peerimport',
  REMOTEIMPORT: 'remoteimport',
};

export const SyncTaskStatuses = {
  SESSION_CREATION: 'SESSION_CREATION',
  REMOTE_QUEUING: 'REMOTE_QUEUING',
  PULLING: 'PULLING',
  LOCAL_DEQUEUING: 'LOCAL_DEQUEUING',
  LOCAL_QUEUING: 'LOCAL_QUEUING',
  PUSHING: 'PUSHING',
  REMOTE_DEQUEUING: 'REMOTE_DEQUEUING',
  REMOVING_FACILITY: 'REMOVING_FACILITY',
  PENDING: 'PENDING',
  COMPLETED: 'COMPLETED',
  CANCELLED: 'CANCELLED',
  FAILED: 'FAILED',
};

const { coreString } = commonCoreStrings.methods;

const syncTaskStatusToStepMap = {
  [SyncTaskStatuses.SESSION_CREATION]: 1,
  [SyncTaskStatuses.REMOTE_QUEUING]: 2,
  [SyncTaskStatuses.PULLING]: 3,
  [SyncTaskStatuses.LOCAL_DEQUEUING]: 4,
  [SyncTaskStatuses.LOCAL_QUEUING]: 5,
  [SyncTaskStatuses.PUSHING]: 6,
  [SyncTaskStatuses.REMOTE_DEQUEUING]: 7,
};

// getTaskString is wrapped in an arrow func to avoid evaluation before i18n is ready
const genericStatusToDescriptionMap = {
  [TaskStatuses.PENDING]: () => getTaskString('taskWaitingStatus'),
  [TaskStatuses.QUEUED]: () => getTaskString('taskWaitingStatus'),
  [TaskStatuses.COMPLETED]: () => getTaskString('taskFinishedStatus'),
  [TaskStatuses.CANCELED]: () => getTaskString('taskCanceledStatus'),
  [TaskStatuses.CANCELING]: () => getTaskString('taskCancelingStatus'),
  [TaskStatuses.FAILED]: () => getTaskString('taskFailedStatus'),
};

export const syncStatusToDescriptionMap = {
  ...genericStatusToDescriptionMap,
  [SyncTaskStatuses.SESSION_CREATION]: () => getTaskString('establishingConnectionStatus'),
  [SyncTaskStatuses.REMOTE_QUEUING]: () => getTaskString('remotelyPreparingDataStatus'),
  [SyncTaskStatuses.PULLING]: () => getTaskString('receivingDataStatus'),
  [SyncTaskStatuses.LOCAL_DEQUEUING]: () => getTaskString('locallyIntegratingDataStatus'),
  [SyncTaskStatuses.LOCAL_QUEUING]: () => getTaskString('locallyPreparingDataStatus'),
  [SyncTaskStatuses.PUSHING]: () => getTaskString('sendingDataStatus'),
  [SyncTaskStatuses.REMOTE_DEQUEUING]: () => getTaskString('remotelyIntegratingDataStatus'),
};

function formatNameWithId(name, id) {
  return coreString('nameWithIdInParens', { name, id: id.slice(0, 4) });
}

const PUSHPULLSTEPS = 7;
const PULLSTEPS = 4;

// Consolidates logic on how Sync-Facility Tasks should be displayed
export function syncFacilityTaskDisplayInfo(task) {
  let statusMsg;
  let bytesTransferredMsg = '';
  let deviceNameMsg = '';
  let headingMsg = '';

  const facilityName = formatNameWithId(task.extra_metadata.facility_name, task.facility_id);

  if (task.type === TaskTypes.SYNCPEERPULL) {
    headingMsg = getTaskString('importFacilityTaskLabel', { facilityName });
  } else {
    headingMsg = getTaskString('syncFacilityTaskLabel', { facilityName });
  }
  // Device info isn't shown on the Setup Wizard version of panel
  if (task.type === TaskTypes.SYNCDATAPORTAL) {
    deviceNameMsg = 'Kolibri Data Portal';
  } else if (task.extra_metadata.device_name) {
    deviceNameMsg = formatNameWithId(
      task.extra_metadata.device_name,
      task.extra_metadata.device_id
    );
  }
  const syncStep = syncTaskStatusToStepMap[task.extra_metadata.sync_state];
  const statusDescription =
    syncStatusToDescriptionMap[task.extra_metadata.sync_state]() ||
    syncStatusToDescriptionMap[task.status]() ||
    getTaskString('taskUnknownStatus');

  if (task.status === TaskStatuses.COMPLETED) {
    statusMsg = getTaskString('taskFinishedStatus');
  } else if (syncStep) {
    statusMsg = getTaskString('syncStepAndDescription', {
      step: syncStep,
      total: task.type === TaskTypes.SYNCPEERPULL ? PULLSTEPS : PUSHPULLSTEPS,
      description: statusDescription(),
    });
  } else {
    if (task.type === TaskTypes.SYNCLOD && task.status === TaskStatuses.FAILED)
      statusMsg = `${statusDescription()}: ${task.exception}`;
    else statusMsg = statusDescription();
  }

  if (task.status === TaskStatuses.COMPLETED) {
    bytesTransferredMsg = getTaskString('syncBytesSentAndReceived', {
      bytesReceived: bytesForHumans(task.extra_metadata.bytes_received),
      bytesSent: bytesForHumans(task.extra_metadata.bytes_sent),
    });
  }

  const canClear = task.clearable;

  return {
    headingMsg,
    statusMsg,
    startedByMsg: getTaskString('taskStartedByLabel', {
      username: task.extra_metadata.started_by_username,
    }),
    bytesTransferredMsg,
    deviceNameMsg,
    isRunning: Boolean(syncStep) && !canClear,
    canClear,
    canCancel: !canClear && task.cancellable,
    canRetry: task.status === TaskStatuses.FAILED,
  };
}

export const removeStatusToDescriptionMap = {
  ...genericStatusToDescriptionMap,
  [TaskStatuses.RUNNING]: () => getTaskString('removingFacilityStatus'),
};

// Consolidates logic on how Remove-Facility Tasks should be displayed
export function removeFacilityTaskDisplayInfo(task) {
  const facilityName = formatNameWithId(
    task.extra_metadata.facility_name,
    task.extra_metadata.facility
  );
  const statusDescription =
    removeStatusToDescriptionMap[task.status]() || getTaskString('taskUnknownStatus');

  return {
    headingMsg: getTaskString('removeFacilityTaskLabel', { facilityName }),
    statusMsg: statusDescription,
    startedByMsg: getTaskString('taskStartedByLabel', {
      username: task.extra_metadata.started_by_username,
    }),
    isRunning: task.status === TaskStatuses.RUNNING,
    canClear: task.clearable,
    canCancel: !task.clearable && task.status !== TaskStatuses.RUNNING,
    canRetry: task.status === TaskStatuses.FAILED,
  };
}

// For the SetupWizard Import Task
export function importFacilityTaskDisplayInfo(task) {
  // Basically takes the sync output and removes things
  const info = syncFacilityTaskDisplayInfo(task);
  info.bytesTransferredMsg = '';
  info.headingMsg = '';

  if (task.status === TaskStatuses.FAILED) {
    info.deviceNameMsg = getTaskString('importFailedStatus', {
      facilityName: task.extra_metadata.facility_name,
    });
    info.statusMsg = getTaskString('taskFailedStatus');
    info.isRunning = false;
  } else if (task.status === TaskStatuses.COMPLETED) {
    info.deviceNameMsg = getTaskString('importSuccessStatus', {
      facilityName: task.extra_metadata.facility_name,
    });
    info.statusMsg = getTaskString('taskFinishedStatus');
    info.isRunning = false;
  } else {
    info.deviceNameMsg = '';
  }
  info.canRetry = false;
  info.canClear = false;
  return info;
}
<|endoftext|>


<commit_before>/*!
 * Copyright 2013 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var _ = require('underscore');
var Etherpad = require('etherpad-lite-client');
var Url = require('url');

var log = require('oae-logger').logger('etherpad');
var Signature = require('oae-util/lib/signature');
var Validator = require('oae-util/lib/validator').Validator;

var etherpadServers = [];
var etherpadConfig = null;

/**
 * Initializes the etherpad client.
 *
 * @param  {Object}   _etherpadConfig     The etherpad config from config.js.
 */
var refreshConfiguration = module.exports.refreshConfiguration = function(_etherpadConfig) {
    // Remember this config.
    etherpadConfig = _etherpadConfig;

    // Remove the old servers (if any).
    etherpadServers = [];

    // Sort the etherpad servers based on their hostname/port.
    // We do this so we have the same "ordered set" of etherpad hosts accross the app node cluster.
    etherpadConfig.hosts.sort(function(a, b) {
        // In case you're running two etherpad instances on 1 host
        if (a.host === b.host) {
            return a.port < b.host;
        } else {
            return a.host < b.host;
        }
    });

    // Connect to each server.
    _.each(etherpadConfig.hosts, function(host, index) {

        // Create an object that contains the client and the external base URL.
        var etherpad = {
            'index': index,
            'client': Etherpad.connect({
                'apikey': _etherpadConfig.apikey,
                'host': host.host,
                'port': host.port
            })
        };
        etherpadServers.push(etherpad);
    });
};

/**
 * Get the etherpad configuration.
 *
 * @return {Object} The etherpad configuration.
 */
var getConfig = module.exports.getConfig = function() {
    return etherpadConfig;
};

/**
 * Creates a pad via the etherpad API.
 *
 * @param  {String}     contentId                       The ID of the collaborative document for which an etherpad pad should be created.
 * @param  {Function}   callback                        Standard callback method.
 * @param  {Object}     callback.err                    Standard error object (if any).
 * @param  {Object}     callback.ids                    An object containing the etherpad IDs for this collaborative document.
 * @param  {String}     callback.ids.etherpadGroupId    The etherpad identifier for the group that was created.
 * @param  {String}     callback.ids.etherpadPadId      The etherpad identifier for the pad that was created.
 */
var createPad = module.exports.createPad = function(contentId, callback) {
    // Because etherpad has a slightly weird system of authenticating users
    // we need to create a group *PER* content item and then create a group pad in this group.

    // Get the client that points to the correct etherpad instance.
    var client = getClient(contentId);

    // Create the group.
    var args = {
        'groupMapper': contentId
    };
    log().trace({'contentId': contentId}, 'Creating etherpad group.');
    client.createGroupIfNotExistsFor(args, function(err, groupData) {
        if (err) {
            log().error({'err': err, 'contentId': contentId}, 'Could not create an etherpad group.');
            return callback({'code': 500, 'msg': err.message});
        }

        // Create the group pad.
        var groupPad = {
            'groupID': groupData.groupID,
            'padName': contentId
        };
        log().trace({'contentId': contentId, 'groupID': groupData.groupID}, 'Creating etherpad group pad.');
        client.createGroupPad(groupPad, function(err, padData) {
            if (err) {
                log().error({'err': err, 'contentId': contentId}, 'Could not create an etherpad group pad.');
                return callback({'code': 500, 'msg': err.message});
            }

            // Store these IDs in the database.
            var ids = {
                'etherpadGroupId': groupData.groupID,
                'etherpadPadId': padData.padID
            };
            log().info({'contentId': contentId, 'groupID': groupData.groupID, 'padID': padData.padID}, 'Created an etherpad group and pad.');
            callback(null, ids);
        });
    });
};

/**
 * Get the HTML for a pad.
 *
 * @param  {String}     contentId       The content id for which the HTML should be retrieved from etherpad.
 * @param  {String}     padId           The ID of the pad for which to retrieve the HTML.
 * @param  {Function}   callback        Standard callback method.
 * @param  {Object}     callback.err    Standard error object (if any.)
 * @param  {String}     callback.html   The HTML fragment for this pad.
 */
var getHTML = module.exports.getHTML = function(contentId, padId, callback) {
    log().trace({'contentId': contentId}, 'Getting etherpad HTML');
    getClient(contentId).getHTML({'padID': padId}, function(err, data) {
        if (err) {
            log().error({'err': err, 'padID': padId, 'contentId': contentId}, 'Could not grab the HTML from etherpad.');
            return callback({'code': 500, 'msg': 'Could not grab the HTML from etherpad.'});
        }

        callback(null, data.html);
    });
};

/**
 * Set the html for a pad.
 *
 * @param  {String}     contentId       The content id for which the HTML should be set in etherpad.
 * @param  {String}     padId           The ID of the pad for which to set the HTML.
 * @param  {String}     html            The HTML fragment to store in etherpad.
 * @param  {Function}   callback        Standard callback method.
 * @param  {Object}     callback.err    Standard error object (if any.)
 */
var setHTML = module.exports.setHTML = function(contentId, padId, html, callback) {
    log().trace({'contentId': contentId, 'html': html}, 'Setting etherpad html');

    // Although Etherpad exposes an API to set the HTML of a pad, it doesn't accept any HTML fragments.
    // We encapsulate the HTML fragment into a simple page and submit that instead.
    var toSend = '<!doctype html><title></title><meta charset="utf-8">' + html + '';
    getClient(contentId).setHTML({'padID': padId, 'html': toSend}, function(err, data) {
        if (err) {
            log().error({'err': err, 'padID': padId, 'contentId': contentId, 'html': html}, 'Could not set the html on the etherpad instance.');
            return callback({'code': 500, 'msg': 'Could not set the html on the etherpad instance.'});
        }

        callback(null);
    });
};

/**
 * Joins the current user in an etherpad.
 * This assumes that the current user has access to the collaborative document.
 *
 * @param  {Context}    ctx             The current context
 * @param  {String}     contentId       The content id of the apd that should be joined..
 * @param  {String}     padId           The ID of the pad that should be joined.
 * @param  {String}     groupId         The ID of the group that is associated to this pad.
 * @param  {Function}   callback        Standard callback method.
 * @param  {String}     callback.url    The public URL that the UI can use to embed the pad.
 */
var joinPad = module.exports.joinPad = function(ctx, contentId, padId, groupId, callback) {
    if (!ctx.user()) {
        return callback({'code': 401, 'msg': 'Anonymous users are not allowed to join collaborative documents.'});
    }

    // Get the etherpad client that will handle this contentId.
    var client = getClient(contentId);

    /*
     *   Joining a pad consists out of three things:
     *    1/ Mapping the OAE user to an etherpad author.
     *    2/ Creating a session for the etherpad author
     *    3/ Returning a url to the UI. It should contain
     *       * The server etherpad is running on (ex: http://7.etherpad.oae.com/)
     *       * The pad URI (ex: /oae/c_cam_abc123)
     *       * The session ID (ex: ?sessionID=s.32b01f91d0e2c9a344)
     */
    var args = {
        'authorMapper': ctx.user().id,
        'name': ctx.user().displayName
    };
    client.createAuthorIfNotExistsFor(args, function(err, author) {
        if (err) {
            log().error({'err': err, 'contentId': contentId, 'principalId': ctx.user().id}, 'Could not create an etherpad author.');
            return callback({'code': 500, 'msg': 'Could not create an author in the etherpad system.'});
        }

        var session = {
            'groupID': groupId,
            'authorID': author.authorID,
            'validUntil': Math.round(Date.now()/1000) + 60*60*24
        };
        log().trace(session, 'Creating a session');
        client.createSession(session, function(err, data) {
            if (err) {
                log().error({'err': err, 'contentId': contentId, 'principalId': ctx.user().id}, 'Could not create an etherpad session.');
                return callback({'code': 500, 'msg': 'Could not create an etherpad session for this user.'});
            }

            // Get the language for the current user.
            var language = 'en';
            var locale = ctx.user().locale;
            if (locale) {
                language = locale.split('_')[0];
            }

            // Construct the URL.
            var url = getPadUrl(contentId, padId, data.sessionID, language);
            return callback(null, {'url': url});
        });
    });
};

/**
 * Get the URL where users can view the etherpad pad.
 * This can be used to embed in the page via an iframe.
 * The URL will be of the form:
 *     /etherpad/0/oae/<pad ID>?sessionID=<session ID>&pathPrefix=/etherpad/0&language=en
 *
 * A couple of notes:
 *     *   The URL that will be embedded should be on the same domain as the current tenant.
 *         This is because Safari will not set cookies that are coming from another domain inside an iframe.
 *     *   We try to shard based on the content ID so that the load is spread across the etherpad cluster.
 *         Note that this is *NOT* sharding based on load, as some documents may be more popular than others.
 *     *   The `/etherpad/0` will have to be stripped off by nginx.
 *     *   We point to `/oae/<pad ID>` which is an endpoint exposed by the `ep_oae` plugin.
 *         That endpoint will take care of sending the session cookie.
 *     *   We need to send a pathPrefix so the `ep_oae` endpoint can construct the
 *         /etherpad/0/p/<pad ID> URL. Etherpad can take it from there.
 *
 * @param  {String}     contentId   The ID of the piece of content.
 * @param  {String}     padId       The ID of the pad in etherpad.
 * @param  {String}     sessionId   The ID of the session that should be included in the query string.
 * @param  {String}     language    The 2 character string that identifies the user's prefered language.
 * @return {String}                 The URL to the pad that can be used to embed in a page.
 */
var getPadUrl = module.exports.getPadUrl = function(contentId, padId, sessionId, language) {
    var serverIndex = _getServer(contentId).index;
    return Url.format({
        'pathname': '/etherpad/' + serverIndex + '/oae/' + padId,
        'query': {
            'sessionID': sessionId,
            'language': language,
            'pathPrefix': '/etherpad/' + serverIndex
        }
    });
};

/**
 * Get an etherpad client that can talk to an etherpad API.
 *
 * @param  {String}     contentId   The ID of the piece of content for which we need to retrieve an etherpad client.
 * @return {Client}                 The request etherpad client.
 */
var getClient = module.exports.getClient = function(contentId) {
    return _getServer(contentId).client;
};

/**
 * Get the server that is tied to a content ID.
 *
 * @param  {String}     contentId   The content ID for which the server should be retrieved.
 * @return {Object}                 The server tied to a collabration document.
 * @api private
 */
var _getServer = function(contentId) {
    var index = _hash(contentId, etherpadServers.length);
    return etherpadServers[index];
};

/**
 * Hashes a string and returns the index.
 *
 * @param  {String}     str     The string to hash.
 * @param  {String}     nr      The upper bound (exclusive) for the index.
 * @return {Number}             The index.
 * @api private
 */
var _hash = function(str, nr) {
    var code = 0;
    for (var i = 0; i < str.length; i++) {
        code += str.charCodeAt(i);
    }
    return code % nr;
};
<commit_msg>(Issue632) Don't reorder the etherpad server list<commit_after>/*!
 * Copyright 2013 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var _ = require('underscore');
var Etherpad = require('etherpad-lite-client');
var Url = require('url');

var log = require('oae-logger').logger('etherpad');
var Signature = require('oae-util/lib/signature');
var Validator = require('oae-util/lib/validator').Validator;

var etherpadServers = [];
var etherpadConfig = null;

/**
 * Initializes the etherpad client.
 *
 * @param  {Object}   _etherpadConfig     The etherpad config from config.js.
 */
var refreshConfiguration = module.exports.refreshConfiguration = function(_etherpadConfig) {
    // Remember this config.
    etherpadConfig = _etherpadConfig;

    // Rebuild the servers list and recreate from the configuration
    etherpadServers = [];
    _.each(etherpadConfig.hosts, function(host, index) {
        // Create the etherpad client with its sharding index
        var etherpad = {
            'index': index,
            'client': Etherpad.connect({
                'apikey': _etherpadConfig.apikey,
                'host': host.host,
                'port': host.port
            })
        };
        etherpadServers.push(etherpad);
    });
};

/**
 * Get the etherpad configuration.
 *
 * @return {Object} The etherpad configuration.
 */
var getConfig = module.exports.getConfig = function() {
    return etherpadConfig;
};

/**
 * Creates a pad via the etherpad API.
 *
 * @param  {String}     contentId                       The ID of the collaborative document for which an etherpad pad should be created.
 * @param  {Function}   callback                        Standard callback method.
 * @param  {Object}     callback.err                    Standard error object (if any).
 * @param  {Object}     callback.ids                    An object containing the etherpad IDs for this collaborative document.
 * @param  {String}     callback.ids.etherpadGroupId    The etherpad identifier for the group that was created.
 * @param  {String}     callback.ids.etherpadPadId      The etherpad identifier for the pad that was created.
 */
var createPad = module.exports.createPad = function(contentId, callback) {
    // Because etherpad has a slightly weird system of authenticating users
    // we need to create a group *PER* content item and then create a group pad in this group.

    // Get the client that points to the correct etherpad instance.
    var client = getClient(contentId);

    // Create the group.
    var args = {
        'groupMapper': contentId
    };
    log().trace({'contentId': contentId}, 'Creating etherpad group.');
    client.createGroupIfNotExistsFor(args, function(err, groupData) {
        if (err) {
            log().error({'err': err, 'contentId': contentId}, 'Could not create an etherpad group.');
            return callback({'code': 500, 'msg': err.message});
        }

        // Create the group pad.
        var groupPad = {
            'groupID': groupData.groupID,
            'padName': contentId
        };
        log().trace({'contentId': contentId, 'groupID': groupData.groupID}, 'Creating etherpad group pad.');
        client.createGroupPad(groupPad, function(err, padData) {
            if (err) {
                log().error({'err': err, 'contentId': contentId}, 'Could not create an etherpad group pad.');
                return callback({'code': 500, 'msg': err.message});
            }

            // Store these IDs in the database.
            var ids = {
                'etherpadGroupId': groupData.groupID,
                'etherpadPadId': padData.padID
            };
            log().info({'contentId': contentId, 'groupID': groupData.groupID, 'padID': padData.padID}, 'Created an etherpad group and pad.');
            callback(null, ids);
        });
    });
};

/**
 * Get the HTML for a pad.
 *
 * @param  {String}     contentId       The content id for which the HTML should be retrieved from etherpad.
 * @param  {String}     padId           The ID of the pad for which to retrieve the HTML.
 * @param  {Function}   callback        Standard callback method.
 * @param  {Object}     callback.err    Standard error object (if any.)
 * @param  {String}     callback.html   The HTML fragment for this pad.
 */
var getHTML = module.exports.getHTML = function(contentId, padId, callback) {
    log().trace({'contentId': contentId}, 'Getting etherpad HTML');
    getClient(contentId).getHTML({'padID': padId}, function(err, data) {
        if (err) {
            log().error({'err': err, 'padID': padId, 'contentId': contentId}, 'Could not grab the HTML from etherpad.');
            return callback({'code': 500, 'msg': 'Could not grab the HTML from etherpad.'});
        }

        callback(null, data.html);
    });
};

/**
 * Set the html for a pad.
 *
 * @param  {String}     contentId       The content id for which the HTML should be set in etherpad.
 * @param  {String}     padId           The ID of the pad for which to set the HTML.
 * @param  {String}     html            The HTML fragment to store in etherpad.
 * @param  {Function}   callback        Standard callback method.
 * @param  {Object}     callback.err    Standard error object (if any.)
 */
var setHTML = module.exports.setHTML = function(contentId, padId, html, callback) {
    log().trace({'contentId': contentId, 'html': html}, 'Setting etherpad html');

    // Although Etherpad exposes an API to set the HTML of a pad, it doesn't accept any HTML fragments.
    // We encapsulate the HTML fragment into a simple page and submit that instead.
    var toSend = '<!doctype html><title></title><meta charset="utf-8">' + html + '';
    getClient(contentId).setHTML({'padID': padId, 'html': toSend}, function(err, data) {
        if (err) {
            log().error({'err': err, 'padID': padId, 'contentId': contentId, 'html': html}, 'Could not set the html on the etherpad instance.');
            return callback({'code': 500, 'msg': 'Could not set the html on the etherpad instance.'});
        }

        callback(null);
    });
};

/**
 * Joins the current user in an etherpad.
 * This assumes that the current user has access to the collaborative document.
 *
 * @param  {Context}    ctx             The current context
 * @param  {String}     contentId       The content id of the apd that should be joined..
 * @param  {String}     padId           The ID of the pad that should be joined.
 * @param  {String}     groupId         The ID of the group that is associated to this pad.
 * @param  {Function}   callback        Standard callback method.
 * @param  {String}     callback.url    The public URL that the UI can use to embed the pad.
 */
var joinPad = module.exports.joinPad = function(ctx, contentId, padId, groupId, callback) {
    if (!ctx.user()) {
        return callback({'code': 401, 'msg': 'Anonymous users are not allowed to join collaborative documents.'});
    }

    // Get the etherpad client that will handle this contentId.
    var client = getClient(contentId);

    /*
     *   Joining a pad consists out of three things:
     *    1/ Mapping the OAE user to an etherpad author.
     *    2/ Creating a session for the etherpad author
     *    3/ Returning a url to the UI. It should contain
     *       * The server etherpad is running on (ex: http://7.etherpad.oae.com/)
     *       * The pad URI (ex: /oae/c_cam_abc123)
     *       * The session ID (ex: ?sessionID=s.32b01f91d0e2c9a344)
     */
    var args = {
        'authorMapper': ctx.user().id,
        'name': ctx.user().displayName
    };
    client.createAuthorIfNotExistsFor(args, function(err, author) {
        if (err) {
            log().error({'err': err, 'contentId': contentId, 'principalId': ctx.user().id}, 'Could not create an etherpad author.');
            return callback({'code': 500, 'msg': 'Could not create an author in the etherpad system.'});
        }

        var session = {
            'groupID': groupId,
            'authorID': author.authorID,
            'validUntil': Math.round(Date.now()/1000) + 60*60*24
        };
        log().trace(session, 'Creating a session');
        client.createSession(session, function(err, data) {
            if (err) {
                log().error({'err': err, 'contentId': contentId, 'principalId': ctx.user().id}, 'Could not create an etherpad session.');
                return callback({'code': 500, 'msg': 'Could not create an etherpad session for this user.'});
            }

            // Get the language for the current user.
            var language = 'en';
            var locale = ctx.user().locale;
            if (locale) {
                language = locale.split('_')[0];
            }

            // Construct the URL.
            var url = getPadUrl(contentId, padId, data.sessionID, language);
            return callback(null, {'url': url});
        });
    });
};

/**
 * Get the URL where users can view the etherpad pad.
 * This can be used to embed in the page via an iframe.
 * The URL will be of the form:
 *     /etherpad/0/oae/<pad ID>?sessionID=<session ID>&pathPrefix=/etherpad/0&language=en
 *
 * A couple of notes:
 *     *   The URL that will be embedded should be on the same domain as the current tenant.
 *         This is because Safari will not set cookies that are coming from another domain inside an iframe.
 *     *   We try to shard based on the content ID so that the load is spread across the etherpad cluster.
 *         Note that this is *NOT* sharding based on load, as some documents may be more popular than others.
 *     *   The `/etherpad/0` will have to be stripped off by nginx.
 *     *   We point to `/oae/<pad ID>` which is an endpoint exposed by the `ep_oae` plugin.
 *         That endpoint will take care of sending the session cookie.
 *     *   We need to send a pathPrefix so the `ep_oae` endpoint can construct the
 *         /etherpad/0/p/<pad ID> URL. Etherpad can take it from there.
 *
 * @param  {String}     contentId   The ID of the piece of content.
 * @param  {String}     padId       The ID of the pad in etherpad.
 * @param  {String}     sessionId   The ID of the session that should be included in the query string.
 * @param  {String}     language    The 2 character string that identifies the user's prefered language.
 * @return {String}                 The URL to the pad that can be used to embed in a page.
 */
var getPadUrl = module.exports.getPadUrl = function(contentId, padId, sessionId, language) {
    var serverIndex = _getServer(contentId).index;
    return Url.format({
        'pathname': '/etherpad/' + serverIndex + '/oae/' + padId,
        'query': {
            'sessionID': sessionId,
            'language': language,
            'pathPrefix': '/etherpad/' + serverIndex
        }
    });
};

/**
 * Get an etherpad client that can talk to an etherpad API.
 *
 * @param  {String}     contentId   The ID of the piece of content for which we need to retrieve an etherpad client.
 * @return {Client}                 The request etherpad client.
 */
var getClient = module.exports.getClient = function(contentId) {
    return _getServer(contentId).client;
};

/**
 * Get the server that is tied to a content ID.
 *
 * @param  {String}     contentId   The content ID for which the server should be retrieved.
 * @return {Object}                 The server tied to a collabration document.
 * @api private
 */
var _getServer = function(contentId) {
    var index = _hash(contentId, etherpadServers.length);
    return etherpadServers[index];
};

/**
 * Hashes a string and returns the index.
 *
 * @param  {String}     str     The string to hash.
 * @param  {String}     nr      The upper bound (exclusive) for the index.
 * @return {Number}             The index.
 * @api private
 */
var _hash = function(str, nr) {
    var code = 0;
    for (var i = 0; i < str.length; i++) {
        code += str.charCodeAt(i);
    }
    return code % nr;
};
<|endoftext|>


<commit_before>
//-----------------------------
// play:
//-----------------------------

App.Screens.play = {
  map:    null,
  player: null,

  enter: function() {
    console.log("entered Screen.play");
    App.display.setOptions({fontSize:28, fontSytle:"bold", bg:"#000"});
    var mapWidth  = 100;
    var mapHeight = 100;

    // create tiles (TODO: move this to Map):
    var tiles     = [];
    for (var x=0; x<mapWidth; x++) {
      tiles.push([]);
      for (var y=0; y<mapHeight; y++) {
        tiles[x].push(App.Tile.null);
      }
    }
    var generator = new ROT.Map.Cellular(mapWidth, mapHeight);
    generator.randomize(0.5);
    var iterations = 1;   //<- more = smoother
    for (var i=0; i<iterations-1; i++) { generator.create(); }
    generator.create(function(x, y, value) { tiles[x][y] = value===1 ? App.Tiles.floor : App.Tiles.wall; });

    this.player = new App.Entity(App.Templates.player);
    this.map    = new App.Map(tiles, this.player);
  },

  exit: function() {
    console.log("exited Screen.play");
  },

  render: function(display) {
    // tiles:
    var startX = Math.max(0, this.player.x - (App.width/2));
    startX     = Math.min(startX, this.map.width - App.width);
    var startY = Math.max(0, this.player.y - (App.height/2));
    startY     = Math.min(startY, this.map.height - App.height);
    var stopX  = startX + App.width;
    var stopY  = startY + App.height;
    for (var x=startX; x<stopX; x++) {
      for (var y=startY; y<stopY; y++) {
        var tile = this.map.getTile(x,y);
        display.draw(x-startX, y-startY, tile.ch, tile.fg, tile.bg);
      }
    }

    // entities: (draw in reverse order to draw player last/on top):
    for (var i=this.map.entities.length-1; i>-1; i--) {
      var entity = this.map.entities[i];
      if (entity.x>=startX && entity.x<stopX && entity.y>=startY && entity.y<stopY) {
        display.draw(entity.x-startX, entity.y-startY, entity.ch, entity.fg, entity.bg);
      }
    }

    // messages:
    var msgY = 0;
    for (var i=0; i<this.player.messages.length; i++) {
      msgY += display.drawText(0, msgY, '%c{white}%b{black}' + this.player.messages[i]);
    }

    // stats:
    var stats = '%c{white}%b{black}';
    stats += vsprintf('HP: %d/%d ', [this.player.hp, this.player.maxHP]);
    display.drawText(0, App.height, stats);
  },

  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.win);
    } else if (keyCode === ROT.VK_ESCAPE) {
      App.switchScreen(App.Screens.lose);
    }
    if (keyCode === ROT.VK_LEFT) {
      this._move(-1, 0);
    } else if (keyCode === ROT.VK_RIGHT) {
      this._move(1, 0);
    } else if (keyCode === ROT.VK_UP) {
      this._move(0, -1);
    } else if (keyCode === ROT.VK_DOWN) {
      this._move(0, 1);
    }
    this.map.engine.unlock();
  },

  _move: function(dirX, dirY) {
    var newX = Math.max(0, Math.min(this.map.width-1,  this.player.x + dirX));
    var newY = Math.max(0, Math.min(this.map.height-1, this.player.y + dirY));
    if (this.player.tryMove(newX, newY, this.map)) {
      App.refresh();
    }
  }
};

//-----------------------------
// start:
//-----------------------------

App.Screens.start = {
  enter: function() {
    console.log("entered Screen.start");
    App.display.setOptions({width:App.width, fontSize:28, fontSytle:"bold", bg:"#00a"});
  },
  exit:  function() {
    console.log("exited Screen.start");
  },
  render: function(display) {
    display.drawText(1,1, "%c{yellow}Start Screen");
    display.drawText(1,2, "Press [Enter] to start");
  },
  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.play);
    }
  }
};

//-----------------------------
// win:
//-----------------------------

App.Screens.win = {
  enter: function() {
    console.log("entered Screen.win");
    App.display.setOptions({width:App.width, fontSize:28, fontSytle:"bold", bg:"#0a0"});
  },
  exit: function() {
    console.log("exited Screen.win");
  },
  render: function(display) {
    display.drawText(1,1, "%c{green}Win Screen");
    display.drawText(1,2, "press [Enter] to play again");
  },
  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.play);
    }
  }
};

//-----------------------------
// lose:
//-----------------------------

App.Screens.lose = {
  enter: function() {
    console.log("entered Screen.lose");
    App.display.setOptions({width:App.width, fontSize:28, fontSytle:"bold", bg:"#a00"});
  },
  exit: function() {
    console.log("exited Screen.lose");
  },
  render: function(display) {
    display.drawText(1,1, "%c{red}Lose Screen");
    display.drawText(1,2, "press [Enter] to play again");
  },
  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.play);
    }
  }
};
<commit_msg>more map generator iterations for smoother caves<commit_after>
//-----------------------------
// play:
//-----------------------------

App.Screens.play = {
  map:    null,
  player: null,

  enter: function() {
    console.log("entered Screen.play");
    App.display.setOptions({fontSize:28, fontSytle:"bold", bg:"#000"});
    var mapWidth  = 100;
    var mapHeight = 100;

    // create tiles (TODO: move this to Map):
    var tiles     = [];
    for (var x=0; x<mapWidth; x++) {
      tiles.push([]);
      for (var y=0; y<mapHeight; y++) {
        tiles[x].push(App.Tile.null);
      }
    }
    var generator = new ROT.Map.Cellular(mapWidth, mapHeight);
    generator.randomize(0.5);
    var iterations = 10;   //<- more = smoother
    for (var i=0; i<iterations-1; i++) { generator.create(); }
    generator.create(function(x, y, value) { tiles[x][y] = value===1 ? App.Tiles.floor : App.Tiles.wall; });

    this.player = new App.Entity(App.Templates.player);
    this.map    = new App.Map(tiles, this.player);
  },

  exit: function() {
    console.log("exited Screen.play");
  },

  render: function(display) {
    // tiles:
    var startX = Math.max(0, this.player.x - (App.width/2));
    startX     = Math.min(startX, this.map.width - App.width);
    var startY = Math.max(0, this.player.y - (App.height/2));
    startY     = Math.min(startY, this.map.height - App.height);
    var stopX  = startX + App.width;
    var stopY  = startY + App.height;
    for (var x=startX; x<stopX; x++) {
      for (var y=startY; y<stopY; y++) {
        var tile = this.map.getTile(x,y);
        display.draw(x-startX, y-startY, tile.ch, tile.fg, tile.bg);
      }
    }

    // entities: (draw in reverse order to draw player last/on top):
    for (var i=this.map.entities.length-1; i>-1; i--) {
      var entity = this.map.entities[i];
      if (entity.x>=startX && entity.x<stopX && entity.y>=startY && entity.y<stopY) {
        display.draw(entity.x-startX, entity.y-startY, entity.ch, entity.fg, entity.bg);
      }
    }

    // messages:
    var msgY = 0;
    for (var i=0; i<this.player.messages.length; i++) {
      msgY += display.drawText(0, msgY, '%c{white}%b{black}' + this.player.messages[i]);
    }

    // stats:
    var stats = '%c{white}%b{black}';
    stats += vsprintf('HP: %d/%d ', [this.player.hp, this.player.maxHP]);
    display.drawText(0, App.height, stats);
  },

  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.win);
    } else if (keyCode === ROT.VK_ESCAPE) {
      App.switchScreen(App.Screens.lose);
    }
    if (keyCode === ROT.VK_LEFT) {
      this._move(-1, 0);
    } else if (keyCode === ROT.VK_RIGHT) {
      this._move(1, 0);
    } else if (keyCode === ROT.VK_UP) {
      this._move(0, -1);
    } else if (keyCode === ROT.VK_DOWN) {
      this._move(0, 1);
    }
    this.map.engine.unlock();
  },

  _move: function(dirX, dirY) {
    var newX = Math.max(0, Math.min(this.map.width-1,  this.player.x + dirX));
    var newY = Math.max(0, Math.min(this.map.height-1, this.player.y + dirY));
    if (this.player.tryMove(newX, newY, this.map)) {
      App.refresh();
    }
  }
};

//-----------------------------
// start:
//-----------------------------

App.Screens.start = {
  enter: function() {
    console.log("entered Screen.start");
    App.display.setOptions({width:App.width, fontSize:28, fontSytle:"bold", bg:"#00a"});
  },
  exit:  function() {
    console.log("exited Screen.start");
  },
  render: function(display) {
    display.drawText(1,1, "%c{yellow}Start Screen");
    display.drawText(1,2, "Press [Enter] to start");
  },
  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.play);
    }
  }
};

//-----------------------------
// win:
//-----------------------------

App.Screens.win = {
  enter: function() {
    console.log("entered Screen.win");
    App.display.setOptions({width:App.width, fontSize:28, fontSytle:"bold", bg:"#0a0"});
  },
  exit: function() {
    console.log("exited Screen.win");
  },
  render: function(display) {
    display.drawText(1,1, "%c{green}Win Screen");
    display.drawText(1,2, "press [Enter] to play again");
  },
  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.play);
    }
  }
};

//-----------------------------
// lose:
//-----------------------------

App.Screens.lose = {
  enter: function() {
    console.log("entered Screen.lose");
    App.display.setOptions({width:App.width, fontSize:28, fontSytle:"bold", bg:"#a00"});
  },
  exit: function() {
    console.log("exited Screen.lose");
  },
  render: function(display) {
    display.drawText(1,1, "%c{red}Lose Screen");
    display.drawText(1,2, "press [Enter] to play again");
  },
  handleInput: function(keyCode) {
    if (keyCode === ROT.VK_RETURN) {
      App.switchScreen(App.Screens.play);
    }
  }
};
<|endoftext|>


<commit_before>
define([
    'flight/lib/component',
    'data',
    './image/image',
    '../properties',
    '../withTypeContent',
    '../withHighlighting',
    'tpl!./entity',
    'tpl!./relationships',
    'util/vertexList/list',
    'detail/dropdowns/propertyForm/propForm',
    'service/ontology',
    'service/vertex',
    'sf'
], function(defineComponent,
    appData,
    Image,
    Properties,
    withTypeContent,
    withHighlighting,
    template,
    relationshipsTemplate,
    VertexList,
    PropertyForm,
    OntologyService,
    VertexService,
    sf) {
    'use strict';

    var ontologyService = new OntologyService();
    var vertexService = new VertexService();

    return defineComponent(Entity, withTypeContent, withHighlighting);

    function Entity(withDropdown) {

        this.defaultAttrs({
            glyphIconSelector: '.entity-glyphIcon',
            propertiesSelector: '.properties',
            relationshipsSelector: '.relationships',
            titleSelector: '.entity-title'
        });

        this.after('teardown', function() {
            this.$node.off('click.paneClick');
        });

        this.after('initialize', function() {
            var self = this;
            this.$node.on('click.paneClick', this.onPaneClicked.bind(this));

            this.on(document, 'verticesUpdated', this.onVerticesUpdated);
            this.on('infiniteScrollRequest', this.handleReferenceLoadingRequest);

            self.loadEntity();
        });

        this.onVerticesUpdated = function(event, data) {
            var self = this;

            data.vertices.forEach(function(vertex) {
                if (vertex.id === self.attr.data.id) {
                    self.select('titleSelector').html(vertex.properties.title.value);
                }
            });
        };

        this.loadEntity = function() {
            var self = this;

            $.when(
                this.handleCancelling(appData.refresh(this.attr.data)),
                this.handleCancelling(ontologyService.concepts())
            ).done(function(vertex, concepts) {
                var concept = concepts.byId[self.attr.data.properties._conceptType.value];

                self.$node.html(template({
                    vertex: self.attr.data,
                    fullscreenButton: self.fullscreenButton([self.attr.data.id]),
                    auditsButton: self.auditsButton()
                }));

                Image.attachTo(self.select('glyphIconSelector'), {
                    data: self.attr.data,
                    service: self.vertexService,
                    defaultIconSrc: concept && concept.glyphIconHref || ''
                });

                Properties.attachTo(self.select('propertiesSelector'), {
                    data: self.attr.data
                });

                self.updateEntityAndArtifactDraggables();

                $.when(
                    self.handleCancelling(self.ontologyService.relationships()),
                    self.handleCancelling(self.vertexService.getVertexRelationships(self.attr.data.id))
                ).done(self.loadRelationships.bind(self, vertex));
            });
        };

        this.loadRelationships = function(vertex, ontologyRelationships, vertexRelationships) {
            var self = this,
                totalReferences = vertexRelationships[0].totalReferences,
                relationships = vertexRelationships[0].relationships;

            // Create source/dest/other properties
            relationships.forEach(function(r) {
                var src, dest, other;
                if (vertex.id == r.relationship.sourceVertexId) {
                    src = vertex;
                    dest = other = r.vertex;
                } else {
                    src = other = r.vertex;
                    dest = vertex;
                }

                r.vertices = {
                    src: src,
                    dest: dest,
                    other: other,
                    classes: {
                        src: self.classesForVertex(src),
                        dest: self.classesForVertex(dest),
                        other: self.classesForVertex(other)
                    }
                };

                r.relationshipInfo = {
                    id: r.relationship.id,
                    properties: $.extend({}, r.relationship.properties, {
                        _type: 'relationship',
                        _rowKey: r.relationship.sourceVertexId + '->' + r.relationship.destVertexId,
                        id: r.relationship.id,
                        relationshipType: r.relationship.label,
                        source: r.relationship.sourceVertexId,
                        target: r.relationship.destVertexId
                    })
                };
                r.displayLabel = ontologyRelationships.byTitle[r.relationship.label].displayName;
            });

            var groupedByType = _.groupBy(relationships, function(r) {

                // Has Entity are collected into references (no matter
                // relationship direction
                if (r.relationship.label === 'hasEntity') {
                    return 'references';
                }

                // Group all that are relations from this vertex (not dest)
                if (r.relationship.sourceVertexId === vertex.id) {
                    return r.displayLabel;
                }

                // Collect all relationships that are destined here
                // into section
                return 'inverse';
            });
            var sortedKeys = Object.keys(groupedByType);
            sortedKeys.sort(function(a,b) {
                // If in inverse group, sort by the type
                if (a === b && a === 'inverse') {
                    return a.displayLabel === b.displayLabel ? 0 : a.displayLabel < b.displayLabel ? -1 : 1;
                }

                // If in references group sort by the title
                if (a === b && a === 'references') {
                    return defaultSort(a.vertex.properties.title.value, b.vertex.properties.title.value);
                }

                // Specifies the special group sort order
                var groups = { inverse:1, references:2 };
                if (groups[a] && groups[b]) {
                    return defaultSort(groups[a], groups[b]);
                } else if (groups[a]) {
                    return 1;
                } else if (groups[b]) {
                    return -1;
                }

                return defaultSort(a, b);

                function defaultSort(x,y) {
                    return x === y ? 0 : x < y ? -1 : 1;
                }
            });

            var $rels = self.select('relationshipsSelector');
            $rels.html(relationshipsTemplate({
                relationshipsGroupedByType: groupedByType,
                sortedKeys: sortedKeys
            }));

            VertexList.attachTo($rels.find('.references'), {
                vertices: _.map(groupedByType.references, function(r) {
                    return r.vertices.other;
                }),
                infiniteScrolling: (groupedByType.references && groupedByType.references.length) > 0,
                total: totalReferences
            });
        };

        this.handleReferenceLoadingRequest = function(evt, data) {
            var self = this;

            this.handleCancelling(this.service.getVertexRelationships(this.attr.data.id, data.paging))
                .done(function(response) {
                    var relationships = response.relationships,
                        total = response.totalReferences;

                    self.trigger(
                        self.select('relationshipsSelector').find('.references'),
                        'addInfiniteVertices',
                        {
                            vertices: _.pluck(relationships, 'vertex'),
                            total: total
                        }
                    );

                });
        };

        this.onPaneClicked = function(evt) {
            var $target = $(evt.target);

            if (!$target.is('.add-new-properties') && $target.parents('.underneath').length === 0) {
                PropertyForm.teardownAll();
            }

            if ($target.is('.entity, .artifact')) {
                var id = $target.data('vertexId');
                this.trigger('selectObjects', { vertices:[appData.vertex(id)] });
                evt.stopPropagation();
            } else if ($target.is('.relationship')) {
                var info = $target.data('info');
                this.trigger('selectObjects', { vertices:[info] });
                evt.stopPropagation();
            }
        };

    }
});

<commit_msg>Show error message when relationships fail to load<commit_after>
define([
    'flight/lib/component',
    'data',
    './image/image',
    '../properties',
    '../withTypeContent',
    '../withHighlighting',
    'tpl!./entity',
    'tpl!./relationships',
    'tpl!util/alert',
    'util/vertexList/list',
    'detail/dropdowns/propertyForm/propForm',
    'service/ontology',
    'service/vertex',
    'sf'
], function(defineComponent,
    appData,
    Image,
    Properties,
    withTypeContent,
    withHighlighting,
    template,
    relationshipsTemplate,
    alertTemplate,
    VertexList,
    PropertyForm,
    OntologyService,
    VertexService,
    sf) {
    'use strict';

    var ontologyService = new OntologyService();
    var vertexService = new VertexService();

    return defineComponent(Entity, withTypeContent, withHighlighting);

    function Entity(withDropdown) {

        this.defaultAttrs({
            glyphIconSelector: '.entity-glyphIcon',
            propertiesSelector: '.properties',
            relationshipsSelector: '.relationships',
            titleSelector: '.entity-title'
        });

        this.after('teardown', function() {
            this.$node.off('click.paneClick');
        });

        this.after('initialize', function() {
            var self = this;
            this.$node.on('click.paneClick', this.onPaneClicked.bind(this));

            this.on(document, 'verticesUpdated', this.onVerticesUpdated);
            this.on('infiniteScrollRequest', this.handleReferenceLoadingRequest);

            self.loadEntity();
        });

        this.onVerticesUpdated = function(event, data) {
            var self = this;

            data.vertices.forEach(function(vertex) {
                if (vertex.id === self.attr.data.id) {
                    self.select('titleSelector').html(vertex.properties.title.value);
                }
            });
        };

        this.loadEntity = function() {
            var self = this;

            $.when(
                this.handleCancelling(appData.refresh(this.attr.data)),
                this.handleCancelling(ontologyService.concepts())
            ).done(function(vertex, concepts) {
                var concept = concepts.byId[self.attr.data.properties._conceptType.value];

                self.$node.html(template({
                    vertex: self.attr.data,
                    fullscreenButton: self.fullscreenButton([self.attr.data.id]),
                    auditsButton: self.auditsButton()
                }));

                Image.attachTo(self.select('glyphIconSelector'), {
                    data: self.attr.data,
                    service: self.vertexService,
                    defaultIconSrc: concept && concept.glyphIconHref || ''
                });

                Properties.attachTo(self.select('propertiesSelector'), {
                    data: self.attr.data
                });

                self.updateEntityAndArtifactDraggables();

                $.when(
                        self.handleCancelling(self.ontologyService.relationships()),
                        self.handleCancelling(self.vertexService.getVertexRelationships(self.attr.data.id))
                    )
                    .fail(function() {
                        self.select('relationshipsSelector').html(alertTemplate({
                            error: 'Unable to load relationships'
                        }));
                    })
                    .done(self.loadRelationships.bind(self, vertex));
            });
        };

        this.loadRelationships = function(vertex, ontologyRelationships, vertexRelationships) {
            var self = this,
                totalReferences = vertexRelationships[0].totalReferences,
                relationships = vertexRelationships[0].relationships;

            // Create source/dest/other properties
            relationships.forEach(function(r) {
                var src, dest, other;
                if (vertex.id == r.relationship.sourceVertexId) {
                    src = vertex;
                    dest = other = r.vertex;
                } else {
                    src = other = r.vertex;
                    dest = vertex;
                }

                r.vertices = {
                    src: src,
                    dest: dest,
                    other: other,
                    classes: {
                        src: self.classesForVertex(src),
                        dest: self.classesForVertex(dest),
                        other: self.classesForVertex(other)
                    }
                };

                r.relationshipInfo = {
                    id: r.relationship.id,
                    properties: $.extend({}, r.relationship.properties, {
                        _type: 'relationship',
                        _rowKey: r.relationship.sourceVertexId + '->' + r.relationship.destVertexId,
                        id: r.relationship.id,
                        relationshipType: r.relationship.label,
                        source: r.relationship.sourceVertexId,
                        target: r.relationship.destVertexId
                    })
                };
                r.displayLabel = ontologyRelationships.byTitle[r.relationship.label].displayName;
            });

            var groupedByType = _.groupBy(relationships, function(r) {

                // Has Entity are collected into references (no matter
                // relationship direction
                if (r.relationship.label === 'hasEntity') {
                    return 'references';
                }

                // Group all that are relations from this vertex (not dest)
                if (r.relationship.sourceVertexId === vertex.id) {
                    return r.displayLabel;
                }

                // Collect all relationships that are destined here
                // into section
                return 'inverse';
            });
            var sortedKeys = Object.keys(groupedByType);
            sortedKeys.sort(function(a,b) {
                // If in inverse group, sort by the type
                if (a === b && a === 'inverse') {
                    return a.displayLabel === b.displayLabel ? 0 : a.displayLabel < b.displayLabel ? -1 : 1;
                }

                // If in references group sort by the title
                if (a === b && a === 'references') {
                    return defaultSort(a.vertex.properties.title.value, b.vertex.properties.title.value);
                }

                // Specifies the special group sort order
                var groups = { inverse:1, references:2 };
                if (groups[a] && groups[b]) {
                    return defaultSort(groups[a], groups[b]);
                } else if (groups[a]) {
                    return 1;
                } else if (groups[b]) {
                    return -1;
                }

                return defaultSort(a, b);

                function defaultSort(x,y) {
                    return x === y ? 0 : x < y ? -1 : 1;
                }
            });

            var $rels = self.select('relationshipsSelector');
            $rels.html(relationshipsTemplate({
                relationshipsGroupedByType: groupedByType,
                sortedKeys: sortedKeys
            }));

            VertexList.attachTo($rels.find('.references'), {
                vertices: _.map(groupedByType.references, function(r) {
                    return r.vertices.other;
                }),
                infiniteScrolling: (groupedByType.references && groupedByType.references.length) > 0,
                total: totalReferences
            });
        };

        this.handleReferenceLoadingRequest = function(evt, data) {
            var self = this;

            this.handleCancelling(this.service.getVertexRelationships(this.attr.data.id, data.paging))
                .done(function(response) {
                    var relationships = response.relationships,
                        total = response.totalReferences;

                    self.trigger(
                        self.select('relationshipsSelector').find('.references'),
                        'addInfiniteVertices',
                        {
                            vertices: _.pluck(relationships, 'vertex'),
                            total: total
                        }
                    );

                });
        };

        this.onPaneClicked = function(evt) {
            var $target = $(evt.target);

            if (!$target.is('.add-new-properties') && $target.parents('.underneath').length === 0) {
                PropertyForm.teardownAll();
            }

            if ($target.is('.entity, .artifact')) {
                var id = $target.data('vertexId');
                this.trigger('selectObjects', { vertices:[appData.vertex(id)] });
                evt.stopPropagation();
            } else if ($target.is('.relationship')) {
                var info = $target.data('info');
                this.trigger('selectObjects', { vertices:[info] });
                evt.stopPropagation();
            }
        };

    }
});

<|endoftext|>


<commit_before>function video(name, deps) {
  console.log('This is where the video plugin code would execute in the node process.');

  //instance variables
  this.deps = deps; //hold a reference to the plugin dependencies if you are going to use them
  this.rov = deps.rov; //explicitlly calling out the rov eventemitter
  this.cockpit = deps.cockpit; //explicitly calling out cockpit eventemitter

}

// Start is executed after all plugins have loaded. Activate listeners here.
video.prototype.start = function start(){
  var self = this; //set closure state variable for use in functions

}

// This is all that is required to enable settings for a plugin. They are
// automatically made editable, saved, etc from this definition.
video.prototype.getSettingSchema = function getSettingSchema(){
//from http://json-schema.org/videos.html
  return [{
	"title": "Video Settings",
	"type": "object",
  "id": "video", //Added to support namespacing configurations
	"properties": {
		"forward_camera_url": {
			"type": "string",
      "default" : "rov/camera1" //Added default
		},
		"framerate": {
			"type": "number",
      "default" : "30" //Added default
		},
		"resolution": {
			"type": "string",
			"default": "1280x720",
      "enum": ["1920x1080","1600x900","1360x768","1280x720","1024x768","800x600"]
		}
	}
}];
};

module.exports = function (name, deps) {
  return new video(name,deps);
};
<commit_msg>Updated default URL for video plugin<commit_after>function video(name, deps) {
  console.log('This is where the video plugin code would execute in the node process.');

  //instance variables
  this.deps = deps; //hold a reference to the plugin dependencies if you are going to use them
  this.rov = deps.rov; //explicitlly calling out the rov eventemitter
  this.cockpit = deps.cockpit; //explicitly calling out cockpit eventemitter

}

// Start is executed after all plugins have loaded. Activate listeners here.
video.prototype.start = function start(){
  var self = this; //set closure state variable for use in functions

}

// This is all that is required to enable settings for a plugin. They are
// automatically made editable, saved, etc from this definition.
video.prototype.getSettingSchema = function getSettingSchema(){
//from http://json-schema.org/videos.html
  return [{
	"title": "Video Settings",
	"type": "object",
  "id": "video", //Added to support namespacing configurations
	"properties": {
		"forward_camera_url": {
			"type": "string",
      "default" : "/rov/forward-camera" //Added default
		},
		"framerate": {
			"type": "number",
      "default" : "30" //Added default
		},
		"resolution": {
			"type": "string",
			"default": "1280x720",
      "enum": ["1920x1080","1600x900","1360x768","1280x720","1024x768","800x600"]
		}
	}
}];
};

module.exports = function (name, deps) {
  return new video(name,deps);
};
<|endoftext|>