<commit_before>module.exports = [
  {
    item: '國外交易手續費',
    match: /^國外交易服務費﹣.*$/,
    rate: 0
  },
  {
    item: '全聯福利中心',
    match: /^全聯.*分公司.*$/,
    rate: 0
  },
  {
    item: 'iCash 自動加值',
    match: /^icash自動加值.*$/,
    rate: 0
  },
  {
    item: '悠遊卡自動加值',
    match: /^悠遊卡自動加值.*$/,
    rate: 0
  },
  {
    item: '全家便利商店',
    match: /^全家便利商店﹣.*$/,
    rate: 0
  },
  {
    item: '活動回饋',
    match: /^活動類.*$/,
    rate: 0,
    note: '特殊活動不計算回饋'
  },
  {
    item: '台灣高鐵',
    match: /^高鐵ＥＣ.*$/,
    rate: 0.02
  },
  {
    item: '台灣高鐵 app 購票',
    match: /^高鐵智慧型手機.*$/,
    rate: 0.02
  },
  {
    item: '台鐵',
    match: /^ｒａｉｌｗａｙTAIPEI\/TW$/,
    rate: 0.02
  },
  {
    item: '91 APP 代收',
    match: /^網路購物﹣９１ＡＰＰ代收服務.*$/,
    rate: 0.02
  },
  {
    item: '支付寶',
    match: /^ALIPAY\*TAOBAO\.COM.*$/,
    rate: 0.02
  },
  {
    item: '淘寶／天貓',
    match: /www\.taobao\.com/,
    rate: 0.02,
    contributor: 'AIC'
  },
  {
    item: '蝦皮拍賣',
    match: /^蝦皮拍賣.*$/,
    rate: 0.02
  },
  {
    item: '蝦皮購物',
    match: /^蝦皮購物.*$/,
    rate: 0.02,
    contributor: 'Sougetu'
  },
  {
    item: '愛貝金流',
    match: /^愛貝金流﹣.*$/,
    rate: 0.02,
    contributor: '王韻筑',
    note: '蝦皮購物使用愛貝金流'
  }
  {
    item: '街口支付',
    match: /^街口﹣.*$/,
    rate: 0.02,
    note: '街口支付有`街口-`和`街口支付-`兩種前綴'
  },
  {
    item: '街口支付',
    match: /^街口支付﹣.*$/,
    rate: 0.02,
    note: '街口支付有`街口-`和`街口支付-`兩種前綴'
  },
  {
    item: '街口電支',
    match: /^街口電支.*$/,
    rate: 0.02
  },
  {
    item: 'LINE Pay',
    match: /^連加＊.*$/,
    rate: 0.02
  },
  {
    item: 'Let\'s Pay',
    match: /^.*ＬＥＴＳＰＡＹTAIPEI\/TW$/,
    rate: 0.02
  },
  {
    item: '台北 101 Apple Store',
    match: /ＡＰＰＬＥＳＴＯＲＥ.*/,
    rate: 0.02,
    note: '在 Apple Store 使用 Apple Pay 會被判斷為數位通路'
  },
  {
    item: 'Apple Store',
    match: /^美商蘋果亞洲股份有限.*$/,
    rate: 0.02,
  },
  {
    item: 'Lativ',
    match: /^米格國際股份有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'UNIQLO 網路購物',
    match: /^台灣優衣庫有限公司ＥＣ.*$/,
    rate: 0.02
  },
  {
    item: 'PChome',
    match: /^網路家庭國際資訊股份.*$/,
    rate: 0.02
  },
  {
    item: 'PChome 商店街個人賣場',
    match: /^商店街市集.*$/,
    rate: 0.02,
    contributor: 'Ron'
  },
  {
    item: 'GoHappy 快樂購物網',
    match: /^ＧＯＨＡＰＰＹ快樂購物網.*$/,
    rate: 0.02,
    contributor: 'Ron'
  },
  {
    item: 'Airbnb',
    match: /^AIRBNB \* .*$/,
    rate: 0.02
  },
  {
    item: '智付通',
    match: /^智付通﹣.*$/,
    rate: 0.02
  },
  {
    item: '綠界科技',
    match: /^綠界科技﹣.*$/,
    rate: 0.02
  },
  {
    item: '星巴克線上儲值',
    match: /^統一星巴克線上儲值.*$/,
    rate: 0.02
  },
  {
    item: '星巴克線上儲值',
    match: /^星巴克線上儲值.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: 'foodpanda',
    match: /^空腹熊貓﹣ＭｉＧＳ.*$/,
    rate: 0.02
  },
  {
    item: 'PChomePay支付連',
    match: /^支付連國際.*$/,
    rate: 0.02
  },
  {
    item: 'Google Wallet',
    match: /^GOOGLE ?\*.*$/,
    rate: 0.02
  },
  {
    item: 'PayPal',
    match: /^PAYPAL\*.*$/,
    rate: 0.02
  },
  {
    item: 'Yahoo 輕鬆付',
    match: /^雅虎輕鬆付.*$/,
    rate: 0.02
  },
  {
    item: '嘖嘖 zeczec',
    match: /^立吉富﹣嘖室股份有限公司.*$/,
    rate: 0.02,
    contributor: '沒關係',
    note: '明細開頭為 立吉富﹣嘖室股份有限公司'
  },
  {
    item: '博客來',
    match: /^博客來數位科技股份有限公司.*$/,
    rate: 0.02
  },
  {
    item: '誠品網路書店',
    match: /^誠品網路書店.*$/,
    rate: 0.02
  },
  {
    item: 'Netflix',
    match: /^NETFLIX\.COM.*$/,
    rate: 0.02,
    contributor: '沒關係'
  },
  {
    item: '歐付寶',
    match: /^allPay Electronic Payment.*$/,
    rate: 0.02
  },
  {
    item: '小米官網',
    match: /^台灣小米通訊有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'Pinkoi',
    match: /^Ｐｉｎｋｏｉ˙ｃｏｍ.*$/,
    rate: 0.02
  },
  {
    item: 'Vacanza Accessory',
    match: /^假期國際有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'iherb',
    match: /^iHerb.*$/,
    rate: 0.02,
    contributor: 'AIC'
  },
  {
    item: 'ORBIS',
    match: /^台灣奧蜜思股份有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'iTunes',
    match: /^ITUNES\.COM\/BILL.*$/,
    rate: 0.02
  },
  {
    item: 'STEAM',
    match: /WWW\.STEAMPOWERED\.COM.*$/,
    rate: 0.02,
    note: '明細開頭為 WWW.STEAMPOWERED.COM'
  },
  {
    item: 'STEAM',
    match: /^STEAMGAMES.*$/,
    rate: 0.02,
    note: '明細開頭為 STEAMGAMES'
  },
  {
    item: 'Yahoo',
    match: /^雅虎奇摩.*$/,
    rate: 0.02
  },
  {
    item: 'momo 購物網',
    match: /^ＭＯＭＯ﹣ＥＣ.*$/,
    rate: 0.02,
    note: '明細上可能為 "MOMO-EC" 或 "富邦媒體科技"'
  },
  {
    item: 'momo 購物網',
    match: /^富邦媒體科技股份有限公司.*$/,
    rate: 0.02,
    note: '明細上可能為 "MOMO-EC" 或 "富邦媒體科技"'
  },
  {
    item: '迪卡儂網路購物',
    match: /^ＤＥＣＡＴＨＬＯＮ.*$/,
    rate: 0.02,
    note: '若於實體店面使用 Apple Pay 等支付有機會列入數位通路'
  },
  {
    item: '樂天市場',
    match: /^樂天市場＊.*$/,
    rate: 0.02,
    contributor: 'leona83525'
  },
  {
    item: '家樂福線上購物',
    match: /^家樂福﹣青海店ＥＣ.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: 'Amazon 日本',
    match: /^AMAZON.CO.JP.*$/,
    rate: 0.02,
    contributor: 'Sougetu'
  },
  {
    item: 'Amazon US',
    match: /^AMAZONMKTPLACEPMTSAMZNAMZN.*$/,
    rate: 0.02,
    contributor: 'tacoceden'
  },
  {
    item: 'Uber',
    match: /^UBERTW.*$/,
    rate: 0.02
  },
  {
    item: 'Uber',
    match: /^UBERTRIPKVGCCHELP.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: 'Uber Eats',
    match: /^UBEREATST32GQHELP.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: '台灣大車隊',
    match: /^台灣大車隊ＡＰＰ.*$/,
    rate: 0.02,
    note: 'App 綁定支付'
  }
];
<commit_msg>Update rate.js<commit_after>module.exports = [
  {
    item: '國外交易手續費',
    match: /^國外交易服務費﹣.*$/,
    rate: 0
  },
  {
    item: '全聯福利中心',
    match: /^全聯.*分公司.*$/,
    rate: 0
  },
  {
    item: 'iCash 自動加值',
    match: /^icash自動加值.*$/,
    rate: 0
  },
  {
    item: '悠遊卡自動加值',
    match: /^悠遊卡自動加值.*$/,
    rate: 0
  },
  {
    item: '全家便利商店',
    match: /^全家便利商店﹣.*$/,
    rate: 0
  },
  {
    item: '活動回饋',
    match: /^活動類.*$/,
    rate: 0,
    note: '特殊活動不計算回饋'
  },
  {
    item: '台灣高鐵',
    match: /^高鐵ＥＣ.*$/,
    rate: 0.02
  },
  {
    item: '台灣高鐵 app 購票',
    match: /^高鐵智慧型手機.*$/,
    rate: 0.02
  },
  {
    item: '台鐵',
    match: /^ｒａｉｌｗａｙTAIPEI\/TW$/,
    rate: 0.02
  },
  {
    item: '91 APP 代收',
    match: /^網路購物﹣９１ＡＰＰ代收服務.*$/,
    rate: 0.02
  },
  {
    item: '支付寶',
    match: /^ALIPAY\*TAOBAO\.COM.*$/,
    rate: 0.02
  },
  {
    item: '淘寶／天貓',
    match: /www\.taobao\.com/,
    rate: 0.02,
    contributor: 'AIC'
  },
  {
    item: '蝦皮拍賣',
    match: /^蝦皮拍賣.*$/,
    rate: 0.02
  },
  {
    item: '蝦皮購物',
    match: /^蝦皮購物.*$/,
    rate: 0.02,
    contributor: 'Sougetu'
  },
  {
    item: '愛貝金流',
    match: /^愛貝金流﹣.*$/,
    rate: 0.02,
    contributor: '王韻筑',
    note: '蝦皮購物使用愛貝金流'
  },
  {
    item: '街口支付',
    match: /^街口﹣.*$/,
    rate: 0.02,
    note: '街口支付有`街口-`和`街口支付-`兩種前綴'
  },
  {
    item: '街口支付',
    match: /^街口支付﹣.*$/,
    rate: 0.02,
    note: '街口支付有`街口-`和`街口支付-`兩種前綴'
  },
  {
    item: '街口電支',
    match: /^街口電支.*$/,
    rate: 0.02
  },
  {
    item: 'LINE Pay',
    match: /^連加＊.*$/,
    rate: 0.02
  },
  {
    item: 'Let\'s Pay',
    match: /^.*ＬＥＴＳＰＡＹTAIPEI\/TW$/,
    rate: 0.02
  },
  {
    item: '台北 101 Apple Store',
    match: /ＡＰＰＬＥＳＴＯＲＥ.*/,
    rate: 0.02,
    note: '在 Apple Store 使用 Apple Pay 會被判斷為數位通路'
  },
  {
    item: 'Apple Store',
    match: /^美商蘋果亞洲股份有限.*$/,
    rate: 0.02,
  },
  {
    item: 'Lativ',
    match: /^米格國際股份有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'UNIQLO 網路購物',
    match: /^台灣優衣庫有限公司ＥＣ.*$/,
    rate: 0.02
  },
  {
    item: 'PChome',
    match: /^網路家庭國際資訊股份.*$/,
    rate: 0.02
  },
  {
    item: 'PChome 商店街個人賣場',
    match: /^商店街市集.*$/,
    rate: 0.02,
    contributor: 'Ron'
  },
  {
    item: 'GoHappy 快樂購物網',
    match: /^ＧＯＨＡＰＰＹ快樂購物網.*$/,
    rate: 0.02,
    contributor: 'Ron'
  },
  {
    item: 'Airbnb',
    match: /^AIRBNB \* .*$/,
    rate: 0.02
  },
  {
    item: '智付通',
    match: /^智付通﹣.*$/,
    rate: 0.02
  },
  {
    item: '綠界科技',
    match: /^綠界科技﹣.*$/,
    rate: 0.02
  },
  {
    item: '星巴克線上儲值',
    match: /^統一星巴克線上儲值.*$/,
    rate: 0.02
  },
  {
    item: '星巴克線上儲值',
    match: /^星巴克線上儲值.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: 'foodpanda',
    match: /^空腹熊貓﹣ＭｉＧＳ.*$/,
    rate: 0.02
  },
  {
    item: 'PChomePay支付連',
    match: /^支付連國際.*$/,
    rate: 0.02
  },
  {
    item: 'Google Wallet',
    match: /^GOOGLE ?\*.*$/,
    rate: 0.02
  },
  {
    item: 'PayPal',
    match: /^PAYPAL\*.*$/,
    rate: 0.02
  },
  {
    item: 'Yahoo 輕鬆付',
    match: /^雅虎輕鬆付.*$/,
    rate: 0.02
  },
  {
    item: '嘖嘖 zeczec',
    match: /^立吉富﹣嘖室股份有限公司.*$/,
    rate: 0.02,
    contributor: '沒關係',
    note: '明細開頭為 立吉富﹣嘖室股份有限公司'
  },
  {
    item: '博客來',
    match: /^博客來數位科技股份有限公司.*$/,
    rate: 0.02
  },
  {
    item: '誠品網路書店',
    match: /^誠品網路書店.*$/,
    rate: 0.02
  },
  {
    item: 'Netflix',
    match: /^NETFLIX\.COM.*$/,
    rate: 0.02,
    contributor: '沒關係'
  },
  {
    item: '歐付寶',
    match: /^allPay Electronic Payment.*$/,
    rate: 0.02
  },
  {
    item: '小米官網',
    match: /^台灣小米通訊有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'Pinkoi',
    match: /^Ｐｉｎｋｏｉ˙ｃｏｍ.*$/,
    rate: 0.02
  },
  {
    item: 'Vacanza Accessory',
    match: /^假期國際有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'iherb',
    match: /^iHerb.*$/,
    rate: 0.02,
    contributor: 'AIC'
  },
  {
    item: 'ORBIS',
    match: /^台灣奧蜜思股份有限公司.*$/,
    rate: 0.02
  },
  {
    item: 'iTunes',
    match: /^ITUNES\.COM\/BILL.*$/,
    rate: 0.02
  },
  {
    item: 'STEAM',
    match: /WWW\.STEAMPOWERED\.COM.*$/,
    rate: 0.02,
    note: '明細開頭為 WWW.STEAMPOWERED.COM'
  },
  {
    item: 'STEAM',
    match: /^STEAMGAMES.*$/,
    rate: 0.02,
    note: '明細開頭為 STEAMGAMES'
  },
  {
    item: 'Yahoo',
    match: /^雅虎奇摩.*$/,
    rate: 0.02
  },
  {
    item: 'momo 購物網',
    match: /^ＭＯＭＯ﹣ＥＣ.*$/,
    rate: 0.02,
    note: '明細上可能為 "MOMO-EC" 或 "富邦媒體科技"'
  },
  {
    item: 'momo 購物網',
    match: /^富邦媒體科技股份有限公司.*$/,
    rate: 0.02,
    note: '明細上可能為 "MOMO-EC" 或 "富邦媒體科技"'
  },
  {
    item: '迪卡儂網路購物',
    match: /^ＤＥＣＡＴＨＬＯＮ.*$/,
    rate: 0.02,
    note: '若於實體店面使用 Apple Pay 等支付有機會列入數位通路'
  },
  {
    item: '樂天市場',
    match: /^樂天市場＊.*$/,
    rate: 0.02,
    contributor: 'leona83525'
  },
  {
    item: '家樂福線上購物',
    match: /^家樂福﹣青海店ＥＣ.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: 'Amazon 日本',
    match: /^AMAZON.CO.JP.*$/,
    rate: 0.02,
    contributor: 'Sougetu'
  },
  {
    item: 'Amazon US',
    match: /^AMAZONMKTPLACEPMTSAMZNAMZN.*$/,
    rate: 0.02,
    contributor: 'tacoceden'
  },
  {
    item: 'Uber',
    match: /^UBERTW.*$/,
    rate: 0.02
  },
  {
    item: 'Uber',
    match: /^UBERTRIPKVGCCHELP.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: 'Uber Eats',
    match: /^UBEREATST32GQHELP.*$/,
    rate: 0.02,
    contributor: '王韻筑'
  },
  {
    item: '台灣大車隊',
    match: /^台灣大車隊ＡＰＰ.*$/,
    rate: 0.02,
    note: 'App 綁定支付'
  }
];
<|endoftext|>


<commit_before>// ==========================================================================
// Project:   SproutCore - JavaScript Application Framework
// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
//            Portions ©2008-2009 Apple Inc. All rights reserved.
// License:   Licened under MIT license (see license.js)
// ==========================================================================

/**
  @namespace

  A Collection View Delegate is consulted by a SC.CollectionView's to make
  policy decisions about certain behaviors such as selection control and
  drag and drop.  If you need to control other aspects of your data, you may
  also want to add the SC.CollectionContent mixin.

  To act as a Collection Delegate, just apply this mixin to your class.  You
  must then set the "delegate" property on the CollectionView to your object.

  Alternatively, if no delegate is set on a CollectionView, but the content
  implements this mixin, the content object will be used as the delegate
  instead.

  If you set an ArrayController or its arrangedObjects property as the content
  of a CollectionView, the ArrayController will automatically act as the
  delegate for the view.

  @since SproutCore 1.0
*/
SC.CollectionViewDelegate = {

  /**
    Used to detect the mixin by SC.CollectionView
  */
  isCollectionViewDelegate: YES,

  // ..........................................................
  // SELECTION
  //

  /**
    This method will be called anytime the collection view is about to
    change the selection in response to user mouse clicks or keyboard events.

    You can use this method to adjust the proposed selection, eliminating any
    selected objects that cannot be selected.  The default implementation of
    this method simply returns the proposed selection.

    @param {SC.CollectionView} view the collection view
    @param {SC.IndexSet} sel Proposed array of selected objects.
    @returns {SC.IndexSet} Actual allow selection index set
  */
  collectionViewSelectionForProposedSelection: function(view, sel) {
    return sel ;
  },

  /**
    Called by the collection when attempting to select an item.  Return the
    actual indexes you want to allow to be selected.  Return null to disallow
    the change.  The default allows all selection.

    @param {SC.CollectionView} view the view collection view
    @param {SC.IndexSet} indexes the indexes to be selected
    @param {Boolean} extend YES if the indexes will extend existing sel
    @returns {SC.IndexSet} allowed index set
  */
  collectionViewShouldSelectIndexes: function (view, indexes, extend) {
    return indexes;
  },

  /**
    Called by the collection when attempting to deselect an item.  Return the
    actual indexes you want to allow to be deselected.  Return null to
    disallow the change.  The default allows all selection.

    Note that you should not modify the passed in IndexSet.  clone it instead.

    @param {SC.CollectionView} view the view collection view
    @param {SC.IndexSet} indexes the indexes to be selected
    @returns {SC.IndexSet} allowed index set
  */
  collectionViewShouldDeselectIndexes: function (view, indexes) {
    return indexes;
  },

  // ..........................................................
  // EDIT OPERATIONS
  //

  /**
    Called by the collection view whenever the deleteSelection() method is
    called.  You can implement this method to get fine-grained control over
    which items can be deleted.  To prevent deletion, return null.

    This method is only called if canDeleteContent is YES on the collection
    view.

    @param {SC.CollectionView} view the collection view
    @param {SC.IndexSet} indexes proposed index set of items to delete.
    @returns {SC.IndexSet} index set allowed to delete or null.
  */
  collectionViewShouldDeleteIndexes: function(view, indexes) {
    return indexes;
  },

  /**
    Called by the collection view to actually delete the selected items.

    The default behavior will use standard array operators to delete the
    indexes from the array.  You can implement this method to provide your own
    deletion method.

    If you simply want to control the items to be deleted, you should instead
    implement collectionViewShouldDeleteItems().  This method will only be
    called if canDeleteContent is YES and collectionViewShouldDeleteIndexes()
    returns a non-empty index set

    @param {SC.CollectionView} view collection view
    @param {SC.IndexSet} indexes the items to delete
    @returns {Boolean} YES if the deletion was a success.
  */
  collectionViewDeleteContent: function(view, content, indexes) {
    if (!content) return NO ;

    if (SC.typeOf(content.destroyAt) === SC.T_FUNCTION) {
      content.destroyAt(indexes);
      return YES ;

    } else if (SC.typeOf(content.removeAt) === SC.T_FUNCTION) {
      content.removeAt(indexes);
      return YES;

    } else return NO ;
  },

  // ..........................................................
  // DRAGGING
  //

  /**
    Called by the collection view just before it starts a drag to give you
    an opportunity to decide if the drag should be allowed.

    You can use this method to implement fine-grained control over when a
    drag will be allowed and when it will not be allowed.  For example, you
    may enable content reordering but then implement this method to prevent
    reordering of certain items in the view.

    The default implementation always returns YES.

    @param view {SC.CollectionView} the collection view
    @returns {Boolean} YES to alow, NO to prevent it
  */
  collectionViewShouldBeginDrag: function(view) { return YES; },

  /**
    Called by the collection view just before it starts a drag so that
    you can provide the data types you would like to support in the data.

    You can implement this method to return an array of the data types you
    will provide for the drag data.

    If you return null or an empty array, can you have set canReorderContent
    to YES on the CollectionView, then the drag will go ahead but only
    reordering will be allowed.  If canReorderContent is NO, then the drag
    will not be allowed to start.

    If you simply want to control whether a drag is allowed or not, you
    should instead implement collectionViewShouldBeginDrag().

    The default returns an empty array.

    @param view {SC.CollectionView} the collection view to begin dragging.
    @returns {Array} array of supported data types.
  */
  collectionViewDragDataTypes: function(view) { return []; },

  /**
    Called by a collection view when a drag concludes to give you the option
    to provide the drag data for the drop.

    This method should be implemented essentially as you would implement the
    dragDataForType() if you were a drag data source.  You will never be asked
    to provide drag data for a reorder event, only for other types of data.

    The default implementation returns null.

    @param view {SC.CollectionView}
      the collection view that initiated the drag

    @param dataType {String} the data type to provide
    @param drag {SC.Drag} the drag object
    @returns {Object} the data object or null if the data could not be provided.
  */
  collectionViewDragDataForType: function(view, drag, dataType) {
    return null ;
  },

  /**
    Called once during a drag the first time view is entered. Return all
    possible drag operations OR'd together.

    @param {SC.CollectionView} view
      the collection view that initiated the drag

    @param {SC.Drag} drag
      the drag object

    @param {Number} proposedDragOperations
      proposed logical OR of allowed drag operations.

    @returns {Number} the allowed drag operations. Defaults to op
  */
  collectionViewComputeDragOperations: function(view, drag, proposedDragOperations) {
    return proposedDragOperations ;
  },

  /**
    Called by the collection view during a drag to let you determine the
    kind and location of a drop you might want to accept.

    You can override this method to implement fine-grained control over how
    and when a dragged item is allowed to be dropped into a collection view.

    This method will be called by the collection view both to determine in
    general which operations you might support and specifically the operations
    you would support if the user dropped an item over a specific location.

    If the proposedDropOperaration parameter is SC.DROP_ON or SC.DROP_BEFORE,
    then the proposedInsertionPoint will be a non-negative value and you
    should determine the specific operations you will support if the user
    dropped the drag item at that point.

    If you do not like the proposed drop operation or insertion point, you
    can override these properties as well by setting the proposedDropOperation
    and proposedInsertionIndex properties on the collection view during this
    method.  These properties are ignored all other times.

    @param view {SC.CollectionView} the collection view
    @param drag {SC.Drag} the current drag object
    @param op {Number} proposed logical OR of allowed drag operations.
    @param proposedInsertionIndex {Number} an index into the content array
      representing the proposed insertion point.
    @param proposedDropOperation {String} the proposed drop operation.  Will be one of SC.DROP_ON, SC.DROP_BEFORE, or SC.DROP_ANY.
    @returns the allowed drag operation.  Defaults to op
  */
  collectionViewValidateDragOperation: function(view, drag, op, proposedInsertionIndex, proposedDropOperaration) {
    // don't allow dropping on by default
    return (proposedDropOperaration & SC.DROP_ON) ? SC.DRAG_NONE : op ;
  },

  /**
    Called by the collection view to actually accept a drop.  This method will
    only be invoked AFTER your validateDrop method has been called to
    determine if you want to even allow the drag operation to go through.

    You should actually make changes to the data model if needed here and
    then return the actual drag operation that was performed.  If you return
    SC.DRAG_NONE and the dragOperation was SC.DRAG_REORDER, then the default
    reorder behavior will be provided by the collection view.

    @param view {SC.CollectionView}
    @param drag {SC.Drag} the current drag object
    @param op {Number} proposed logical OR of allowed drag operations.
    @param proposedInsertionIndex {Number} an index into the content array representing the proposed insertion point.
    @param proposedDropOperation {String} the proposed drop operation.  Will be one of SC.DROP_ON, SC.DROP_BEFORE, or SC.DROP_ANY.
    @returns the allowed drag operation.  Defaults to proposedDragOperation
  */
  collectionViewPerformDragOperation: function(view, drag, op, proposedInsertionIndex, proposedDropOperaration) {
    return SC.DRAG_NONE ;
  },

  /**
    Renders a drag view for the passed content indexes. If you return null
    from this, then a default drag view will be generated for you.

    @param {SC.CollectionView} view
    @param {SC.IndexSet} dragContent
    @returns {SC.View} view or null
  */
  collectionViewDragViewFor: function(view, dragContent) {
    return null;
  },

  /**
    Allows the ghost view created in collectionViewDragViewFor to be displayed
    like a cursor instead of the default implementation. This sets the view
    origin to be the location of the mouse cursor.

    @property {Boolean} ghost view acts like a cursor
  */
  ghostActsLikeCursor: NO

};
<commit_msg>Fix for spelling mistake proposedDropOperaration to proposedDropOperation<commit_after>// ==========================================================================
// Project:   SproutCore - JavaScript Application Framework
// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
//            Portions ©2008-2009 Apple Inc. All rights reserved.
// License:   Licened under MIT license (see license.js)
// ==========================================================================

/**
  @namespace

  A Collection View Delegate is consulted by a SC.CollectionView's to make
  policy decisions about certain behaviors such as selection control and
  drag and drop.  If you need to control other aspects of your data, you may
  also want to add the SC.CollectionContent mixin.

  To act as a Collection Delegate, just apply this mixin to your class.  You
  must then set the "delegate" property on the CollectionView to your object.

  Alternatively, if no delegate is set on a CollectionView, but the content
  implements this mixin, the content object will be used as the delegate
  instead.

  If you set an ArrayController or its arrangedObjects property as the content
  of a CollectionView, the ArrayController will automatically act as the
  delegate for the view.

  @since SproutCore 1.0
*/
SC.CollectionViewDelegate = {

  /**
    Used to detect the mixin by SC.CollectionView
  */
  isCollectionViewDelegate: YES,

  // ..........................................................
  // SELECTION
  //

  /**
    This method will be called anytime the collection view is about to
    change the selection in response to user mouse clicks or keyboard events.

    You can use this method to adjust the proposed selection, eliminating any
    selected objects that cannot be selected.  The default implementation of
    this method simply returns the proposed selection.

    @param {SC.CollectionView} view the collection view
    @param {SC.IndexSet} sel Proposed array of selected objects.
    @returns {SC.IndexSet} Actual allow selection index set
  */
  collectionViewSelectionForProposedSelection: function(view, sel) {
    return sel ;
  },

  /**
    Called by the collection when attempting to select an item.  Return the
    actual indexes you want to allow to be selected.  Return null to disallow
    the change.  The default allows all selection.

    @param {SC.CollectionView} view the view collection view
    @param {SC.IndexSet} indexes the indexes to be selected
    @param {Boolean} extend YES if the indexes will extend existing sel
    @returns {SC.IndexSet} allowed index set
  */
  collectionViewShouldSelectIndexes: function (view, indexes, extend) {
    return indexes;
  },

  /**
    Called by the collection when attempting to deselect an item.  Return the
    actual indexes you want to allow to be deselected.  Return null to
    disallow the change.  The default allows all selection.

    Note that you should not modify the passed in IndexSet.  clone it instead.

    @param {SC.CollectionView} view the view collection view
    @param {SC.IndexSet} indexes the indexes to be selected
    @returns {SC.IndexSet} allowed index set
  */
  collectionViewShouldDeselectIndexes: function (view, indexes) {
    return indexes;
  },

  // ..........................................................
  // EDIT OPERATIONS
  //

  /**
    Called by the collection view whenever the deleteSelection() method is
    called.  You can implement this method to get fine-grained control over
    which items can be deleted.  To prevent deletion, return null.

    This method is only called if canDeleteContent is YES on the collection
    view.

    @param {SC.CollectionView} view the collection view
    @param {SC.IndexSet} indexes proposed index set of items to delete.
    @returns {SC.IndexSet} index set allowed to delete or null.
  */
  collectionViewShouldDeleteIndexes: function(view, indexes) {
    return indexes;
  },

  /**
    Called by the collection view to actually delete the selected items.

    The default behavior will use standard array operators to delete the
    indexes from the array.  You can implement this method to provide your own
    deletion method.

    If you simply want to control the items to be deleted, you should instead
    implement collectionViewShouldDeleteItems().  This method will only be
    called if canDeleteContent is YES and collectionViewShouldDeleteIndexes()
    returns a non-empty index set

    @param {SC.CollectionView} view collection view
    @param {SC.IndexSet} indexes the items to delete
    @returns {Boolean} YES if the deletion was a success.
  */
  collectionViewDeleteContent: function(view, content, indexes) {
    if (!content) return NO ;

    if (SC.typeOf(content.destroyAt) === SC.T_FUNCTION) {
      content.destroyAt(indexes);
      return YES ;

    } else if (SC.typeOf(content.removeAt) === SC.T_FUNCTION) {
      content.removeAt(indexes);
      return YES;

    } else return NO ;
  },

  // ..........................................................
  // DRAGGING
  //

  /**
    Called by the collection view just before it starts a drag to give you
    an opportunity to decide if the drag should be allowed.

    You can use this method to implement fine-grained control over when a
    drag will be allowed and when it will not be allowed.  For example, you
    may enable content reordering but then implement this method to prevent
    reordering of certain items in the view.

    The default implementation always returns YES.

    @param view {SC.CollectionView} the collection view
    @returns {Boolean} YES to alow, NO to prevent it
  */
  collectionViewShouldBeginDrag: function(view) { return YES; },

  /**
    Called by the collection view just before it starts a drag so that
    you can provide the data types you would like to support in the data.

    You can implement this method to return an array of the data types you
    will provide for the drag data.

    If you return null or an empty array, can you have set canReorderContent
    to YES on the CollectionView, then the drag will go ahead but only
    reordering will be allowed.  If canReorderContent is NO, then the drag
    will not be allowed to start.

    If you simply want to control whether a drag is allowed or not, you
    should instead implement collectionViewShouldBeginDrag().

    The default returns an empty array.

    @param view {SC.CollectionView} the collection view to begin dragging.
    @returns {Array} array of supported data types.
  */
  collectionViewDragDataTypes: function(view) { return []; },

  /**
    Called by a collection view when a drag concludes to give you the option
    to provide the drag data for the drop.

    This method should be implemented essentially as you would implement the
    dragDataForType() if you were a drag data source.  You will never be asked
    to provide drag data for a reorder event, only for other types of data.

    The default implementation returns null.

    @param view {SC.CollectionView}
      the collection view that initiated the drag

    @param dataType {String} the data type to provide
    @param drag {SC.Drag} the drag object
    @returns {Object} the data object or null if the data could not be provided.
  */
  collectionViewDragDataForType: function(view, drag, dataType) {
    return null ;
  },

  /**
    Called once during a drag the first time view is entered. Return all
    possible drag operations OR'd together.

    @param {SC.CollectionView} view
      the collection view that initiated the drag

    @param {SC.Drag} drag
      the drag object

    @param {Number} proposedDragOperations
      proposed logical OR of allowed drag operations.

    @returns {Number} the allowed drag operations. Defaults to op
  */
  collectionViewComputeDragOperations: function(view, drag, proposedDragOperations) {
    return proposedDragOperations ;
  },

  /**
    Called by the collection view during a drag to let you determine the
    kind and location of a drop you might want to accept.

    You can override this method to implement fine-grained control over how
    and when a dragged item is allowed to be dropped into a collection view.

    This method will be called by the collection view both to determine in
    general which operations you might support and specifically the operations
    you would support if the user dropped an item over a specific location.

    If the proposedDropOperation parameter is SC.DROP_ON or SC.DROP_BEFORE,
    then the proposedInsertionPoint will be a non-negative value and you
    should determine the specific operations you will support if the user
    dropped the drag item at that point.

    If you do not like the proposed drop operation or insertion point, you
    can override these properties as well by setting the proposedDropOperation
    and proposedInsertionIndex properties on the collection view during this
    method.  These properties are ignored all other times.

    @param view {SC.CollectionView} the collection view
    @param drag {SC.Drag} the current drag object
    @param op {Number} proposed logical OR of allowed drag operations.
    @param proposedInsertionIndex {Number} an index into the content array
      representing the proposed insertion point.
    @param proposedDropOperation {String} the proposed drop operation.  Will be one of SC.DROP_ON, SC.DROP_BEFORE, or SC.DROP_ANY.
    @returns the allowed drag operation.  Defaults to op
  */
  collectionViewValidateDragOperation: function(view, drag, op, proposedInsertionIndex, proposedDropOperation) {
    // don't allow dropping on by default
    return (proposedDropOperation & SC.DROP_ON) ? SC.DRAG_NONE : op ;
  },

  /**
    Called by the collection view to actually accept a drop.  This method will
    only be invoked AFTER your validateDrop method has been called to
    determine if you want to even allow the drag operation to go through.

    You should actually make changes to the data model if needed here and
    then return the actual drag operation that was performed.  If you return
    SC.DRAG_NONE and the dragOperation was SC.DRAG_REORDER, then the default
    reorder behavior will be provided by the collection view.

    @param view {SC.CollectionView}
    @param drag {SC.Drag} the current drag object
    @param op {Number} proposed logical OR of allowed drag operations.
    @param proposedInsertionIndex {Number} an index into the content array representing the proposed insertion point.
    @param proposedDropOperation {String} the proposed drop operation.  Will be one of SC.DROP_ON, SC.DROP_BEFORE, or SC.DROP_ANY.
    @returns the allowed drag operation.  Defaults to proposedDragOperation
  */
  collectionViewPerformDragOperation: function(view, drag, op, proposedInsertionIndex, proposedDropOperation) {
    return SC.DRAG_NONE ;
  },

  /**
    Renders a drag view for the passed content indexes. If you return null
    from this, then a default drag view will be generated for you.

    @param {SC.CollectionView} view
    @param {SC.IndexSet} dragContent
    @returns {SC.View} view or null
  */
  collectionViewDragViewFor: function(view, dragContent) {
    return null;
  },

  /**
    Allows the ghost view created in collectionViewDragViewFor to be displayed
    like a cursor instead of the default implementation. This sets the view
    origin to be the location of the mouse cursor.

    @property {Boolean} ghost view acts like a cursor
  */
  ghostActsLikeCursor: NO

};
<|endoftext|>


<commit_before>var data_url = "/donation_data";
var data_update_url = "/donation_data/update";

$(function() {
	$.get(data_url).done(function(fullData) {
        initWithData(fullData);

        //now that we have our charts, start refreshing the data every 5 minutes
        var mostRecentUpdate = new Date();

        var performUpdate = function() {
            $.get(data_update_url, {'since':mostRecentUpdate.toISOString()}).done(function(partialData) {
                fullData = fullData.concat(partialData);
                initWithData(fullData);

                mostRecentUpdate = new Date();

                //do another update in 5 minutes
                setTimeout(performUpdate, getRefreshTimeout());
            });
        }
        setTimeout(performUpdate, getRefreshTimeout());
    });

    //auto refresh the whole page after 2 hours
    setTimeout(function() {
        window.location.reload(true);
    }, 2 * 60 * 60 * 1000);
});

var getRefreshTimeout = function() {
    //5 minutes, plus a random value between 0 and 30 seconds
    return 5 * 60 * 1000 + 30 *1000 * Math.random();
}

var initWithData = function(donationData) {
	var current_totals = donationData.map(function(item) {
        return item.total;
    });
    var old_totals = donationData.map(function(item) {
        return item.total_2016;
    });
	var timestamps = donationData.map(function(item) {
		return Date.parse(item.timestamp);
	});

	//create a series for dollars per minute
	var hourlyTimeStamps
	var dollarsPerHour = [];
	var dollarsPerDonation = [];
	for (var i = 0; i < current_totals.length - 1; i++) {
		var deltaTotal = current_totals[i + 1] - current_totals[i];
		var hoursBetweenSamples = (timestamps[i + 1] - timestamps[i]) / (1000 * 60 * 60);
		dollarsPerHour.push(deltaTotal / (hoursBetweenSamples > 0 ? hoursBetweenSamples : 1));
	}
	ironOutTopPercent(dollarsPerHour, 0.025);

	//data series for dollars per donation
	var dollarsPerDonation = [];
	for (var i = 0; i < current_totals.length - 1; i++) {
		var deltaTotal = current_totals[i + 1] - current_totals[i];
		var deltaCount = donationData[i + 1].count - donationData[i].count;
		dollarsPerDonation.push(deltaTotal / (deltaCount > 0 ? deltaCount : 1));
	}
	ironOutTopPercent(dollarsPerDonation, 0.025);

	//create our various charts
	var totals_chart = createTotalsChart(timestamps, current_totals, old_totals);

	//remove the first timestamp since we'll no longer be using it
	timestamps.splice(0,1);
	var rate_chart = createRateChart(timestamps, dollarsPerHour);
	var per_donation_chart = createPerDonationChart(timestamps, dollarsPerDonation);
}

var createTotalsChart = function(timestamps, totals, old_totals) {
	Highcharts.setOptions({
        global: {
            useUTC: false
        }
    });

    return Highcharts.chart('total-container', {
        chart: {
            type: 'spline',
            animation: Highcharts.svg,
        },
        title: {
            text: 'gdq-charts.xyz - Unofficial SGDQ 2017 Donation Chart',
        },
        subtitle: {
            text: "No refresh necessary; Data auto-updates every 15 minutes",
        },
        xAxis: {
            type: 'datetime',
            labels: {
                format: '{value:%a %l%P}',
            },
        },
        yAxis: [{ // Primary yAxis
            title: {
                text: 'Donation Total',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
            min: 0,
        }, {
            linkedTo: 0,
            opposite: true,
            title: {
                text: 'Donation Total',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
        }],
        tooltip: {
            formatter: function () {
                return '' + this.series.name + '
' +
                    Highcharts.dateFormat('%A, %b %e %l:%M%P', this.x) + '
$' +
                    Highcharts.numberFormat(this.y, 0);
            }
        },
        legend: {
            enabled: true,
            layout: 'vertical',
            align: 'right',
            verticalAlign: 'top',
            x: -200,
            floating: true,
        },
        series: [{
            name: 'Donation Total (Current)',
            data: _.zip(timestamps, totals),
            color: Highcharts.getOptions().colors[0]
        },{
            name: 'Donation Total (2016)',
            data: _.zip(timestamps, old_totals),
            color: Highcharts.getOptions().colors[3]
        },

        ]
    });
};

var createRateChart = function(timestamps, rate_data) {

	Highcharts.setOptions({
        global: {
            useUTC: false
        }
    });

    return Highcharts.chart('rate-container', {
        chart: {
            type: 'spline',
            animation: Highcharts.svg,
        },
        title: {
            text: '$ Per Hour'
        },
        subtitle: {
            text: "Average, top 2.5% donations removed",
        },
        xAxis: {
            type: 'datetime',
            labels: {
                format: '{value:%a %l%P}',
            },
        },
        yAxis: [{ // Primary yAxis
            title: {
                text: '$ Per Hour',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            min: 0,
        }, {
            linkedTo:0,
            opposite:true,
            title: {
                text: '$ Per Hour',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            min: 0,
        }],
        tooltip: {
            formatter: function () {
                return '' + this.series.name + '
' +
                    Highcharts.dateFormat('%A, %b %e %l:%M%P', this.x) + '
$' +
                    Highcharts.numberFormat(this.y, 0);
            }
        },
        legend: {
            enabled: false
        },
        series: [{
            name: '$ Per Hour [Average]',
            data: _.zip(timestamps, rate_data),
            color: Highcharts.getOptions().colors[1]
        }]
    });
};

var createPerDonationChart = function(timestamps, per_donation_data) {
	Highcharts.setOptions({
        global: {
            useUTC: false
        }
    });

    return Highcharts.chart('per-donation-container', {
        chart: {
            type: 'spline',
            animation: Highcharts.svg,
        },
        title: {
            text: '$ Per Donation'
        },
        subtitle: {
            text: "Average, top 2.5% donations removed",
        },
        xAxis: {
            type: 'datetime',
            labels: {
                format: '{value:%a %l%P}',
            },
        },
        yAxis: [{ // Primary yAxis
            title: {
                text: '$ Per Donation [Average]',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            labels: {
                format: '${value:,.2f}',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            min:0,
        }, {
            linkedTo: 0,
            opposite: true,
            title: {
                text: '$ Per Donation [Average]',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            labels: {
                format: '${value:,.2f}',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            min:0,
        }],
        tooltip: {
            formatter: function () {
                return '' + this.series.name + '
' +
                    Highcharts.dateFormat('%A, %b %e %l:%M%P', this.x) + '
$' +
                    Highcharts.numberFormat(this.y, 0);
            }
        },
        legend: {
            enabled: false
        },
        series: [{
            name: '$ Per Donation [Average]',
            data: _.zip(timestamps, per_donation_data),
            color: Highcharts.getOptions().colors[2]
        }]
    });
};

//replaces the largest n% of the numbers in the provided list with an average of the numbers to the left and right
//the goal is to provide a way to eliminate outliers
var ironOutTopPercent = function(listOfNumbers, topPercent) {
    var size = listOfNumbers.length;
	var topValues = topN(listOfNumbers, Math.floor(topPercent * size));
	topValues = arrayToObjectKeys(topValues);

	for(var i = 1; i < size - 1; i++) {
		if(listOfNumbers[i] in topValues) {
			listOfNumbers[i] = (listOfNumbers[i - 1] + listOfNumbers[i + 1])/2;
		}
	}

    if(listOfNumbers[size - 1] in topValues) {
        listOfNumbers[size - 1] = (listOfNumbers[size - 2] + listOfNumbers[size - 3])/2;
    }
}

//given a list of numbers and n, return the n largest values of listOfNumbers, in sorted order
var topN = function(listOfNumbers, n) {
	var values = listOfNumbers.slice(0, n);
	values.sort();

	for(var i = n; i < listOfNumbers.length; i++) {
		var insertIndex = _.sortedIndex(values, listOfNumbers[i]);
		if(insertIndex > 0) {
			values.splice(insertIndex, 0, listOfNumbers[i]);
			values.splice(0, 1);
		}
	}

	return values;
}

var arrayToObjectKeys = function(arr) {
	return arr.reduce(function(acc, cur, i) {
		acc[cur] = true;
		return acc;
	}, {});
}<commit_msg>Attempt at smoothing the x-per-hour charts<commit_after>var data_url = "/donation_data";
var data_update_url = "/donation_data/update";

$(function() {
	$.get(data_url).done(function(fullData) {
        initWithData(fullData);

        //now that we have our charts, start refreshing the data every 5 minutes
        var mostRecentUpdate = new Date();

        var performUpdate = function() {
            $.get(data_update_url, {'since':mostRecentUpdate.toISOString()}).done(function(partialData) {

                //only redraw the chart if there's something to redraw
                if(partialData.length > 0) {
                    fullData = fullData.concat(partialData);
                    initWithData(fullData);
                }

                mostRecentUpdate = new Date();

                //do another update in 5 minutes
                setTimeout(performUpdate, getRefreshTimeout());
            });
        }
        setTimeout(performUpdate, getRefreshTimeout());
    });

    //auto refresh the whole page after 2 hours
    setTimeout(function() {
        window.location.reload(true);
    }, 2 * 60 * 60 * 1000);
});

var getRefreshTimeout = function() {
    //5 minutes, plus a random value between 0 and 30 seconds
    return 5 * 60 * 1000 + 30 *1000 * Math.random();
}

var initWithData = function(donationData) {
	var current_totals = donationData.map(function(item) {
        return item.total;
    });
    var old_totals = donationData.map(function(item) {
        return item.total_2016;
    });
	var timestamps = donationData.map(function(item) {
		return Date.parse(item.timestamp);
	});

	//create a series for dollars per minute
	var hourlyTimeStamps
	var dollarsPerHour = [];
	var dollarsPerDonation = [];
	for (var i = 0; i < current_totals.length - 1; i++) {
		var deltaTotal = current_totals[i + 1] - current_totals[i];
		var hoursBetweenSamples = (timestamps[i + 1] - timestamps[i]) / (1000 * 60 * 60);
		dollarsPerHour.push(deltaTotal / (hoursBetweenSamples > 0 ? hoursBetweenSamples : 1));
	}
	dollarsPerHour = ironOutTopPercent(dollarsPerHour, 0.025);

	//data series for dollars per donation
	var dollarsPerDonation = [];
	for (var i = 0; i < current_totals.length - 1; i++) {
		var deltaTotal = current_totals[i + 1] - current_totals[i];
		var deltaCount = donationData[i + 1].count - donationData[i].count;
		dollarsPerDonation.push(deltaTotal / (deltaCount > 0 ? deltaCount : 1));
	}
	dollarsPerDonation = ironOutTopPercent(dollarsPerDonation, 0.025);

	//create our various charts
	var totals_chart = createTotalsChart(timestamps, current_totals, old_totals);

	//remove the first timestamp since we'll no longer be using it
	timestamps.splice(0,1);
	var rate_chart = createRateChart(timestamps, dollarsPerHour);
	var per_donation_chart = createPerDonationChart(timestamps, dollarsPerDonation);
}

var createTotalsChart = function(timestamps, totals, old_totals) {
	Highcharts.setOptions({
        global: {
            useUTC: false
        }
    });

    return Highcharts.chart('total-container', {
        chart: {
            type: 'spline',
            animation: Highcharts.svg,
        },
        title: {
            text: 'gdq-charts.xyz - Unofficial SGDQ 2017 Donation Chart',
        },
        subtitle: {
            text: "No refresh necessary; Data auto-updates every 15 minutes",
        },
        xAxis: {
            type: 'datetime',
            labels: {
                format: '{value:%a %l%P}',
            },
        },
        yAxis: [{ // Primary yAxis
            title: {
                text: 'Donation Total',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
            min: 0,
        }, {
            linkedTo: 0,
            opposite: true,
            title: {
                text: 'Donation Total',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[0]
                }
            },
        }],
        tooltip: {
            formatter: function () {
                return '' + this.series.name + '
' +
                    Highcharts.dateFormat('%A, %b %e %l:%M%P', this.x) + '
$' +
                    Highcharts.numberFormat(this.y, 0);
            }
        },
        legend: {
            enabled: true,
            layout: 'vertical',
            align: 'right',
            verticalAlign: 'top',
            x: -200,
            floating: true,
        },
        series: [{
            name: 'Donation Total (Current)',
            data: _.zip(timestamps, totals),
            color: Highcharts.getOptions().colors[0]
        },{
            name: 'Donation Total (2016)',
            data: _.zip(timestamps, old_totals),
            color: Highcharts.getOptions().colors[3]
        },

        ]
    });
};

var createRateChart = function(timestamps, rate_data) {

	Highcharts.setOptions({
        global: {
            useUTC: false
        }
    });

    return Highcharts.chart('rate-container', {
        chart: {
            type: 'spline',
            animation: Highcharts.svg,
        },
        title: {
            text: '$ Per Hour'
        },
        subtitle: {
            text: "Average, top 2.5% donations removed",
        },
        xAxis: {
            type: 'datetime',
            labels: {
                format: '{value:%a %l%P}',
            },
        },
        yAxis: [{ // Primary yAxis
            title: {
                text: '$ Per Hour',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            min: 0,
        }, {
            linkedTo:0,
            opposite:true,
            title: {
                text: '$ Per Hour',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            labels: {
                format: '${value:,.0f}',
                style: {
                    color: Highcharts.getOptions().colors[1]
                }
            },
            min: 0,
        }],
        tooltip: {
            formatter: function () {
                return '' + this.series.name + '
' +
                    Highcharts.dateFormat('%A, %b %e %l:%M%P', this.x) + '
$' +
                    Highcharts.numberFormat(this.y, 0);
            }
        },
        legend: {
            enabled: false
        },
        series: [{
            name: '$ Per Hour [Average]',
            data: _.zip(timestamps, rate_data),
            color: Highcharts.getOptions().colors[1]
        }]
    });
};

var createPerDonationChart = function(timestamps, per_donation_data) {
	Highcharts.setOptions({
        global: {
            useUTC: false
        }
    });

    return Highcharts.chart('per-donation-container', {
        chart: {
            type: 'spline',
            animation: Highcharts.svg,
        },
        title: {
            text: '$ Per Donation'
        },
        subtitle: {
            text: "Average, top 2.5% donations removed",
        },
        xAxis: {
            type: 'datetime',
            labels: {
                format: '{value:%a %l%P}',
            },
        },
        yAxis: [{ // Primary yAxis
            title: {
                text: '$ Per Donation [Average]',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            labels: {
                format: '${value:,.2f}',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            min:0,
        }, {
            linkedTo: 0,
            opposite: true,
            title: {
                text: '$ Per Donation [Average]',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            labels: {
                format: '${value:,.2f}',
                style: {
                    color: Highcharts.getOptions().colors[2]
                }
            },
            min:0,
        }],
        tooltip: {
            formatter: function () {
                return '' + this.series.name + '
' +
                    Highcharts.dateFormat('%A, %b %e %l:%M%P', this.x) + '
$' +
                    Highcharts.numberFormat(this.y, 0);
            }
        },
        legend: {
            enabled: false
        },
        series: [{
            name: '$ Per Donation [Average]',
            data: _.zip(timestamps, per_donation_data),
            color: Highcharts.getOptions().colors[2]
        }]
    });
};

//replaces the largest n% of the numbers in the provided list with an average of the numbers to the left and right
//the goal is to provide a way to eliminate outliers
var ironOutTopPercent = function(listOfNumbers, topPercent) {
    var size = listOfNumbers.length;
	var topValues = topN(listOfNumbers, Math.floor(topPercent * size));
	topValues = arrayToObjectKeys(topValues);

	for(var i = 1; i < size - 1; i++) {
		if(listOfNumbers[i] in topValues) {
			listOfNumbers[i] = (listOfNumbers[i - 1] + listOfNumbers[i + 1])/2;
		}
	}

    if(listOfNumbers[size - 1] in topValues) {
        listOfNumbers[size - 1] = (listOfNumbers[size - 2] + listOfNumbers[size - 3])/2;
    }

    var averageWeights = [0.1, 0.2, 0.4, 0.2, 0.1];

    return listOfNumbers.map(function(element, i) {
        return averagedSample(listOfNumbers, averageWeights, i);
    });
}

var averagedSample = function(inputArray, weightArray, index) {
    var average = 0;

    for(var i = 0; i < weightArray.length; i++) {
        var sampleIndex = index - Math.floor(weightArray.length / 2) + i;
        var sample;

        if(sampleIndex < 0) {
            sample = inputArray[0];
        } else if(sampleIndex >= inputArray.length) {
            sample = inputArray[inputArray.length - 1];
        } else {
            sample = inputArray[sampleIndex];
        }

        average += weightArray[i] * sample;
    }

    return average;
}

//given a list of numbers and n, return the n largest values of listOfNumbers, in sorted order
var topN = function(listOfNumbers, n) {
	var values = listOfNumbers.slice(0, n);
	values.sort();

	for(var i = n; i < listOfNumbers.length; i++) {
		var insertIndex = _.sortedIndex(values, listOfNumbers[i]);
		if(insertIndex > 0) {
			values.splice(insertIndex, 0, listOfNumbers[i]);
			values.splice(0, 1);
		}
	}

	return values;
}

var arrayToObjectKeys = function(arr) {
	return arr.reduce(function(acc, cur, i) {
		acc[cur] = true;
		return acc;
	}, {});
}<|endoftext|>


<commit_before>define([
    'preact',
    'htm',
    'uuid',
    '../lib/kb_lib/windowChannel',
    // 'kb_lib2/windowChannel',
    'kb_lib/httpUtils',
    './AutoPostForm',
    './IFrame',

    'css!./IFrameController.css'
], (
    preact,
    htm,
    {v4: uuidv4},
    {WindowChannelInit},
    httpUtils,
    AutoPostForm,
    IFrame
) => {

    const {h, Component, render} = preact;
    const html = htm.bind(h);

    const SHOW_LOADING_AFTER = 1000;
    const SHOW_SLOW_LOADING_AFTER = 5000;
    const SHOW_SUPER_SLOW_LOADING_AFTER = 30000;

    class IFrameController extends Component {
        constructor(props) {
            super(props);

            const {runtime} = props;

            this.runtime = runtime;

            const id = uuidv4();
            this.id = `host_ ${id}`;

            this.receivers = [];

            this.channel = null;

            this.hostChannelId = uuidv4();
            this.pluginChannelId = uuidv4();

            // channel = chan.makeChannel(hostChannelId);

            // this.channel = new WindowChannel({
            //     host: document.location.origin,
            //     to: uuidv4()
            // });
            // this.channel.setDebug(true);

            this.state = {
                loading: 'yes',
            };

            this.loadingTimer = null;
        }

        componentDidMount() {
            // Listen for slow loading plugins.
            this.monitorLoad();
        }

        monitorLoad() {
            this.loadingTimer = window.setTimeout(() => {
                this.setState({
                    loading: 'slow'
                });
                this.monitorSlowLoad();
            }, SHOW_LOADING_AFTER);
        }

        monitorSlowLoad() {
            this.loadingTimer = window.setTimeout(() => {
                this.setState({
                    loading: 'veryslow'
                });
                this.monitorSuperSlowLoad();
            }, SHOW_SLOW_LOADING_AFTER);
        }

        monitorSuperSlowLoad() {
            this.loadingTimer = window.setTimeout(() => {
                this.setState({
                    loading: 'superslow'
                });
            }, SHOW_SUPER_SLOW_LOADING_AFTER);
        }

        setupAndStartChannel(iframeWindow) {
            const chan = new WindowChannelInit({
                id: this.hostChannelId,
                window: iframeWindow,
                host: window.document.location.origin
            });
            this.channel = chan.makeChannel(this.pluginChannelId);
            this.channel.on('get-auth-status', () => {
                this.channel.send('auth-status', {
                    token: this.runtime.service('session').getAuthToken(),
                    username: this.runtime.service('session').getUsername()
                });
            });

            this.channel.on('get-config', () => {
                this.channel.send('config', {
                    value: this.runtime.rawConfig()
                });
            });

            this.channel.on('add-button', ({button}) => {
                button.callback = () => {
                    this.channel.send.apply(this.iframeChannel, button.callbackMessage);
                };
                this.runtime.send('ui', 'addButton', button);
            });

            this.channel.on('open-window', ({url}) => {
                window.location.href = url;
                // window.open(url, name);
            });

            this.channel.on('set-plugin-params', ({pluginParams}) => {
                if (Object.keys(pluginParams).length === 0) {
                    window.location.search = '';
                    return;
                }
                const query = {};
                if (pluginParams.query) {
                    query.query = pluginParams.query;
                }
                if (pluginParams.dataPrivacy && pluginParams.dataPrivacy.length > 0) {
                    query.dataPrivacy = pluginParams.dataPrivacy.join(',');
                }
                if (pluginParams.workspaceTypes && pluginParams.workspaceTypes.length > 0) {
                    query.workspaceTypes = pluginParams.workspaceTypes.join(',');
                }
                if (pluginParams.dataTypes) {
                    query.dataTypes = pluginParams.dataTypes.join(',');
                }

                // prepare the params.
                const queryString = httpUtils.encodeQuery(query);

                const currentLocation = window.location.toString();
                const currentURL = new URL(currentLocation);
                currentURL.search = queryString;
                history.replaceState(null, '', currentURL.toString());
            });

            this.channel.on('send-instrumentation', (instrumentation) => {
                this.runtime.service('instrumentation').send(instrumentation);
            });

            this.channel.on('ui-navigate', (to) => {
                this.runtime.send('app', 'navigate', to);
            });

            this.channel.on('post-form', (config) => {
                this.formPost(config);
            });

            this.channel.on('clicked', () => {
                window.document.body.click();
            });

            this.channel.on('click', () => {
                window.document.body.click();
            });

            this.channel.on('set-title', (config) => {
                this.runtime.send('ui', 'setTitle', config.title);
            });

            this.channel.on('ui-auth-navigate', ({nextRequest, tokenInfo}) => {
                const authSession = this.runtime.service('session').getClient();
                authSession.setSessionCookie(tokenInfo.token, tokenInfo.expires);
                return authSession.evaluateSession().then(() => {
                    this.runtime.send('app', 'navigate', nextRequest);
                });
            });

            /*
            examples:
            {
                type: 'warning',
                id: 'connection',
                icon: 'exclamation-triangle',
                message: message.message,
                description: message.description
            }
            {
                type: 'success',
                id: 'connection',
                icon: 'check',
                message: message.message,
                description: message.description,
                autodismiss: 5000
            }
            */
            this.channel.on('notification', (notification) => {
                this.runtime.send('notification', 'notify', notification);
            });

            this.channel.start();
        }

        formPost({action, params}) {
            const donorNode = document.createElement('div');
            document.body.appendChild(donorNode);
            const props = {
                action, params
            };
            render(html`<${AutoPostForm} ...${props} />`, donorNode);
        }

        setupChannelSends() {
            this.receivers.push(this.runtime.receive('session', 'loggedin', () => {
                this.channel.send('loggedin', {
                    token: this.runtime.service('session').getAuthToken(),
                    username: this.runtime.service('session').getUsername(),
                    realname: this.runtime.service('session').getRealname(),
                    email: this.runtime.service('session').getEmail()
                });
            }));
            this.receivers.push(this.runtime.receive('session', 'loggedout', () => {
                this.channel.send('loggedout', {});
            }));
        }

        setupCommunication(iframeWindow) {
            return new Promise((resolve, reject) => {
                this.setupAndStartChannel(iframeWindow);
                this.props.pipe.tap(({view, params, request}) => {
                    const path = params.path || [];
                    const message = {
                        view, to: view, path, params, request
                    };
                    this.channel.send('navigate', message);
                });
                this.props.pipe.start();
                this.channel.once('ready',
                    ({channelId}) => {
                        // console.warn('[ready]', channelId);
                        this.channel.partnerId = channelId;
                        // TODO: narrow and improve the config support for plugins
                        // E.g.
                        // const config = this.runtime.rawConfig();
                        // const pluginConfig = {
                        //     baseURL: config.deploy.services.urlBase,
                        //     services: config.services
                        // };

                        // const params = Object.entries(this.props.params.routeParams).reduce((params, [key, param]) => {
                        //     params[key] = param.value;
                        //     return params;
                        // }, {});
                        const params = Object.assign({}, this.props.params.routeParams);
                        params.view = this.props.params.view;

                        const startMessage = {
                            authorization: {
                                token: this.runtime.service('session').getAuthToken(),
                                username: this.runtime.service('session').getUsername(),
                                realname: this.runtime.service('session').getRealname(),
                                email: this.runtime.service('session').getEmail(),
                                roles: this.runtime.service('session').getRoles().map(({id}) => {
                                    return id;
                                })
                            },
                            config: this.runtime.rawConfig(),
                            view: this.props.params.view.value,
                            params
                        };
                        // console.warn('[ready]', startMessage, this.channel);
                        this.channel.send('start', startMessage);
                        // Any sends to the channel should only be enabled after the
                        // start message is received.
                        this.setupChannelSends();
                    });

                // Sent by the plugin to indicate that the plugin has finished loading.
                this.channel.once('started', () => {
                    if (this.loadingTimer !== null) {
                        window.clearTimeout(this.loadingTimer);
                        this.loadingTimer = null;
                    }
                    this.setState({
                        loading: null
                    });
                    resolve();
                });

                // Sent by a plugin if it encounters an error and doesn't want to display it
                // itself, or it occurs before it is able to render anything.
                this.channel.once('start-error', (config) => {
                    reject(new Error(config.message));
                });
            },
            (error) => {
                console.error('ERROR!', error.message);
            });
        }

        componentWillUnmount() {
            const currentLocation = window.location.toString();
            const currentURL = new URL(currentLocation);
            currentURL.search = '';
            history.replaceState(null, '', currentURL.toString());

            this.receivers.forEach((receiver) => {
                this.runtime.drop(receiver);
            });

            if (this.channel) {
                this.channel.stop();
                this.channel = null;
            }
        }

        iframeMounted(w) {
            this.setupCommunication(w)
                .then(() => {
                    // TODO: remove because this duplicates start behaviour
                    // In order to do that, plugins which don't route on 'start'
                    // need to be updated.
                    // const params = this.props.params.routeParams;
                    // const params =  Object.entries(this.props.params.routeParams).reduce((params, [key, param]) => {
                    //     params[key] = param.value;
                    //     return params;
                    // }, {});
                    const params = Object.assign({}, this.props.params.routeParams);
                    const path = params.path || [];
                    const view = this.props.params.view;
                    params.view = view;
                    const message = {
                        path, params, view, to: view
                    };
                    this.channel.send('navigate', message);
                });
        }

        renderIFrame() {
            const props = {
                origin: document.location.origin,
                pathRoot: this.props.pluginPath,
                hostChannelId: this.hostChannelId,
                pluginChannelId: this.pluginChannelId,
                whenMounted: (w) => {
                    // this.channel.setWindow(this.iframe.window);
                    this.iframeMounted(w);
                },
                hostId: this.id,
                params: this.props.params,
                runtime: this.props.runtime,
                original: this.props.original
            };

            return html`
                <${IFrame} ...${props} />
            `;
        }

        renderLoading() {
            if (this.state.loading === null) {
                return;
            }
            let message, color;
            switch (this.state.loading) {
            case 'yes': // don't show anything yet...
                return;
            case 'slow':
                message = 'Loading Plugin...';
                color = 'black';
                break;
            case 'veryslow':
                message = 'Loading Plugin - still loading ...';
                color = '#8a6d3b';
                break;
            case 'superslow':
                message = 'Loading Plugin - your connection appears to be slow, still loading ...';
                color = '#a94442';
                break;
            }
            return html`

                        ${message}

            `;
        }

        render() {
            return html`

                ${this.renderLoading()}
                ${this.renderIFrame()}

            `;
        }
    }

    return IFrameController;
});
<commit_msg>add duplicate "authentication" property for plugin start message<commit_after>define([
    'preact',
    'htm',
    'uuid',
    '../lib/kb_lib/windowChannel',
    // 'kb_lib2/windowChannel',
    'kb_lib/httpUtils',
    './AutoPostForm',
    './IFrame',

    'css!./IFrameController.css'
], (
    preact,
    htm,
    {v4: uuidv4},
    {WindowChannelInit},
    httpUtils,
    AutoPostForm,
    IFrame
) => {

    const {h, Component, render} = preact;
    const html = htm.bind(h);

    const SHOW_LOADING_AFTER = 1000;
    const SHOW_SLOW_LOADING_AFTER = 5000;
    const SHOW_SUPER_SLOW_LOADING_AFTER = 30000;

    class IFrameController extends Component {
        constructor(props) {
            super(props);

            const {runtime} = props;

            this.runtime = runtime;

            const id = uuidv4();
            this.id = `host_ ${id}`;

            this.receivers = [];

            this.channel = null;

            this.hostChannelId = uuidv4();
            this.pluginChannelId = uuidv4();

            // channel = chan.makeChannel(hostChannelId);

            // this.channel = new WindowChannel({
            //     host: document.location.origin,
            //     to: uuidv4()
            // });
            // this.channel.setDebug(true);

            this.state = {
                loading: 'yes',
            };

            this.loadingTimer = null;
        }

        componentDidMount() {
            // Listen for slow loading plugins.
            this.monitorLoad();
        }

        monitorLoad() {
            this.loadingTimer = window.setTimeout(() => {
                this.setState({
                    loading: 'slow'
                });
                this.monitorSlowLoad();
            }, SHOW_LOADING_AFTER);
        }

        monitorSlowLoad() {
            this.loadingTimer = window.setTimeout(() => {
                this.setState({
                    loading: 'veryslow'
                });
                this.monitorSuperSlowLoad();
            }, SHOW_SLOW_LOADING_AFTER);
        }

        monitorSuperSlowLoad() {
            this.loadingTimer = window.setTimeout(() => {
                this.setState({
                    loading: 'superslow'
                });
            }, SHOW_SUPER_SLOW_LOADING_AFTER);
        }

        setupAndStartChannel(iframeWindow) {
            const chan = new WindowChannelInit({
                id: this.hostChannelId,
                window: iframeWindow,
                host: window.document.location.origin
            });
            this.channel = chan.makeChannel(this.pluginChannelId);
            this.channel.on('get-auth-status', () => {
                this.channel.send('auth-status', {
                    token: this.runtime.service('session').getAuthToken(),
                    username: this.runtime.service('session').getUsername()
                });
            });

            this.channel.on('get-config', () => {
                this.channel.send('config', {
                    value: this.runtime.rawConfig()
                });
            });

            this.channel.on('add-button', ({button}) => {
                button.callback = () => {
                    this.channel.send.apply(this.iframeChannel, button.callbackMessage);
                };
                this.runtime.send('ui', 'addButton', button);
            });

            this.channel.on('open-window', ({url}) => {
                window.location.href = url;
                // window.open(url, name);
            });

            this.channel.on('set-plugin-params', ({pluginParams}) => {
                if (Object.keys(pluginParams).length === 0) {
                    window.location.search = '';
                    return;
                }
                const query = {};
                if (pluginParams.query) {
                    query.query = pluginParams.query;
                }
                if (pluginParams.dataPrivacy && pluginParams.dataPrivacy.length > 0) {
                    query.dataPrivacy = pluginParams.dataPrivacy.join(',');
                }
                if (pluginParams.workspaceTypes && pluginParams.workspaceTypes.length > 0) {
                    query.workspaceTypes = pluginParams.workspaceTypes.join(',');
                }
                if (pluginParams.dataTypes) {
                    query.dataTypes = pluginParams.dataTypes.join(',');
                }

                // prepare the params.
                const queryString = httpUtils.encodeQuery(query);

                const currentLocation = window.location.toString();
                const currentURL = new URL(currentLocation);
                currentURL.search = queryString;
                history.replaceState(null, '', currentURL.toString());
            });

            this.channel.on('send-instrumentation', (instrumentation) => {
                this.runtime.service('instrumentation').send(instrumentation);
            });

            this.channel.on('ui-navigate', (to) => {
                this.runtime.send('app', 'navigate', to);
            });

            this.channel.on('post-form', (config) => {
                this.formPost(config);
            });

            this.channel.on('clicked', () => {
                window.document.body.click();
            });

            this.channel.on('click', () => {
                window.document.body.click();
            });

            this.channel.on('set-title', (config) => {
                this.runtime.send('ui', 'setTitle', config.title);
            });

            this.channel.on('ui-auth-navigate', ({nextRequest, tokenInfo}) => {
                const authSession = this.runtime.service('session').getClient();
                authSession.setSessionCookie(tokenInfo.token, tokenInfo.expires);
                return authSession.evaluateSession().then(() => {
                    this.runtime.send('app', 'navigate', nextRequest);
                });
            });

            /*
            examples:
            {
                type: 'warning',
                id: 'connection',
                icon: 'exclamation-triangle',
                message: message.message,
                description: message.description
            }
            {
                type: 'success',
                id: 'connection',
                icon: 'check',
                message: message.message,
                description: message.description,
                autodismiss: 5000
            }
            */
            this.channel.on('notification', (notification) => {
                this.runtime.send('notification', 'notify', notification);
            });

            this.channel.start();
        }

        formPost({action, params}) {
            const donorNode = document.createElement('div');
            document.body.appendChild(donorNode);
            const props = {
                action, params
            };
            render(html`<${AutoPostForm} ...${props} />`, donorNode);
        }

        setupChannelSends() {
            this.receivers.push(this.runtime.receive('session', 'loggedin', () => {
                this.channel.send('loggedin', {
                    token: this.runtime.service('session').getAuthToken(),
                    username: this.runtime.service('session').getUsername(),
                    realname: this.runtime.service('session').getRealname(),
                    email: this.runtime.service('session').getEmail()
                });
            }));
            this.receivers.push(this.runtime.receive('session', 'loggedout', () => {
                this.channel.send('loggedout', {});
            }));
        }

        setupCommunication(iframeWindow) {
            return new Promise((resolve, reject) => {
                this.setupAndStartChannel(iframeWindow);
                this.props.pipe.tap(({view, params, request}) => {
                    const path = params.path || [];
                    const message = {
                        view, to: view, path, params, request
                    };
                    this.channel.send('navigate', message);
                });
                this.props.pipe.start();
                this.channel.once('ready',
                    ({channelId}) => {
                        // console.warn('[ready]', channelId);
                        this.channel.partnerId = channelId;
                        // TODO: narrow and improve the config support for plugins
                        // E.g.
                        // const config = this.runtime.rawConfig();
                        // const pluginConfig = {
                        //     baseURL: config.deploy.services.urlBase,
                        //     services: config.services
                        // };

                        // const params = Object.entries(this.props.params.routeParams).reduce((params, [key, param]) => {
                        //     params[key] = param.value;
                        //     return params;
                        // }, {});
                        const params = Object.assign({}, this.props.params.routeParams);
                        params.view = this.props.params.view;

                        const startMessage = {
                            authentication: {
                                token: this.runtime.service('session').getAuthToken(),
                                username: this.runtime.service('session').getUsername(),
                                realname: this.runtime.service('session').getRealname(),
                                email: this.runtime.service('session').getEmail(),
                                roles: this.runtime.service('session').getRoles().map(({id}) => {
                                    return id;
                                })
                            },
                            // TODO: remove when all plugins converted.
                            authorization: {
                                token: this.runtime.service('session').getAuthToken(),
                                username: this.runtime.service('session').getUsername(),
                                realname: this.runtime.service('session').getRealname(),
                                email: this.runtime.service('session').getEmail(),
                                roles: this.runtime.service('session').getRoles().map(({id}) => {
                                    return id;
                                })
                            },
                            config: this.runtime.rawConfig(),
                            view: this.props.params.view.value,
                            params
                        };
                        // console.warn('[ready]', startMessage, this.channel);
                        this.channel.send('start', startMessage);
                        // Any sends to the channel should only be enabled after the
                        // start message is received.
                        this.setupChannelSends();
                    });

                // Sent by the plugin to indicate that the plugin has finished loading.
                this.channel.once('started', () => {
                    if (this.loadingTimer !== null) {
                        window.clearTimeout(this.loadingTimer);
                        this.loadingTimer = null;
                    }
                    this.setState({
                        loading: null
                    });
                    resolve();
                });

                // Sent by a plugin if it encounters an error and doesn't want to display it
                // itself, or it occurs before it is able to render anything.
                this.channel.once('start-error', (config) => {
                    reject(new Error(config.message));
                });
            },
            (error) => {
                console.error('ERROR!', error.message);
            });
        }

        componentWillUnmount() {
            const currentLocation = window.location.toString();
            const currentURL = new URL(currentLocation);
            currentURL.search = '';
            history.replaceState(null, '', currentURL.toString());

            this.receivers.forEach((receiver) => {
                this.runtime.drop(receiver);
            });

            if (this.channel) {
                this.channel.stop();
                this.channel = null;
            }
        }

        iframeMounted(w) {
            this.setupCommunication(w)
                .then(() => {
                    // TODO: remove because this duplicates start behaviour
                    // In order to do that, plugins which don't route on 'start'
                    // need to be updated.
                    // const params = this.props.params.routeParams;
                    // const params =  Object.entries(this.props.params.routeParams).reduce((params, [key, param]) => {
                    //     params[key] = param.value;
                    //     return params;
                    // }, {});
                    const params = Object.assign({}, this.props.params.routeParams);
                    const path = params.path || [];
                    const view = this.props.params.view;
                    params.view = view;
                    const message = {
                        path, params, view, to: view
                    };
                    this.channel.send('navigate', message);
                });
        }

        renderIFrame() {
            const props = {
                origin: document.location.origin,
                pathRoot: this.props.pluginPath,
                hostChannelId: this.hostChannelId,
                pluginChannelId: this.pluginChannelId,
                whenMounted: (w) => {
                    // this.channel.setWindow(this.iframe.window);
                    this.iframeMounted(w);
                },
                hostId: this.id,
                params: this.props.params,
                runtime: this.props.runtime,
                original: this.props.original
            };

            return html`
                <${IFrame} ...${props} />
            `;
        }

        renderLoading() {
            if (this.state.loading === null) {
                return;
            }
            let message, color;
            switch (this.state.loading) {
            case 'yes': // don't show anything yet...
                return;
            case 'slow':
                message = 'Loading Plugin...';
                color = 'black';
                break;
            case 'veryslow':
                message = 'Loading Plugin - still loading ...';
                color = '#8a6d3b';
                break;
            case 'superslow':
                message = 'Loading Plugin - your connection appears to be slow, still loading ...';
                color = '#a94442';
                break;
            }
            return html`

                        ${message}

            `;
        }

        render() {
            return html`

                ${this.renderLoading()}
                ${this.renderIFrame()}

            `;
        }
    }

    return IFrameController;
});
<|endoftext|>


<commit_before>var classof = require('../internals/classof-raw');

// `thisNumberValue` abstract operation
// https://tc39.es/ecma262/#sec-thisnumbervalue
module.exports = function (value) {
  if (typeof value != 'number' && classof(value) != 'Number') {
    throw TypeError('Incorrect invocation');
  }
  return +value;
};
<commit_msg>simplify `thisNumberValue`<commit_after>var valueOf = 1.0.valueOf;

// `thisNumberValue` abstract operation
// https://tc39.es/ecma262/#sec-thisnumbervalue
module.exports = function (value) {
  return valueOf.call(value);
};
<|endoftext|>


<commit_before>var gulp = require('gulp'),
    gutil = require('gulp-util'),
    path = require('path'),
    argv = require('yargs')
       .alias('p', 'port')
       .alias('s', 'server')
       .argv;

var devServer = {
  port: argv.port || Math.round(31337 + Math.random() * 1000),
  server: argv.server || '0.0.0.0',
  livereload: 35000 + Math.round((Math.random() * 1000)),
  root: './dist'
};

var paths = {
  scripts: ['src/**/*.js'],
  markup: ['src/*.html'],
  styles: { paths: [ path.join(__dirname, 'src/styles') ] }
};

gulp.task('default', ['build', 'startStaticServer', 'watchChanges']);
gulp.task('build', ['makeDist', 'runBrowserify', 'copyDist', 'compileLess']);

gulp.task('runBrowserify', runBrowserify);
gulp.task('compileLess', compileLess);
gulp.task('makeDist', makeDist);
gulp.task('copyDist', copyDist);
gulp.task('watchChanges', watchChanges);
gulp.task('startStaticServer', startStaticServer);

function runBrowserify() {
  var fs = require('fs');

  var bundle = require('browserify')()
    .add('./src/scripts/index.js')
    .bundle()
    .on('error', function (err) {
        gutil.log(gutil.colors.red('Failed to browserify'), gutil.colors.yellow(err.message));
    });
  bundle.pipe(fs.createWriteStream(path.join(__dirname + '/dist/bundle.js')));
}

function compileLess() {
  var less = require('gulp-less')(paths.styles);
  less.on('error', function (err) {
    gutil.log(gutil.colors.red('Failed to compile less: '), gutil.colors.yellow(err.message));
  });

	gulp.src('src/styles/style.less')
		.pipe(less)
		.pipe(gulp.dest('dist/styles'));
}

function makeDist() {
  var fs = require('fs');
  if (!fs.existsSync('./dist')) {
    fs.mkdirSync('./dist');
  }
}

function copyDist() {
  var concat = require('gulp-concat');

  gulp.src('./src/index.html')
      .pipe(gulp.dest('./dist'));

  gulp.src('./node_modules/angular/lib/angular.min.js')
      .pipe(concat('external.min.js'))
      .pipe(gulp.dest('./dist'));

  gulp.src('./node_modules/twitter-bootstrap-3.0.0/fonts/*')
      .pipe(gulp.dest('./dist/fonts/'));
}

function watchChanges() {
  gulp.watch(paths.scripts, ['runBrowserify']);
  gulp.watch('src/styles/*.less', ['compileLess']);
  gulp.watch(paths.markup, ['copyDist']);
  gulp.watch('dist/**').on('change', notifyLivereload);
}

var lr;
function startStaticServer() {
  var express = require('express');
  var app = express();
  app.use(require('connect-livereload')({port: devServer.livereload }));
  app.use(express.static(devServer.root));
  app.listen(devServer.port, devServer.server, function () {
    gutil.log("opened server on http://" + devServer.server + ":" + devServer.port);
  });

  lr = require('tiny-lr')();
  lr.listen(devServer.livereload);
}

function notifyLivereload(event) {
  var fileName = require('path').relative(devServer.root, event.path);
  lr.changed({ body: { files: [fileName] } });
}
<commit_msg>Watch all files in src for browserify<commit_after>var gulp = require('gulp'),
    gutil = require('gulp-util'),
    path = require('path'),
    argv = require('yargs')
       .alias('p', 'port')
       .alias('s', 'server')
       .argv;

var devServer = {
  port: argv.port || Math.round(31337 + Math.random() * 1000),
  server: argv.server || '0.0.0.0',
  livereload: 35000 + Math.round((Math.random() * 1000)),
  root: './dist'
};

var paths = {
  scripts: ['src/**/*.*'],
  markup: ['src/*.html'],
  styles: { paths: [ path.join(__dirname, 'src/styles') ] }
};

gulp.task('default', ['build', 'startStaticServer', 'watchChanges']);
gulp.task('build', ['makeDist', 'runBrowserify', 'copyDist', 'compileLess']);

gulp.task('runBrowserify', runBrowserify);
gulp.task('compileLess', compileLess);
gulp.task('makeDist', makeDist);
gulp.task('copyDist', copyDist);
gulp.task('watchChanges', watchChanges);
gulp.task('startStaticServer', startStaticServer);

function runBrowserify() {
  var fs = require('fs');

  var bundle = require('browserify')()
    .add('./src/scripts/index.js')
    .bundle()
    .on('error', function (err) {
        gutil.log(gutil.colors.red('Failed to browserify'), gutil.colors.yellow(err.message));
    });
  bundle.pipe(fs.createWriteStream(path.join(__dirname + '/dist/bundle.js')));
}

function compileLess() {
  var less = require('gulp-less')(paths.styles);
  less.on('error', function (err) {
    gutil.log(gutil.colors.red('Failed to compile less: '), gutil.colors.yellow(err.message));
  });

	gulp.src('src/styles/style.less')
		.pipe(less)
		.pipe(gulp.dest('dist/styles'));
}

function makeDist() {
  var fs = require('fs');
  if (!fs.existsSync('./dist')) {
    fs.mkdirSync('./dist');
  }
}

function copyDist() {
  var concat = require('gulp-concat');

  gulp.src('./src/index.html')
      .pipe(gulp.dest('./dist'));

  gulp.src('./node_modules/angular/lib/angular.min.js')
      .pipe(concat('external.min.js'))
      .pipe(gulp.dest('./dist'));

  gulp.src('./node_modules/twitter-bootstrap-3.0.0/fonts/*')
      .pipe(gulp.dest('./dist/fonts/'));
}

function watchChanges() {
  gulp.watch(paths.scripts, ['runBrowserify']);
  gulp.watch('src/styles/*.less', ['compileLess']);
  gulp.watch(paths.markup, ['copyDist']);
  gulp.watch('dist/**').on('change', notifyLivereload);
}

var lr;
function startStaticServer() {
  var express = require('express');
  var app = express();
  app.use(require('connect-livereload')({port: devServer.livereload }));
  app.use(express.static(devServer.root));
  app.listen(devServer.port, devServer.server, function () {
    gutil.log("opened server on http://" + devServer.server + ":" + devServer.port);
  });

  lr = require('tiny-lr')();
  lr.listen(devServer.livereload);
}

function notifyLivereload(event) {
  var fileName = require('path').relative(devServer.root, event.path);
  lr.changed({ body: { files: [fileName] } });
}
<|endoftext|>