<commit_before>/**
 * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */

/**
 * Adds common functionality to a SlickGrid that is backed by a DataView.
 */
(function($) {

  /** @constructor */
  function PopupManager(_$layout, _$popupList, _grid, _gridName, _dataView, _liveResultsClient, _userConfig) {

    var self = this;
    var _logger = new Logger("PopupManager", "debug");
    var _$details = [];
    var _$explains = {};

    function getPopup(className, $cell, rowId, colId) {
      var $popup = $("

");
      $popup.data("rowId", rowId);
      $popup.data("colId", colId);
      $popup.appendTo($cell.parent().parent());
      return $popup;
    }

    function revealPopup($cell, $popup, component) {
      $popup.position({
        my: "right top",
        at: "right bottom",
        of: $cell,
        offset: "0 -1",
        collision: "none"
      }).show('blind');
      $popup.data("component", component);
    }

    function removePopup($popup) {
      var component = $popup.data("component");
      if (component && component.destroy) {
        component.destroy();
      }
      $popup.remove();
      $popup = null;
    }

    function removeDetail(rowId, colId) {
      for (var idx = 0; idx < _$details.length; idx++) {
        var $popup = _$details[idx];
        var existingRowId = $popup.data("rowId");
        var existingColId = $popup.data("colId");
        if (existingRowId != rowId || existingColId != colId) {
          continue;
        }
        removePopup($popup);
        _$details.splice(idx, 1);
        _liveResultsClient.stopDetailedCellUpdates(_gridName, rowId, colId);
        var row = _dataView.getItemById(rowId);
        delete row.detailComponents[colId];
        return true;
      };
      return false;
    }

    function closeExplain($popup, $cell, rowId, colId) {
      delete _$explains[rowId][colId];
      if ($.isEmptyObject(_$explains[rowId])) {
        delete _$explains[rowId];
      }
      removePopup($popup);

      _liveResultsClient.stopDepGraphExplain(rowId, colId);
      var row = _dataView.getItemById(rowId);
      delete row.explainComponents[colId];
      $cell.unbind('mouseenter', handleExplainCellHoverIn);
      $cell.unbind('mouseleave', handleExplainCellHoverOut);
      $cell.removeClass("explain");
      $cell.removeClass("explain-hover");

      if ($.isEmptyObject(_$explains)) {
        _$layout.close('east');
      }

      return true;
    }

    function handleCloseClick(e) {
      var $popup = $(e.target).closest('.popup');
      var rowId = $popup.data("rowId");
      var colId = $popup.data("colId");
      var $cell = $popup.data("cell");
      closeExplain($popup, $cell, rowId, colId);
    }

    function handleExplainPopupHoverIn(e) {
      var $popup = $(e.target).closest(".popup");
      $popup.addClass("popup-hover");
      var $cell = $popup.data("cell");
      $cell.addClass("explain-hover");
    }

    function handleExplainPopupHoverOut(e) {
      var $popup = $(e.target).closest(".popup");
      $popup.removeClass("popup-hover");
      var $cell = $popup.data("cell");
      $cell.removeClass("explain-hover");
    }

    function handleExplainCellHoverIn(e) {
      var $cell = $(e.target).closest('.slick-cell');
      $cell.addClass('explain-hover');

      var rowId = $cell.data("rowId");
      var colId = $cell.data("colId");
      var $popup = _$explains[rowId][colId];
      $popup.addClass('popup-hover');
    }

    function handleExplainCellHoverOut(e) {
      var $cell = $(e.target).closest('.slick-cell');
      $cell.removeClass('explain-hover');

      var rowId = $cell.data("rowId");
      var colId = $cell.data("colId");
      var $popup = _$explains[rowId][colId].removeClass("popup-hover");
    }

    function associatePopupWithNewCell($cell, $popup, rowId, colId) {
      $popup.data('cell', $cell);
      $cell.hover(handleExplainCellHoverIn, handleExplainCellHoverOut);
      $cell.data('rowId', rowId);
      $cell.data('colId', colId);
      $cell.addClass("explain");
    }

    //-----------------------------------------------------------------------
    // Public API

    this.toggleDetail = function($cell, columnStructure, formatter, rowId) {
      var colId = columnStructure.colId;
      if (removeDetail(rowId, colId)) {
        return;
      }

      var row = _dataView.getItemById(rowId);
      if (!row.detailComponents) {
        row.detailComponents = {};
      }

      var $popup = getPopup('detail-popup', $cell, rowId, colId);
      var $content = $("

").appendTo($popup);
      var detailComponent = formatter.createDetail($popup, $content, rowId, columnStructure, _userConfig, row[columnStructure.colId]);

      if (detailComponent.resize) {
        var afterResized = function() {
          var detailComponent = $popup.data("component");
          if (detailComponent && detailComponent.resize) {
            detailComponent.resize();
          }
        };
        $popup.resizable({
          handles: 'se',
          helper: 'ui-state-highlight ui-corner-bottom',
          minWidth: 300,
          minHeight: 200,
          stop: afterResized});
      }

      revealPopup($cell, $popup, detailComponent);
      row.detailComponents[colId] = detailComponent;
      _$details.push($popup);

      _liveResultsClient.startDetailedCellUpdates(_gridName, rowId, colId);
      _liveResultsClient.triggerImmediateUpdate();
    }

    this.openExplain = function($cell, colId, rowId, popupTitle) {
      if (_dataView.rows[rowId][colId] == false) {
        // Not in dep graph
        return;
      }
      if (_$explains[rowId] && _$explains[rowId][colId]) {
        // Already exists - find it
        var $popup = _$explains[rowId][colId];
      } else {
        // Create it
        var row = _dataView.getItemById(rowId);
        if (!row.explainComponents) {
          row.explainComponents = {};
        }

        _$layout.open('east', true, 'none');
        var $closeText = $("Close").click(handleCloseClick);
        var $popupHead = $("

").append($closeText).append("Dependency Graph for " + popupTitle + "
");
        var $popupContent = $("

").height(300).width("100%");
        var $popup = $("

").append($popupHead).append($popupContent).hover(handleExplainPopupHoverIn, handleExplainPopupHoverOut);
        $popup.data('rowId', rowId);
        $popup.data('colId', colId);
        _$popupList.append($popup);
        var depGraphViewer = new DepGraphViewer($popupContent, rowId, colId, _liveResultsClient, _userConfig);
        $popupContent.resizable({
          handles: 'se',
          helper: 'ui-state-highlight ui-corner-bottom',
          minHeight: 200,
          resize: function(event, ui) { ui.size.width = "100%"; },
          stop: function() { depGraphViewer.resize(); }
        });

        associatePopupWithNewCell($cell, $popup, rowId, colId);

        row.explainComponents[colId] = depGraphViewer;
        if (!_$explains[rowId]) {
          _$explains[rowId] = {};
        }
        _$explains[rowId][colId] = $popup;

        _liveResultsClient.startDepGraphExplain(rowId, colId);
        _liveResultsClient.triggerImmediateUpdate();
      }

      // Scroll into view
      _$popupList.parent().scrollTo($popup, 800);
    }

    this.onNewCell = function($cell, rowId, colId) {
      if (!_$explains[rowId]) {
        return;
      }
      var $popup = _$explains[rowId][colId];
      if (!$popup) {
        return;
      }
      associatePopupWithNewCell($cell, $popup, rowId, colId);
    }

  }

  //-------------------------------------------------------------------------

  $.extend(true, window, {
    PopupManager : PopupManager
  });

}(jQuery));<commit_msg>[PLAT-1864] Fixing dep graphs on partially-collapsed portfolio<commit_after>/**
 * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */

/**
 * Adds common functionality to a SlickGrid that is backed by a DataView.
 */
(function($) {

  /** @constructor */
  function PopupManager(_$layout, _$popupList, _grid, _gridName, _dataView, _liveResultsClient, _userConfig) {

    var self = this;
    var _logger = new Logger("PopupManager", "debug");
    var _$details = [];
    var _$explains = {};

    function getPopup(className, $cell, rowId, colId) {
      var $popup = $("

");
      $popup.data("rowId", rowId);
      $popup.data("colId", colId);
      $popup.appendTo($cell.parent().parent());
      return $popup;
    }

    function revealPopup($cell, $popup, component) {
      $popup.position({
        my: "right top",
        at: "right bottom",
        of: $cell,
        offset: "0 -1",
        collision: "none"
      }).show('blind');
      $popup.data("component", component);
    }

    function removePopup($popup) {
      var component = $popup.data("component");
      if (component && component.destroy) {
        component.destroy();
      }
      $popup.remove();
      $popup = null;
    }

    function removeDetail(rowId, colId) {
      for (var idx = 0; idx < _$details.length; idx++) {
        var $popup = _$details[idx];
        var existingRowId = $popup.data("rowId");
        var existingColId = $popup.data("colId");
        if (existingRowId != rowId || existingColId != colId) {
          continue;
        }
        removePopup($popup);
        _$details.splice(idx, 1);
        _liveResultsClient.stopDetailedCellUpdates(_gridName, rowId, colId);
        var row = _dataView.getItemById(rowId);
        delete row.detailComponents[colId];
        return true;
      };
      return false;
    }

    function closeExplain($popup, $cell, rowId, colId) {
      delete _$explains[rowId][colId];
      if ($.isEmptyObject(_$explains[rowId])) {
        delete _$explains[rowId];
      }
      removePopup($popup);

      _liveResultsClient.stopDepGraphExplain(rowId, colId);
      var row = _dataView.getItemById(rowId);
      delete row.explainComponents[colId];
      $cell.unbind('mouseenter', handleExplainCellHoverIn);
      $cell.unbind('mouseleave', handleExplainCellHoverOut);
      $cell.removeClass("explain");
      $cell.removeClass("explain-hover");

      if ($.isEmptyObject(_$explains)) {
        _$layout.close('east');
      }

      return true;
    }

    function handleCloseClick(e) {
      var $popup = $(e.target).closest('.popup');
      var rowId = $popup.data("rowId");
      var colId = $popup.data("colId");
      var $cell = $popup.data("cell");
      closeExplain($popup, $cell, rowId, colId);
    }

    function handleExplainPopupHoverIn(e) {
      var $popup = $(e.target).closest(".popup");
      $popup.addClass("popup-hover");
      var $cell = $popup.data("cell");
      $cell.addClass("explain-hover");
    }

    function handleExplainPopupHoverOut(e) {
      var $popup = $(e.target).closest(".popup");
      $popup.removeClass("popup-hover");
      var $cell = $popup.data("cell");
      $cell.removeClass("explain-hover");
    }

    function handleExplainCellHoverIn(e) {
      var $cell = $(e.target).closest('.slick-cell');
      $cell.addClass('explain-hover');

      var rowId = $cell.data("rowId");
      var colId = $cell.data("colId");
      var $popup = _$explains[rowId][colId];
      $popup.addClass('popup-hover');
    }

    function handleExplainCellHoverOut(e) {
      var $cell = $(e.target).closest('.slick-cell');
      $cell.removeClass('explain-hover');

      var rowId = $cell.data("rowId");
      var colId = $cell.data("colId");
      var $popup = _$explains[rowId][colId].removeClass("popup-hover");
    }

    function associatePopupWithNewCell($cell, $popup, rowId, colId) {
      $popup.data('cell', $cell);
      $cell.hover(handleExplainCellHoverIn, handleExplainCellHoverOut);
      $cell.data('rowId', rowId);
      $cell.data('colId', colId);
      $cell.addClass("explain");
    }

    //-----------------------------------------------------------------------
    // Public API

    this.toggleDetail = function($cell, columnStructure, formatter, rowId) {
      var colId = columnStructure.colId;
      if (removeDetail(rowId, colId)) {
        return;
      }

      var row = _dataView.getItemById(rowId);
      if (!row.detailComponents) {
        row.detailComponents = {};
      }

      var $popup = getPopup('detail-popup', $cell, rowId, colId);
      var $content = $("

").appendTo($popup);
      var detailComponent = formatter.createDetail($popup, $content, rowId, columnStructure, _userConfig, row[columnStructure.colId]);

      if (detailComponent.resize) {
        var afterResized = function() {
          var detailComponent = $popup.data("component");
          if (detailComponent && detailComponent.resize) {
            detailComponent.resize();
          }
        };
        $popup.resizable({
          handles: 'se',
          helper: 'ui-state-highlight ui-corner-bottom',
          minWidth: 300,
          minHeight: 200,
          stop: afterResized});
      }

      revealPopup($cell, $popup, detailComponent);
      row.detailComponents[colId] = detailComponent;
      _$details.push($popup);

      _liveResultsClient.startDetailedCellUpdates(_gridName, rowId, colId);
      _liveResultsClient.triggerImmediateUpdate();
    }

    this.openExplain = function($cell, colId, rowId, popupTitle) {
      if (_dataView.getItemById(rowId)[colId] == false) {
        // Not in dep graph
        return;
      }
      if (_$explains[rowId] && _$explains[rowId][colId]) {
        // Already exists - find it
        var $popup = _$explains[rowId][colId];
      } else {
        // Create it
        var row = _dataView.getItemById(rowId);
        if (!row.explainComponents) {
          row.explainComponents = {};
        }

        _$layout.open('east', true, 'none');
        var $closeText = $("Close").click(handleCloseClick);
        var $popupHead = $("

").append($closeText).append("Dependency Graph for " + popupTitle + "
");
        var $popupContent = $("

").height(300).width("100%");
        var $popup = $("

").append($popupHead).append($popupContent).hover(handleExplainPopupHoverIn, handleExplainPopupHoverOut);
        $popup.data('rowId', rowId);
        $popup.data('colId', colId);
        _$popupList.append($popup);
        var depGraphViewer = new DepGraphViewer($popupContent, rowId, colId, _liveResultsClient, _userConfig);
        $popupContent.resizable({
          handles: 'se',
          helper: 'ui-state-highlight ui-corner-bottom',
          minHeight: 200,
          resize: function(event, ui) { ui.size.width = "100%"; },
          stop: function() { depGraphViewer.resize(); }
        });

        associatePopupWithNewCell($cell, $popup, rowId, colId);

        row.explainComponents[colId] = depGraphViewer;
        if (!_$explains[rowId]) {
          _$explains[rowId] = {};
        }
        _$explains[rowId][colId] = $popup;

        _liveResultsClient.startDepGraphExplain(rowId, colId);
        _liveResultsClient.triggerImmediateUpdate();
      }

      // Scroll into view
      _$popupList.parent().scrollTo($popup, 800);
    }

    this.onNewCell = function($cell, rowId, colId) {
      if (!_$explains[rowId]) {
        return;
      }
      var $popup = _$explains[rowId][colId];
      if (!$popup) {
        return;
      }
      associatePopupWithNewCell($cell, $popup, rowId, colId);
    }

  }

  //-------------------------------------------------------------------------

  $.extend(true, window, {
    PopupManager : PopupManager
  });

}(jQuery));<|endoftext|>


<commit_before>/**
 * Created by Obscurity on 2016/5/28.
 */

var index = function* (next) {
    var Column = global.database.models.column;
    var columns = yield Column.find({}).sort({updated_at: -1});
    this.render("./columns/index", {title: "专栏目录", current_user: this.session.user, current_module: this.current_module, columns: columns});
};

var show = function* (next) {
    var Column = global.database.models.column;
    var Article = global.database.models.article;
    var column = yield Column.findById(this.params.column_id);
    var columns = yield Column.find({});
    var articles = yield Article.find({column_id: this.params.column_id}).sort({order: 1});
    this.render("./columns/show", {title: column.name, current_user: this.session.user, current_module: this.current_module, columns: columns, column: column, articles: articles});
};

var init = function* (next) {
    this.render("./columns/new");
};

var create = function* (next) {
    var Column = global.database.models.column;
    var column = new Column();
    column.name = this.request.body.name;
    column.introduction = this.request.body.introduction;
    column.save();
    this.redirect(this.app.url("columns-detail", {"column_id": column._id}));
};

var edit = function* (next) {
    var Column = global.database.models.column;
    var column = yield Column.findById(this.params.column_id);
    this.render("./columns/edit", {title: "编辑专栏信息", current_user: this.session.user, current_module: this.current_module, column: column});
};

var update = function* (next) {
    var Column = global.database.models.column;
    var column = yield Column.findById(this.params.column_id);
    column.name = this.request.body.name;
    column.introduction = this.request.body.introduction;
    column.save();
    this.redirect(this.app.url("columns-detail", {"column_id": column._id}));
};

var destroy = function* (next) {
    var Column = global.database.models.column;
    var Article = global.database.models.article;
    yield Column.findByIdAndRemove(this.params.column_id);
    yield Article.remove({ column_id: this.params.column_id});
    this.redirect(this.app.url("columns-list"));
};

module.exports = {
    index: index,
    show: show,
    init: init,
    edit: edit,
    create: create,
    update: update,
    destroy: destroy,
    article: require('./articles')
};<commit_msg>修正专栏的排序问题<commit_after>/**
 * Created by Obscurity on 2016/5/28.
 */

var index = function* (next) {
    var Column = global.database.models.column;
    var columns = yield Column.find({}).sort({updated_at: -1});
    this.render("./columns/index", {title: "专栏目录", current_user: this.session.user, current_module: this.current_module, columns: columns});
};

var show = function* (next) {
    var Column = global.database.models.column;
    var Article = global.database.models.article;
    var column = yield Column.findById(this.params.column_id);
    var columns = yield Column.find({}).sort({updated_at: -1});;
    var articles = yield Article.find({column_id: this.params.column_id}).sort({order: 1});
    this.render("./columns/show", {title: column.name, current_user: this.session.user, current_module: this.current_module, columns: columns, column: column, articles: articles});
};

var init = function* (next) {
    this.render("./columns/new");
};

var create = function* (next) {
    var Column = global.database.models.column;
    var column = new Column();
    column.name = this.request.body.name;
    column.introduction = this.request.body.introduction;
    column.save();
    this.redirect(this.app.url("columns-detail", {"column_id": column._id}));
};

var edit = function* (next) {
    var Column = global.database.models.column;
    var column = yield Column.findById(this.params.column_id);
    this.render("./columns/edit", {title: "编辑专栏信息", current_user: this.session.user, current_module: this.current_module, column: column});
};

var update = function* (next) {
    var Column = global.database.models.column;
    var column = yield Column.findById(this.params.column_id);
    column.name = this.request.body.name;
    column.introduction = this.request.body.introduction;
    column.save();
    this.redirect(this.app.url("columns-detail", {"column_id": column._id}));
};

var destroy = function* (next) {
    var Column = global.database.models.column;
    var Article = global.database.models.article;
    yield Column.findByIdAndRemove(this.params.column_id);
    yield Article.remove({ column_id: this.params.column_id});
    this.redirect(this.app.url("columns-list"));
};

module.exports = {
    index: index,
    show: show,
    init: init,
    edit: edit,
    create: create,
    update: update,
    destroy: destroy,
    article: require('./articles')
};<|endoftext|>


<commit_before>// Copyright 2018, University of Colorado Boulder

/**
 * A generic accessibility type that will alert positional alerts based on a locationProperty and bounds (see
 * BorderAlertsDescriber) encapsulating the draggable area.
 *
 * General usage involves calling this endDrag() function from all dragListeners that you want this functionality to describe
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const AlertableDef = require( 'SCENERY_PHET/accessibility/AlertableDef' );
  const BorderAlertsDescriber = require( 'SCENERY_PHET/accessibility/describers/BorderAlertsDescriber' );
  const DirectionEnum = require( 'SCENERY_PHET/accessibility/describers/DirectionEnum' );
  const Range = require( 'DOT/Range' );
  const sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  const SceneryPhetA11yStrings = require( 'SCENERY_PHET/SceneryPhetA11yStrings' );
  const Utterance = require( 'SCENERY_PHET/accessibility/Utterance' );
  const utteranceQueue = require( 'SCENERY_PHET/accessibility/utteranceQueue' );

  // a11y strings
  let downString = SceneryPhetA11yStrings.down.value;
  let leftString = SceneryPhetA11yStrings.left.value;
  let rightString = SceneryPhetA11yStrings.right.value;
  let upString = SceneryPhetA11yStrings.up.value;

  // constants
  // threshold for diagonal movement is +/- 15 degrees from diagonals
  let DIAGONAL_MOVEMENT_THRESHOLD = 15 * Math.PI / 180;

  // mapping from alerting direction to the radian range that fills that space in the unit circle.
  let DIRECTION_MAP = {
    UP: new Range( -3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, -Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD ),
    DOWN: new Range( Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, 3 * Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD ),
    RIGHT: new Range( -Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD ),

    // atan2 wraps around PI, so we will use absolute value in checks
    LEFT: new Range( 3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, Math.PI ),

    UP_LEFT: new Range( -3 * Math.PI - DIAGONAL_MOVEMENT_THRESHOLD, -3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD ),
    DOWN_LEFT: new Range( 3 * Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD, 3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD ),
    UP_RIGHT: new Range( -Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD, -Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD ),
    DOWN_RIGHT: new Range( Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD, Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD )
  };
  let DIRECTION_MAP_KEYS = Object.keys( DIRECTION_MAP );

  let DEFAULT_MOVEMENT_ALERTS = {
    LEFT: leftString,
    RIGHT: rightString,
    UP: upString,
    DOWN: downString
  };

  /**
   * @param {Object} [options]
   * @constructor
   */
  class MovementDescriber {
    constructor( locationProperty, options ) {

      options = _.extend( {

        // see BorderAlertsDescriber
        borderAlertsOptions: null,

        // see DirectionEnum for allowed keys. Any missing keys will not be alerted. Use `{}` to omit movementAlerts
        movementAlerts: DEFAULT_MOVEMENT_ALERTS,

        // if false then diagonal alerts will be converted to two primary direction alerts that are alerted back to back
        // i.e. UP_LEFT becomes "UP" and "LEFT"
        alertDiagonal: false
      }, options );

      assert && assert( options.movementAlerts instanceof Object );
      assert && assert( !Array.isArray( options.movementAlerts ) ); // should not be an Array
      if ( assert ) {
        const movementAlertKeys = Object.keys( options.movementAlerts );

        for ( let i = 0; i < movementAlertKeys.length; i++ ) {
          let key = movementAlertKeys[ i ];
          assert( DirectionEnum.keys.indexOf( key ) >= 0, `unexpected key: ${key}. Keys should be the same as those in DirectionEnum` );
          assert( typeof options.movementAlerts[ key ] === 'string' || options.movementAlerts[ key ] instanceof Utterance );
        }
      }

      // @private
      this.movementAlerts = options.movementAlerts;
      this.alertDiagonal = options.alertDiagonal;

      // This sub-describer handles the logic for alerting when an item is on the edge of the movement space
      this.borderAlertsDescriber = new BorderAlertsDescriber( options.borderAlertsOptions );

      // @protected
      this.locationProperty = locationProperty;
      this.lastAlertedLocation = locationProperty.get(); // initial value of the locationProperty
    }

    /**
     * Simple alert for the Describer
     * @param {string|Utterance} alertable - anything that can be passed to UtteranceQueue
     */
    alert( alertable ) {
      assert && assert( AlertableDef.isAlertableDef( alertable ), 'trying to alert something that isn\'t alertable' );
      utteranceQueue.addToBack( alertable );
      this.lastAlertedLocation = this.locationProperty.get();
    }

    /**
     * Can be called with multiple directions, or just a single direction
     * @protected
     * @param {Array.<DirectionEnum>|DirectionEnum} directions
     */
    alertDirections( directions ) {
      if ( typeof directions === 'string' ) {
        directions = [ directions ];
      }

      // support if an instance doesn't want to alert in all directions
      directions.forEach( direction => {
        this.alert( new Utterance( {
          alert: this.movementAlerts[ direction ],
          uniqueGroupId: 'directionalMovement' + direction
        } ) );
      } );
    }

    /**
     * Alert a movement direction. The direction from this.lastAlertedLocation relative to the current value of the locationProperty
     * Call this from a listener or when the locationProperty has changed enough.
     * Can be overridden. Easy to implement method with the following schema:
     * (1) get the current value of the location property, and make sure it has changed enough from the lastAlertedLocation
     * (2) get the directions from the difference,
     * (3) alert those directions by calling this.alertDirections or this.alert,
     * see friction/view/describers/BookMovementDescriber.
     *
     * NOTE: don't call UtteranceQueue from the subtype!!!
     * NOTE: PhET a11y convention suggests that this should be called on drag end.
     *
     * @public
     */
    alertDirectionalMovement() {

      let newLocation = this.locationProperty.get();
      if ( !newLocation.equals( this.lastAlertedLocation ) ) {

        let directions = this.getDirections( newLocation, this.lastAlertedLocation );

        // make sure that these alerts exist
        if ( assert ) {
          directions.map( direction => { assert( this.movementAlerts[ direction ] && typeof this.movementAlerts[ direction ] === 'string' ); } );
        }
        this.alertDirections( directions );
      }
    }

    /**
     * Get the direction of movement that would take you from point A to point B, returning one of DirectionEnum,
     * LEFT, RIGHT,  UP, DOWN,  UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT. Uses Math.atan2, so the angle is mapped from
     * 0 to +/- Math.PI.
     *
     * @param  {Vector2} pointA
     * @param  {Vector2} pointB
     * @returns {Array.<DirectionEnum>} - contains one or two of the values in DirectionEnum, depending on whether or no you get
     *                            diagonal directions or their composite. See options.alertDiagonal for more info
     * @static
     */
    getDirections( pointA, pointB ) {
      let direction;

      let dx = pointA.x - pointB.x;
      let dy = pointA.y - pointB.y;
      let angle = Math.atan2( dy, dx );

      // atan2 wraps around Math.PI, so special check for moving left from absolute value
      if ( DIRECTION_MAP.LEFT.contains( Math.abs( angle ) ) ) {
        direction = DirectionEnum.LEFT;
      }

      // otherwise, angle will be in one of the ranges in DIRECTION_MAP
      for ( let i = 0; i < DIRECTION_MAP_KEYS.length; i++ ) {
        let entry = DIRECTION_MAP[ DIRECTION_MAP_KEYS[ i ] ];
        if ( entry.contains( angle ) ) {
          direction = DirectionEnum[ DIRECTION_MAP_KEYS[ i ] ];
          break;
        }
      }

      // This includes complex directions like "UP_LEFT"
      if ( this.alertDiagonal ) {
        return [ direction ];
      }
      else {
        return DirectionEnum.directionToRelativeDirections( direction );
      }

    }

    /**
     * @public
     * @param {window.Event} [domEvent]
     */
    endDrag( domEvent ) {

      // better to have the movement alerts, then the alert about the border
      this.alertDirectionalMovement();
      this.borderAlertsDescriber.endDrag( this.locationProperty.get(), domEvent );
    }

    /**
     * @public
     */
    reset() {
      this.borderAlertsDescriber.reset();
    }

    /**
     * get the default movement alerts
     * @returns {{LEFT: string, RIGHT: string, UP: string, DOWN: string}}
     */
    static getDefaultMovementAlerts() {
      return _.extend( {}, DEFAULT_MOVEMENT_ALERTS ); // clone
    }
  }

  return sceneryPhet.register( 'MovementDescriber', MovementDescriber );
} );<commit_msg>string|Utterance -> AlertableDef, see #409<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * A generic accessibility type that will alert positional alerts based on a locationProperty and bounds (see
 * BorderAlertsDescriber) encapsulating the draggable area.
 *
 * General usage involves calling this endDrag() function from all dragListeners that you want this functionality to describe
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const AlertableDef = require( 'SCENERY_PHET/accessibility/AlertableDef' );
  const BorderAlertsDescriber = require( 'SCENERY_PHET/accessibility/describers/BorderAlertsDescriber' );
  const DirectionEnum = require( 'SCENERY_PHET/accessibility/describers/DirectionEnum' );
  const Range = require( 'DOT/Range' );
  const sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  const SceneryPhetA11yStrings = require( 'SCENERY_PHET/SceneryPhetA11yStrings' );
  const Utterance = require( 'SCENERY_PHET/accessibility/Utterance' );
  const utteranceQueue = require( 'SCENERY_PHET/accessibility/utteranceQueue' );

  // a11y strings
  let downString = SceneryPhetA11yStrings.down.value;
  let leftString = SceneryPhetA11yStrings.left.value;
  let rightString = SceneryPhetA11yStrings.right.value;
  let upString = SceneryPhetA11yStrings.up.value;

  // constants
  // threshold for diagonal movement is +/- 15 degrees from diagonals
  let DIAGONAL_MOVEMENT_THRESHOLD = 15 * Math.PI / 180;

  // mapping from alerting direction to the radian range that fills that space in the unit circle.
  let DIRECTION_MAP = {
    UP: new Range( -3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, -Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD ),
    DOWN: new Range( Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, 3 * Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD ),
    RIGHT: new Range( -Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD ),

    // atan2 wraps around PI, so we will use absolute value in checks
    LEFT: new Range( 3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD, Math.PI ),

    UP_LEFT: new Range( -3 * Math.PI - DIAGONAL_MOVEMENT_THRESHOLD, -3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD ),
    DOWN_LEFT: new Range( 3 * Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD, 3 * Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD ),
    UP_RIGHT: new Range( -Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD, -Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD ),
    DOWN_RIGHT: new Range( Math.PI / 4 - DIAGONAL_MOVEMENT_THRESHOLD, Math.PI / 4 + DIAGONAL_MOVEMENT_THRESHOLD )
  };
  let DIRECTION_MAP_KEYS = Object.keys( DIRECTION_MAP );

  let DEFAULT_MOVEMENT_ALERTS = {
    LEFT: leftString,
    RIGHT: rightString,
    UP: upString,
    DOWN: downString
  };

  /**
   * @param {Object} [options]
   * @constructor
   */
  class MovementDescriber {
    constructor( locationProperty, options ) {

      options = _.extend( {

        // see BorderAlertsDescriber
        borderAlertsOptions: null,

        // see DirectionEnum for allowed keys. Any missing keys will not be alerted. Use `{}` to omit movementAlerts
        movementAlerts: DEFAULT_MOVEMENT_ALERTS,

        // if false then diagonal alerts will be converted to two primary direction alerts that are alerted back to back
        // i.e. UP_LEFT becomes "UP" and "LEFT"
        alertDiagonal: false
      }, options );

      assert && assert( options.movementAlerts instanceof Object );
      assert && assert( !Array.isArray( options.movementAlerts ) ); // should not be an Array
      if ( assert ) {
        const movementAlertKeys = Object.keys( options.movementAlerts );

        for ( let i = 0; i < movementAlertKeys.length; i++ ) {
          let key = movementAlertKeys[ i ];
          assert( DirectionEnum.keys.indexOf( key ) >= 0, `unexpected key: ${key}. Keys should be the same as those in DirectionEnum` );
          assert( typeof options.movementAlerts[ key ] === 'string' || options.movementAlerts[ key ] instanceof Utterance );
        }
      }

      // @private
      this.movementAlerts = options.movementAlerts;
      this.alertDiagonal = options.alertDiagonal;

      // This sub-describer handles the logic for alerting when an item is on the edge of the movement space
      this.borderAlertsDescriber = new BorderAlertsDescriber( options.borderAlertsOptions );

      // @protected
      this.locationProperty = locationProperty;
      this.lastAlertedLocation = locationProperty.get(); // initial value of the locationProperty
    }

    /**
     * Simple alert for the Describer
     * @param {AlertableDef} alertable - anything that can be passed to UtteranceQueue
     */
    alert( alertable ) {
      assert && assert( AlertableDef.isAlertableDef( alertable ), 'trying to alert something that isn\'t alertable' );
      utteranceQueue.addToBack( alertable );
      this.lastAlertedLocation = this.locationProperty.get();
    }

    /**
     * Can be called with multiple directions, or just a single direction
     * @protected
     * @param {Array.<DirectionEnum>|DirectionEnum} directions
     */
    alertDirections( directions ) {
      if ( typeof directions === 'string' ) {
        directions = [ directions ];
      }

      // support if an instance doesn't want to alert in all directions
      directions.forEach( direction => {
        this.alert( new Utterance( {
          alert: this.movementAlerts[ direction ],
          uniqueGroupId: 'directionalMovement' + direction
        } ) );
      } );
    }

    /**
     * Alert a movement direction. The direction from this.lastAlertedLocation relative to the current value of the locationProperty
     * Call this from a listener or when the locationProperty has changed enough.
     * Can be overridden. Easy to implement method with the following schema:
     * (1) get the current value of the location property, and make sure it has changed enough from the lastAlertedLocation
     * (2) get the directions from the difference,
     * (3) alert those directions by calling this.alertDirections or this.alert,
     * see friction/view/describers/BookMovementDescriber.
     *
     * NOTE: don't call UtteranceQueue from the subtype!!!
     * NOTE: PhET a11y convention suggests that this should be called on drag end.
     *
     * @public
     */
    alertDirectionalMovement() {

      let newLocation = this.locationProperty.get();
      if ( !newLocation.equals( this.lastAlertedLocation ) ) {

        let directions = this.getDirections( newLocation, this.lastAlertedLocation );

        // make sure that these alerts exist
        if ( assert ) {
          directions.map( direction => { assert( this.movementAlerts[ direction ] && typeof this.movementAlerts[ direction ] === 'string' ); } );
        }
        this.alertDirections( directions );
      }
    }

    /**
     * Get the direction of movement that would take you from point A to point B, returning one of DirectionEnum,
     * LEFT, RIGHT,  UP, DOWN,  UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT. Uses Math.atan2, so the angle is mapped from
     * 0 to +/- Math.PI.
     *
     * @param  {Vector2} pointA
     * @param  {Vector2} pointB
     * @returns {Array.<DirectionEnum>} - contains one or two of the values in DirectionEnum, depending on whether or no you get
     *                            diagonal directions or their composite. See options.alertDiagonal for more info
     * @static
     */
    getDirections( pointA, pointB ) {
      let direction;

      let dx = pointA.x - pointB.x;
      let dy = pointA.y - pointB.y;
      let angle = Math.atan2( dy, dx );

      // atan2 wraps around Math.PI, so special check for moving left from absolute value
      if ( DIRECTION_MAP.LEFT.contains( Math.abs( angle ) ) ) {
        direction = DirectionEnum.LEFT;
      }

      // otherwise, angle will be in one of the ranges in DIRECTION_MAP
      for ( let i = 0; i < DIRECTION_MAP_KEYS.length; i++ ) {
        let entry = DIRECTION_MAP[ DIRECTION_MAP_KEYS[ i ] ];
        if ( entry.contains( angle ) ) {
          direction = DirectionEnum[ DIRECTION_MAP_KEYS[ i ] ];
          break;
        }
      }

      // This includes complex directions like "UP_LEFT"
      if ( this.alertDiagonal ) {
        return [ direction ];
      }
      else {
        return DirectionEnum.directionToRelativeDirections( direction );
      }

    }

    /**
     * @public
     * @param {window.Event} [domEvent]
     */
    endDrag( domEvent ) {

      // better to have the movement alerts, then the alert about the border
      this.alertDirectionalMovement();
      this.borderAlertsDescriber.endDrag( this.locationProperty.get(), domEvent );
    }

    /**
     * @public
     */
    reset() {
      this.borderAlertsDescriber.reset();
    }

    /**
     * get the default movement alerts
     * @returns {{LEFT: string, RIGHT: string, UP: string, DOWN: string}}
     */
    static getDefaultMovementAlerts() {
      return _.extend( {}, DEFAULT_MOVEMENT_ALERTS ); // clone
    }
  }

  return sceneryPhet.register( 'MovementDescriber', MovementDescriber );
} );<|endoftext|>


<commit_before>define('app/views/home', [
    'app/views/mistscreen',
    'text!app/templates/home.html','ember'],
    /**
     *
     * Home page
     *
     * @returns Class
     */
    function(MistScreen, home_html) {
        return MistScreen.extend({
            template: Ember.Handlebars.compile(home_html),

            didInsertElement: function() { // Overrides mistscreen

                try {
                    $("[data-role=page]").page('destroy').page();
                } catch(e){}

                // Welcome message to the new users

                // Get data from registration
                var data = JSON.parse(sessionStorage.getItem('data'));
                sessionStorage.clear();

                if (data) {

                    var promo = null;
                    var selectedPlan = data.selectedPlan; // Selected plans are only "Lite" and "Trial" at this point
                    var hasPromo = data.hasPromo;
                    var controller = Mist.notificationController;
                    controller.set('msgHeader', 'Welcome to Mist.io!');

                    if (selectedPlan == 'Trial' && hasPromo) {
                        controller.set('msgPart1', 'Enjoy your 15-day trial, it\'s totaly free!');

                        controller.set('msgPart2', 'Don\'t forget that there is a discount waiting just for you on the "account-settings" page. \
                                                    Make sure to use it before it\'s expiration!');
                    } else if (selectedPlan == 'Lite' && hasPromo) {
                        controller.set('msgPart1', 'You have chosen the "Lite" plan, but in order to give you a taste \
                                                    of our monitoring services, we assigned you a trial.');
                        controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');

                        controller.set('msgPart3', 'Don\'t forget that there is a discount waiting just for you on the "account-settings" page. \
                                                    Make sure to use it before it\'s expiration!');
                    } else if (selectedPlan == 'Trial') {
                        controller.set('msgPart1', 'Enjoy your 15-day trial, it\'s totaly free!');
                    } else if (selectedPlan == 'Lite') {
                        controller.set('msgPart1', 'You have chosen the "Lite" plan, but in order to give you a taste \
                                                    of our monitoring services, we assigned you a trial.');
                        controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');
                    } else if (hasPromo) {
                        controller.set('msgPart1', 'In order to give you a taste of our monitoring services, we assigned you a trial.');
                        controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');
                        controller.set('msgPart3', 'Don\'t forget that there is a discount waiting just for you on the "account-settings" page. \
                                                    Make sure to use it before it\'s expiration!');
                    } else {
                        //controller.set('msgPart1', 'In order to give you a taste of our monitoring services, we assigned you a trial.');
                        //controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');
                    }

                    controller.set('msgPart4', 'To manage your account settings click "Me" > "My account".');

                    Ember.run.later(function() {
                        controller.showMessagebox();
                    }, 1500);
                }
            }
        });
    }
);
<commit_msg>Make fix more generic<commit_after>define('app/views/home', [
    'app/views/mistscreen',
    'text!app/templates/home.html','ember'],
    /**
     *
     * Home page
     *
     * @returns Class
     */
    function(MistScreen, home_html) {
        return MistScreen.extend({
            template: Ember.Handlebars.compile(home_html),

            didInsertElement: function() { // Overrides mistscreen

                this._super();

                // Welcome message to the new users

                // Get data from registration
                var data = JSON.parse(sessionStorage.getItem('data'));
                sessionStorage.clear();

                if (data) {

                    var promo = null;
                    var selectedPlan = data.selectedPlan; // Selected plans are only "Lite" and "Trial" at this point
                    var hasPromo = data.hasPromo;
                    var controller = Mist.notificationController;
                    controller.set('msgHeader', 'Welcome to Mist.io!');

                    if (selectedPlan == 'Trial' && hasPromo) {
                        controller.set('msgPart1', 'Enjoy your 15-day trial, it\'s totaly free!');

                        controller.set('msgPart2', 'Don\'t forget that there is a discount waiting just for you on the "account-settings" page. \
                                                    Make sure to use it before it\'s expiration!');
                    } else if (selectedPlan == 'Lite' && hasPromo) {
                        controller.set('msgPart1', 'You have chosen the "Lite" plan, but in order to give you a taste \
                                                    of our monitoring services, we assigned you a trial.');
                        controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');

                        controller.set('msgPart3', 'Don\'t forget that there is a discount waiting just for you on the "account-settings" page. \
                                                    Make sure to use it before it\'s expiration!');
                    } else if (selectedPlan == 'Trial') {
                        controller.set('msgPart1', 'Enjoy your 15-day trial, it\'s totaly free!');
                    } else if (selectedPlan == 'Lite') {
                        controller.set('msgPart1', 'You have chosen the "Lite" plan, but in order to give you a taste \
                                                    of our monitoring services, we assigned you a trial.');
                        controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');
                    } else if (hasPromo) {
                        controller.set('msgPart1', 'In order to give you a taste of our monitoring services, we assigned you a trial.');
                        controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');
                        controller.set('msgPart3', 'Don\'t forget that there is a discount waiting just for you on the "account-settings" page. \
                                                    Make sure to use it before it\'s expiration!');
                    } else {
                        //controller.set('msgPart1', 'In order to give you a taste of our monitoring services, we assigned you a trial.');
                        //controller.set('msgPart2', 'It is completely free and lasts for 15 days, enjoy!');
                    }

                    controller.set('msgPart4', 'To manage your account settings click "Me" > "My account".');

                    Ember.run.later(function() {
                        controller.showMessagebox();
                    }, 1500);
                }
            }
        });
    }
);
<|endoftext|>


<commit_before>import { EMPTY_STRING, TYPE_FUNCTION, TYPE_STRING } from './const'
import { AttrAssembler } from './attr'
import { ChildNodeAssembler } from './childnode'
import { NodeAssembler } from './node'

const { isArray, prototype : { forEach, map } } = Array
const { Node, document } = window
const { getNodeOf } = NodeAssembler
const DEFAULT_PROPERTY_NAME = 'childNodes'

/**
 * @param {Array} array
 * @returns {Array}
 */
function flatten(array) {
    const result = []
    array.forEach(item => {
        if(isArray(item)) {
            result.push(...flatten(item))
        }
        else if(item) {
            result.push(item)
        }
    })
    return result
}

/**
 * @see {@link https://www.w3.org/TR/dom/#interface-parentnode}
 * @abstract
 */
export class ParentNodeAssembler extends ChildNodeAssembler {
    /**
     * Append child nodes to the node
     * @param {String|Node|ChildNodeAssembler|Array|*} childNodes
     */
    append(...childNodes) {
        flatten(childNodes).forEach(this.appendChild, this)
    }

    /**
     * @param {*} child
     * @returns {ChildNodeAssembler|*}
     * todo => append()
     */
    appendChild(child) {
        const node = this.node
        if(typeof child === TYPE_STRING) {
            return this.getInstanceOf(node.appendChild(document.createTextNode(child)))
        }
        else if(child instanceof NodeAssembler || child instanceof Node) {
            const instance = this.getInstanceOf(child)
            instance.parentNode = node
            return instance
        }
        else throw TypeError(`Failed to execute 'appendChild' on '${ this.constructor.name }': parameter is not of expected type.`)
    }

    /**
     * @param {ChildNodeAssembler|Node|*} object
     * @returns {Boolean}
     */
    contains(object) {
        return this.node.contains(getNodeOf(object))
    }

    /**
     * @param {Function|String|*} subject
     * @param {Function|String} [filter]
     * @returns {ChildNodeAssembler|*|null}
     */
    find(subject, filter) {
        if(typeof filter === TYPE_FUNCTION) {
            const instances = this.findAll(subject, filter)
            return instances[0] || null
        }
        let selector = typeof subject === TYPE_STRING? subject : subject.selector
        if(typeof filter === TYPE_STRING) {
            selector += filter
        }
        const node = this.node.querySelector(selector)
        if(node) {
            const isAttr = subject === AttrAssembler || AttrAssembler.isPrototypeOf(subject)
            const elementAssembler = isAttr || typeof subject === TYPE_STRING?
                this.constructor.elementAssembler :
                subject
            const element = this.getInstanceOf(node, elementAssembler)
            return isAttr? element.getAttributeNode(subject) : element
        }
        return null
    }

    /**
     * @param {Function|String|*} subject
     * @param {Function|String} [filter]
     * @returns {ChildNodeAssembler[]|*[]}
     */
    findAll(subject, filter) {
        const isString = typeof subject === TYPE_STRING
        let selector = isString? subject : subject.selector
        if(typeof filter === TYPE_STRING) {
            selector += filter
        }
        const nodeList = this.node.querySelectorAll(selector)
        const isAttr = subject === AttrAssembler || AttrAssembler.isPrototypeOf(subject)
        const elementAssembler = isAttr || isString?
            this.constructor.elementAssembler :
            subject
        const results = map.call(nodeList, node => {
            const element = this.getInstanceOf(node, elementAssembler)
            return isAttr? element.getAttributeNode(subject) : element
        })
        return typeof filter === TYPE_FUNCTION? results.filter(filter) : results
    }

    /**
     * @returns {Boolean}
     */
    hasChildNodes() {
        return this.node.hasChildNodes()
    }

    /**
     * @param {ChildNodeAssembler|Node|*} node
     * @param {ChildNodeAssembler|Node|*} child
     * @returns {ChildNodeAssembler|*}
     * todo => prepend()
     */
    insertBefore(node, child) {
        return this.getInstanceOf(this.node.insertBefore(getNodeOf(node), getNodeOf(child)))
    }

    /**
     * Prepend child nodes to the node
     * @param {String|Node|ChildNodeAssembler|Array|*} childNodes
     */
    prepend(...childNodes) {
        this.node.prepend(...flatten(childNodes).map(getNodeOf))
    }

    /**
     * @param {ChildNodeAssembler|Node|*} child
     * todo => ChildNodeAssembler.remove()
     */
    removeChild(child) {
        if(child instanceof ChildNodeAssembler) {
            child.remove()
            return child
        }
        else {
            return this.getInstanceOf(this.node.removeChild(child))
        }
    }

    /**
     * @param {ChildNodeAssembler|Node|*} node
     * @param {ChildNodeAssembler|Node|*} child
     * @returns {ChildNodeAssembler|*}
     * todo => ChildNodeAssembler.replaceWith()
     */
    replaceChild(node, child) {
        return this.getInstanceOf(this.node.replaceChild(getNodeOf(node), getNodeOf(child)))
    }

    /**
     * Replace child nodes to the node
     * @param {String|Node|ChildNodeAssembler|Array|*} childNodes
     */
    set childNodes(childNodes) {
        if(this.hasChildNodes()) {
            this.textContent = EMPTY_STRING
        }
        this.append(childNodes)
    }

    /**
     * Get an array of child nodes
     * @returns {*} {Array}
     */
    get childNodes() {
        return map.call(this.node.childNodes, node => this.getInstanceOf(node))
    }

    /**
     * @returns {ChildNodeAssembler|*}
     */
    get firstChild() {
        return this.getInstanceOf(this.node.firstChild)
    }

    /**
     * @returns {ChildNodeAssembler|*}
     */
    get lastChild() {
        return this.getInstanceOf(this.node.lastChild)
    }

    /**
     * Set a text content of the node
     * @param {String} textContent
     */
    set textContent(textContent) {
        this.node.textContent = textContent
    }

    /**
     * Get a text content of the node
     * @returns {String}
     */
    get textContent() {
        return this.node.textContent
    }

    /**
     * @returns {String}
     * @override
     */
    static get defaultPropertyName() {
        return DEFAULT_PROPERTY_NAME
    }
}

NodeAssembler.ParentNodeAssembler = ParentNodeAssembler
<commit_msg>Edit todos<commit_after>import { EMPTY_STRING, TYPE_FUNCTION, TYPE_STRING } from './const'
import { AttrAssembler } from './attr'
import { ChildNodeAssembler } from './childnode'
import { NodeAssembler } from './node'

const { isArray, prototype : { forEach, map } } = Array
const { Node, document } = window
const { getNodeOf } = NodeAssembler
const DEFAULT_PROPERTY_NAME = 'childNodes'

/**
 * @param {Array} array
 * @returns {Array}
 */
function flatten(array) {
    const result = []
    array.forEach(item => {
        if(isArray(item)) {
            result.push(...flatten(item))
        }
        else if(item) {
            result.push(item)
        }
    })
    return result
}

/**
 * @see {@link https://www.w3.org/TR/dom/#interface-parentnode}
 * @abstract
 */
export class ParentNodeAssembler extends ChildNodeAssembler {
    /**
     * Append child nodes to the node
     * @param {String|Node|ChildNodeAssembler|Array|*} childNodes
     */
    append(...childNodes) {
        flatten(childNodes).forEach(this.appendChild, this)
    }

    /**
     * @param {*} child
     * @returns {ChildNodeAssembler|*}
     */
    appendChild(child) {
        const node = this.node
        if(typeof child === TYPE_STRING) {
            return this.getInstanceOf(node.appendChild(document.createTextNode(child)))
        }
        else if(child instanceof NodeAssembler || child instanceof Node) {
            const instance = this.getInstanceOf(child)
            instance.parentNode = node
            return instance
        }
        else throw TypeError(`Failed to execute 'appendChild' on '${ this.constructor.name }': parameter is not of expected type.`)
    }

    /**
     * @param {ChildNodeAssembler|Node|*} object
     * @returns {Boolean}
     */
    contains(object) {
        return this.node.contains(getNodeOf(object))
    }

    /**
     * @param {Function|String|*} subject
     * @param {Function|String} [filter]
     * @returns {ChildNodeAssembler|*|null}
     */
    find(subject, filter) {
        if(typeof filter === TYPE_FUNCTION) {
            const instances = this.findAll(subject, filter)
            return instances[0] || null
        }
        let selector = typeof subject === TYPE_STRING? subject : subject.selector
        if(typeof filter === TYPE_STRING) {
            selector += filter
        }
        const node = this.node.querySelector(selector)
        if(node) {
            const isAttr = subject === AttrAssembler || AttrAssembler.isPrototypeOf(subject)
            const elementAssembler = isAttr || typeof subject === TYPE_STRING?
                this.constructor.elementAssembler :
                subject
            const element = this.getInstanceOf(node, elementAssembler)
            return isAttr? element.getAttributeNode(subject) : element
        }
        return null
    }

    /**
     * @param {Function|String|*} subject
     * @param {Function|String} [filter]
     * @returns {ChildNodeAssembler[]|*[]}
     */
    findAll(subject, filter) {
        const isString = typeof subject === TYPE_STRING
        let selector = isString? subject : subject.selector
        if(typeof filter === TYPE_STRING) {
            selector += filter
        }
        const nodeList = this.node.querySelectorAll(selector)
        const isAttr = subject === AttrAssembler || AttrAssembler.isPrototypeOf(subject)
        const elementAssembler = isAttr || isString?
            this.constructor.elementAssembler :
            subject
        const results = map.call(nodeList, node => {
            const element = this.getInstanceOf(node, elementAssembler)
            return isAttr? element.getAttributeNode(subject) : element
        })
        return typeof filter === TYPE_FUNCTION? results.filter(filter) : results
    }

    /**
     * @returns {Boolean}
     */
    hasChildNodes() {
        return this.node.hasChildNodes()
    }

    /**
     * @param {ChildNodeAssembler|Node|*} node
     * @param {ChildNodeAssembler|Node|*} child
     * @returns {ChildNodeAssembler|*}
     */
    insertBefore(node, child) {
        return this.getInstanceOf(this.node.insertBefore(getNodeOf(node), getNodeOf(child)))
    }

    /**
     * Prepend child nodes to the node
     * @param {String|Node|ChildNodeAssembler|Array|*} childNodes
     * todo support Attr and AttrAssembler
     */
    prepend(...childNodes) {
        this.node.prepend(...flatten(childNodes).map(getNodeOf))
    }

    /**
     * @param {ChildNodeAssembler|Node|*} child
     */
    removeChild(child) {
        if(child instanceof ChildNodeAssembler) {
            child.remove()
            return child
        }
        else {
            return this.getInstanceOf(this.node.removeChild(child))
        }
    }

    /**
     * @param {ChildNodeAssembler|Node|*} node
     * @param {ChildNodeAssembler|Node|*} child
     * @returns {ChildNodeAssembler|*}
     */
    replaceChild(node, child) {
        return this.getInstanceOf(this.node.replaceChild(getNodeOf(node), getNodeOf(child)))
    }

    /**
     * Replace child nodes to the node
     * @param {String|Node|ChildNodeAssembler|Array|*} childNodes
     */
    set childNodes(childNodes) {
        if(this.hasChildNodes()) {
            this.textContent = EMPTY_STRING
        }
        this.append(childNodes)
    }

    /**
     * Get an array of child nodes
     * @returns {*} {Array}
     */
    get childNodes() {
        return map.call(this.node.childNodes, node => this.getInstanceOf(node))
    }

    /**
     * @returns {ChildNodeAssembler|*}
     */
    get firstChild() {
        return this.getInstanceOf(this.node.firstChild)
    }

    /**
     * @returns {ChildNodeAssembler|*}
     */
    get lastChild() {
        return this.getInstanceOf(this.node.lastChild)
    }

    /**
     * Set a text content of the node
     * @param {String} textContent
     */
    set textContent(textContent) {
        this.node.textContent = textContent
    }

    /**
     * Get a text content of the node
     * @returns {String}
     */
    get textContent() {
        return this.node.textContent
    }

    /**
     * @returns {String}
     * @override
     */
    static get defaultPropertyName() {
        return DEFAULT_PROPERTY_NAME
    }
}

NodeAssembler.ParentNodeAssembler = ParentNodeAssembler
<|endoftext|>


<commit_before>/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { pasteHandler, serialize } from '@wordpress/blocks';
import { documentHasSelection } from '@wordpress/dom';
import { withDispatch, useSelect, useDispatch } from '@wordpress/data';

/**
 * Internal dependencies
 */
import { ensureAllowedBlocksOnPaste } from '../../helpers';

function CopyPasteHandler( { children, onCopy, clientId, isSelected } ) {
	const {
		isFirstPage,
		canUserUseUnfilteredHTML,
	} = useSelect(
		( select ) => {
			const {
				getBlockOrder,
				getSettings,
			} = select( 'core/block-editor' );
			const { __experimentalCanUserUseUnfilteredHTML } = getSettings();
			return {
				isFirstPage: getBlockOrder().indexOf( clientId ) === 0,
				canUserUseUnfilteredHTML: __experimentalCanUserUseUnfilteredHTML,
			};
		}, [ clientId ]
	);

	const { insertBlocks } = useDispatch( 'core/block-editor' );

	const onPaste = ( event ) => {
		// Ignore if the Page is not the selected page.
		if ( ! isSelected ) {
			return;
		}
		const clipboardData = event.clipboardData;

		let plainText = '';
		let html = '';

		// IE11 only supports `Text` as an argument for `getData` and will
		// otherwise throw an invalid argument error, so we try the standard
		// arguments first, then fallback to `Text` if they fail.
		try {
			plainText = clipboardData.getData( 'text/plain' );
			html = clipboardData.getData( 'text/html' );
		} catch ( error1 ) {
			try {
				html = clipboardData.getData( 'Text' );
			} catch ( error2 ) {
				// Some browsers like UC Browser paste plain text by default and
				// don't support clipboardData at all, so allow default
				// behaviour.
				return;
			}
		}

		event.preventDefault();

		const mode = 'BLOCKS';

		const content = pasteHandler( {
			HTML: html,
			plainText,
			mode,
			canUserUseUnfilteredHTML,
		} );

		if ( content.length > 0 ) {
			insertBlocks( ensureAllowedBlocksOnPaste( content, clientId, isFirstPage ), null, clientId );
		}
	};

	return (

			{ children }

	);
}

CopyPasteHandler.propTypes = {
	children: PropTypes.object.isRequired,
	clientId: PropTypes.string.isRequired,
	isSelected: PropTypes.bool.isRequired,
	onCopy: PropTypes.func.isRequired,
};

export default withDispatch( ( dispatch, ownProps, { select } ) => {
	const {
		getBlocksByClientId,
		getSelectedBlockClientIds,
		hasMultiSelection,
	} = select( 'core/block-editor' );
	const { clearCopiedMarkup, setCopiedMarkup } = dispatch( 'amp/story' );

	/**
	 * Copy handler for ensuring that the store's copiedMarkup is in sync with what's actually in clipBoard.
	 * If it's not a block that's being copied, let's clear the copiedMarkup.
	 * Otherwise, let's set the copied markup.
	 */
	const onCopy = () => {
		const selectedBlockClientIds = getSelectedBlockClientIds();

		if ( selectedBlockClientIds.length === 0 ) {
			clearCopiedMarkup();
			return;
		}

		// Let native copy behaviour take over in input fields.
		if ( ! hasMultiSelection() && documentHasSelection() ) {
			clearCopiedMarkup();
			return;
		}
		const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) );
		setCopiedMarkup( serialized );
	};

	return {
		onCopy,
	};
} )( CopyPasteHandler );
<commit_msg>Handle cut keyboard requests. (#3231)<commit_after>/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { pasteHandler, serialize } from '@wordpress/blocks';
import { documentHasSelection } from '@wordpress/dom';
import { withDispatch, useSelect, useDispatch } from '@wordpress/data';

/**
 * Internal dependencies
 */
import { ensureAllowedBlocksOnPaste } from '../../helpers';

function CopyPasteHandler( { children, onCopy, onCut, clientId, isSelected } ) {
	const {
		isFirstPage,
		canUserUseUnfilteredHTML,
	} = useSelect(
		( select ) => {
			const {
				getBlockOrder,
				getSettings,
			} = select( 'core/block-editor' );
			const { __experimentalCanUserUseUnfilteredHTML } = getSettings();
			return {
				isFirstPage: getBlockOrder().indexOf( clientId ) === 0,
				canUserUseUnfilteredHTML: __experimentalCanUserUseUnfilteredHTML,
			};
		}, [ clientId ]
	);

	const { insertBlocks } = useDispatch( 'core/block-editor' );

	const onPaste = ( event ) => {
		// Ignore if the Page is not the selected page.
		if ( ! isSelected ) {
			return;
		}
		const clipboardData = event.clipboardData;

		let plainText = '';
		let html = '';

		// IE11 only supports `Text` as an argument for `getData` and will
		// otherwise throw an invalid argument error, so we try the standard
		// arguments first, then fallback to `Text` if they fail.
		try {
			plainText = clipboardData.getData( 'text/plain' );
			html = clipboardData.getData( 'text/html' );
		} catch ( error1 ) {
			try {
				html = clipboardData.getData( 'Text' );
			} catch ( error2 ) {
				// Some browsers like UC Browser paste plain text by default and
				// don't support clipboardData at all, so allow default
				// behaviour.
				return;
			}
		}

		event.preventDefault();

		const mode = 'BLOCKS';

		const content = pasteHandler( {
			HTML: html,
			plainText,
			mode,
			canUserUseUnfilteredHTML,
		} );

		if ( content.length > 0 ) {
			insertBlocks( ensureAllowedBlocksOnPaste( content, clientId, isFirstPage ), null, clientId );
		}
	};

	return (

			{ children }

	);
}

CopyPasteHandler.propTypes = {
	children: PropTypes.object.isRequired,
	clientId: PropTypes.string.isRequired,
	isSelected: PropTypes.bool.isRequired,
	onCopy: PropTypes.func.isRequired,
	onCut: PropTypes.func.isRequired,
};

export default withDispatch( ( dispatch, ownProps, { select } ) => {
	const {
		getBlocksByClientId,
		getSelectedBlockClientIds,
		hasMultiSelection,
	} = select( 'core/block-editor' );
	const { removeBlock } = dispatch( 'core/block-editor' );
	const { clearCopiedMarkup, setCopiedMarkup } = dispatch( 'amp/story' );

	/**
	 * Copy handler for ensuring that the store's copiedMarkup is in sync with what's actually in clipBoard.
	 * If it's not a block that's being copied, let's clear the copiedMarkup.
	 * Otherwise, let's set the copied markup.
	 */
	const onCopy = () => {
		const selectedBlockClientIds = getSelectedBlockClientIds();

		if ( selectedBlockClientIds.length === 0 ) {
			clearCopiedMarkup();
			return;
		}

		// Let native copy behaviour take over in input fields.
		if ( ! hasMultiSelection() && documentHasSelection() ) {
			clearCopiedMarkup();
			return;
		}
		const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) );
		setCopiedMarkup( serialized );
	};

	/**
	 * Cut handler for ensuring that the store's cutMarkup is in sync with what's actually in clipBoard.
	 * If it's not a block that's being cut, let's clear the cutMarkup.
	 * Otherwise, let's set the cut markup.
	 */
	const onCut = () => {
		const selectedBlockClientIds = getSelectedBlockClientIds();

		if ( selectedBlockClientIds.length === 0 ) {
			return;
		}
		// Reuse code in onCode.
		onCopy();
		// Remove selected Blocks.
		for ( const clientId of selectedBlockClientIds ) {
			removeBlock( clientId );
		}
	};

	return {
		onCopy,
		onCut,
	};
} )( CopyPasteHandler );
<|endoftext|>


<commit_before>// Karma configuration
// Generated on Fri Nov 25 2016 21:41:08 GMT-0800 (PST)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: [
        'jasmine',
        'detectBrowsers'
    ],

    // list of files / patterns to load in the browser
    files: [
      'specs/*.spec.js'
    ],

    // list of files to exclude
    exclude: [
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'specs/*.spec.js' : ['webpack']
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    customLaunchers: {
      Chrome_travis_ci: {
        base: 'Chrome',
        flags: ['--no-sandbox']
      }
    },
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    detectBrowsers: {
      enabled: true,
      usePhantomJS: false
    },

    plugins: [
      'karma-jasmine',
      'karma-webpack',
      'karma-chrome-launcher',
      'karma-firefox-launcher',
      'karma-ie-launcher',
      'karma-edge-launcher',
      'karma-safari-launcher',
      'karma-detect-browsers'
    ],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,

        postDetection: function(browsers) {
          if(process.env.TRAVIS) {
            var i = browsers.indexOf('Chrome');
            if (i !== -1) {
              browsers[i] = 'Chrome_travis_ci';
            }
          }
          return browsers;
        }
      },

      // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity
  })
}
<commit_msg>chrome fix for travis-ci<commit_after>// Karma configuration
// Generated on Fri Nov 25 2016 21:41:08 GMT-0800 (PST)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: [
        'jasmine',
        'detectBrowsers'
    ],

    // list of files / patterns to load in the browser
    files: [
      'specs/*.spec.js'
    ],

    // list of files to exclude
    exclude: [
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'specs/*.spec.js' : ['webpack']
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    customLaunchers: {
      Chrome_travis_ci: {
        base: 'Chrome',
        flags: ['--no-sandbox']
      }
    },
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    detectBrowsers: {
      enabled: true,
      usePhantomJS: false
    },

    plugins: [
      'karma-jasmine',
      'karma-webpack',
      'karma-chrome-launcher',
      'karma-firefox-launcher',
      'karma-ie-launcher',
      'karma-edge-launcher',
      'karma-safari-launcher',
      'karma-detect-browsers'
    ],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,

    postDetection: function(browsers) {
        if(process.env.TRAVIS) {
          var i = browsers.indexOf('Chrome');
          if (i !== -1) {
            browsers[i] = 'Chrome_travis_ci';
          }
        }
        return browsers;
      },

      // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity
  })
}
<|endoftext|>


<commit_before>/* @flow */

import EventEmitter from 'events'
import fs from 'fs-extra'
import _ from 'lodash'
import os from 'os'
import path from 'path'
import readdirp from 'readdirp'
import url from 'url'

import './globals' // FIXME Use bluebird promises as long as we need asCallback
import pkg from '../package.json'
import Config from './config'
import logger, { LOG_FILE, LOG_FILENAME } from './logger'
import Pouch from './pouch'
import Ignore from './ignore'
import Merge from './merge'
import Prep from './prep'
import Local from './local'
import Remote from './remote'
import Sync from './sync'
import SyncState from './syncstate'
import Registration from './remote/registration'

import type { Callback } from './utils/func'
import type { SyncMode } from './sync'

const log = logger({
  component: 'App'
})

// App is the entry point for the CLI and GUI.
// They both can do actions and be notified by events via an App instance.
class App {
  lang: string
  basePath: string
  config: Config
  pouch: Pouch
  events: EventEmitter
  ignore: Ignore
  merge: Merge
  prep: Prep
  local: Local
  remote: Remote
  sync: Sync

  static logger: Function

  // basePath is the directory where the config and pouch are saved
  constructor (basePath: string) {
    log.info(this.debugInformations(), 'App initialization')
    this.lang = 'fr'
    if (basePath == null) { basePath = os.homedir() }
    basePath = path.resolve(basePath)
    this.basePath = path.join(basePath, '.cozy-desktop')
    this.config = new Config(this.basePath)
    this.pouch = new Pouch(this.config)
    this.events = new SyncState()
  }

  // Parse the URL
  parseCozyUrl (cozyUrl: string) {
    if (cozyUrl.indexOf(':') === -1) {
      if (cozyUrl.indexOf('.') === -1) {
        cozyUrl += '.cozycloud.cc'
      }
      cozyUrl = `https://${cozyUrl}`
    }
    return url.parse(cozyUrl)
  }

  // Check that the cozyUrl is valid
  checkCozyUrl (cozyUrl: string) {
    let parsed = this.parseCozyUrl(cozyUrl)
    cozyUrl = url.format(parsed)
    if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
      let err = new Error(`Your URL looks invalid: ${cozyUrl}`)
      log.warn(err)
      throw err
    }
    return cozyUrl
  }

  // Returns an object including the syncPath only when valid, or with an error
  // otherwise.
  checkSyncPath (syncPath: string) {
    // FIXME: Temporarily forbid synchronization of a non-empty dir
    try {
      if (fs.readdirSync(syncPath).length !== 0) {
        return {syncPath, error: 'Please choose an empty directory'}
      }
    } catch (err) {
      if (err.code !== 'ENOENT') throw err
    }

    // We do not allow syncing the whole user home directory, the system users
    // directory or the whole system:
    // - It would probably to big regarding the current local events squashing
    //   implementation.
    // - It could conflict with another synchronization tool.
    // - Writing some third-party file with the corresponding app running could
    //   make it crash.
    // - Some files are device-specific and should not be synchronized anyway.
    //
    // We could exclude relevant files by default at some point, but it would
    // require many iterations to make it reliable.
    if ((os.homedir() + path.sep).startsWith(syncPath)) {
      return {syncPath, error: 'You cannot synchronize your whole system or personal folder'}
    }

    return {syncPath}
  }

  // Return a promise for registering a device on the remote cozy
  registerRemote (cozyUrl: string, redirectURI: ?string, onRegistered: ?Function, deviceName: string) {
    const registration = new Registration(cozyUrl, this.config)
    return registration.process(pkg, redirectURI, onRegistered, deviceName)
  }

  // Save the config with all the informations for synchonization
  saveConfig (cozyUrl: string, syncPath: string) {
    fs.ensureDirSync(syncPath)
    this.config.cozyUrl = cozyUrl
    this.config.syncPath = syncPath
    this.config.persist()
    log.info('The remote Cozy has properly been configured ' +
             'to work with current device.')
  }

  // Register current device to remote Cozy and then save related informations
  // to the config file (used by CLI, not GUI)
  async addRemote (cozyUrl: string, syncPath: string, deviceName: string) {
    try {
      const registered = await this.registerRemote(cozyUrl, null, null, deviceName)
      log.info(`Device ${registered.deviceName} has been added to ${cozyUrl}`)
      this.saveConfig(cozyUrl, syncPath)
    } catch (err) {
      log.error('An error occured while registering your device.')
      let parsed: Object = this.parseCozyUrl(cozyUrl)
      if (err.code === 'ENOTFOUND') {
        log.warn(`The DNS resolution for ${parsed.hostname} failed.`)
        log.warn('Are you sure the domain is OK?')
      } else if (err === 'Bad credentials') {
        log.warn(err)
        log.warn('Are you sure there are no typo on the passphrase?')
      } else {
        log.error(err)
        if (parsed.protocol === 'http:') {
          log.warn('Did you try with an httpS URL?')
        }
      }
    }
  }

  // Unregister current device from remote Cozy and then remove remote from
  // the config file
  async removeRemote () {
    try {
      if (!this.remote) {
        this.instanciate()
      }
      await this.remote.unregister()
      await this.removeConfig()
      log.info('Current device properly removed from remote cozy.')
      return null
    } catch (err) {
      log.error('An error occured while unregistering your device.')
      log.error(err)
      return err
    }
  }

  async removeConfig () {
    await this.pouch.db.destroy()
    for (const name of await fs.readdir(this.basePath)) {
      if (name.startsWith(LOG_FILENAME)) continue
      await fs.remove(path.join(this.basePath, name))
    }
  }

  // Send an issue by mail to the support
  sendMailToSupport (content: string) {
    const logs = fs.readFileSync(LOG_FILE, 'utf-8')
    content = content + '\r\n\r\n-------- debug info --------\r\n' +
      _.map(this.debugInformations(), (v, k) => `${k}: ${v}`).join('\r\n')
    const args = {
      mode: 'from',
      to: [
        { name: 'Support', email: 'contact@cozycloud.cc' }
      ],
      subject: 'Ask support for cozy-desktop',
      parts: [
        { type: 'text/plain', body: content }
      ],
      attachments: [
        { filename: 'logs.txt', content: logs }
      ]
    }
    return this.remote.sendMail(args)
  }

  // Load ignore rules
  loadIgnore () {
    let ignored
    try {
      let syncPath = this.config.syncPath
      ignored = fs.readFileSync(path.join(syncPath, '.cozyignore'))
      ignored = ignored.toString().split('\n')
    } catch (error) {
      ignored = []
    }
    this.ignore = new Ignore(ignored).addDefaultRules()
  }

  // Instanciate some objects before sync
  instanciate () {
    this.loadIgnore()
    this.merge = new Merge(this.pouch)
    this.prep = new Prep(this.merge, this.ignore, this.config)
    this.local = this.merge.local = new Local(this.config, this.prep, this.pouch, this.events)
    this.remote = this.merge.remote = new Remote(this.config, this.prep, this.pouch, this.events)
    this.sync = new Sync(this.pouch, this.local, this.remote, this.ignore, this.events)
    this.sync.diskUsage = this.diskUsage
  }

  // Start the synchronization
  startSync (mode: SyncMode) {
    this.config.saveMode(mode)
    log.info('Run first synchronisation...')
    return this.sync.start(mode)
  }

  // Stop the synchronisation
  stopSync () {
    if (!this.sync) {
      // $FlowFixMe
      return Promise.resolve()
    }
    return this.sync.stop()
  }

  // Start database sync process and setup file change watcher
  synchronize (mode: SyncMode) {
    if (!this.config.isValid()) {
      log.error('No configuration found, please run add-remote-cozy' +
                'command before running a synchronization.')
      throw new Error('No client configured')
    }
    this.instanciate()
    return this.startSync(mode)
  }

  // Display a list of watchers for debugging purpose
  debugWatchers () {
    if (this.local) {
      this.local.watcher.debug()
    }
  }

  debugInformations () {
    return {
      appVersion: pkg.version,
      osType: os.type(),
      osRelease: os.release(),
      osArch: os.arch()
    }
  }

  // Call the callback for each file
  walkFiles (args: {ignored?: any}, callback: Callback) {
    this.loadIgnore()
    let options = {
      root: this.config.syncPath,
      directoryFilter: '!.cozy-desktop',
      entryType: 'both'
    }
    readdirp(options)
      .on('warn', err => log.warn(err))
      .on('error', err => log.error(err))
      .on('data', data => {
        let doc = {
          _id: data.path,
          docType: data.stat.isFile() ? 'file' : 'folder'
        }
        if (this.ignore.isIgnored(doc) === (args.ignored != null)) {
          callback(data.path)
        }
      })
  }

  // Recreate the local pouch database
  resetDatabase () {
    log.info('Recreates the local database...')
    this.pouch.resetDatabase(function () {
      log.info('Database recreated')
    })
  }

  // Return the whole content of the database
  allDocs (callback: Callback) {
    this.pouch.db.allDocs({include_docs: true}, callback)
  }

  // Return all docs for a given query
  query (query: any, callback: Callback) {
    this.pouch.db.query(query, {include_docs: true}, callback)
  }

  // Get disk space informations from the cozy
  diskUsage (): Promise<*> {
    if (!this.remote) this.instanciate()
    return this.remote.diskUsage()
  }
}

App.logger = logger

export default App
<commit_msg>core: Introduce COZY_DESKTOP_SUPPORT_EMAIL env var<commit_after>/* @flow */

import EventEmitter from 'events'
import fs from 'fs-extra'
import _ from 'lodash'
import os from 'os'
import path from 'path'
import readdirp from 'readdirp'
import url from 'url'

import './globals' // FIXME Use bluebird promises as long as we need asCallback
import pkg from '../package.json'
import Config from './config'
import logger, { LOG_FILE, LOG_FILENAME } from './logger'
import Pouch from './pouch'
import Ignore from './ignore'
import Merge from './merge'
import Prep from './prep'
import Local from './local'
import Remote from './remote'
import Sync from './sync'
import SyncState from './syncstate'
import Registration from './remote/registration'

import type { Callback } from './utils/func'
import type { SyncMode } from './sync'

const log = logger({
  component: 'App'
})

const SUPPORT_EMAIL = process.env.COZY_DESKTOP_SUPPORT_EMAIL || 'contact@cozycloud.cc'

// App is the entry point for the CLI and GUI.
// They both can do actions and be notified by events via an App instance.
class App {
  lang: string
  basePath: string
  config: Config
  pouch: Pouch
  events: EventEmitter
  ignore: Ignore
  merge: Merge
  prep: Prep
  local: Local
  remote: Remote
  sync: Sync

  static logger: Function

  // basePath is the directory where the config and pouch are saved
  constructor (basePath: string) {
    log.info(this.debugInformations(), 'App initialization')
    this.lang = 'fr'
    if (basePath == null) { basePath = os.homedir() }
    basePath = path.resolve(basePath)
    this.basePath = path.join(basePath, '.cozy-desktop')
    this.config = new Config(this.basePath)
    this.pouch = new Pouch(this.config)
    this.events = new SyncState()
  }

  // Parse the URL
  parseCozyUrl (cozyUrl: string) {
    if (cozyUrl.indexOf(':') === -1) {
      if (cozyUrl.indexOf('.') === -1) {
        cozyUrl += '.cozycloud.cc'
      }
      cozyUrl = `https://${cozyUrl}`
    }
    return url.parse(cozyUrl)
  }

  // Check that the cozyUrl is valid
  checkCozyUrl (cozyUrl: string) {
    let parsed = this.parseCozyUrl(cozyUrl)
    cozyUrl = url.format(parsed)
    if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
      let err = new Error(`Your URL looks invalid: ${cozyUrl}`)
      log.warn(err)
      throw err
    }
    return cozyUrl
  }

  // Returns an object including the syncPath only when valid, or with an error
  // otherwise.
  checkSyncPath (syncPath: string) {
    // FIXME: Temporarily forbid synchronization of a non-empty dir
    try {
      if (fs.readdirSync(syncPath).length !== 0) {
        return {syncPath, error: 'Please choose an empty directory'}
      }
    } catch (err) {
      if (err.code !== 'ENOENT') throw err
    }

    // We do not allow syncing the whole user home directory, the system users
    // directory or the whole system:
    // - It would probably to big regarding the current local events squashing
    //   implementation.
    // - It could conflict with another synchronization tool.
    // - Writing some third-party file with the corresponding app running could
    //   make it crash.
    // - Some files are device-specific and should not be synchronized anyway.
    //
    // We could exclude relevant files by default at some point, but it would
    // require many iterations to make it reliable.
    if ((os.homedir() + path.sep).startsWith(syncPath)) {
      return {syncPath, error: 'You cannot synchronize your whole system or personal folder'}
    }

    return {syncPath}
  }

  // Return a promise for registering a device on the remote cozy
  registerRemote (cozyUrl: string, redirectURI: ?string, onRegistered: ?Function, deviceName: string) {
    const registration = new Registration(cozyUrl, this.config)
    return registration.process(pkg, redirectURI, onRegistered, deviceName)
  }

  // Save the config with all the informations for synchonization
  saveConfig (cozyUrl: string, syncPath: string) {
    fs.ensureDirSync(syncPath)
    this.config.cozyUrl = cozyUrl
    this.config.syncPath = syncPath
    this.config.persist()
    log.info('The remote Cozy has properly been configured ' +
             'to work with current device.')
  }

  // Register current device to remote Cozy and then save related informations
  // to the config file (used by CLI, not GUI)
  async addRemote (cozyUrl: string, syncPath: string, deviceName: string) {
    try {
      const registered = await this.registerRemote(cozyUrl, null, null, deviceName)
      log.info(`Device ${registered.deviceName} has been added to ${cozyUrl}`)
      this.saveConfig(cozyUrl, syncPath)
    } catch (err) {
      log.error('An error occured while registering your device.')
      let parsed: Object = this.parseCozyUrl(cozyUrl)
      if (err.code === 'ENOTFOUND') {
        log.warn(`The DNS resolution for ${parsed.hostname} failed.`)
        log.warn('Are you sure the domain is OK?')
      } else if (err === 'Bad credentials') {
        log.warn(err)
        log.warn('Are you sure there are no typo on the passphrase?')
      } else {
        log.error(err)
        if (parsed.protocol === 'http:') {
          log.warn('Did you try with an httpS URL?')
        }
      }
    }
  }

  // Unregister current device from remote Cozy and then remove remote from
  // the config file
  async removeRemote () {
    try {
      if (!this.remote) {
        this.instanciate()
      }
      await this.remote.unregister()
      await this.removeConfig()
      log.info('Current device properly removed from remote cozy.')
      return null
    } catch (err) {
      log.error('An error occured while unregistering your device.')
      log.error(err)
      return err
    }
  }

  async removeConfig () {
    await this.pouch.db.destroy()
    for (const name of await fs.readdir(this.basePath)) {
      if (name.startsWith(LOG_FILENAME)) continue
      await fs.remove(path.join(this.basePath, name))
    }
  }

  // Send an issue by mail to the support
  sendMailToSupport (content: string) {
    const logs = fs.readFileSync(LOG_FILE, 'utf-8')
    content = content + '\r\n\r\n-------- debug info --------\r\n' +
      _.map(this.debugInformations(), (v, k) => `${k}: ${v}`).join('\r\n')
    const args = {
      mode: 'from',
      to: [
        { name: 'Support', email: SUPPORT_EMAIL }
      ],
      subject: 'Ask support for cozy-desktop',
      parts: [
        { type: 'text/plain', body: content }
      ],
      attachments: [
        { filename: 'logs.txt', content: logs }
      ]
    }
    return this.remote.sendMail(args)
  }

  // Load ignore rules
  loadIgnore () {
    let ignored
    try {
      let syncPath = this.config.syncPath
      ignored = fs.readFileSync(path.join(syncPath, '.cozyignore'))
      ignored = ignored.toString().split('\n')
    } catch (error) {
      ignored = []
    }
    this.ignore = new Ignore(ignored).addDefaultRules()
  }

  // Instanciate some objects before sync
  instanciate () {
    this.loadIgnore()
    this.merge = new Merge(this.pouch)
    this.prep = new Prep(this.merge, this.ignore, this.config)
    this.local = this.merge.local = new Local(this.config, this.prep, this.pouch, this.events)
    this.remote = this.merge.remote = new Remote(this.config, this.prep, this.pouch, this.events)
    this.sync = new Sync(this.pouch, this.local, this.remote, this.ignore, this.events)
    this.sync.diskUsage = this.diskUsage
  }

  // Start the synchronization
  startSync (mode: SyncMode) {
    this.config.saveMode(mode)
    log.info('Run first synchronisation...')
    return this.sync.start(mode)
  }

  // Stop the synchronisation
  stopSync () {
    if (!this.sync) {
      // $FlowFixMe
      return Promise.resolve()
    }
    return this.sync.stop()
  }

  // Start database sync process and setup file change watcher
  synchronize (mode: SyncMode) {
    if (!this.config.isValid()) {
      log.error('No configuration found, please run add-remote-cozy' +
                'command before running a synchronization.')
      throw new Error('No client configured')
    }
    this.instanciate()
    return this.startSync(mode)
  }

  // Display a list of watchers for debugging purpose
  debugWatchers () {
    if (this.local) {
      this.local.watcher.debug()
    }
  }

  debugInformations () {
    return {
      appVersion: pkg.version,
      osType: os.type(),
      osRelease: os.release(),
      osArch: os.arch()
    }
  }

  // Call the callback for each file
  walkFiles (args: {ignored?: any}, callback: Callback) {
    this.loadIgnore()
    let options = {
      root: this.config.syncPath,
      directoryFilter: '!.cozy-desktop',
      entryType: 'both'
    }
    readdirp(options)
      .on('warn', err => log.warn(err))
      .on('error', err => log.error(err))
      .on('data', data => {
        let doc = {
          _id: data.path,
          docType: data.stat.isFile() ? 'file' : 'folder'
        }
        if (this.ignore.isIgnored(doc) === (args.ignored != null)) {
          callback(data.path)
        }
      })
  }

  // Recreate the local pouch database
  resetDatabase () {
    log.info('Recreates the local database...')
    this.pouch.resetDatabase(function () {
      log.info('Database recreated')
    })
  }

  // Return the whole content of the database
  allDocs (callback: Callback) {
    this.pouch.db.allDocs({include_docs: true}, callback)
  }

  // Return all docs for a given query
  query (query: any, callback: Callback) {
    this.pouch.db.query(query, {include_docs: true}, callback)
  }

  // Get disk space informations from the cozy
  diskUsage (): Promise<*> {
    if (!this.remote) this.instanciate()
    return this.remote.diskUsage()
  }
}

App.logger = logger

export default App
<|endoftext|>


<commit_before>$.fn.userAutocomplete = function () {
  'use strict';

  function formatUser(user) {
    return Select2.util.escapeMarkup(user.email);
  }

  this.select2({
    minimumInputLength: 1,
    multiple: true,
    initSelection: function (element, callback) {
      $.get(Spree.routes.users_api, {
        ids: element.val(),
        token: Spree.api_key
      }, function (data) {
        callback(data.users);
      });
    },
    ajax: {
      url: Spree.routes.users_api,
      datatype: 'json',
      data: function (term) {
        return {
          q: term,
          token: Spree.api_key
        };
      },
      results: function (data) {
        return {
          results: data.users
        };
      }
    },
    formatResult: formatUser,
    formatSelection: formatUser
  });
};

$(document).ready(function () {
  $('.user_picker').userAutocomplete();
});
<commit_msg>Search term should be contained as a ransack prefix in params<commit_after>$.fn.userAutocomplete = function () {
  'use strict';

  function formatUser(user) {
    return Select2.util.escapeMarkup(user.email);
  }

  this.select2({
    minimumInputLength: 1,
    multiple: true,
    initSelection: function (element, callback) {
      $.get(Spree.routes.users_api, {
        ids: element.val(),
        token: Spree.api_key
      }, function (data) {
        callback(data.users);
      });
    },
    ajax: {
      url: Spree.routes.users_api,
      datatype: 'json',
      data: function (term) {
        return {
          q: {
            email_cont: term
          },
          token: Spree.api_key
        };
      },
      results: function (data) {
        return {
          results: data.users
        };
      }
    },
    formatResult: formatUser,
    formatSelection: formatUser
  });
};

$(document).ready(function () {
  $('.user_picker').userAutocomplete();
});
<|endoftext|>