<commit_before>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * A volume list model. This model combines the 2 lists.
 * @param {cr.ui.ArrayDataModel} volumesList The first list of the model.
 * @param {cr.ui.ArrayDataModel} pinnedList The second list of the model.
 * @constructor
 * @extends {cr.EventTarget}
 */
function VolumeListModel(volumesList, pinnedList) {
  this.volumesList_ = volumesList;
  this.pinnedList_ = pinnedList;

  // Generates a combined 'permuted' event from an event of either list.
  var permutedHandler = function(listNum, e) {
    var permutedEvent = new Event('permuted');
    var newPermutation = [];
    var newLength;
    if (listNum == 1) {
      newLength = e.newLength + this.pinnedList_.length;
      for (var i = 0; i < e.permutation.length; i++) {
        newPermutation[i] = e.permutation[i];
      }
      for (var i = 0; i < this.pinnedList_.length; i++) {
        newPermutation[i + e.permutation.length] = i + e.newLength;
      }
    } else {
      var volumesLen = this.volumesList_.length;
      newLength = e.newLength + volumesLen;
      for (var i = 0; i < volumesLen; i++) {
        newPermutation[i] = i;
      }
      for (var i = 0; i < e.permutation.length; i++) {
        newPermutation[i + volumesLen] =
            (e.permutation[i] !== -1) ? (e.permutation[i] + volumesLen) : -1;
      }
    }

    permutedEvent.newLength = newLength;
    permutedEvent.permutation = newPermutation;
    this.dispatchEvent(permutedEvent);
  };
  this.volumesList_.addEventListener('permuted', permutedHandler.bind(this, 1));
  this.pinnedList_.addEventListener('permuted', permutedHandler.bind(this, 2));

  // Generates a combined 'change' event from an event of either list.
  var changeHandler = function(listNum, e) {
    var changeEvent = new Event('change');
    changeEvent.index =
        (listNum == 1) ? e.index : (e.index + this.volumesList_.length);
    this.dispatchEvent(changeEvent);
  };
  this.volumesList_.addEventListener('change', changeHandler.bind(this, 1));
  this.pinnedList_.addEventListener('change', changeHandler.bind(this, 2));

  // 'splice' and 'sorted' events are not implemented, since they are not used
  // in list.js.
}

/**
 * VolumeList inherits cr.EventTarget.
 */
VolumeListModel.prototype = {
  __proto__: cr.EventTarget.prototype,
  get length() { return this.length_(); }
};

/**
 * Returns the item at the given index.
 * @param {number} index The index of the entry to get.
 * @return {?string} The path at the given index.
 */
VolumeListModel.prototype.item = function(index) {
  var offset = this.volumesList_.length;
  if (index < offset) {
    var entry = this.volumesList_.item(index);
    return entry ? entry.fullPath : undefined;
  } else {
    return this.pinnedList_.item(index - offset);
  }
};

/**
 * Type of the item on the volume list.
 * @enum {number}
 */
VolumeListModel.ItemType = {
  ROOT: 1,
  PINNED: 2
};

/**
 * Returns the type of the item at the given index.
 * @param {number} index The index of the entry to get.
 * @return {VolumeListModel.ItemType} The type of the item.
 */
VolumeListModel.prototype.getItemType = function(index) {
  var offset = this.volumesList_.length;
  return index < offset ?
      VolumeListModel.ItemType.ROOT : VolumeListModel.ItemType.PINNED;
};

/**
 * Returns the number of items in the model.
 * @return {number} The length of the model.
 * @private
 */
VolumeListModel.prototype.length_ = function() {
  return this.volumesList_.length + this.pinnedList_.length;
};

/**
 * Returns the first matching item.
 * @param {Entry} item The entry to find.
 * @param {number=} opt_fromIndex If provided, then the searching start at
 *     the {@code opt_fromIndex}.
 * @return {number} The index of the first found element or -1 if not found.
 */
VolumeListModel.prototype.indexOf = function(item, opt_fromIndex) {
  for (var i = opt_fromIndex || 0; i < this.length; i++) {
    if (item === this.item(i))
      return i;
  }
  return -1;
};

/**
 * A volume item.
 * @constructor
 * @extends {HTMLLIElement}
 */
var VolumeItem = cr.ui.define('li');

VolumeItem.prototype = {
  __proto__: HTMLLIElement.prototype,

  decorate: function() {
    // Nothing to do.
  },
};

/**
 * A volume list.
 * @constructor
 * @extends {cr.ui.List}
 */
function VolumeList() {
}

/**
 * VolumeList inherits cr.ui.List.
 */
VolumeList.prototype.__proto__ = cr.ui.List.prototype;

/**
 * @param {HTMLElement} el Element to be DirectoryItem.
 * @param {DirectoryModel} directoryModel Current DirectoryModel.
 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
 *     folders.
 */
VolumeList.decorate = function(el, directoryModel, pinnedFolderModel) {
  el.__proto__ = VolumeList.prototype;
  el.decorate(directoryModel, pinnedFolderModel);
};

/**
 * @param {DirectoryModel} directoryModel Current DirectoryModel.
 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
 *     folders.
 */
VolumeList.prototype.decorate = function(directoryModel, pinnedFolderModel) {
  cr.ui.List.decorate(this);
  this.__proto__ = VolumeList.prototype;

  this.directoryModel_ = directoryModel;
  this.volumeManager_ = VolumeManager.getInstance();
  this.selectionModel = new cr.ui.ListSingleSelectionModel();

  this.directoryModel_.addEventListener('directory-changed',
      this.onCurrentDirectoryChanged_.bind(this));
  this.selectionModel.addEventListener(
      'change', this.onSelectionChange_.bind(this));
  this.selectionModel.addEventListener(
      'beforeChange', this.onBeforeSelectionChange_.bind(this));
  this.currentVolume_ = null;

  this.scrollBar_ = new ScrollBar();
  this.scrollBar_.initialize(this.parentNode, this);

  // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox'
  // role for better accessibility on ChromeOS.
  this.setAttribute('role', 'listbox');

  var self = this;
  this.itemConstructor = function(path) {
    return self.renderRoot_(path);
  };

  this.pinnedItemList_ = pinnedFolderModel;

  this.dataModel =
      new VolumeListModel(this.directoryModel_.getRootsList(),
                          this.pinnedItemList_);
};

/**
 * Creates an element of a volume. This method is called from cr.ui.List
 * internally.
 * @param {string} path Path of the directory to be rendered.
 * @return {VolumeItem} Rendered element.
 * @private
 */
VolumeList.prototype.renderRoot_ = function(path) {
  var li = new VolumeItem;
  // TODO(yoshiki): Move the following initialization code to the constructor
  // of VolumeItem.
  li.className = 'root-item';
  li.setAttribute('role', 'option');
  var dm = this.directoryModel_;
  var handleClick = function() {
    if (li.selected && path !== dm.getCurrentDirPath()) {
      this.directoryModel_.changeDirectory(path);
    }
  }.bind(this);
  li.addEventListener('click', handleClick);
  li.addEventListener(cr.ui.TouchHandler.EventType.TOUCH_START, handleClick);

  var isRoot = PathUtil.isRootPath(path);
  var rootType = PathUtil.getRootType(path);

  var iconDiv = cr.doc.createElement('div');
  iconDiv.className = 'volume-icon';
  iconDiv.setAttribute('volume-type-icon', rootType);
  if (rootType === RootType.REMOVABLE) {
    iconDiv.setAttribute('volume-subtype',
        this.volumeManager_.getDeviceType(path));
  }
  li.appendChild(iconDiv);

  var div = cr.doc.createElement('div');
  div.className = 'root-label';

  div.textContent = PathUtil.getFolderLabel(path);
  li.appendChild(div);

  if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) {
    var eject = cr.doc.createElement('div');
    eject.className = 'root-eject';
    eject.addEventListener('click', function(event) {
      event.stopPropagation();
      var unmountCommand = cr.doc.querySelector('command#unmount');
      // Let's make sure 'canExecute' state of the command is properly set for
      // the root before executing it.
      unmountCommand.canExecuteChange(li);
      unmountCommand.execute(li);
    }.bind(this));
    // Block other mouse handlers.
    eject.addEventListener('mouseup', function(e) { e.stopPropagation() });
    eject.addEventListener('mousedown', function(e) { e.stopPropagation() });
    li.appendChild(eject);
  }

  if (this.contextMenu_ && (!isRoot ||
      rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS))
    cr.ui.contextMenuHandler.setContextMenu(li, this.contextMenu_);

  cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR);
  cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR);

  // If the current directory is already set.
  if (this.currentVolume_ == path) {
    setTimeout(function() {
      this.selectedItem = path;
    }.bind(this), 0);
  }

  return li;
};

/**
 * Sets a context menu. Context menu is enabled only on archive and removable
 * volumes as of now.
 *
 * @param {cr.ui.Menu} menu Context menu.
 */
VolumeList.prototype.setContextMenu = function(menu) {
  this.contextMenu_ = menu;

  for (var i = 0; i < this.dataModel.length; i++) {
    var path = this.dataModel.item(i);
    var itemType = this.dataModel.getItemType(i);
    var type = PathUtil.getRootType(path);
    // Context menu is set only to archive and removable volumes.
    if (itemType == VolumeListModel.ItemType.PINNED ||
        type == RootType.ARCHIVE || type == RootType.REMOVABLE) {
      cr.ui.contextMenuHandler.setContextMenu(this.getListItemByIndex(i),
                                              this.contextMenu_);
    }
  }
};

/**
 * Selects the n-th volume from the list.
 * @param {number} index Volume index.
 * @return {boolean} True for success, otherwise false.
 */
VolumeList.prototype.selectByIndex = function(index) {
  if (index < 0 || index > this.dataModel.length - 1)
    return false;

  var newPath = this.dataModel.item(index);
  if (!newPath || this.currentVolume_ == newPath)
    return false;

  this.currentVolume_ = newPath;
  this.directoryModel_.changeDirectory(this.currentVolume_);
  return true;
};

/**
 * Handler before root item change.
 * @param {Event} event The event.
 * @private
 */
VolumeList.prototype.onBeforeSelectionChange_ = function(event) {
  if (event.changes.length == 1 && !event.changes[0].selected)
    event.preventDefault();
};

/**
 * Handler for root item being clicked.
 * @param {Event} event The event.
 * @private
 */
VolumeList.prototype.onSelectionChange_ = function(event) {
  // This handler is invoked even when the volume list itself changes the
  // selection. In such case, we shouldn't handle the event.
  if (this.dontHandleSelectionEvent_)
    return;

  this.selectByIndex(this.selectionModel.selectedIndex);
};

/**
 * Invoked when the current directory is changed.
 * @param {Event} event The event.
 * @private
 */
VolumeList.prototype.onCurrentDirectoryChanged_ = function(event) {
  var path = event.newDirEntry.fullPath || this.dataModel.getCurrentDirPath();
  var newRootPath = PathUtil.getRootPath(path);

  // Sets |this.currentVolume_| in advance to prevent |onSelectionChange_()|
  // from calling |DirectoryModel.ChangeDirectory()| again.
  this.currentVolume_ = newRootPath;

  // Synchronizes the volume list selection with the current directory, after
  // it is changed outside of the volume list.

  // (1) Select the nearest parent directory (including the pinned directories).
  var bestMatchIndex = -1;
  var bestMatchSubStringLen = 0;
  for (var i = 0; i < this.dataModel.length; i++) {
    var itemPath = this.dataModel.item(i);
    if (path.indexOf(itemPath) == 0) {
      if (bestMatchSubStringLen < itemPath.length) {
        bestMatchIndex = i;
        bestMatchSubStringLen = itemPath.length;
      }
    }
  }
  if (bestMatchIndex != -1) {
    // Not to invoke the handler of this instance, sets the guard.
    this.dontHandleSelectionEvent_ = true;
    this.selectionModel.selectedIndex = bestMatchIndex;
    this.dontHandleSelectionEvent_ = false;
    return;
  }

  // (2) Selects the volume of the current directory.
  for (var i = 0; i < this.dataModel.length; i++) {
    var itemPath = this.dataModel.item(i);
    if (PathUtil.getRootPath(itemPath) == newRootPath) {
      // Not to invoke the handler of this instance, sets the guard.
      this.dontHandleSelectionEvent_ = true;
      this.selectionModel.selectedIndex = i;
      this.dontHandleSelectionEvent_ = false;
      return;
    }
  }
};
<commit_msg>Cleanup: Remove unnecessary property: VolumeList.currentVolume_<commit_after>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * A volume list model. This model combines the 2 lists.
 * @param {cr.ui.ArrayDataModel} volumesList The first list of the model.
 * @param {cr.ui.ArrayDataModel} pinnedList The second list of the model.
 * @constructor
 * @extends {cr.EventTarget}
 */
function VolumeListModel(volumesList, pinnedList) {
  this.volumesList_ = volumesList;
  this.pinnedList_ = pinnedList;

  // Generates a combined 'permuted' event from an event of either list.
  var permutedHandler = function(listNum, e) {
    var permutedEvent = new Event('permuted');
    var newPermutation = [];
    var newLength;
    if (listNum == 1) {
      newLength = e.newLength + this.pinnedList_.length;
      for (var i = 0; i < e.permutation.length; i++) {
        newPermutation[i] = e.permutation[i];
      }
      for (var i = 0; i < this.pinnedList_.length; i++) {
        newPermutation[i + e.permutation.length] = i + e.newLength;
      }
    } else {
      var volumesLen = this.volumesList_.length;
      newLength = e.newLength + volumesLen;
      for (var i = 0; i < volumesLen; i++) {
        newPermutation[i] = i;
      }
      for (var i = 0; i < e.permutation.length; i++) {
        newPermutation[i + volumesLen] =
            (e.permutation[i] !== -1) ? (e.permutation[i] + volumesLen) : -1;
      }
    }

    permutedEvent.newLength = newLength;
    permutedEvent.permutation = newPermutation;
    this.dispatchEvent(permutedEvent);
  };
  this.volumesList_.addEventListener('permuted', permutedHandler.bind(this, 1));
  this.pinnedList_.addEventListener('permuted', permutedHandler.bind(this, 2));

  // Generates a combined 'change' event from an event of either list.
  var changeHandler = function(listNum, e) {
    var changeEvent = new Event('change');
    changeEvent.index =
        (listNum == 1) ? e.index : (e.index + this.volumesList_.length);
    this.dispatchEvent(changeEvent);
  };
  this.volumesList_.addEventListener('change', changeHandler.bind(this, 1));
  this.pinnedList_.addEventListener('change', changeHandler.bind(this, 2));

  // 'splice' and 'sorted' events are not implemented, since they are not used
  // in list.js.
}

/**
 * VolumeList inherits cr.EventTarget.
 */
VolumeListModel.prototype = {
  __proto__: cr.EventTarget.prototype,
  get length() { return this.length_(); }
};

/**
 * Returns the item at the given index.
 * @param {number} index The index of the entry to get.
 * @return {?string} The path at the given index.
 */
VolumeListModel.prototype.item = function(index) {
  var offset = this.volumesList_.length;
  if (index < offset) {
    var entry = this.volumesList_.item(index);
    return entry ? entry.fullPath : undefined;
  } else {
    return this.pinnedList_.item(index - offset);
  }
};

/**
 * Type of the item on the volume list.
 * @enum {number}
 */
VolumeListModel.ItemType = {
  ROOT: 1,
  PINNED: 2
};

/**
 * Returns the type of the item at the given index.
 * @param {number} index The index of the entry to get.
 * @return {VolumeListModel.ItemType} The type of the item.
 */
VolumeListModel.prototype.getItemType = function(index) {
  var offset = this.volumesList_.length;
  return index < offset ?
      VolumeListModel.ItemType.ROOT : VolumeListModel.ItemType.PINNED;
};

/**
 * Returns the number of items in the model.
 * @return {number} The length of the model.
 * @private
 */
VolumeListModel.prototype.length_ = function() {
  return this.volumesList_.length + this.pinnedList_.length;
};

/**
 * Returns the first matching item.
 * @param {Entry} item The entry to find.
 * @param {number=} opt_fromIndex If provided, then the searching start at
 *     the {@code opt_fromIndex}.
 * @return {number} The index of the first found element or -1 if not found.
 */
VolumeListModel.prototype.indexOf = function(item, opt_fromIndex) {
  for (var i = opt_fromIndex || 0; i < this.length; i++) {
    if (item === this.item(i))
      return i;
  }
  return -1;
};

/**
 * A volume item.
 * @constructor
 * @extends {HTMLLIElement}
 */
var VolumeItem = cr.ui.define('li');

VolumeItem.prototype = {
  __proto__: HTMLLIElement.prototype,

  decorate: function() {
    // Nothing to do.
  },
};

/**
 * A volume list.
 * @constructor
 * @extends {cr.ui.List}
 */
function VolumeList() {
}

/**
 * VolumeList inherits cr.ui.List.
 */
VolumeList.prototype.__proto__ = cr.ui.List.prototype;

/**
 * @param {HTMLElement} el Element to be DirectoryItem.
 * @param {DirectoryModel} directoryModel Current DirectoryModel.
 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
 *     folders.
 */
VolumeList.decorate = function(el, directoryModel, pinnedFolderModel) {
  el.__proto__ = VolumeList.prototype;
  el.decorate(directoryModel, pinnedFolderModel);
};

/**
 * @param {DirectoryModel} directoryModel Current DirectoryModel.
 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
 *     folders.
 */
VolumeList.prototype.decorate = function(directoryModel, pinnedFolderModel) {
  cr.ui.List.decorate(this);
  this.__proto__ = VolumeList.prototype;

  this.directoryModel_ = directoryModel;
  this.volumeManager_ = VolumeManager.getInstance();
  this.selectionModel = new cr.ui.ListSingleSelectionModel();

  this.directoryModel_.addEventListener('directory-changed',
      this.onCurrentDirectoryChanged_.bind(this));
  this.selectionModel.addEventListener(
      'change', this.onSelectionChange_.bind(this));
  this.selectionModel.addEventListener(
      'beforeChange', this.onBeforeSelectionChange_.bind(this));

  this.scrollBar_ = new ScrollBar();
  this.scrollBar_.initialize(this.parentNode, this);

  // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox'
  // role for better accessibility on ChromeOS.
  this.setAttribute('role', 'listbox');

  var self = this;
  this.itemConstructor = function(path) {
    return self.renderRoot_(path);
  };

  this.pinnedItemList_ = pinnedFolderModel;

  this.dataModel =
      new VolumeListModel(this.directoryModel_.getRootsList(),
                          this.pinnedItemList_);
};

/**
 * Creates an element of a volume. This method is called from cr.ui.List
 * internally.
 * @param {string} path Path of the directory to be rendered.
 * @return {VolumeItem} Rendered element.
 * @private
 */
VolumeList.prototype.renderRoot_ = function(path) {
  var li = new VolumeItem;
  // TODO(yoshiki): Move the following initialization code to the constructor
  // of VolumeItem.
  li.className = 'root-item';
  li.setAttribute('role', 'option');
  var dm = this.directoryModel_;
  var handleClick = function() {
    if (li.selected && path !== dm.getCurrentDirPath()) {
      this.directoryModel_.changeDirectory(path);
    }
  }.bind(this);
  li.addEventListener('click', handleClick);
  li.addEventListener(cr.ui.TouchHandler.EventType.TOUCH_START, handleClick);

  var isRoot = PathUtil.isRootPath(path);
  var rootType = PathUtil.getRootType(path);

  var iconDiv = cr.doc.createElement('div');
  iconDiv.className = 'volume-icon';
  iconDiv.setAttribute('volume-type-icon', rootType);
  if (rootType === RootType.REMOVABLE) {
    iconDiv.setAttribute('volume-subtype',
        this.volumeManager_.getDeviceType(path));
  }
  li.appendChild(iconDiv);

  var div = cr.doc.createElement('div');
  div.className = 'root-label';

  div.textContent = PathUtil.getFolderLabel(path);
  li.appendChild(div);

  if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) {
    var eject = cr.doc.createElement('div');
    eject.className = 'root-eject';
    eject.addEventListener('click', function(event) {
      event.stopPropagation();
      var unmountCommand = cr.doc.querySelector('command#unmount');
      // Let's make sure 'canExecute' state of the command is properly set for
      // the root before executing it.
      unmountCommand.canExecuteChange(li);
      unmountCommand.execute(li);
    }.bind(this));
    // Block other mouse handlers.
    eject.addEventListener('mouseup', function(e) { e.stopPropagation() });
    eject.addEventListener('mousedown', function(e) { e.stopPropagation() });
    li.appendChild(eject);
  }

  if (this.contextMenu_ && (!isRoot ||
      rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS))
    cr.ui.contextMenuHandler.setContextMenu(li, this.contextMenu_);

  cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR);
  cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR);

  // If the current directory is already set.
  if (this.currentVolume_ == path) {
    setTimeout(function() {
      this.selectedItem = path;
    }.bind(this), 0);
  }

  return li;
};

/**
 * Sets a context menu. Context menu is enabled only on archive and removable
 * volumes as of now.
 *
 * @param {cr.ui.Menu} menu Context menu.
 */
VolumeList.prototype.setContextMenu = function(menu) {
  this.contextMenu_ = menu;

  for (var i = 0; i < this.dataModel.length; i++) {
    var path = this.dataModel.item(i);
    var itemType = this.dataModel.getItemType(i);
    var type = PathUtil.getRootType(path);
    // Context menu is set only to archive and removable volumes.
    if (itemType == VolumeListModel.ItemType.PINNED ||
        type == RootType.ARCHIVE || type == RootType.REMOVABLE) {
      cr.ui.contextMenuHandler.setContextMenu(this.getListItemByIndex(i),
                                              this.contextMenu_);
    }
  }
};

/**
 * Selects the n-th volume from the list.
 * @param {number} index Volume index.
 * @return {boolean} True for success, otherwise false.
 */
VolumeList.prototype.selectByIndex = function(index) {
  if (index < 0 || index > this.dataModel.length - 1)
    return false;

  var newPath = this.dataModel.item(index);
  if (!newPath)
    return false;

  // Prevents double-moving to the current directory.
  if (this.directoryModel_.getCurrentDirEntry().fullPath == newPath)
    return false;

  this.directoryModel_.changeDirectory(newPath);
  return true;
};

/**
 * Handler before root item change.
 * @param {Event} event The event.
 * @private
 */
VolumeList.prototype.onBeforeSelectionChange_ = function(event) {
  if (event.changes.length == 1 && !event.changes[0].selected)
    event.preventDefault();
};

/**
 * Handler for root item being clicked.
 * @param {Event} event The event.
 * @private
 */
VolumeList.prototype.onSelectionChange_ = function(event) {
  // This handler is invoked even when the volume list itself changes the
  // selection. In such case, we shouldn't handle the event.
  if (this.dontHandleSelectionEvent_)
    return;

  this.selectByIndex(this.selectionModel.selectedIndex);
};

/**
 * Invoked when the current directory is changed.
 * @param {Event} event The event.
 * @private
 */
VolumeList.prototype.onCurrentDirectoryChanged_ = function(event) {
  var path = event.newDirEntry.fullPath || this.dataModel.getCurrentDirPath();
  var newRootPath = PathUtil.getRootPath(path);

  // Synchronizes the volume list selection with the current directory, after
  // it is changed outside of the volume list.

  // (1) Select the nearest parent directory (including the pinned directories).
  var bestMatchIndex = -1;
  var bestMatchSubStringLen = 0;
  for (var i = 0; i < this.dataModel.length; i++) {
    var itemPath = this.dataModel.item(i);
    if (path.indexOf(itemPath) == 0) {
      if (bestMatchSubStringLen < itemPath.length) {
        bestMatchIndex = i;
        bestMatchSubStringLen = itemPath.length;
      }
    }
  }
  if (bestMatchIndex != -1) {
    // Not to invoke the handler of this instance, sets the guard.
    this.dontHandleSelectionEvent_ = true;
    this.selectionModel.selectedIndex = bestMatchIndex;
    this.dontHandleSelectionEvent_ = false;
    return;
  }

  // (2) Selects the volume of the current directory.
  for (var i = 0; i < this.dataModel.length; i++) {
    var itemPath = this.dataModel.item(i);
    if (PathUtil.getRootPath(itemPath) == newRootPath) {
      // Not to invoke the handler of this instance, sets the guard.
      this.dontHandleSelectionEvent_ = true;
      this.selectionModel.selectedIndex = i;
      this.dontHandleSelectionEvent_ = false;
      return;
    }
  }
};
<|endoftext|>


<commit_before>(function() {
	var recursive=function(str,o,c) {
		var p=[],s=str,o=str.match(o)||[],c=str.match(c)||[];
		var _build=function(str,o,i) {
			var s=str.substr(0,str.indexOf(o[i]));
			var e=str.substr(str.indexOf(o[i])+o[i].length);
			return s+_noop(o[i].length)+e;
		};
		var _noop=function(l) {
			var s=[];
			for(var i=0;i<l;i++) s.push('-');
			return s.join('');
		};
		var _judge=function(arr) {
			for(var i=0,c=0,p=0,t=[];i<arr.length;i++) {
				if(!c) p=i;
				if(arr[i]=='o') c++;
				if(arr[i]=='c') c--;
				if(!c && arr[i]) t.push(s.substr(p,i+1-p));
			}
			return t;
		};
		for(var i=0;i<o.length;i++) {
			p[str.indexOf(o[i])]='o';
			str=_build(str,o,i);
		}
		for(var i=0;i<c.length;i++) {
			p[str.indexOf(c[i])+c[i].length-1]='c';
			str=_build(str,c,i);
		}
		return _judge(p);
	};

	var replace=function(str,a,b) {
		var s=str.substr(0,str.indexOf(a));
		var e=str.substr(str.indexOf(a)+a.length);
		return s+b+e;
	};

	var reference=function(str,re) {
		var ret;
		str.replace(re,function() {
			ret=arguments;
			return ret[0];
		});
		return ret;
	};

	var evalString=new Function('exp',
		"return exp;"
	);

	this.juicer=function(tpl,data) {
		var fn=arguments.callee;
		var re={
			'for':/{@for\s+([^}]*)}([\s\S]*){@\/for}/igm,
			'if':/{@if\s+([^}]*)}([\s\S]*){@\/if}/igm,
			'unit':/\[#(.*?)(\s+condition=["|'](.*?)["|'])?\]/igm,
		};
		var _for=function(str) {
			var ref=reference(str,re.for),chain=ref[1],inner=ref[2];
			if(!data[chain]) return '';
			for(var i=0,t=[];i<data[chain].length;i++) t.push(fn(inner,data[chain][i]));
			return t.join('');
		};
		var _if=function(str) {
			var ref=reference(str,re.if),condition=ref[1],inner=ref[2];
			if(evalString(fn(condition,data))) return fn(inner,data);
			return '';
		};
		var _unit=function($a,$1,$2,$3) {
			if(!$3 || evalString(fn($3,data))) return data[$1]?typeof(data[$1])=='object'?true:data[$1]:'';
			return '';
		};

		for(var i=0,__f=recursive(tpl,/{@for[^}]*?}/igm,/{@\/for}/igm);i<__f.length;i++) {
			tpl=replace(tpl,__f[i],_for(__f[i]));
		}

		for(var i=0,__i=recursive(tpl,/{@if[^}]*?}/igm,/{@\/if}/igm);i<__i.length;i++) {
			tpl=replace(tpl,__i[i],_if(__i[i]));
		}

		tpl=tpl.replace(re.unit,_unit);

		return tpl;
	};
})();
<commit_msg>fix syntax bug under ie.<commit_after>(function() {
	var recursive=function(str,o,c) {
		var p=[],s=str,o=str.match(o)||[],c=str.match(c)||[];
		var _build=function(str,o,i) {
			var s=str.substr(0,str.indexOf(o[i]));
			var e=str.substr(str.indexOf(o[i])+o[i].length);
			return s+_noop(o[i].length)+e;
		};
		var _noop=function(l) {
			var s=[];
			for(var i=0;i<l;i++) s.push('-');
			return s.join('');
		};
		var _judge=function(arr) {
			for(var i=0,c=0,p=0,t=[];i<arr.length;i++) {
				if(!c) p=i;
				if(arr[i]=='o') c++;
				if(arr[i]=='c') c--;
				if(!c && arr[i]) t.push(s.substr(p,i+1-p));
			}
			return t;
		};
		for(var i=0;i<o.length;i++) {
			p[str.indexOf(o[i])]='o';
			str=_build(str,o,i);
		}
		for(var i=0;i<c.length;i++) {
			p[str.indexOf(c[i])+c[i].length-1]='c';
			str=_build(str,c,i);
		}
		return _judge(p);
	};

	var replace=function(str,a,b) {
		var s=str.substr(0,str.indexOf(a));
		var e=str.substr(str.indexOf(a)+a.length);
		return s+b+e;
	};

	var reference=function(str,re) {
		var ret;
		str.replace(re,function() {
			ret=arguments;
			return ret[0];
		});
		return ret;
	};

	var evalString=new Function('exp',
		"return exp;"
	);

	this.juicer=function(tpl,data) {
		var fn=arguments.callee;
		var re={
			'_for':/{@for\s+([^}]*)}([\s\S]*){@\/for}/igm,
			'_if':/{@if\s+([^}]*)}([\s\S]*){@\/if}/igm,
			'_unit':/\[#(.*?)(\s+condition=["|'](.*?)["|'])?\]/igm,
		};
		var _for=function(str) {
			var ref=reference(str,re._for),chain=ref[1],inner=ref[2];
			if(!data[chain]) return '';
			for(var i=0,t=[];i<data[chain].length;i++) t.push(fn(inner,data[chain][i]));
			return t.join('');
		};
		var _if=function(str) {
			var ref=reference(str,re._if),condition=ref[1],inner=ref[2];
			if(evalString(fn(condition,data))) return fn(inner,data);
			return '';
		};
		var _unit=function($a,$1,$2,$3) {
			if(!$3 || evalString(fn($3,data))) return data[$1]?typeof(data[$1])=='object'?true:data[$1]:'';
			return '';
		};

		for(var i=0,__f=recursive(tpl,/{@for[^}]*?}/igm,/{@\/for}/igm);i<__f.length;i++) {
			tpl=replace(tpl,__f[i],_for(__f[i]));
		}

		for(var i=0,__i=recursive(tpl,/{@if[^}]*?}/igm,/{@\/if}/igm);i<__i.length;i++) {
			tpl=replace(tpl,__i[i],_if(__i[i]));
		}

		tpl=tpl.replace(re._unit,_unit);

		return tpl;
	};
})();
<|endoftext|>


<commit_before>/*
	@author: guokai
	@email/gtalk: badkaikai@gmail.com
	@blog/website: http://benben.cc
	@license: apache license,version 2.0

	@usage: http://paulguo.github.com/Juicer
	@version: 0.1.1
	@build: 110626120407
*/

(function() {
	var JSON;if(!JSON){JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||"null"}v=partial.length===0?"[]":gap?"[\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"]":"["+partial.join(",")+"]";gap=mind;return v}if(rep&&typeof rep==="object"){length=rep.length;for(i=0;i<length;i+=1){if(typeof rep[i]==="string"){k=rep[i];v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v)}}}}else{for(k in value){if(Object.prototype.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v)}}}}v=partial.length===0?"{}":gap?"{\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"}":"{"+partial.join(",")+"}";gap=mind;return v}}if(typeof JSON.stringify!=="function"){JSON.stringify=function(value,replacer,space){var i;gap="";indent="";if(typeof space==="number"){for(i=0;i<space;i+=1){indent+=" "}}else{if(typeof space==="string"){indent=space}}rep=replacer;if(replacer&&typeof replacer!=="function"&&(typeof replacer!=="object"||typeof replacer.length!=="number")){throw new Error("JSON.stringify")}return str("",{"":value})}}if(typeof JSON.parse!=="function"){JSON.parse=function(text,reviver){var j;function walk(holder,key){var k,v,value=holder[key];if(value&&typeof value==="object"){for(k in value){if(Object.prototype.hasOwnProperty.call(value,k)){v=walk(value,k);if(v!==undefined){value[k]=v}else{delete value[k]}}}}return reviver.call(holder,key,value)}text=String(text);cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})}if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""))){j=eval("("+text+")");return typeof reviver==="function"?walk({"":j},""):j}throw new SyntaxError("JSON.parse")}}}());
	var __hash=function(str) {
		var hash=0;
		if(str.length==0) return hash;
		for(var i=0;i<str.length;i++) {
			var _char=str.charCodeAt(i);
			hash=((hash<<5)-hash)+_char;
			hash=hash&hash;
		}
		return hash;
	};

	var __cache={};
	var __re={
		'_for':/{@for\s+([^}]*)}([\s\S]*){@\/for}/igm,
		'_if':/{@if\s+([^}]*)}([\s\S]*){@\/if}/igm,
		'_unit':/\[#(.*?)(\s+condition=["|'](.*?)["|'])?\]/igm,
		'_forstart':/{@for[^}]*?}/igm,
		'_forend':/{@\/for}/igm,
		'_ifstart':/{@if[^}]*?}/igm,
		'_ifend':/{@\/if}/igm,
		'_function':/^function[^{]*?{([\s\S]*)}$/igm
	};

	var __recursive=function(str,o,c) {
		var p=[],s=str,o=str.match(o)||[],c=str.match(c)||[];

		if(!str || !o.length || !c.length) return [];

		var _build=function(str,o,i) {
			var s=str.substr(0,str.indexOf(o[i]));
			var e=str.substr(str.indexOf(o[i])+o[i].length);
			return s+_noop(o[i].length)+e;
		};

		var _noop=function(l) {
			var s=[];
			for(var i=0;i<l;i++) s.push('-');
			return s.join('');
		};

		var _judge=function(arr) {
			for(var i=0,c=0,p=0,t=[];i<arr.length;i++) {
				if(!c) p=i;
				if(arr[i]=='o') c++;
				if(arr[i]=='c') c--;
				if(!c && arr[i]) t.push(s.substr(p,i+1-p));
			}
			return t;
		};
		if(!str || !o.length || !c.length) return [];
		for(var i=0;i<o.length;i++) {
			p[str.indexOf(o[i])]='o';
			str=_build(str,o,i);
		}

		for(var i=0;i<c.length;i++) {
			p[str.indexOf(c[i])+c[i].length-1]='c';
			str=_build(str,c,i);
		}

		return _judge(p);
	};

	var __replace=function(str,a,b) {
		var s=str.substr(0,str.indexOf(a));
		var e=str.substr(str.indexOf(a)+a.length);
		return s+b+e;
	};

	var __replaceAll=function(str,start,end,cb) {
		if(!str) return str;
		for(var i=0,__f=__recursive(str,start,end);i<__f.length;i++) {
			str=__replace(str,__f[i],cb(__f[i]));
		}
		return str;
	};

	var __reference=function(str,re) {
		var ret=[];
		if(!str) return ret;
		str.replace(re,function() {
			ret=arguments;
			return ret[0];
		});
		return ret;
	};

	var __evalString=new Function('exp',
		'return exp;'
	);

	var __escape=function(html) {
		return String(html)
			.replace(/&(?!\w+;)/g,'&')
			.replace(/</g,'<')
			.replace(/>/g,'>')
			.replace(/"/g,'"');
	};

	var __juicer=function(tpl,data,option) {
		var fn=arguments.callee;
		var html=tpl;
		var opt={cache:false};

		if(!option || option.cache!==false) {
			var ukey=tpl+__hash(JSON.stringify(data));
			if(__cache[ukey]) return __cache[ukey];
		}

		var _for=function(str) {
			var ref=__reference(str,__re._for),chain=ref[1],inner=ref[2];
			if(!data[chain]) return '';
			for(var i=0,t=[];i<data[chain].length;i++) {
				var dat=data[chain][i];dat['.']=i;
				t.push(fn(inner,dat,opt));
			}
			return t.join('');
		};

		var _if=function(str) {
			var ref=__reference(str,__re._if),condition=ref[1],inner=ref[2];
			if(__evalString(fn(condition,data,opt))) {
				return fn(inner,data,opt);
			}
			return '';
		};

		var _unit=function($a,$1,$2,$3) {
			if(!$3 || __evalString(fn($3,data,opt))) {
				if($1.search('#')!==-1 && ($1=$1.substr(1))) return data && data[$1]!==undefined?typeof(data[$1])=='object'?true:__escape(data[$1]):'';
				return data && data[$1]!==undefined?typeof(data[$1])=='object'?true:data[$1]:'';
			}
			return '';
		};

		html=__replaceAll(html,__re._forstart,__re._forend,_for);
		html=__replaceAll(html,__re._ifstart,__re._ifend,_if);
		html=html.replace(__re._unit,_unit);

		if(!option || option.cache!==false) {
			__cache[ukey]=html;
		}

		return html;
	};

	this.juicer=__juicer;
})();
<commit_msg>version 0.2.0 which had been reconstructed.<commit_after>/*
	@author: guokai
	@email/gtalk: badkaikai@gmail.com
	@blog/website: http://benben.cc
	@license: apache license,version 2.0
	@version: 0.2.0
*/

(function() {
	var juicer={
		version:'0.1.1'
	};

	this.__cache={};

	this.__escapehtml={
		__escapehash:{
			'<':'<',
			'>':'>',
			'"':'"',
			'&':'&'
		},
		__escapereplace:function(k) {
			return __escapehtml.__escapehash[k];
		},
		__escape:function(str) {
			return typeof(str)!=='string'?str:str.replace(/[&<>"]/igm,__escapehtml.__escapereplace);
		}
	};

	juicer.settings = {
		forstart:/{@for\s*([\w\.]*?)\s*as\s*(\w*?)}/igm,
		forend:/{@\/for}/igm,
		ifstart:/{@if\s*([^}]*?)}/igm,
		ifend:/{@\/if}/igm,
		interpolate:/\${([\s\S]+?)}/igm,
		noneencode:/\$\${([\s\S]+?)}/igm
	};

	juicer.template=function() {
		this.parse=function(tpl) {
			var buf=[].join('');

			tpl=tpl.replace(juicer.settings.forstart,function($,varname,alias) {
					return '<% for(var i=0,l='+varname+'.length;i<l;i++) {var '+alias+'='+varname+'[i]; %>';
				})
				.replace(juicer.settings.forend,'<% } %>')
				.replace(juicer.settings.ifstart,function($,condition) {
					return '<% if('+condition+') { %>';
				})
				.replace(juicer.settings.ifend,'<% } %>')
				.replace(juicer.settings.noneencode,function($,varname) {
					return '<%= '+varname+' %>';
				})
				.replace(juicer.settings.interpolate,function($,varname) {
					return '<%= __escapehtml.__escape('+varname+') %>';
				});

			buf+="var p=[];";
			buf+="with(data) {"+
					"p.push('" +
						tpl
							.replace(/[\r\t\n]/g," ")
							.split("<%").join("\t")
							.replace(/((^|%>)[^\t]*)'/g,"$1\r")
							.replace(/\t=(.*?)%>/g,"',$1,'")
							.split("\t").join("');")
							.split("%>").join("p.push('")
							.split("\r").join("\\'")+
					"');"+
				"};"+
				"return p.join('');";

			this.render=new Function('data',buf);
			return this;
		};
	};

	juicer.compile=function(tpl,options) {
		var engine=__cache[tpl]?__cache[tpl]:new this.template().parse(tpl);
		if(!options || options.cache!==false) __cache[tpl]=engine;
		return engine;
	};

	juicer.to_html=function(tpl,data,options) {
		return this.compile(tpl,options).render(data);
	};

	typeof(module)!=='undefined' && module.exports?module.exports=juicer:this.juicer=juicer;
})();
<|endoftext|>


<commit_before>Flow {
    document.query: 'body'
}
<commit_msg>Default view style now containts top bar for iOS<commit_after>Column {
    if (Device.platform === 'iOS') {
        padding.top: 20
    }

    Flow {
        layout.fillWidth: true
        layout.fillHeight: true
        document.query: 'body'
    }
}
<|endoftext|>


<commit_before>var React = require('react-native');
// var api = require('../Utils/api');

var {
  View,
  Text,
  StyleSheet,
  Navigator,
  TouchableOpacity
} = React;

class RequestFeedbackView extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      userId: this.props.route.userId,
      classId: this.props.route.classId
    };
  }

  selectFeebackOption(type) {
    console.log(type);
    // this.props.navigator.push({
    //   component: RequestFeedbackView,
    //   userId: this.state.userId,
    //   classId: classId,
    //   sceneConfig: {
    //     ...Navigator.SceneConfigs.FloatFromBottom,
    //     gestures: {}
    //   }
    // });
    // this.setState({
    //   isLoading: false,
    //   error:false
    // });
  }

  render() {
    return (
      <View style={{flex: 1, backgroundColor: '#ededed'}}>
        <View style={styles.viewContainer}>
          <View style={styles.titleContainer}>
            <Text style={styles.pageText}> Request Feedback: </Text>
          </View>
          <View style={styles.buttonsContainer}>
            <View style={styles.buttonContainer}>
              <TouchableOpacity onPress={this.selectFeebackOption.bind(this, 'thumbs')} style={styles.button}>
                <Text style={styles.buttonText}> Thumbs Check </Text>
              </TouchableOpacity>
            </View>
            <View style={styles.buttonContainer}>
              <TouchableOpacity onPress={this.selectFeebackOption.bind(this, 'multChoice')} style={styles.button}>
                <Text style={styles.buttonText}> Multiple Choice </Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  viewContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  pageText: {
    fontSize: 20
  },
  buttonsContainer: {
    padding: 20
  },
  buttonContainer: {
    margin: 20
  },
  button: {

  },
  buttonText: {
    fontSize: 20
  }
});

module.exports = RequestFeedbackView;<commit_msg>Factor out hardcoded feedback options into state<commit_after>var React = require('react-native');
// var api = require('../Utils/api');

var {
  View,
  Text,
  StyleSheet,
  Navigator,
  TouchableOpacity
} = React;

class RequestFeedbackView extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      userId: this.props.route.userId,
      classId: this.props.route.classId,
      lessonId: 'default',
      feedbackOptions: [
        {
          id: 1,
          name: 'Thumbs Check',
          /*options:{for preset answers?}*/
        },
        {
          id: 2,
          name: 'Multiple Choice'
        }
      ]
    };
  }

  selectFeebackOption(optionId) {
    console.log(optionId);
    // this.props.navigator.push({
    //   component: RequestFeedbackView,
    //   userId: this.state.userId,
    //   classId: classId,
    //   sceneConfig: {
    //     ...Navigator.SceneConfigs.FloatFromBottom,
    //     gestures: {}
    //   }
    // });
    // this.setState({
    //   isLoading: false,
    //   error:false
    // });
  }

  renderFeedbackOptions(options) {
    return options.map((option, index) => {
      return (
        <View style={styles.buttonContainer} key={index}>
          <TouchableOpacity onPress={this.selectFeebackOption.bind(this, option.id)} style={styles.button}>
            <Text style={styles.buttonText}> {option.name} </Text>
          </TouchableOpacity>
        </View>
      )
    })
  }

  render() {
    //need end class / back button
    return (
      <View style={{flex: 1, backgroundColor: '#ededed'}}>
        <View style={styles.viewContainer}>
          <View style={styles.titleContainer}>
            <Text style={styles.pageText}> Request Feedback: </Text>
          </View>
          <View style={styles.buttonsContainer}>
            {this.renderFeedbackOptions(this.state.feedbackOptions)}
          </View>
        </View>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  viewContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  pageText: {
    fontSize: 20
  },
  buttonsContainer: {
    padding: 20
  },
  buttonContainer: {
    margin: 20
  },
  button: {

  },
  buttonText: {
    fontSize: 20
  }
});

module.exports = RequestFeedbackView;<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { graphql, compose } from 'react-apollo';
import { connect } from 'react-redux';
import { exec } from 'child_process';
import fs from 'fs';
import { remote } from 'electron';
import PropTypes from 'prop-types';

import DesktopRecorder from '../libs/DesktopRecorder';
import MicrophoneRecorder from '../libs/MicrophoneRecorder';
import { mergeVideoAndAudio } from '../helpers/video';
import callApi, { getFileBlob, uploadFile } from '../helpers/apiCaller';

import GameShow from '../components/GameShow/GameShow';
import Loader from '../components/Loader';

import fullGameQuery from '../graphql/fullGame.graphql';
import addToMetric from '../graphql/addToMetric.graphql';

const { app } = remote;
const videoReader = new FileReader();
const audioReader = new FileReader();

const mapStateToProps = ({ game }) => (
  {
    ...game,
    downloadId: game.id
  }
);

const withGraphql = compose(
  graphql(addToMetric, {
    props: ({ mutate }) => ({
      incrementMetric: (gameId, metric) => mutate({ variables: { gameId, metric } }),
    }),
  }),
  graphql(fullGameQuery, {
    props: ({ data }) => {
      if (!data.game) return { loading: data.loading };
      if (data.error) return { hasErrors: true };
      return {
        game: data.game,
      };
    },
    options: (props) => ({ variables: { id: props.match.params.id } })
  })
);

const micOptions = {
  audioBitsPerSecond: 128000,
  mimeType: 'audio/webm;codecs=opus'
};

class GamePage extends Component {
  state = {
    activeSession: null,
    audioFile: null,
    desktopRecorder: null,
    finalVideo: null,
    micRecorder: null,
    micAllowed: true,
    s3Url: '',
    videoFile: null
  }

  componentWillReceiveProps({ game, loading }) {
    if (!loading && loading !== this.props.loading) {
      this.setState({
        desktopRecorder: new DesktopRecorder(this.onMediaStop, game.title),
        micRecorder: new MicrophoneRecorder(null, this.onMediaStop, micOptions)
      });
    }
  }

  handleChange = ({ target }) => {
    const { name } = target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    this.setState({ [name]: value });
  }

  onMediaStop = (type, blobObject) => {
    this.saveRecordedFile(type, blobObject.blob);
  }

  saveRecordedFile = (type, blob) => {
    const { micAllowed } = this.state;
    const reader = type === 'mic' ? audioReader : videoReader;
    const name = type === 'mic' ? 'audioFile' : 'videoFile';
    const appDataPath = app.getPath('appData');
    const path = `${appDataPath}/ASLibrary/Sessions/${type}-${new Date().getTime()}.webm`;

    reader.onload = () => {
      const buffer = Buffer.from(reader.result);
      fs.writeFile(path, buffer, {}, err => {
        if (err) {
          console.error(err);
          return;
        }
        if (micAllowed) {
          this.setState({ [name]: path }, this.mergeBlobs);
        } else {
          this.setState({ finalVideo: path });
        }
      });
    };

    reader.readAsArrayBuffer(blob);
  }

  getFileAndUpload = (path) => {
    getFileBlob(path, (blob) => {
      this.uploadVideo(blob);
    });
  }

  uploadVideo = (file) => {
    const { game } = this.props;
    const fileName = `test-${game._id}-${new Date().getTime()}.${file.type.split('/')[1]}`;
    let s3Url;
    callApi(`sign-s3?file-name=${fileName}&file-type=${file.type}`)
      .then(res => res.json())
      .then(({ signedRequest, url }) => {
        s3Url = url;
        return uploadFile(file, signedRequest);
      })
      .then(res => {
        if (res.status !== 200) return Promise.reject();
        this.setState({ s3Url });
        return s3Url;
      })
      .catch(() => console.log('error ocurred'));
  }

  mergeBlobs = () => {
    const { game } = this.props;
    const { audioFile, videoFile } = this.state;
    const appDataPath = app.getPath('appData');

    if (audioFile === null || videoFile === null) {
      setTimeout(() => this.mergeBlobs(), 500);
      return;
    }

    const output = `${appDataPath}/ASLibrary/Sessions/merged-${game._id}-${new Date().getTime()}.mp4`;

    mergeVideoAndAudio(audioFile, videoFile, output, (processedUrl) => {
      this.setState({ finalVideo: processedUrl });
      this.getFileAndUpload(processedUrl);
    });
  }

  openGame = (localPath, type = 'play') => {
    const { game } = this.props;
    const { micRecorder, desktopRecorder, micAllowed } = this.state;
    const openCommand = process.platform === 'darwin'
      ? `open -a ${localPath} --wait-apps`
      : localPath;

    if (type !== 'session') {
      exec(openCommand, error => { if (error) throw error; });
      return;
    }

    exec(openCommand, (error) => {
      if (error) throw error;

      // Game was closed
      desktopRecorder.stopRecording();
      if (micAllowed) micRecorder.stopRecording();
      document.getElementById(`feedback-${game._id}`).style.display = 'block';
    });

    setTimeout(() => desktopRecorder.startRecording(), 5000);
    if (micAllowed) setTimeout(() => micRecorder.startRecording(), 5000);
  };

  render() {
    const {
      game,
      loading,
      incrementMetric,
      isDownloading,
      downloadId
    } = this.props;
    const { activeSession, finalVideo, micAllowed } = this.state;

    return (
      loading
        ? <Loader />
        : <GameShow
          activeSession={activeSession || undefined}
          game={game}
          isDownloading={isDownloading}
          downloadId={downloadId}
          incrementMetric={incrementMetric}
          openGame={this.openGame}
          finalVideo={finalVideo}
          micAllowed={micAllowed}
          handleChange={this.handleChange}
        />
    );
  }
}

GamePage.propTypes = {
  loading: PropTypes.bool,
  game: PropTypes.object,
  isDownloading: PropTypes.bool,
  downloadId: PropTypes.string.isRequired,
  incrementMetric: PropTypes.func.isRequired
};

GamePage.defaultProps = {
  loading: false,
  game: {},
  isDownloading: false,
};

const GamePageWithProps = connect(mapStateToProps, null)(GamePage);
const GamePageWithData = withGraphql(GamePageWithProps);

export default GamePageWithData;
<commit_msg>Upload video file when recording missing file<commit_after>import React, { Component } from 'react';
import { graphql, compose } from 'react-apollo';
import { connect } from 'react-redux';
import { exec } from 'child_process';
import fs from 'fs';
import { remote } from 'electron';
import PropTypes from 'prop-types';

import DesktopRecorder from '../libs/DesktopRecorder';
import MicrophoneRecorder from '../libs/MicrophoneRecorder';
import { mergeVideoAndAudio } from '../helpers/video';
import callApi, { getFileBlob, uploadFile } from '../helpers/apiCaller';

import GameShow from '../components/GameShow/GameShow';
import Loader from '../components/Loader';

import fullGameQuery from '../graphql/fullGame.graphql';
import addToMetric from '../graphql/addToMetric.graphql';

const { app } = remote;
const videoReader = new FileReader();
const audioReader = new FileReader();

const mapStateToProps = ({ game }) => (
  {
    ...game,
    downloadId: game.id
  }
);

const withGraphql = compose(
  graphql(addToMetric, {
    props: ({ mutate }) => ({
      incrementMetric: (gameId, metric) => mutate({ variables: { gameId, metric } }),
    }),
  }),
  graphql(fullGameQuery, {
    props: ({ data }) => {
      if (!data.game) return { loading: data.loading };
      if (data.error) return { hasErrors: true };
      return {
        game: data.game,
      };
    },
    options: (props) => ({ variables: { id: props.match.params.id } })
  })
);

const micOptions = {
  audioBitsPerSecond: 128000,
  mimeType: 'audio/webm;codecs=opus'
};

class GamePage extends Component {
  state = {
    activeSession: null,
    audioFile: null,
    desktopRecorder: null,
    finalVideo: null,
    micRecorder: null,
    micAllowed: true,
    s3Url: '',
    videoFile: null
  }

  componentWillReceiveProps({ game, loading }) {
    if (!loading && loading !== this.props.loading) {
      this.setState({
        desktopRecorder: new DesktopRecorder(this.onMediaStop, game.title),
        micRecorder: new MicrophoneRecorder(null, this.onMediaStop, micOptions)
      });
    }
  }

  handleChange = ({ target }) => {
    const { name } = target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    this.setState({ [name]: value });
  }

  onMediaStop = (type, blobObject) => {
    this.saveRecordedFile(type, blobObject);
  }

  saveRecordedFile = (type, { blob, url }) => {
    const { micAllowed } = this.state;
    const reader = type === 'mic' ? audioReader : videoReader;
    const name = type === 'mic' ? 'audioFile' : 'videoFile';
    const appDataPath = app.getPath('appData');
    const path = `${appDataPath}/ASLibrary/Sessions/${type}-${new Date().getTime()}.webm`;

    if (!micAllowed && type !== 'mic') {
      this.setState({ finalVideo: url });
      this.uploadVideo(blob);
      return;
    }

    reader.onload = () => {
      const buffer = Buffer.from(reader.result);
      fs.writeFile(path, buffer, {}, err => {
        if (err) {
          console.error(err);
          return;
        }
        this.setState({ [name]: path }, this.mergeBlobs);
      });
    };

    reader.readAsArrayBuffer(blob);
  }

  getFileAndUpload = (path) => {
    getFileBlob(path, (blob) => {
      this.uploadVideo(blob);
    });
  }

  uploadVideo = (file) => {
    const { game } = this.props;
    const fileName = `test-${game._id}-${new Date().getTime()}.${file.type.split('/')[1]}`;
    let s3Url;
    callApi(`sign-s3?file-name=${fileName}&file-type=${file.type}`)
      .then(res => res.json())
      .then(({ signedRequest, url }) => {
        s3Url = url;
        return uploadFile(file, signedRequest);
      })
      .then(res => {
        if (res.status !== 200) return Promise.reject();
        this.setState({ s3Url });
        return s3Url;
      })
      .catch(() => console.log('error ocurred'));
  }

  mergeBlobs = () => {
    const { game } = this.props;
    const { audioFile, videoFile } = this.state;
    const appDataPath = app.getPath('appData');

    if (audioFile === null || videoFile === null) {
      setTimeout(() => this.mergeBlobs(), 500);
      return;
    }

    const output = `${appDataPath}/ASLibrary/Sessions/merged-${game._id}-${new Date().getTime()}.mp4`;

    mergeVideoAndAudio(audioFile, videoFile, output, (processedUrl) => {
      this.setState({ finalVideo: processedUrl });
      this.getFileAndUpload(processedUrl);
    });
  }

  openGame = (localPath, type = 'play') => {
    const { game } = this.props;
    const { micRecorder, desktopRecorder, micAllowed } = this.state;
    const openCommand = process.platform === 'darwin'
      ? `open -a ${localPath} --wait-apps`
      : localPath;

    if (type !== 'session') {
      exec(openCommand, error => { if (error) throw error; });
      return;
    }

    exec(openCommand, (error) => {
      if (error) throw error;

      // Game was closed
      desktopRecorder.stopRecording();
      if (micAllowed) micRecorder.stopRecording();
      document.getElementById(`feedback-${game._id}`).style.display = 'block';
    });

    setTimeout(() => desktopRecorder.startRecording(), 5000);
    if (micAllowed) setTimeout(() => micRecorder.startRecording(), 5000);
  };

  render() {
    const {
      game,
      loading,
      incrementMetric,
      isDownloading,
      downloadId
    } = this.props;
    const { activeSession, finalVideo, micAllowed } = this.state;

    return (
      loading
        ? <Loader />
        : <GameShow
          activeSession={activeSession || undefined}
          game={game}
          isDownloading={isDownloading}
          downloadId={downloadId}
          incrementMetric={incrementMetric}
          openGame={this.openGame}
          finalVideo={finalVideo}
          micAllowed={micAllowed}
          handleChange={this.handleChange}
        />
    );
  }
}

GamePage.propTypes = {
  loading: PropTypes.bool,
  game: PropTypes.object,
  isDownloading: PropTypes.bool,
  downloadId: PropTypes.string.isRequired,
  incrementMetric: PropTypes.func.isRequired
};

GamePage.defaultProps = {
  loading: false,
  game: {},
  isDownloading: false,
};

const GamePageWithProps = connect(mapStateToProps, null)(GamePage);
const GamePageWithData = withGraphql(GamePageWithProps);

export default GamePageWithData;
<|endoftext|>


<commit_before>import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import fetch from 'isomorphic-unfetch'

let apolloClient = null

// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
  global.fetch = fetch
}

function create (initialState) {
  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: new HttpLink({
      uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
      opts: {
        credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
      }
    }),
    cache: new InMemoryCache().restore(initialState || {}),
  })
}

export default function initApollo (initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (!process.browser) {
    return create(initialState)
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = create(initialState)
  }

  return apolloClient
}
<commit_msg>Fixed how credentials opt is passed to HttpLink (#3269)<commit_after>import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import fetch from 'isomorphic-unfetch'

let apolloClient = null

// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
  global.fetch = fetch
}

function create (initialState) {
  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: new HttpLink({
      uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
      credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
    }),
    cache: new InMemoryCache().restore(initialState || {}),
  })
}

export default function initApollo (initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (!process.browser) {
    return create(initialState)
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = create(initialState)
  }

  return apolloClient
}
<|endoftext|>


<commit_before>const express = require('express');
const request = require('request');
const app = express();
var SelfReloadJSON = require('self-reload-json');
const appRoot = require('app-root-path');
var config = new SelfReloadJSON(appRoot + '/data/settings.json');
var updates = new SelfReloadJSON(appRoot + '/data/updates.json');

var doUpdates = false;

module.exports.set = function(app) {
    app.get('/updates', function(req, res) {
        res.setHeader('Content-Type', 'application/json');
        if (config.settings.token != "") {
            doUpdates = true;
        } else {
            doUpdates = false
        }
        getUpdates(function(err, result) {
            if (err) {
                response.send(500, { error: 'something went wrong' });
            } else {
                updates.updates = result;
                updates.save();
                res.send(result);
            }
        });
    });

    setInterval(function() {
        if (doUpdates) {
            getUpdates(function(err, result) {
                if (err) {
                    console.log('something went wrong');
                } else {
                    updates.updates = result;
                    updates.save();
                }
            });
        }
    }, 5000);

    var getUpdates = function(callback) {
        var endpoint = "/updates";
        var token = config.settings.token;
        var url = config.settings.apiUrl + endpoint + '?access_token=' + token;
        //console.log("getting updates from " + url);

        request({
            url: url,
            json: true
        }, function(error, response, body) {

            if (!error && response.statusCode === 200) {
                callback(null, body);
            } else {
                callback(error);
            }
        });
    };
};<commit_msg>fix for updates error<commit_after>const express = require('express');
const request = require('request');
const app = express();
var SelfReloadJSON = require('self-reload-json');
const appRoot = require('app-root-path');
var config = new SelfReloadJSON(appRoot + '/data/settings.json');
var updates = new SelfReloadJSON(appRoot + '/data/updates.json');

var doUpdates = false;

module.exports.set = function(app) {
    app.get('/updates', function(req, res) {
        res.setHeader('Content-Type', 'application/json');
        if (config.settings.token != "") {
            doUpdates = true;
        } else {
            doUpdates = false
        }
        getUpdates(function(err, result) {
            if (err) {
                res.send(500, { error: 'something went wrong' });
            } else {
                updates.updates = result;
                updates.save();
                res.send(result);
            }
        });
    });

    setInterval(function() {
        if (doUpdates) {
            getUpdates(function(err, result) {
                if (err) {
                    console.log('something went wrong');
                } else {
                    updates.updates = result;
                    updates.save();
                }
            });
        }
    }, 5000);

    var getUpdates = function(callback) {
        var endpoint = "/updates";
        var token = config.settings.token;
        var url = config.settings.apiUrl + endpoint + '?access_token=' + token;
        //console.log("getting updates from " + url);

        request({
            url: url,
            json: true
        }, function(error, response, body) {

            if (!error && response.statusCode === 200) {
                callback(null, body);
            } else {
                callback(error);
            }
        });
    };
};<|endoftext|>


<commit_before>var excludeIndex = 0;

function displayDifference(pointList, canvasSelector) {
	for (let canvas of document.querySelectorAll(canvasSelector)) {
		var ctx = canvas.getContext("2d");
		ctx.fillStyle = "#FF0000";
		ctx.save();
		for(var i= 0; i < pointList.length; i++)
		{
			ctx.fillRect(pointList[i][0],pointList[i][1],1,1);
		}
	}
}

/**
 * Extract any int from the provided string
 * e.g: 'foo_4' => 4
 * @param strValue
 * @returns
 */
function getIntValue(strValue) {
	if (typeof strValue === 'number') {
		return strValue;
	}

	var number = strValue.match(/\d+/);
	return parseInt(number, 10);
}

/**
 * initialize drawing on canvas
 * @param excludeTable		Table to which we write the new exclusion zones
 * @param testStepId
 * @returns
 */
function initDraw(excludeTable, testStepId, snapshotId) {

	var canvas = document.getElementById('canvas_' + testStepId + '_' + snapshotId);
	var staticCanvas = document.getElementById('canvasStatic_' + testStepId + '_' + snapshotId);

    function setMousePosition(e) {
		let scrollPosition = $('#editionModal_' + testStepId + '_' + snapshotId).scrollTop();
        let ev = e || window.event; //Moz || IE
        /*if (ev.pageX) { //Moz
            mouse.x = ev.pageX;
            mouse.y = ev.pageY;
        } else if (ev.clientX) { //IE*/
            mouse.x = ev.clientX + document.body.scrollLeft;
            mouse.y = ev.clientY + scrollPosition;
        //}
    }

    function getCanvasRatio() {
        let canvasWidth = document.getElementById("stepSnapshot_" + testStepId + '_' + snapshotId).clientWidth;
        return getIntValue(canvas.getAttribute("width")) / getIntValue(canvasWidth);
    }

    function getCanvasCoords() {
    	return $('#canvas_' + testStepId + '_' + snapshotId).offset();
    }

    function updateSizeAndCoords() {
    	canvasCoords = getCanvasCoords();
    	canvas.style.height = document.getElementById("stepSnapshot_" + testStepId + '_' + snapshotId).clientHeight;
    	canvasRatio = getCanvasRatio();
    }

    var mouse = {
        x: 0,
        y: 0,
        startX: 0,
        startY: 0
    };

    var canvasCoords = null;
    var element = null;
    var canvasRatio = null;

    updateSizeAndCoords();

    $( window ).resize(function() {
    	updateSizeAndCoords();
    });

    canvas.onmousemove = function (e) {
//    	console.log("scroll: " + $(document).scrollTop())
        setMousePosition(e);
        if (element !== null) {
            element.style.width = Math.abs(mouse.x - mouse.startX) + 'px';
            element.style.height = Math.abs(mouse.y - mouse.startY) + 'px';
            element.style.left = (mouse.x - mouse.startX < 0) ? mouse.x - canvasCoords.left  + 'px' : mouse.startX - canvasCoords.left + 'px';
            element.style.top = (mouse.y - mouse.startY < 0) ? mouse.y - canvasCoords.top + $(document).scrollTop() + 'px' : mouse.startY - canvasCoords.top + $(document).scrollTop() + 'px';

        }
    }

    canvas.onclick = function (e) {

        if (element !== null) {

            canvas.style.cursor = "default";
            console.log("finsihed.");

            // change coordinates according to ratio between real image size and its size on screen
            var realLeft = Math.round(getIntValue(element.style.left) * canvasRatio);
            var realTop = Math.round(getIntValue(element.style.top) * canvasRatio);
            var realWidth = Math.round(getIntValue(element.style.width) * canvasRatio);
            var realHeight = Math.round(getIntValue(element.style.height) * canvasRatio);

            // change coordinates in pixels to coordinates in percentages, so that rectangles are resized when browser window is also resized
            element.style.width = getIntValue(element.style.width) * 100 / canvas.clientWidth + '%';
            element.style.height = getIntValue(element.style.height) * 100 / canvas.clientHeight + '%';
            element.style.left = getIntValue(element.style.left) * 100 / canvas.clientWidth + '%';
            element.style.top = getIntValue(element.style.top) * 100 / canvas.clientHeight + '%';

            // add line to exclude excludeTable
            let row = document.createElement('tr');
            let colActive = document.createElement('td');
            let activeBox = document.createElement('input')
            activeBox.type = 'checkbox';
            activeBox.checked = 'checked';
            activeBox.setAttribute('name', 'new_exclude_' + testStepId + '_' + snapshotId);
            activeBox.setAttribute('r_x', realLeft);
            activeBox.setAttribute('r_y', realTop);
            activeBox.setAttribute('r_w', realWidth);
            activeBox.setAttribute('r_h', realHeight);

			// clic will enable/disable the static element (on summary) and its equivalent on edition modal
            activeBox.setAttribute('onclick', "toggleElement('new_exclude_" + excludeIndex + "');toggleElement('new_exclude_" + excludeIndex + "Static');");
            colActive.appendChild(activeBox);
            let colPosition = document.createElement('td');
            colPosition.innerHTML = '(x, y, width, height)=(' + realLeft + ', ' + realTop + ', ' + realWidth + ', ' + realHeight + ')';
            row.appendChild(colActive);
            row.appendChild(colPosition);
            excludeTable.appendChild(row);

			// copy the created rectangle so that it is displayed in summary
			var elementCopy = element.cloneNode(true);
			elementCopy.id = element.id + "Static";

			staticCanvas.appendChild(elementCopy);
            element = null;

        } else {

            excludeIndex++;
            console.log("begun.");
            mouse.startX = mouse.x;
            mouse.startY = mouse.y;
            element = document.createElement('div');
            element.className = 'rectangle'
            element.id = 'new_exclude_' + excludeIndex;
            element.style.left = mouse.x + 'px';
            element.style.top = mouse.y + 'px';
            canvas.appendChild(element);
            canvas.style.cursor = "crosshair";

        }
    }
}

/**
 * draw rectangles on canvas, based on the content of the table containing "exclude" elements
 * @param canvas			the canvas element where to draw rectangles
 * @param snapshotHeight	height of the snapshot on which rectangles will be drawn. It helps calculating canvas ratio
 * @param idSuffix			a suffix to add to 'id' of each rectangle to differentiate rectangles from editable zone and rectangles from summary
 * @param testStepId
 * @returns
 */
function drawExistingExcludeZones(canvas, snapshotHeight, idSuffix, testStepId, snapshotId) {

	canvas.style.height = snapshotHeight;
	var canvasRatio = getIntValue(canvas.clientWidth) / getIntValue(canvas.getAttribute("width"));
	var currentExcludes = document.querySelectorAll('#excludeZoneTable_' + testStepId + '_' + snapshotId + ' input');

	for(var i= 0; i < currentExcludes.length; i++) {
		if (currentExcludes[i].checked) {

			var idx = getIntValue(currentExcludes[i].id)

			element = document.createElement('div');
		    element.className = 'rectangle'
		    element.id = 'exclude_' + idx + idSuffix;

		    element.style.left = Math.round(getIntValue(currentExcludes[i].getAttribute('r_x')) * canvasRatio) * 100 / canvas.clientWidth + '%';
		    element.style.top = Math.round(getIntValue(currentExcludes[i].getAttribute('r_y')) * canvasRatio) * 100 / canvas.clientHeight + '%';
		    element.style.width = Math.round(getIntValue(currentExcludes[i].getAttribute('r_w')) * canvasRatio) * 100 / canvas.clientWidth + '%';
		    element.style.height = Math.round(getIntValue(currentExcludes[i].getAttribute('r_h')) * canvasRatio) * 100 / canvas.clientHeight + '%';

		    canvas.appendChild(element);
		}
	}
}

/**
 * Store in database the list of exclusion zones. Look for newly created zones (name=new_exclude_<stepId>) and for current
 * @param snapshotId
 * @param refSnapshotId
 * @param testCaseId
 * @param testStepId
 * @returns
 */
function updateExcludeZones(snapshotId, refSnapshotId, testCaseId, testStepId) {
	var newExcludes = document.getElementsByName('new_exclude_' + testStepId + '_' + snapshotId);

	// add new exclude zones
	for(let i= 0; i < newExcludes.length; i++) {
		if (newExcludes[i].checked) {
			$.post({
				url: '/snapshot/api/exclude/',
				data: "x=" + newExcludes[i].getAttribute('r_x')
					+ "&y=" + newExcludes[i].getAttribute('r_y')
					+ "&width=" + newExcludes[i].getAttribute('r_w')
					+ "&height=" + newExcludes[i].getAttribute('r_h')
					+ "&snapshot=" + refSnapshotId

			});
		}
	}

	// remove exclude zones that should not be kept
	var currentExcludes = document.querySelectorAll('#excludeZoneTable_' + testStepId + '_' + snapshotId + ' input');

	for(let i= 0; i < currentExcludes.length; i++) {
		if (!currentExcludes[i].checked) {

			$.ajax({
				type: 'DELETE',
				url: '/snapshot/api/exclude/' + getIntValue(currentExcludes[i].id) + '/',
				async: false
			});
		}
	}

	// recompute difference
	$.ajax({
		type: 'POST',
		url: '/snapshot/compare/compute/' + snapshotId + '/',
		async: false
	});

	// reload step
	updatePanel('/snapshot/compare/picture/' + testCaseId + '/' + testStepId, 'step_' + testStepId);

}<commit_msg>issue #51: make all ajax calls asynchronous when saving as it sometimes lead to diff being computed without the new exclude zones<commit_after>var excludeIndex = 0;

function displayDifference(pointList, canvasSelector) {
	for (let canvas of document.querySelectorAll(canvasSelector)) {
		var ctx = canvas.getContext("2d");
		ctx.fillStyle = "#FF0000";
		ctx.save();
		for(var i= 0; i < pointList.length; i++)
		{
			ctx.fillRect(pointList[i][0],pointList[i][1],1,1);
		}
	}
}

/**
 * Extract any int from the provided string
 * e.g: 'foo_4' => 4
 * @param strValue
 * @returns
 */
function getIntValue(strValue) {
	if (typeof strValue === 'number') {
		return strValue;
	}

	var number = strValue.match(/\d+/);
	return parseInt(number, 10);
}

/**
 * initialize drawing on canvas
 * @param excludeTable		Table to which we write the new exclusion zones
 * @param testStepId
 * @returns
 */
function initDraw(excludeTable, testStepId, snapshotId) {

	var canvas = document.getElementById('canvas_' + testStepId + '_' + snapshotId);
	var staticCanvas = document.getElementById('canvasStatic_' + testStepId + '_' + snapshotId);

    function setMousePosition(e) {
		let scrollPosition = $('#editionModal_' + testStepId + '_' + snapshotId).scrollTop();
        let ev = e || window.event; //Moz || IE
        /*if (ev.pageX) { //Moz
            mouse.x = ev.pageX;
            mouse.y = ev.pageY;
        } else if (ev.clientX) { //IE*/
            mouse.x = ev.clientX + document.body.scrollLeft;
            mouse.y = ev.clientY + scrollPosition;
        //}
    }

    function getCanvasRatio() {
        let canvasWidth = document.getElementById("stepSnapshot_" + testStepId + '_' + snapshotId).clientWidth;
        return getIntValue(canvas.getAttribute("width")) / getIntValue(canvasWidth);
    }

    function getCanvasCoords() {
    	return $('#canvas_' + testStepId + '_' + snapshotId).offset();
    }

    function updateSizeAndCoords() {
    	canvasCoords = getCanvasCoords();
    	canvas.style.height = document.getElementById("stepSnapshot_" + testStepId + '_' + snapshotId).clientHeight;
    	canvasRatio = getCanvasRatio();
    }

    var mouse = {
        x: 0,
        y: 0,
        startX: 0,
        startY: 0
    };

    var canvasCoords = null;
    var element = null;
    var canvasRatio = null;

    updateSizeAndCoords();

    $( window ).resize(function() {
    	updateSizeAndCoords();
    });

    canvas.onmousemove = function (e) {
//    	console.log("scroll: " + $(document).scrollTop())
        setMousePosition(e);
        if (element !== null) {
            element.style.width = Math.abs(mouse.x - mouse.startX) + 'px';
            element.style.height = Math.abs(mouse.y - mouse.startY) + 'px';
            element.style.left = (mouse.x - mouse.startX < 0) ? mouse.x - canvasCoords.left  + 'px' : mouse.startX - canvasCoords.left + 'px';
            element.style.top = (mouse.y - mouse.startY < 0) ? mouse.y - canvasCoords.top + $(document).scrollTop() + 'px' : mouse.startY - canvasCoords.top + $(document).scrollTop() + 'px';

        }
    }

    canvas.onclick = function (e) {

        if (element !== null) {

            canvas.style.cursor = "default";
            console.log("finsihed.");

            // change coordinates according to ratio between real image size and its size on screen
            var realLeft = Math.round(getIntValue(element.style.left) * canvasRatio);
            var realTop = Math.round(getIntValue(element.style.top) * canvasRatio);
            var realWidth = Math.round(getIntValue(element.style.width) * canvasRatio);
            var realHeight = Math.round(getIntValue(element.style.height) * canvasRatio);

            // change coordinates in pixels to coordinates in percentages, so that rectangles are resized when browser window is also resized
            element.style.width = getIntValue(element.style.width) * 100 / canvas.clientWidth + '%';
            element.style.height = getIntValue(element.style.height) * 100 / canvas.clientHeight + '%';
            element.style.left = getIntValue(element.style.left) * 100 / canvas.clientWidth + '%';
            element.style.top = getIntValue(element.style.top) * 100 / canvas.clientHeight + '%';

            // add line to exclude excludeTable
            let row = document.createElement('tr');
            let colActive = document.createElement('td');
            let activeBox = document.createElement('input')
            activeBox.type = 'checkbox';
            activeBox.checked = 'checked';
            activeBox.setAttribute('name', 'new_exclude_' + testStepId + '_' + snapshotId);
            activeBox.setAttribute('r_x', realLeft);
            activeBox.setAttribute('r_y', realTop);
            activeBox.setAttribute('r_w', realWidth);
            activeBox.setAttribute('r_h', realHeight);

			// clic will enable/disable the static element (on summary) and its equivalent on edition modal
            activeBox.setAttribute('onclick', "toggleElement('new_exclude_" + excludeIndex + "');toggleElement('new_exclude_" + excludeIndex + "Static');");
            colActive.appendChild(activeBox);
            let colPosition = document.createElement('td');
            colPosition.innerHTML = '(x, y, width, height)=(' + realLeft + ', ' + realTop + ', ' + realWidth + ', ' + realHeight + ')';
            row.appendChild(colActive);
            row.appendChild(colPosition);
            excludeTable.appendChild(row);

			// copy the created rectangle so that it is displayed in summary
			var elementCopy = element.cloneNode(true);
			elementCopy.id = element.id + "Static";

			staticCanvas.appendChild(elementCopy);
            element = null;

        } else {

            excludeIndex++;
            console.log("begun.");
            mouse.startX = mouse.x;
            mouse.startY = mouse.y;
            element = document.createElement('div');
            element.className = 'rectangle'
            element.id = 'new_exclude_' + excludeIndex;
            element.style.left = mouse.x + 'px';
            element.style.top = mouse.y + 'px';
            canvas.appendChild(element);
            canvas.style.cursor = "crosshair";

        }
    }
}

/**
 * draw rectangles on canvas, based on the content of the table containing "exclude" elements
 * @param canvas			the canvas element where to draw rectangles
 * @param snapshotHeight	height of the snapshot on which rectangles will be drawn. It helps calculating canvas ratio
 * @param idSuffix			a suffix to add to 'id' of each rectangle to differentiate rectangles from editable zone and rectangles from summary
 * @param testStepId
 * @returns
 */
function drawExistingExcludeZones(canvas, snapshotHeight, idSuffix, testStepId, snapshotId) {

	// do not draw if canvas / snapshot is not visible
	if (snapshotHeight == 0) {
		return
	}

	canvas.style.height = snapshotHeight;
	var canvasRatio = getIntValue(canvas.clientWidth) / getIntValue(canvas.getAttribute("width"));
	var currentExcludes = document.querySelectorAll('#excludeZoneTable_' + testStepId + '_' + snapshotId + ' input');
	//console.log("canvas: " + canvas.clientWidth + '-' + canvas.clientHeight + '-' + snapshotHeight);

	for(var i= 0; i < currentExcludes.length; i++) {
		if (currentExcludes[i].checked) {

			var idx = getIntValue(currentExcludes[i].id)

			element = document.createElement('div');
		    element.className = 'rectangle'
		    element.id = 'exclude_' + idx + idSuffix;

		    element.style.left = Math.round(getIntValue(currentExcludes[i].getAttribute('r_x')) * canvasRatio) * 100 / canvas.clientWidth + '%';
		    element.style.top = Math.round(getIntValue(currentExcludes[i].getAttribute('r_y')) * canvasRatio) * 100 / snapshotHeight + '%';
		    element.style.width = Math.round(getIntValue(currentExcludes[i].getAttribute('r_w')) * canvasRatio) * 100 / canvas.clientWidth + '%';
		    element.style.height = Math.round(getIntValue(currentExcludes[i].getAttribute('r_h')) * canvasRatio) * 100 / snapshotHeight + '%';

		    canvas.appendChild(element);
		}
	}
}

/**
 * Store in database the list of exclusion zones. Look for newly created zones (name=new_exclude_<stepId>) and for current
 * @param snapshotId
 * @param refSnapshotId
 * @param testCaseId
 * @param testStepId
 * @returns
 */
function updateExcludeZones(snapshotId, refSnapshotId, testCaseId, testStepId) {
	var newExcludes = document.getElementsByName('new_exclude_' + testStepId + '_' + snapshotId);

	// add new exclude zones
	// these ajax calls are not asynchronous as they MUST finish before we call computing
	for(let i= 0; i < newExcludes.length; i++) {
		if (newExcludes[i].checked) {
			$.ajax({
				type: 'POST',
				async: false,
				url: '/snapshot/api/exclude/',
				data: "x=" + newExcludes[i].getAttribute('r_x')
					+ "&y=" + newExcludes[i].getAttribute('r_y')
					+ "&width=" + newExcludes[i].getAttribute('r_w')
					+ "&height=" + newExcludes[i].getAttribute('r_h')
					+ "&snapshot=" + refSnapshotId

			});
		}
	}

	// remove exclude zones that should not be kept
	var currentExcludes = document.querySelectorAll('#excludeZoneTable_' + testStepId + '_' + snapshotId + ' input');

	for(let i= 0; i < currentExcludes.length; i++) {
		if (!currentExcludes[i].checked) {

			// these ajax calls are not asynchronous as they MUST finish before we call computing
			$.ajax({
				type: 'DELETE',
				url: '/snapshot/api/exclude/' + getIntValue(currentExcludes[i].id) + '/',
				async: false
			});
		}
	}

	// recompute difference
	$.ajax({
		type: 'POST',
		url: '/snapshot/compare/compute/' + snapshotId + '/',
		async: false
	});

	// reload step
	updatePanel('/snapshot/compare/picture/' + testCaseId + '/' + testStepId, 'step_' + testStepId);

}<|endoftext|>


<commit_before>const url = require('url')
const request = require('request-promise')
const cheerio = require('cheerio')

function getTitle (uri) {
  const options = {
    transform: function (body) {
      return cheerio.load(body)
    },
    uri
  }

  return request(options)
    .then(function ($) {
      return $('meta[property="og:title"]').attr('content') || ''
    })
}
/**
 * Test a list of titles of diverse medias
 */

// The list of articles
const uris = [
  'http://www.abc.es/espana/abci-policia-entrega-juez-pendrive-sobre-caso-pujol-aparecio-ordenando-unos-cajones-201702091240_noticia.html',
  'http://www.ara.cat/societat/Quatre-controlats-Catalunya-aquesta-tecnica_0_1741625927.html',
  'http://www.elconfidencial.com/espana/2017-02-09/vistalegricidio-batalla-final-podemos-iglesias-errejon_1328697/',
  'http://www.eldiario.es/politica/Policia-PP-despues-cuestionar-sentencias_0_610688927.html',
  'http://www.elespanol.com/espana/20170213/193480960_0.html',
  'http://www.elmundo.es/espana/2017/02/09/589c3833e5fdeabc7a8b460b.html',
  'http://cultura.elpais.com/cultura/2017/02/08/actualidad/1486573775_868895.html',
  'http://www.elperiodico.com/es/noticias/politica/ana-mato-comparecer-juicio-gurtel-declarar-5796132',
  'http://www.esdiario.com/655099781/Garcia-desarma-a-Herrera-ante-la-divertida-mirada-de-Gabilondo-y-Del-Olmo.html',
  'http://www.europapress.es/sociedad/noticia-papa-garantiza-severidad-extrema-contra-pederastas-cura-puede-llegar-causar-tanto-mal-20170213142414.html',
  'http://www.huffingtonpost.es/2017/02/13/fernandez-ordonez_n_14718872.html',
  'http://www.lainformacion.com/politica/partidos/Gestora-PSOE-pablismo-leninismo-Podemos-Va_0_999200280.html',
  'http://www.larazon.es/espana/una-condena-del-supremo-a-homs-arrastraria-a-artur-mas-GJ14480710',
  'http://www.lavanguardia.com/economia/20170209/414177052262/alemania-exportaciones-superavit-2016-trump.html',
  'http://www.lavozdegalicia.es/noticia/espana/2017/02/13/juez-atribuye-presidente-murcia-delitos-fraude-cohecho/00031486982881679285690.htm',
  'http://www.libertaddigital.com/espana/politica/2017-02-13/rajoy-sobre-la-tension-entre-cospedal-y-santamaria-por-que-tengo-que-cambiarlas-1276592588/',
  'http://www.naciodigital.cat/noticia/125316/govern/pot/fer/millorar/acollida/refugiats',
  'https://okdiario.com/espana/2017/02/13/iglesias-aplica-lista-cremallera-relega-errejon-5o-puesto-del-consejo-ciudadano-748821',
  'http://www.publico.es/politica/iglesias-errejon-debe-primera-linea.html'
]

// From list of URIs to list of Promises
Promise
  .all(uris.map(getTitle))
  .then(articles => {
    for (let i = 0; i < uris.length; i++) {
      console.log(
        i,
        url.parse(uris[i]).hostname,
        articles[i].slice(0, 80)
      )
    }
  })
<commit_msg>Update the testing script to test 19 articles<commit_after>const url = require('url')
const downloadArticle = require('../src/downloadArticle')
/**
 * Test a list of titles of diverse medias
 */

// The list of articles
const uris = [
  'http://www.abc.es/espana/abci-policia-entrega-juez-pendrive-sobre-caso-pujol-aparecio-ordenando-unos-cajones-201702091240_noticia.html',
  'http://www.ara.cat/societat/Quatre-controlats-Catalunya-aquesta-tecnica_0_1741625927.html',
  'http://www.elconfidencial.com/espana/2017-02-09/vistalegricidio-batalla-final-podemos-iglesias-errejon_1328697/',
  'http://www.eldiario.es/politica/Policia-PP-despues-cuestionar-sentencias_0_610688927.html',
  'http://www.elespanol.com/espana/20170213/193480960_0.html',
  'http://www.elmundo.es/espana/2017/02/09/589c3833e5fdeabc7a8b460b.html',
  'http://cultura.elpais.com/cultura/2017/02/08/actualidad/1486573775_868895.html',
  'http://www.elperiodico.com/es/noticias/politica/ana-mato-comparecer-juicio-gurtel-declarar-5796132',
  'http://www.esdiario.com/655099781/Garcia-desarma-a-Herrera-ante-la-divertida-mirada-de-Gabilondo-y-Del-Olmo.html',
  'http://www.europapress.es/sociedad/noticia-papa-garantiza-severidad-extrema-contra-pederastas-cura-puede-llegar-causar-tanto-mal-20170213142414.html',
  'http://www.huffingtonpost.es/2017/02/13/fernandez-ordonez_n_14718872.html',
  'http://www.lainformacion.com/politica/partidos/Gestora-PSOE-pablismo-leninismo-Podemos-Va_0_999200280.html',
  'http://www.larazon.es/espana/una-condena-del-supremo-a-homs-arrastraria-a-artur-mas-GJ14480710',
  'http://www.lavanguardia.com/economia/20170209/414177052262/alemania-exportaciones-superavit-2016-trump.html',
  'http://www.lavozdegalicia.es/noticia/espana/2017/02/13/juez-atribuye-presidente-murcia-delitos-fraude-cohecho/00031486982881679285690.htm',
  'http://www.libertaddigital.com/espana/politica/2017-02-13/rajoy-sobre-la-tension-entre-cospedal-y-santamaria-por-que-tengo-que-cambiarlas-1276592588/',
  'http://www.naciodigital.cat/noticia/125316/govern/pot/fer/millorar/acollida/refugiats',
  'https://okdiario.com/espana/2017/02/13/iglesias-aplica-lista-cremallera-relega-errejon-5o-puesto-del-consejo-ciudadano-748821',
  'http://www.publico.es/politica/iglesias-errejon-debe-primera-linea.html'
]

// From list of URIs to list of Promises
const p = uris
  .map(uri => downloadArticle(uri)
    .then(article => {
      console.log(uri)
      console.log('title: %s', article.title.slice(0, 80))
      console.log('image: %s', url.resolve(uri, article.image))
      console.log('html:  %s', article.html.slice(0, 80))
      console.log()
      return ''
    })
  )

console.log()

Promise.all(p)
  .then(() => { console.log('parsing finished') })
  .catch(err => console.log(err))
<|endoftext|>


<commit_before>import $ from 'blingblingjs'
import hotkeys from 'hotkeys-js'

import { EditText } from './text'
import { canMoveLeft, canMoveRight, canMoveUp } from './move'
import { watchImagesForUpload } from './imageswap'
import { htmlStringToDom } from './utils'

// todo: alignment guides
export function Selectable() {
  const elements          = $('body')
  let selected            = []
  let selectedCallbacks   = []

  watchImagesForUpload()

  elements.on('click', e => {
    e.preventDefault()
    e.stopPropagation()
    if (isOffBounds(e.target)) return
    if (!e.shiftKey) unselect_all()
    select(e.target)
  })

  elements.on('dblclick', e => {
    e.preventDefault()
    e.stopPropagation()
    if (isOffBounds(e.target)) return
    EditText([e.target], {focus:true})
    $('tool-pallete')[0].toolSelected('text')
  })

  hotkeys('esc', _ =>
    selected.length && unselect_all())

  hotkeys('cmd+d', e => {
    const root_node = selected[0]
    if (!root_node) return

    const deep_clone = root_node.cloneNode(true)
    selected.push(deep_clone)
    root_node.parentNode.insertBefore(deep_clone, root_node.nextSibling)
    e.preventDefault()
  })

  hotkeys('backspace,del,delete', e =>
    selected.length && delete_all())

  document.addEventListener('copy', e => {
    if (selected[0] && this.node_clipboard !== selected[0])
      e.clipboardData.setData('text/html', selected[0].outerHTML)
  })

  document.addEventListener('paste', e => {
    const potentialHTML = e.clipboardData.getData('text/html')
    if (selected[0] && potentialHTML) {
      e.preventDefault()
      selected[0].appendChild(
        htmlStringToDom(potentialHTML))
    }
  })

  hotkeys('cmd+e,cmd+shift+e', (e, {key}) => {
    e.preventDefault()

    // TODO: need a much smarter system here
    // only expands base tag names atm
    if (selected[0].nodeName !== 'DIV')
      expandSelection({
        root_node: selected[0],
        all: key.includes('shift'),
      })
  })

  elements.on('selectstart', e =>
    !isOffBounds(e.target)
    && selected.length
    && selected[0].textContent != e.target.textContent
    && e.preventDefault())

  hotkeys('tab,shift+tab,enter,shift+enter', (e, {key}) => {
    if (selected.length !== 1) return

    e.preventDefault()
    e.stopPropagation()

    const current = selected[0]

    if (key.includes('shift')) {
      if (key.includes('tab') && canMoveLeft(current)) {
        unselect_all()
        select(canMoveLeft(current))
      }
      if (key.includes('enter') && canMoveUp(current)) {
        unselect_all()
        select(current.parentNode)
      }
    }
    else {
      if (key.includes('tab') && canMoveRight(current)) {
        unselect_all()
        select(canMoveRight(current))
      }
      if (key.includes('enter') && current.children.length) {
        unselect_all()
        select(current.children[0])
      }
    }
  })

  elements.on('mouseover', ({target}) =>
    !isOffBounds(target) && target.setAttribute('data-hover', true))

  elements.on('mouseout', ({target}) =>
    target.removeAttribute('data-hover'))

  elements.on('click', ({target}) => {
    if (!isOffBounds(target) && !selected.filter(el => el == target).length)
      unselect_all()
    tellWatchers()
  })

  const select = el => {
    if (el.nodeName === 'svg' || el.ownerSVGElement) return

    el.setAttribute('data-selected', true)
    selected.unshift(el)
    tellWatchers()
  }

  const unselect_all = () => {
    selected
      .forEach(el =>
        $(el).attr({
          'data-selected': null,
          'data-selected-hide': null,
        }))

    selected = []
  }

  const delete_all = () => {
    selected.forEach(el =>
      el.remove())
    selected = []
  }

  const expandSelection = ({root_node, all}) => {
    if (all) {
      const unselecteds = $(root_node.nodeName.toLowerCase() + ':not([data-selected])')
      unselecteds.forEach(select)
    }
    else {
      const potentials = $(root_node.nodeName.toLowerCase())
      if (!potentials) return

      const root_node_index = potentials.reduce((index, node, i) =>
        node == root_node
          ? index = i
          : index
      , null)

      if (root_node_index !== null) {
        if (!potentials[root_node_index + 1]) {
          const potential = potentials.filter(el => !el.attr('data-selected'))[0]
          if (potential) select(potential)
        }
        else {
          select(potentials[root_node_index + 1])
        }
      }
    }
  }

  const isOffBounds = node =>
    node.closest && (node.closest('tool-pallete') || node.closest('.metatip'))

  const onSelectedUpdate = cb =>
    selectedCallbacks.push(cb) && cb(selected)

  const removeSelectedCallback = cb =>
    selectedCallbacks = selectedCallbacks.filter(callback => callback != cb)

  const tellWatchers = () =>
    selectedCallbacks.forEach(cb => cb(selected))

  return {
    select,
    unselect_all,
    onSelectedUpdate,
    removeSelectedCallback,
  }
}<commit_msg>added cmd+x (cut), fixed local instance copy/paste<commit_after>import $ from 'blingblingjs'
import hotkeys from 'hotkeys-js'

import { EditText } from './text'
import { canMoveLeft, canMoveRight, canMoveUp } from './move'
import { watchImagesForUpload } from './imageswap'
import { htmlStringToDom } from './utils'

// todo: alignment guides
export function Selectable() {
  const elements          = $('body')
  let selected            = []
  let selectedCallbacks   = []

  watchImagesForUpload()

  elements.on('click', e => {
    e.preventDefault()
    e.stopPropagation()
    if (isOffBounds(e.target)) return
    if (!e.shiftKey) unselect_all()
    select(e.target)
  })

  elements.on('dblclick', e => {
    e.preventDefault()
    e.stopPropagation()
    if (isOffBounds(e.target)) return
    EditText([e.target], {focus:true})
    $('tool-pallete')[0].toolSelected('text')
  })

  hotkeys('esc', _ =>
    selected.length && unselect_all())

  hotkeys('cmd+d', e => {
    const root_node = selected[0]
    if (!root_node) return

    const deep_clone = root_node.cloneNode(true)
    deep_clone.removeAttribute('data-selected')
    // selected.push(deep_clone)
    root_node.parentNode.insertBefore(deep_clone, root_node.nextSibling)
    e.preventDefault()
  })

  hotkeys('backspace,del,delete', e =>
    selected.length && delete_all())

  document.addEventListener('copy', e => {
    if (selected[0] && this.node_clipboard !== selected[0]) {
      let $node = selected.slice(0,1)[0]
      $node.removeAttribute('data-selected')
      this.copy_backup = $node.outerHTML
      e.clipboardData.setData('text/html', this.copy_backup)
    }
  })

  document.addEventListener('cut', e => {
    if (selected[0] && this.node_clipboard !== selected[0]) {
      let $node = selected.slice(0,1)[0]
      $node.removeAttribute('data-selected')
      this.copy_backup = $node.outerHTML
      e.clipboardData.setData('text/html', this.copy_backup)
      selected[0].remove()
    }
  })

  document.addEventListener('paste', e => {
    const potentialHTML = e.clipboardData.getData('text/html') || this.copy_backup
    if (selected[0] && potentialHTML) {
      e.preventDefault()
      selected[0].appendChild(
        htmlStringToDom(potentialHTML))
    }
  })

  hotkeys('cmd+e,cmd+shift+e', (e, {key}) => {
    e.preventDefault()

    // TODO: need a much smarter system here
    // only expands base tag names atm
    if (selected[0].nodeName !== 'DIV')
      expandSelection({
        root_node: selected[0],
        all: key.includes('shift'),
      })
  })

  elements.on('selectstart', e =>
    !isOffBounds(e.target)
    && selected.length
    && selected[0].textContent != e.target.textContent
    && e.preventDefault())

  hotkeys('tab,shift+tab,enter,shift+enter', (e, {key}) => {
    if (selected.length !== 1) return

    e.preventDefault()
    e.stopPropagation()

    const current = selected[0]

    if (key.includes('shift')) {
      if (key.includes('tab') && canMoveLeft(current)) {
        unselect_all()
        select(canMoveLeft(current))
      }
      if (key.includes('enter') && canMoveUp(current)) {
        unselect_all()
        select(current.parentNode)
      }
    }
    else {
      if (key.includes('tab') && canMoveRight(current)) {
        unselect_all()
        select(canMoveRight(current))
      }
      if (key.includes('enter') && current.children.length) {
        unselect_all()
        select(current.children[0])
      }
    }
  })

  elements.on('mouseover', ({target}) =>
    !isOffBounds(target) && target.setAttribute('data-hover', true))

  elements.on('mouseout', ({target}) =>
    target.removeAttribute('data-hover'))

  elements.on('click', ({target}) => {
    if (!isOffBounds(target) && !selected.filter(el => el == target).length)
      unselect_all()
    tellWatchers()
  })

  const select = el => {
    if (el.nodeName === 'svg' || el.ownerSVGElement) return

    el.setAttribute('data-selected', true)
    selected.unshift(el)
    tellWatchers()
  }

  const unselect_all = () => {
    selected
      .forEach(el =>
        $(el).attr({
          'data-selected': null,
          'data-selected-hide': null,
        }))

    selected = []
  }

  const delete_all = () => {
    selected.forEach(el =>
      el.remove())
    selected = []
  }

  const expandSelection = ({root_node, all}) => {
    if (all) {
      const unselecteds = $(root_node.nodeName.toLowerCase() + ':not([data-selected])')
      unselecteds.forEach(select)
    }
    else {
      const potentials = $(root_node.nodeName.toLowerCase())
      if (!potentials) return

      const root_node_index = potentials.reduce((index, node, i) =>
        node == root_node
          ? index = i
          : index
      , null)

      if (root_node_index !== null) {
        if (!potentials[root_node_index + 1]) {
          const potential = potentials.filter(el => !el.attr('data-selected'))[0]
          if (potential) select(potential)
        }
        else {
          select(potentials[root_node_index + 1])
        }
      }
    }
  }

  const isOffBounds = node =>
    node.closest && (node.closest('tool-pallete') || node.closest('.metatip'))

  const onSelectedUpdate = cb =>
    selectedCallbacks.push(cb) && cb(selected)

  const removeSelectedCallback = cb =>
    selectedCallbacks = selectedCallbacks.filter(callback => callback != cb)

  const tellWatchers = () =>
    selectedCallbacks.forEach(cb => cb(selected))

  return {
    select,
    unselect_all,
    onSelectedUpdate,
    removeSelectedCallback,
  }
}<|endoftext|>