<commit_before>import $ from 'jquery';
import * as utils from './utilities';

const REGEXP_ACTIONS = /^(e|w|s|n|se|sw|ne|nw|all|crop|move|zoom)$/;

function getPointer({ pageX, pageY }, endOnly) {
  const end = {
    endX: pageX,
    endY: pageY,
  };

  if (endOnly) {
    return end;
  }

  return $.extend({
    startX: pageX,
    startY: pageY,
  }, end);
}

export default {
  resize() {
    const self = this;
    const restore = self.options.restore;
    const $container = self.$container;
    const container = self.container;

    // Check `container` is necessary for IE8
    if (self.disabled || !container) {
      return;
    }

    const ratio = $container.width() / container.width;

    // Resize when width changed or height changed
    if (ratio !== 1 || $container.height() !== container.height) {
      let canvasData;
      let cropBoxData;

      if (restore) {
        canvasData = self.getCanvasData();
        cropBoxData = self.getCropBoxData();
      }

      self.render();

      if (restore) {
        self.setCanvasData($.each(canvasData, (i, n) => {
          canvasData[i] = n * ratio;
        }));
        self.setCropBoxData($.each(cropBoxData, (i, n) => {
          cropBoxData[i] = n * ratio;
        }));
      }
    }
  },

  dblclick() {
    const self = this;

    if (self.disabled) {
      return;
    }

    self.setDragMode(self.$dragBox.hasClass('cropper-crop') ? 'move' : 'crop');
  },

  wheel(event) {
    const self = this;
    const e = event.originalEvent || event;
    const ratio = Number(self.options.wheelZoomRatio) || 0.1;

    if (self.disabled) {
      return;
    }

    event.preventDefault();

    // Limit wheel speed to prevent zoom too fast
    if (self.wheeling) {
      return;
    }

    self.wheeling = true;

    setTimeout(() => {
      self.wheeling = false;
    }, 50);

    let delta = 1;

    if (e.deltaY) {
      delta = e.deltaY > 0 ? 1 : -1;
    } else if (e.wheelDelta) {
      delta = -e.wheelDelta / 120;
    } else if (e.detail) {
      delta = e.detail > 0 ? 1 : -1;
    }

    self.zoom(-delta * ratio, event);
  },

  cropStart(e) {
    const self = this;

    if (self.disabled) {
      return;
    }

    const options = self.options;
    const pointers = self.pointers;
    const originalEvent = e.originalEvent;
    let action;

    if (originalEvent && originalEvent.changedTouches) {
      // Handle touch event
      $.each(originalEvent.changedTouches, (i, touch) => {
        pointers[touch.identifier] = getPointer(touch);
      });
    } else {
      // Handle mouse event and pointer event
      pointers[(originalEvent && originalEvent.pointerId) || 0] = getPointer(originalEvent || e);
    }

    if (utils.objectKeys(pointers).length > 1 && options.zoomable && options.zoomOnTouch) {
      action = 'zoom';
    } else {
      action = $(e.target).data('action');
    }

    if (REGEXP_ACTIONS.test(action)) {
      if (self.trigger('cropstart', {
        originalEvent,
        action,
      }).isDefaultPrevented()) {
        return;
      }

      e.preventDefault();

      self.action = action;
      self.cropping = false;

      if (action === 'crop') {
        self.cropping = true;
        self.$dragBox.addClass('cropper-modal');
      }
    }
  },

  cropMove(e) {
    const self = this;
    const action = self.action;

    if (self.disabled || !action) {
      return;
    }

    const pointers = self.pointers;
    const originalEvent = e.originalEvent;

    e.preventDefault();

    if (self.trigger('cropmove', {
      originalEvent,
      action,
    }).isDefaultPrevented()) {
      return;
    }

    if (originalEvent && originalEvent.changedTouches) {
      $.each(originalEvent.changedTouches, (i, touch) => {
        $.extend(pointers[touch.identifier], getPointer(touch, true));
      });
    } else {
      $.extend(
        pointers[(originalEvent && originalEvent.pointerId) || 0],
        getPointer(originalEvent || e, true)
      );
    }

    self.change(e);
  },

  cropEnd(e) {
    const self = this;
    const action = self.action;

    if (self.disabled || !action) {
      return;
    }

    const pointers = self.pointers;
    const originalEvent = e.originalEvent;

    e.preventDefault();

    if (originalEvent && originalEvent.changedTouches) {
      $.each(originalEvent.changedTouches, (i, touch) => {
        delete pointers[touch.identifier];
      });
    } else {
      delete pointers[(originalEvent && originalEvent.pointerId) || 0];
    }

    if (!utils.objectKeys(pointers).length) {
      self.action = '';
    }

    if (self.cropping) {
      self.cropping = false;
      self.$dragBox.toggleClass('cropper-modal', self.cropped && self.options.modal);
    }

    self.trigger('cropend', {
      originalEvent,
      action,
    });
  },
};
<commit_msg>Ignore double click when dragMode is none<commit_after>import $ from 'jquery';
import * as utils from './utilities';

const REGEXP_ACTIONS = /^(e|w|s|n|se|sw|ne|nw|all|crop|move|zoom)$/;

function getPointer({ pageX, pageY }, endOnly) {
  const end = {
    endX: pageX,
    endY: pageY,
  };

  if (endOnly) {
    return end;
  }

  return $.extend({
    startX: pageX,
    startY: pageY,
  }, end);
}

export default {
  resize() {
    const self = this;
    const restore = self.options.restore;
    const $container = self.$container;
    const container = self.container;

    // Check `container` is necessary for IE8
    if (self.disabled || !container) {
      return;
    }

    const ratio = $container.width() / container.width;

    // Resize when width changed or height changed
    if (ratio !== 1 || $container.height() !== container.height) {
      let canvasData;
      let cropBoxData;

      if (restore) {
        canvasData = self.getCanvasData();
        cropBoxData = self.getCropBoxData();
      }

      self.render();

      if (restore) {
        self.setCanvasData($.each(canvasData, (i, n) => {
          canvasData[i] = n * ratio;
        }));
        self.setCropBoxData($.each(cropBoxData, (i, n) => {
          cropBoxData[i] = n * ratio;
        }));
      }
    }
  },

  dblclick() {
    const self = this;

    if (self.disabled || self.options.dragMode === 'none') {
      return;
    }

    self.setDragMode(self.$dragBox.hasClass('cropper-crop') ? 'move' : 'crop');
  },

  wheel(event) {
    const self = this;
    const e = event.originalEvent || event;
    const ratio = Number(self.options.wheelZoomRatio) || 0.1;

    if (self.disabled) {
      return;
    }

    event.preventDefault();

    // Limit wheel speed to prevent zoom too fast
    if (self.wheeling) {
      return;
    }

    self.wheeling = true;

    setTimeout(() => {
      self.wheeling = false;
    }, 50);

    let delta = 1;

    if (e.deltaY) {
      delta = e.deltaY > 0 ? 1 : -1;
    } else if (e.wheelDelta) {
      delta = -e.wheelDelta / 120;
    } else if (e.detail) {
      delta = e.detail > 0 ? 1 : -1;
    }

    self.zoom(-delta * ratio, event);
  },

  cropStart(e) {
    const self = this;

    if (self.disabled) {
      return;
    }

    const options = self.options;
    const pointers = self.pointers;
    const originalEvent = e.originalEvent;
    let action;

    if (originalEvent && originalEvent.changedTouches) {
      // Handle touch event
      $.each(originalEvent.changedTouches, (i, touch) => {
        pointers[touch.identifier] = getPointer(touch);
      });
    } else {
      // Handle mouse event and pointer event
      pointers[(originalEvent && originalEvent.pointerId) || 0] = getPointer(originalEvent || e);
    }

    if (utils.objectKeys(pointers).length > 1 && options.zoomable && options.zoomOnTouch) {
      action = 'zoom';
    } else {
      action = $(e.target).data('action');
    }

    if (REGEXP_ACTIONS.test(action)) {
      if (self.trigger('cropstart', {
        originalEvent,
        action,
      }).isDefaultPrevented()) {
        return;
      }

      e.preventDefault();

      self.action = action;
      self.cropping = false;

      if (action === 'crop') {
        self.cropping = true;
        self.$dragBox.addClass('cropper-modal');
      }
    }
  },

  cropMove(e) {
    const self = this;
    const action = self.action;

    if (self.disabled || !action) {
      return;
    }

    const pointers = self.pointers;
    const originalEvent = e.originalEvent;

    e.preventDefault();

    if (self.trigger('cropmove', {
      originalEvent,
      action,
    }).isDefaultPrevented()) {
      return;
    }

    if (originalEvent && originalEvent.changedTouches) {
      $.each(originalEvent.changedTouches, (i, touch) => {
        $.extend(pointers[touch.identifier], getPointer(touch, true));
      });
    } else {
      $.extend(
        pointers[(originalEvent && originalEvent.pointerId) || 0],
        getPointer(originalEvent || e, true)
      );
    }

    self.change(e);
  },

  cropEnd(e) {
    const self = this;
    const action = self.action;

    if (self.disabled || !action) {
      return;
    }

    const pointers = self.pointers;
    const originalEvent = e.originalEvent;

    e.preventDefault();

    if (originalEvent && originalEvent.changedTouches) {
      $.each(originalEvent.changedTouches, (i, touch) => {
        delete pointers[touch.identifier];
      });
    } else {
      delete pointers[(originalEvent && originalEvent.pointerId) || 0];
    }

    if (!utils.objectKeys(pointers).length) {
      self.action = '';
    }

    if (self.cropping) {
      self.cropping = false;
      self.$dragBox.toggleClass('cropper-modal', self.cropped && self.options.modal);
    }

    self.trigger('cropend', {
      originalEvent,
      action,
    });
  },
};
<|endoftext|>


<commit_before>define(['text!./ammap3.html', 'app', 'lodash','ammap3','ammap3WorldLow','ammap-theme','ammap-resp'], function(template, app, _,ammap3) { 'use strict';

app.directive('ammap3',[ function () {
    return {
        restrict: 'EAC',
        template: template,
        replace: true,
        require : ['^reportingDisplay','^ammap3'],
        scope: {
              items: '=ngModel',
        },
          link : function ($scope, $element, $attr, requiredDirectives)
        {
               var reportingDIsplay = requiredDirectives[0];
               var ammap3= requiredDirectives[1];
               $scope.$watch('items',function(){ammap3.progressColorMap();});
               initMap();
               ammap3.writeMap($scope.mapData);
               ammap3.initAsslegend();
               ammap3.writeMap();

               //=======================================================================
               //
               //=======================================================================
               function initMap () {
                  $scope.mapData ={

                      "type": "map",
                      "theme": "light",
                      "responsive": {
                          "enabled": true
                        },
                      "dataProvider": {
                        "map": "worldLow",
                        "getAreasFromMap": true
                      },
                      "areasSettings": {
                        "autoZoom": true,
                        "selectedColor": "#009b48",
                        "selectable": true,
                        "color": "#428bca"
                      },
                      "smallMap": {},
                      "export": {
                        "enabled": true,
                        "position": "bottom-right"
                      },
                    };//
                }//$scope.initMap
          },//link
          controller : ["$scope", function($scope) {

              //=======================================================================
              //
              //=======================================================================
              function writeMap(mapData) {
                    if(!mapData) mapData= getMapData();
                    $scope.map = AmCharts.makeChart( "mapdiv", mapData); //jshint ignore:line
                    $scope.map.write("mapdiv");
              }

              //=======================================================================
              //
              //=======================================================================
              function progressColorMap() {

                    var countryMap={};
                    var changed=null;
                    _.each($scope.items,function(docs,countyCode){
                            var doc =docs[0]; // get first from sorted list
                                if(doc.schema_s=='nationalAssessment')
                                {
                                      if(!changed)hideAreas();
                                      changeAreaColor(countyCode,progressToColor(doc.progress_EN_t));
                                      changed=1;//flag not to recolor entire map again
                                }

                    });
                    $scope.map.validateData(); // updates map with color changes
              }//progressColorMap

              // //=======================================================================
              // //
              // //=======================================================================
              function changeAreaColor(id,color) {
                          getMapObject(id).colorReal=color;
               }//getMapObject

              // //=======================================================================
              // // changes color of all un colored areas
              // //=======================================================================
              function hideAreas() {
                    // Walkthrough areas
                    _.each($scope.map.dataProvider.areas,function(area){
                          area.colorReal='#dddddd';
                    });
               }//getMapObject

              // //=======================================================================
              // //
              // //=======================================================================
              function getMapObject(id) {
                   var index = _.findIndex($scope.map.dataProvider.areas, function(area) {

                          return area.id === id.toUpperCase();
                    });

                    return $scope.map.dataProvider.areas[index];
               }//getMapObject

              //=======================================================================
              //
              //=======================================================================
              function progressToColor(progress) {

                  switch(progress.trim())
                  {
                      case "On track to exceed target":
                          return '#1074bc';
                      case "On track to achieve target":
                          return '#109e49';
                      case "Progress towards target but at an  insufficient rate":
                          return '#fec210';
                      case "No significant change":
                          return '#ee1d23';
                      case "Moving away from target":
                          return '#6c1c67';
                  }

              }//readQueryString

              //=======================================================================
              //
              //=======================================================================
              function getMapData() {

                    return $scope.mapData;
              }

              //=======================================================================
              //
              //=======================================================================
              function setMapData(name,value) {
                    if(name && !value)$scope.mapData=name;
                    else
                      $scope.mapData[name]=value;
              }

              //=======================================================================
              //
              //=======================================================================
              function setLegendMapData(name,value) {
                    if(name && !value)$scope.mapData.legend=name;
                    else
                      $scope.mapData.legend[name]=value;
              }//setLegendMapData

              //=======================================================================
              //
              //=======================================================================
              function setResponsiveMapData(name,value) {
                    if(name && !value)$scope.mapData.responsive=name;
                    else
                      $scope.mapData.responsive[name]=value;
              }//setLegendMapData

              //=======================================================================
              //
              //=======================================================================
              function initAsslegend() {
                  setLegendMapData( {
                    "width": "100%",
                    "marginRight": 27,
                    "marginLeft": 27,
                    "equalWidths": false,
                    "backgroundAlpha": 0.5,
                    "backgroundColor": "#FFFFFF",
                    "borderColor": "#ffffff",
                    "borderAlpha": 1,
                    "top": 358,
                    "left": 0,
                    "horizontalGap": 10,
                    "data": [ {
                      "title": "Exceed Target",
                      "color": "#1074bc",
                      "description":"desc1"
                    }, {
                      "title": "Meet Target",
                      "color": "#109e49",
                        "description":"desc2"
                    }, {
                      "title": "Insuffient Rate",
                      "color": "#fec210",
                        "description":"desc3"
                    }, {
                      "title": "No Progress",
                      "color": "#ee1d23",
                        "description":"desc4"
                    },
                    {
                      "title": "Moving Away",
                      "color": "#6c1c67",
                        "description":"desc5"
                    }
                   ]
                 });

            }// initAsslegend

              this.writeMap=writeMap;
              this.getMapData =getMapData;
              this.setMapData =setMapData;
              this.setLegendMapData=setLegendMapData;
              this.setResponsiveMapData=setResponsiveMapData;
              this.initAsslegend=initAsslegend;
              this.progressColorMap=progressColorMap;
          }],
    }; // return
  }]);  //app.directive('searchFilterCountries
});// define
<commit_msg>Remove needed calls<commit_after>define(['text!./ammap3.html', 'app', 'lodash','ammap3','ammap3WorldLow','ammap-theme','ammap-resp'], function(template, app, _,ammap3) { 'use strict';

app.directive('ammap3',[ function () {
    return {
        restrict: 'EAC',
        template: template,
        replace: true,
        require : ['^reportingDisplay','^ammap3'],
        scope: {
              items: '=ngModel',
        },
          link : function ($scope, $element, $attr, requiredDirectives)
        {
               var reportingDIsplay = requiredDirectives[0];
               var ammap3= requiredDirectives[1];
               $scope.$watch('items',function(){ammap3.progressColorMap();});
               initMap();
               //ammap3.writeMap($scope.mapData);
               ammap3.initAsslegend();
               ammap3.writeMap();
               console.log(ammap3);
               $scope.map.addListener("clickMapObject", function(event) {
                        alert('here');
                        console.log(event.mapObject);
               });
               //=======================================================================
               //
               //=======================================================================
               function initMap () {
                  $scope.mapData ={

                      "type": "map",
                      "theme": "light",
                      "responsive": {
                          "enabled": true
                        },
                      "dataProvider": {
                        "map": "worldLow",
                        "getAreasFromMap": true
                      },
                      "areasSettings": {
                        "autoZoom": true,
                        "selectedColor": "#009b48",
                        "selectable": true,
                        "color": "#428bca"
                      },
                      "smallMap": {},
                      "export": {
                        "enabled": true,
                        "position": "bottom-right"
                      },
                    };//
                }//$scope.initMap
          },//link
          controller : ["$scope", function($scope) {

              //=======================================================================
              //
              //=======================================================================
              function writeMap(mapData) {
                    if(!mapData) mapData= getMapData();
                    $scope.map = AmCharts.makeChart( "mapdiv", mapData); //jshint ignore:line
                    $scope.map.write("mapdiv");
              }

              //=======================================================================
              //
              //=======================================================================
              function progressColorMap() {

                    var countryMap={};
                    var changed=null;
                    _.each($scope.items,function(docs,countyCode){
                            var doc =docs[0]; // get first from sorted list
                                if(doc.schema_s=='nationalAssessment')
                                {
                                      if(!changed)hideAreas();
                                      changeAreaColor(countyCode,progressToColor(doc.progress_EN_t));
                                      changed=1;//flag not to recolor entire map again
                                }

                    });
                    $scope.map.validateData(); // updates map with color changes
              }//progressColorMap

              // //=======================================================================
              // //
              // //=======================================================================
              function changeAreaColor(id,color) {
                          getMapObject(id).colorReal=color;
               }//getMapObject

              // //=======================================================================
              // // changes color of all un colored areas
              // //=======================================================================
              function hideAreas() {
                    // Walkthrough areas
                    _.each($scope.map.dataProvider.areas,function(area){
                          area.colorReal='#dddddd';
                    });
               }//getMapObject

              // //=======================================================================
              // //
              // //=======================================================================
              function getMapObject(id) {
                   var index = _.findIndex($scope.map.dataProvider.areas, function(area) {

                          return area.id === id.toUpperCase();
                    });

                    return $scope.map.dataProvider.areas[index];
               }//getMapObject

              //=======================================================================
              //
              //=======================================================================
              function progressToColor(progress) {

                  switch(progress.trim())
                  {
                      case "On track to exceed target":
                          return '#1074bc';
                      case "On track to achieve target":
                          return '#109e49';
                      case "Progress towards target but at an  insufficient rate":
                          return '#fec210';
                      case "No significant change":
                          return '#ee1d23';
                      case "Moving away from target":
                          return '#6c1c67';
                  }

              }//readQueryString

              //=======================================================================
              //
              //=======================================================================
              function getMapData() {

                    return $scope.mapData;
              }

              //=======================================================================
              //
              //=======================================================================
              function setMapData(name,value) {
                    if(name && !value)$scope.mapData=name;
                    else
                      $scope.mapData[name]=value;
              }

              //=======================================================================
              //
              //=======================================================================
              function setLegendMapData(name,value) {
                    if(name && !value)$scope.mapData.legend=name;
                    else
                      $scope.mapData.legend[name]=value;
              }//setLegendMapData

              //=======================================================================
              //
              //=======================================================================
              function setResponsiveMapData(name,value) {
                    if(name && !value)$scope.mapData.responsive=name;
                    else
                      $scope.mapData.responsive[name]=value;
              }//setLegendMapData

              //=======================================================================
              //
              //=======================================================================
              function initAsslegend() {
                  setLegendMapData( {
                    "width": "100%",
                    "marginRight": 27,
                    "marginLeft": 27,
                    "equalWidths": false,
                    "backgroundAlpha": 0.5,
                    "backgroundColor": "#FFFFFF",
                    "borderColor": "#ffffff",
                    "borderAlpha": 1,
                    "top": 358,
                    "left": 0,
                    "horizontalGap": 10,
                    "data": [ {
                      "title": "Exceed Target",
                      "color": "#1074bc",
                      "description":"desc1"
                    }, {
                      "title": "Meet Target",
                      "color": "#109e49",
                        "description":"desc2"
                    }, {
                      "title": "Insuffient Rate",
                      "color": "#fec210",
                        "description":"desc3"
                    }, {
                      "title": "No Progress",
                      "color": "#ee1d23",
                        "description":"desc4"
                    },
                    {
                      "title": "Moving Away",
                      "color": "#6c1c67",
                        "description":"desc5"
                    }
                   ]
                 });

            }// initAsslegend

              this.writeMap=writeMap;
              this.getMapData =getMapData;
              this.setMapData =setMapData;
              this.setLegendMapData=setLegendMapData;
              this.setResponsiveMapData=setResponsiveMapData;
              this.initAsslegend=initAsslegend;
              this.progressColorMap=progressColorMap;
          }],
    }; // return
  }]);  //app.directive('searchFilterCountries
});// define
<|endoftext|>


<commit_before>'use strict';

// reply.js

var base = require('./_base.js');

module.exports = function (warp) {
    return base.defineModel(warp, 'Reply', [
        base.column_id('topic_id', { index: true }),
        base.column_id('user_id', { index: true }),
        base.column_boolean('deleted'),
        base.column_bigint('upvotes'),
        base.column_bigint('downvotes'),
        base.column_bigint('score'),
        base.column_text('content', { type: 'text' })
    ], {
        table: 'replies'
    });
};
<commit_msg>add default values for model<commit_after>'use strict';

// reply.js

var base = require('./_base.js');

module.exports = function (warp) {
    return base.defineModel(warp, 'Reply', [
        base.column_id('topic_id', { index: true }),
        base.column_id('user_id', { index: true }),
        base.column_boolean('deleted', { defaultValue: false }),
        base.column_bigint('upvotes', { defaultValue: 0}),
        base.column_bigint('downvotes', { defaultValue: 0}),
        base.column_bigint('score', { defaultValue: 0}),
        base.column_text('content', { type: 'text' })
    ], {
        table: 'replies'
    });
};
<|endoftext|>


<commit_before>/* safe.js - Building a safer world for Pebble.JS Developers
 *
 * This library provides wrapper around all the asynchronous handlers that developers
 * have access to so that error messages are caught and displayed nicely in the pebble tool
 * console.
 */

/* global __loader */

var ajax = require('ajax');

var safe = {};

/* The name of the concatenated file to translate */
safe.translateName = 'pebble-js-app.js';

safe.indent = '    ';

/* Translates a source line position to the originating file */
safe.translatePos = function(name, lineno, colno) {
  if (name === safe.translateName) {
    var pkg = __loader.getPackageByLineno(lineno);
    if (pkg) {
      name = pkg.filename;
      lineno -= pkg.lineno;
    }
  }
  return name + ':' + lineno + ':' + colno;
};

/* Translates a node style stack trace line */
var translateLineV8 = function(line, msg, name, lineno, colno) {
  var pos = safe.translatePos(name, lineno, colno);
  return msg + '(' + pos + ')';
};

var makeTranslateStack = function(stackLineRegExp, translateLine) {
  return function(stack) {
    var lines = stack.split('\n');
    for (var i = lines.length - 1; i >= 0; --i) {
      var line = lines[i];
      var m = line.match(stackLineRegExp);
      if (m) {
        line = lines[i] = translateLine.apply(this, m);
      }
      if (line.match(module.filename)) {
        lines.splice(--i, 2);
      }
    }
    return lines.join('\n');
  };
};

/* Matches <msg> '(' <name> ':' <lineno> ':' <colno> ')' */
var stackLineRegExpV8 = /(.*)\(([^\s@:]+):(\d+):(\d+)\)/;

safe.translateStackV8 = makeTranslateStack(stackLineRegExpV8, translateLineV8);

/* Translates an iOS stack trace line to node style */
var translateLineIOS = function(line, scope, name, lineno, colno) {
  var pos = safe.translatePos(name, lineno, colno);
  return safe.indent + 'at ' + (scope ? scope  + ' (' + pos + ')' : pos);
};

/* Matches (<scope> '@' )? <name> ':' <lineno> ':' <colno> */
var stackLineRegExpIOS = /(?:([^\s@]+)@)?([^\s@:]+):(\d+):(\d+)/;

safe.translateStackIOS = makeTranslateStack(stackLineRegExpIOS, translateLineIOS);

safe.translateStackAndroid = function(stack) {
  var lines = stack.split('\n');
  for (var i = lines.length - 1; i > 0; --i) {
    var line = lines[i];
    var name, lineno, colno;
    if (line.match(/jskit_startup\.html/)) {
      lines.splice(i, 1);
    } else {
      /* Matches <name> ':' <lineno> ':' <colno> */
      var m = line.match(/^.*\/(.*?):(\d+):(\d+)/);
      if (m) {
        name = m[1];
        lineno = m[2];
        colno = m[3];
      }
    }
    if (name) {
      var pos = safe.translatePos(name, lineno, colno);
      if (line.match(/\(.*\)/)) {
        line = line.replace(/\(.*\)/, '(' + pos + ')');
      } else {
        line = line.replace(/[^\s\/]*\/.*$/, pos);
      }
      lines[i] = line;
    }
  }
  return lines.join('\n');
};

/* Translates a stack trace to the originating files */
safe.translateStack = function(stack) {
  if (Pebble.platform === 'pypkjs') {
    return safe.translateStackV8(stack);
  } else if (stack.match('com.getpebble.android')) {
    return safe.translateStackAndroid(stack);
  } else {
    return safe.translateStackIOS(stack);
  }
};

safe.translateError = function(err, intro) {
  var name = err.name;
  var message = err.message || err.toString();
  var stack = err.stack;
  var result = [intro || 'JavaScript Error:'];
  if (message && (!stack || !stack.match(message))) {
    if (name && !message.match(message)) {
      message = name + ': ' + message;
    }
    result.push(message);
  }
  if (stack) {
    result.push(safe.translateStack(stack));
  }
  return result.join('\n');
};

/* Dumps error messages to the console. */
safe.dumpError = function(err, intro) {
  if (typeof err === 'object') {
    console.log(safe.translateError(err, intro));
  } else {
    console.log('Error: dumpError argument is not an object');
  }
};

/* Logs runtime warnings to the console. */
safe.warn = function(message, name) {
  var err = new Error(message);
  err.name = name || 'Warning';
  safe.dumpError(err, 'Warning:');
};

/* Takes a function and return a new function with a call to it wrapped in a try/catch statement */
safe.protect = function(fn) {
  return fn ? function() {
    try {
      fn.apply(this, arguments);
    } catch (err) {
      safe.dumpError(err);
    }
  } : undefined;
};

/* Wrap event handlers added by Pebble.addEventListener */
var pblAddEventListener = Pebble.addEventListener;
Pebble.addEventListener = function(eventName, eventCallback) {
  pblAddEventListener.call(this, eventName, safe.protect(eventCallback));
};

var pblSendMessage = Pebble.sendAppMessage;
Pebble.sendAppMessage = function(message, success, failure) {
  return pblSendMessage.call(this, message, safe.protect(success), safe.protect(failure));
};

/* Wrap setTimeout and setInterval */
var originalSetTimeout = setTimeout;
window.setTimeout = function(callback, delay) {
  if (safe.warnSetTimeoutNotFunction !== false && typeof callback !== 'function') {
    safe.warn('setTimeout was called with a `' + (typeof callback) + '` type. ' +
              'Did you mean to pass a function?');
    safe.warnSetTimeoutNotFunction = false;
  }
  return originalSetTimeout(safe.protect(callback), delay);
};

var originalSetInterval = setInterval;
window.setInterval = function(callback, delay) {
  if (safe.warnSetIntervalNotFunction !== false && typeof callback !== 'function') {
    safe.warn('setInterval was called with a `' + (typeof callback) + '` type. ' +
              'Did you mean to pass a function?');
    safe.warnSetIntervalNotFunction = false;
  }
  return originalSetInterval(safe.protect(callback), delay);
};

/* Wrap the success and failure callback of the ajax library */
ajax.onHandler = function(eventName, callback) {
  return safe.protect(callback);
};

/* Wrap the geolocation API Callbacks */
var watchPosition = navigator.geolocation.watchPosition;
navigator.geolocation.watchPosition = function(success, error, options) {
  return watchPosition.call(this, safe.protect(success), safe.protect(error), options);
};
var getCurrentPosition = navigator.geolocation.getCurrentPosition;
navigator.geolocation.getCurrentPosition = function(success, error, options) {
  return getCurrentPosition.call(this, safe.protect(success), safe.protect(error), options);
};

module.exports = safe;
<commit_msg>Change safe to wrap ajax only if it exists<commit_after>/* safe.js - Building a safer world for Pebble.JS Developers
 *
 * This library provides wrapper around all the asynchronous handlers that developers
 * have access to so that error messages are caught and displayed nicely in the pebble tool
 * console.
 */

/* global __loader */

var safe = {};

/* The name of the concatenated file to translate */
safe.translateName = 'pebble-js-app.js';

safe.indent = '    ';

/* Translates a source line position to the originating file */
safe.translatePos = function(name, lineno, colno) {
  if (name === safe.translateName) {
    var pkg = __loader.getPackageByLineno(lineno);
    if (pkg) {
      name = pkg.filename;
      lineno -= pkg.lineno;
    }
  }
  return name + ':' + lineno + ':' + colno;
};

/* Translates a node style stack trace line */
var translateLineV8 = function(line, msg, name, lineno, colno) {
  var pos = safe.translatePos(name, lineno, colno);
  return msg + '(' + pos + ')';
};

var makeTranslateStack = function(stackLineRegExp, translateLine) {
  return function(stack) {
    var lines = stack.split('\n');
    for (var i = lines.length - 1; i >= 0; --i) {
      var line = lines[i];
      var m = line.match(stackLineRegExp);
      if (m) {
        line = lines[i] = translateLine.apply(this, m);
      }
      if (line.match(module.filename)) {
        lines.splice(--i, 2);
      }
    }
    return lines.join('\n');
  };
};

/* Matches <msg> '(' <name> ':' <lineno> ':' <colno> ')' */
var stackLineRegExpV8 = /(.*)\(([^\s@:]+):(\d+):(\d+)\)/;

safe.translateStackV8 = makeTranslateStack(stackLineRegExpV8, translateLineV8);

/* Translates an iOS stack trace line to node style */
var translateLineIOS = function(line, scope, name, lineno, colno) {
  var pos = safe.translatePos(name, lineno, colno);
  return safe.indent + 'at ' + (scope ? scope  + ' (' + pos + ')' : pos);
};

/* Matches (<scope> '@' )? <name> ':' <lineno> ':' <colno> */
var stackLineRegExpIOS = /(?:([^\s@]+)@)?([^\s@:]+):(\d+):(\d+)/;

safe.translateStackIOS = makeTranslateStack(stackLineRegExpIOS, translateLineIOS);

safe.translateStackAndroid = function(stack) {
  var lines = stack.split('\n');
  for (var i = lines.length - 1; i > 0; --i) {
    var line = lines[i];
    var name, lineno, colno;
    if (line.match(/jskit_startup\.html/)) {
      lines.splice(i, 1);
    } else {
      /* Matches <name> ':' <lineno> ':' <colno> */
      var m = line.match(/^.*\/(.*?):(\d+):(\d+)/);
      if (m) {
        name = m[1];
        lineno = m[2];
        colno = m[3];
      }
    }
    if (name) {
      var pos = safe.translatePos(name, lineno, colno);
      if (line.match(/\(.*\)/)) {
        line = line.replace(/\(.*\)/, '(' + pos + ')');
      } else {
        line = line.replace(/[^\s\/]*\/.*$/, pos);
      }
      lines[i] = line;
    }
  }
  return lines.join('\n');
};

/* Translates a stack trace to the originating files */
safe.translateStack = function(stack) {
  if (Pebble.platform === 'pypkjs') {
    return safe.translateStackV8(stack);
  } else if (stack.match('com.getpebble.android')) {
    return safe.translateStackAndroid(stack);
  } else {
    return safe.translateStackIOS(stack);
  }
};

safe.translateError = function(err, intro) {
  var name = err.name;
  var message = err.message || err.toString();
  var stack = err.stack;
  var result = [intro || 'JavaScript Error:'];
  if (message && (!stack || !stack.match(message))) {
    if (name && !message.match(message)) {
      message = name + ': ' + message;
    }
    result.push(message);
  }
  if (stack) {
    result.push(safe.translateStack(stack));
  }
  return result.join('\n');
};

/* Dumps error messages to the console. */
safe.dumpError = function(err, intro) {
  if (typeof err === 'object') {
    console.log(safe.translateError(err, intro));
  } else {
    console.log('Error: dumpError argument is not an object');
  }
};

/* Logs runtime warnings to the console. */
safe.warn = function(message, name) {
  var err = new Error(message);
  err.name = name || 'Warning';
  safe.dumpError(err, 'Warning:');
};

/* Takes a function and return a new function with a call to it wrapped in a try/catch statement */
safe.protect = function(fn) {
  return fn ? function() {
    try {
      fn.apply(this, arguments);
    } catch (err) {
      safe.dumpError(err);
    }
  } : undefined;
};

/* Wrap event handlers added by Pebble.addEventListener */
var pblAddEventListener = Pebble.addEventListener;
Pebble.addEventListener = function(eventName, eventCallback) {
  pblAddEventListener.call(this, eventName, safe.protect(eventCallback));
};

var pblSendMessage = Pebble.sendAppMessage;
Pebble.sendAppMessage = function(message, success, failure) {
  return pblSendMessage.call(this, message, safe.protect(success), safe.protect(failure));
};

/* Wrap setTimeout and setInterval */
var originalSetTimeout = setTimeout;
window.setTimeout = function(callback, delay) {
  if (safe.warnSetTimeoutNotFunction !== false && typeof callback !== 'function') {
    safe.warn('setTimeout was called with a `' + (typeof callback) + '` type. ' +
              'Did you mean to pass a function?');
    safe.warnSetTimeoutNotFunction = false;
  }
  return originalSetTimeout(safe.protect(callback), delay);
};

var originalSetInterval = setInterval;
window.setInterval = function(callback, delay) {
  if (safe.warnSetIntervalNotFunction !== false && typeof callback !== 'function') {
    safe.warn('setInterval was called with a `' + (typeof callback) + '` type. ' +
              'Did you mean to pass a function?');
    safe.warnSetIntervalNotFunction = false;
  }
  return originalSetInterval(safe.protect(callback), delay);
};

/* Wrap the geolocation API Callbacks */
var watchPosition = navigator.geolocation.watchPosition;
navigator.geolocation.watchPosition = function(success, error, options) {
  return watchPosition.call(this, safe.protect(success), safe.protect(error), options);
};

var getCurrentPosition = navigator.geolocation.getCurrentPosition;
navigator.geolocation.getCurrentPosition = function(success, error, options) {
  return getCurrentPosition.call(this, safe.protect(success), safe.protect(error), options);
};

var ajax;

/* Try to load the ajax library if available and silently fail if it is not found. */
try {
  ajax = require('ajax');
} catch (err) {}

/* Wrap the success and failure callback of the ajax library */
if (ajax) {
  ajax.onHandler = function(eventName, callback) {
    return safe.protect(callback);
  };
}

module.exports = safe;
<|endoftext|>


<commit_before>'use strict';

/**
 * Updates branches
 *
 * @param  {{nodes:Array<string>=, repeatPath: string=, repeatIndex: number=}=} updated The object containing info on updated data nodes
 */

var $ = require( 'jquery' );

module.exports = {
    update: function( updated, forceClearIrrelevant ) {
        var $nodes;

        if ( !this.form ) {
            throw new Error( 'Branch module not correctly instantiated with form property.' );
        }

        $nodes = this.form.getRelatedNodes( 'data-relevant', '', updated );

        this.updateNodes( $nodes, forceClearIrrelevant );
    },
    updateNodes: function( $nodes, forceClearIrrelevant ) {
        var p;
        var $branchNode;
        var result;
        var insideRepeat;
        var insideRepeatClone;
        var cacheIndex;
        var relevantCache = {};
        var alreadyCovered = [];
        var branchChange = false;
        var that = this;
        var clonedRepeatsPresent;

        clonedRepeatsPresent = ( this.form.repeatsPresent && this.form.view.$.find( '.or-repeat.clone' ).length > 0 ) ? true : false;

        $nodes.each( function() {
            var $node = $( this );
            var context;
            var $parentGroups;
            var pathParts;
            var parentPath;

            //note that $(this).attr('name') is not the same as p.path for repeated radiobuttons!
            if ( $.inArray( $node.attr( 'name' ), alreadyCovered ) !== -1 ) {
                return;
            }

            // since this result is almost certainly not empty, closest() is the most efficient
            $branchNode = $node.closest( '.or-branch' );

            p = {};
            cacheIndex = null;

            p.relevant = that.form.input.getRelevant( $node );
            p.path = that.form.input.getName( $node );

            if ( $branchNode.length !== 1 ) {
                if ( $node.parentsUntil( '.or', '#or-calculated-items' ).length === 0 ) {
                    console.error( 'could not find branch node for ', $( this ) );
                }
                return;
            }

            /*
             * Check if the (calculate without form control) node is part of a repeat that has no instances
             */
            pathParts = p.path.split( '/' );
            if ( pathParts.length > 3 ) {
                parentPath = pathParts.splice( 0, pathParts.length - 1 ).join( '/' );
                $parentGroups = that.form.view.$.find( '.or-group[name="' + parentPath + '"],.or-group-data[name="' + parentPath + '"]' )
                    // now remove the groups that have a repeat-info child without repeat instance siblings
                    .filter( function() {
                        var $g = $( this );
                        return $g.children( '.or-repeat' ).length > 0 || $g.children( '.or-repeat-info' ).length === 0;
                    } ); //.eq( index )
                // If the parent doesn't exist in the DOM it means there is a repeat ancestor and there are no instances of that repeat.
                // Hence that relevant does not need to be evaluated (and would fail otherwise because the context doesn't exist).
                if ( $parentGroups.length === 0 ) {
                    return;
                }
            }

            /*
             * Determining ancestry is expensive. Using the knowledge most forms don't use repeats and
             * if they do, they usually don't have cloned repeats during initialization we perform first a check for .repeat.clone.
             * The first condition is usually false (and is a very quick one-time check) so this presents a big performance boost
             * (6-7 seconds of loading time on the bench6 form)
             */
            // TODO: these checks fail miserably for calculated items that do not have a form control
            insideRepeat = clonedRepeatsPresent && $branchNode.parentsUntil( '.or', '.or-repeat' ).length > 0;
            insideRepeatClone = clonedRepeatsPresent && $branchNode.parentsUntil( '.or', '.or-repeat.clone' ).length > 0;

            /*
             * If the relevant is placed on a group and that group contains repeats with the same name,
             * but currently has 0 repeats, the context will not be available. This same logic is applied in output.js.
             */
            if ( $node.children( '.or-repeat-info[data-name="' + p.path + '"]' ).length && !$node.children( '.or-repeat[name="' + p.path + '"]' ).length ) {
                context = null;
            } else {
                context = p.path;
            }
            /*
             * Determining the index is expensive, so we only do this when the branch is inside a cloned repeat.
             * It can be safely set to 0 for other branches.
             */
            p.ind = ( context && insideRepeatClone ) ? that.form.input.getIndex( $node ) : 0;
            /*
             * Caching is only possible for expressions that do not contain relative paths to nodes.
             * So, first do a *very* aggresive check to see if the expression contains a relative path.
             * This check assumes that child nodes (e.g. "mychild = 'bob'") are NEVER used in a relevant
             * expression, which may prove to be incorrect.
             */
            if ( p.relevant.indexOf( '..' ) === -1 ) {
                if ( !insideRepeat ) {
                    cacheIndex = p.relevant;
                } else {
                    // The path is stripped of the last nodeName to record the context.
                    // This might be dangerous, but until we find a bug, it helps in those forms where one group contains
                    // many sibling questions that each have the same relevant.
                    cacheIndex = p.relevant + '__' + p.path.substring( 0, p.path.lastIndexOf( '/' ) ) + '__' + p.ind;
                }
            }
            if ( cacheIndex && typeof relevantCache[ cacheIndex ] !== 'undefined' ) {
                result = relevantCache[ cacheIndex ];
            } else {
                result = that.evaluate( p.relevant, context, p.ind );
                relevantCache[ cacheIndex ] = result;
            }

            if ( !insideRepeat ) {
                alreadyCovered.push( $( this ).attr( 'name' ) );
            }

            if ( that.process( $branchNode, p.path, result, forceClearIrrelevant ) === true ) {
                branchChange = true;
            }
        } );

        if ( branchChange ) {
            this.form.view.$.trigger( 'changebranch' );
        }
    },
    /**
     * Evaluates a relevant expression (for future fancy stuff this is placed in a separate function)
     *
     * @param  {string} expr        [description]
     * @param  {string} contextPath [description]
     * @param  {number} index       [description]
     * @return {boolean}             [description]
     */
    evaluate: function( expr, contextPath, index ) {
        var result = this.form.model.evaluate( expr, 'boolean', contextPath, index );
        return result;
    },
    /**
     * Processes the evaluation result for a branch
     *
     * @param { jQuery } $branchNode [description]
     * @param { string } path Path of branch node
     * @param { boolean } result      result of relevant evaluation
     * @param { =boolean } forceClearIrrelevant Whether to force clearing of irrelevant nodes and descendants
     */
    process: function( $branchNode, path, result, forceClearIrrelevant ) {
        if ( result === true ) {
            return this.enable( $branchNode, path );
        } else {
            return this.disable( $branchNode, path, forceClearIrrelevant );
        }
    },

    /**
     * Checks whether branch currently has 'relevant' state
     *
     * @param  {jQuery} $branchNode [description]
     * @return {boolean}             [description]
     */
    selfRelevant: function( $branchNode ) {
        return !$branchNode.hasClass( 'disabled' ) && !$branchNode.hasClass( 'pre-init' );
    },

    /**
     * Enables and reveals a branch node/group
     *
     * @param  {jQuery} $branchNode The jQuery object to reveal and enable
     */
    enable: function( $branchNode, path ) {
        var change = false;

        if ( !this.selfRelevant( $branchNode ) ) {
            change = true;
            $branchNode.removeClass( 'disabled pre-init' );
            // Update calculated items, both individual question or descendants of group
            this.form.calc.update( {
                relevantPath: path
            } );
            // Update outputs that are children of branch
            // TODO this re-evaluates all outputs in the form which is not efficient!
            this.form.output.update();
            this.form.widgets.enable( $branchNode );
            this.activate( $branchNode );
        }
        return change;
    },

    /**
     * Disables and hides a branch node/group
     *
     * @param  {jQuery} $branchNode The jQuery object to hide and disable
     */
    disable: function( $branchNode, path, forceClearIrrelevant ) {
        var virgin = $branchNode.hasClass( 'pre-init' );
        var change = false;

        if ( virgin || this.selfRelevant( $branchNode ) || forceClearIrrelevant ) {
            change = true;
            // if the branch was previously enabled, keep any default values
            if ( !virgin ) {
                if ( this.form.options.clearIrrelevantImmediately || forceClearIrrelevant ) {
                    this.clear( $branchNode, path );
                }
            } else {
                $branchNode.removeClass( 'pre-init' );
            }

            this.deactivate( $branchNode );
        }
        return change;
    },
    /**
     * Clears values from branchnode.
     * This function is separated so it can be overridden in custom apps.
     *
     * @param  {[type]} $branchNode [description]
     * @return {boolean}             [description]
     */
    clear: function( $branchNode, path ) {
        // A change event ensures the model is updated
        // An inputupdate event is required to update widgets
        $branchNode.clearInputs( 'change', 'inputupdate.enketo' );
        // Update calculated items if branch is a group
        // We exclude question branches here because those will have been cleared already in the previous line.
        if ( $branchNode.is( '.or-group, .or-group-data' ) ) {
            this.form.calc.update( {
                relevantPath: path
            } );
        }
    },
    setDisabledProperty: function( $branchNode, bool ) {
        var type = $branchNode.prop( 'nodeName' ).toLowerCase();

        if ( type === 'label' ) {
            $branchNode.children( 'input, select, textarea' ).prop( 'disabled', bool );
        } else if ( type === 'fieldset' || type === 'section' ) {
            // TODO: a <section> cannot be disabled like this
            $branchNode.prop( 'disabled', bool );
        } else {
            $branchNode.find( 'fieldset, input, select, textarea' ).prop( 'disabled', bool );
        }
    },
    /**
     * Activates form controls.
     * This function is separated so it can be overridden in custom apps.
     *
     * @param  {[type]} $branchNode [description]
     * @return {[type]}            [description]
     */
    activate: function( $branchNode ) {
        this.setDisabledProperty( $branchNode, false );
    },
    /**
     * Deactivates form controls.
     * This function is separated so it can be overridden in custom apps.
     *
     * @param  {[type]} $branchNode [description]
     * @return {[type]}             [description]
     */
    deactivate: function( $branchNode ) {
        $branchNode.addClass( 'disabled' );
        this.form.widgets.disable( $branchNode );
        this.setDisabledProperty( $branchNode, true );
    }
};
<commit_msg>changed: refactored<commit_after>'use strict';

/**
 * Updates branches
 *
 * @param  {{nodes:Array<string>=, repeatPath: string=, repeatIndex: number=}=} updated The object containing info on updated data nodes
 */

var $ = require( 'jquery' );

module.exports = {
    update: function( updated, forceClearIrrelevant ) {
        var $nodes;

        if ( !this.form ) {
            throw new Error( 'Branch module not correctly instantiated with form property.' );
        }

        $nodes = this.form.getRelatedNodes( 'data-relevant', '', updated );

        this.updateNodes( $nodes, forceClearIrrelevant );
    },
    updateNodes: function( $nodes, forceClearIrrelevant ) {
        var p;
        var $branchNode;
        var result;
        var insideRepeat;
        var insideRepeatClone;
        var cacheIndex;
        var relevantCache = {};
        var alreadyCovered = [];
        var branchChange = false;
        var that = this;
        var clonedRepeatsPresent = this.form.repeatsPresent && this.form.view.$.find( '.or-repeat.clone' ).length > 0;

        $nodes.each( function() {
            var $node = $( this );
            var context;
            var $parentGroups;
            var pathParts;
            var parentPath;

            //note that $(this).attr('name') is not the same as p.path for repeated radiobuttons!
            if ( $.inArray( $node.attr( 'name' ), alreadyCovered ) !== -1 ) {
                return;
            }

            // since this result is almost certainly not empty, closest() is the most efficient
            $branchNode = $node.closest( '.or-branch' );

            p = {};
            cacheIndex = null;

            p.relevant = that.form.input.getRelevant( $node );
            p.path = that.form.input.getName( $node );

            if ( $branchNode.length !== 1 ) {
                if ( $node.parentsUntil( '.or', '#or-calculated-items' ).length === 0 ) {
                    console.error( 'could not find branch node for ', $( this ) );
                }
                return;
            }

            /*
             * Check if the (calculate without form control) node is part of a repeat that has no instances
             */
            pathParts = p.path.split( '/' );
            if ( pathParts.length > 3 ) {
                parentPath = pathParts.splice( 0, pathParts.length - 1 ).join( '/' );
                $parentGroups = that.form.view.$.find( '.or-group[name="' + parentPath + '"],.or-group-data[name="' + parentPath + '"]' )
                    // now remove the groups that have a repeat-info child without repeat instance siblings
                    .filter( function() {
                        var $g = $( this );
                        return $g.children( '.or-repeat' ).length > 0 || $g.children( '.or-repeat-info' ).length === 0;
                    } ); //.eq( index )
                // If the parent doesn't exist in the DOM it means there is a repeat ancestor and there are no instances of that repeat.
                // Hence that relevant does not need to be evaluated (and would fail otherwise because the context doesn't exist).
                if ( $parentGroups.length === 0 ) {
                    return;
                }
            }

            /*
             * Determining ancestry is expensive. Using the knowledge most forms don't use repeats and
             * if they do, they usually don't have cloned repeats during initialization we perform first a check for .repeat.clone.
             * The first condition is usually false (and is a very quick one-time check) so this presents a big performance boost
             * (6-7 seconds of loading time on the bench6 form)
             */
            // TODO: these checks fail miserably for calculated items that do not have a form control
            insideRepeat = clonedRepeatsPresent && $branchNode.parentsUntil( '.or', '.or-repeat' ).length > 0;
            insideRepeatClone = clonedRepeatsPresent && $branchNode.parentsUntil( '.or', '.or-repeat.clone' ).length > 0;

            /*
             * If the relevant is placed on a group and that group contains repeats with the same name,
             * but currently has 0 repeats, the context will not be available. This same logic is applied in output.js.
             */
            if ( $node.children( '.or-repeat-info[data-name="' + p.path + '"]' ).length && !$node.children( '.or-repeat[name="' + p.path + '"]' ).length ) {
                context = null;
            } else {
                context = p.path;
            }
            /*
             * Determining the index is expensive, so we only do this when the branch is inside a cloned repeat.
             * It can be safely set to 0 for other branches.
             */
            p.ind = ( context && insideRepeatClone ) ? that.form.input.getIndex( $node ) : 0;
            /*
             * Caching is only possible for expressions that do not contain relative paths to nodes.
             * So, first do a *very* aggresive check to see if the expression contains a relative path.
             * This check assumes that child nodes (e.g. "mychild = 'bob'") are NEVER used in a relevant
             * expression, which may prove to be incorrect.
             */
            if ( p.relevant.indexOf( '..' ) === -1 ) {
                if ( !insideRepeat ) {
                    cacheIndex = p.relevant;
                } else {
                    // The path is stripped of the last nodeName to record the context.
                    // This might be dangerous, but until we find a bug, it helps in those forms where one group contains
                    // many sibling questions that each have the same relevant.
                    cacheIndex = p.relevant + '__' + p.path.substring( 0, p.path.lastIndexOf( '/' ) ) + '__' + p.ind;
                }
            }
            if ( cacheIndex && typeof relevantCache[ cacheIndex ] !== 'undefined' ) {
                result = relevantCache[ cacheIndex ];
            } else {
                result = that.evaluate( p.relevant, context, p.ind );
                relevantCache[ cacheIndex ] = result;
            }

            if ( !insideRepeat ) {
                alreadyCovered.push( $( this ).attr( 'name' ) );
            }

            if ( that.process( $branchNode, p.path, result, forceClearIrrelevant ) === true ) {
                branchChange = true;
            }
        } );

        if ( branchChange ) {
            this.form.view.$.trigger( 'changebranch' );
        }
    },
    /**
     * Evaluates a relevant expression (for future fancy stuff this is placed in a separate function)
     *
     * @param  {string} expr        [description]
     * @param  {string} contextPath [description]
     * @param  {number} index       [description]
     * @return {boolean}             [description]
     */
    evaluate: function( expr, contextPath, index ) {
        var result = this.form.model.evaluate( expr, 'boolean', contextPath, index );
        return result;
    },
    /**
     * Processes the evaluation result for a branch
     *
     * @param { jQuery } $branchNode [description]
     * @param { string } path Path of branch node
     * @param { boolean } result      result of relevant evaluation
     * @param { =boolean } forceClearIrrelevant Whether to force clearing of irrelevant nodes and descendants
     */
    process: function( $branchNode, path, result, forceClearIrrelevant ) {
        if ( result === true ) {
            return this.enable( $branchNode, path );
        } else {
            return this.disable( $branchNode, path, forceClearIrrelevant );
        }
    },

    /**
     * Checks whether branch currently has 'relevant' state
     *
     * @param  {jQuery} $branchNode [description]
     * @return {boolean}             [description]
     */
    selfRelevant: function( $branchNode ) {
        return !$branchNode.hasClass( 'disabled' ) && !$branchNode.hasClass( 'pre-init' );
    },

    /**
     * Enables and reveals a branch node/group
     *
     * @param  {jQuery} $branchNode The jQuery object to reveal and enable
     */
    enable: function( $branchNode, path ) {
        var change = false;

        if ( !this.selfRelevant( $branchNode ) ) {
            change = true;
            $branchNode.removeClass( 'disabled pre-init' );
            // Update calculated items, both individual question or descendants of group
            this.form.calc.update( {
                relevantPath: path
            } );
            // Update outputs that are children of branch
            // TODO this re-evaluates all outputs in the form which is not efficient!
            this.form.output.update();
            this.form.widgets.enable( $branchNode );
            this.activate( $branchNode );
        }
        return change;
    },

    /**
     * Disables and hides a branch node/group
     *
     * @param  {jQuery} $branchNode The jQuery object to hide and disable
     */
    disable: function( $branchNode, path, forceClearIrrelevant ) {
        var virgin = $branchNode.hasClass( 'pre-init' );
        var change = false;

        if ( virgin || this.selfRelevant( $branchNode ) || forceClearIrrelevant ) {
            change = true;
            // if the branch was previously enabled, keep any default values
            if ( !virgin ) {
                if ( this.form.options.clearIrrelevantImmediately || forceClearIrrelevant ) {
                    this.clear( $branchNode, path );
                }
            } else {
                $branchNode.removeClass( 'pre-init' );
            }

            this.deactivate( $branchNode );
        }
        return change;
    },
    /**
     * Clears values from branchnode.
     * This function is separated so it can be overridden in custom apps.
     *
     * @param  {[type]} $branchNode [description]
     * @return {boolean}             [description]
     */
    clear: function( $branchNode, path ) {
        // A change event ensures the model is updated
        // An inputupdate event is required to update widgets
        $branchNode.clearInputs( 'change', 'inputupdate.enketo' );
        // Update calculated items if branch is a group
        // We exclude question branches here because those will have been cleared already in the previous line.
        if ( $branchNode.is( '.or-group, .or-group-data' ) ) {
            this.form.calc.update( {
                relevantPath: path
            } );
        }
    },
    setDisabledProperty: function( $branchNode, bool ) {
        var type = $branchNode.prop( 'nodeName' ).toLowerCase();

        if ( type === 'label' ) {
            $branchNode.children( 'input, select, textarea' ).prop( 'disabled', bool );
        } else if ( type === 'fieldset' || type === 'section' ) {
            // TODO: a <section> cannot be disabled like this
            $branchNode.prop( 'disabled', bool );
        } else {
            $branchNode.find( 'fieldset, input, select, textarea' ).prop( 'disabled', bool );
        }
    },
    /**
     * Activates form controls.
     * This function is separated so it can be overridden in custom apps.
     *
     * @param  {[type]} $branchNode [description]
     * @return {[type]}            [description]
     */
    activate: function( $branchNode ) {
        this.setDisabledProperty( $branchNode, false );
    },
    /**
     * Deactivates form controls.
     * This function is separated so it can be overridden in custom apps.
     *
     * @param  {[type]} $branchNode [description]
     * @return {[type]}             [description]
     */
    deactivate: function( $branchNode ) {
        $branchNode.addClass( 'disabled' );
        this.form.widgets.disable( $branchNode );
        this.setDisabledProperty( $branchNode, true );
    }
};
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import axios from 'axios'
import classNames from 'classnames'
import update from 'immutability-helper'

import { path } from '../Routes'

import ProductForm from './ProductForm'
import Images from '../Images/Images'

import buttons from '../Buttons.module.css'
import page from '../Page.module.css'
import form from '../Form.module.css'
import styles from './Variant.module.css'

class Form extends React.Component {
  state = {
    variant: null,
    values: {
      color_id: '',
      state: '',
      created_at: null,
      latest: false,
      sale: false,
      pinned: false,
      desc: '',
      comp: '',
      price: '',
      price_last: '',
      product_id: this.props.product ? this.props.product.id : [],
      product_attributes: this.props.product || null,
      availabilities_attributes: [],
      images: [],
      image_ids: null,
      images_attributes: [],

    },
    dictionaries: {
      colors: this.props.colors || [],
      stores: this.props.stores || [],
      sizes: this.props.sizes || [],
      categories: this.props.categories || [],
    },
  }

  componentDidMount() {
    if (this.props.id) {
      this._loadAsyncData(this.props.id);
    }
  }

  render () {
    const { title, id } = this.props
    const { values, variant} = this.state
    const { colors, stores, sizes, categories } = this.state.dictionaries

    return (

{variant ? `Редактирование: ${title}` : 'Новый товар'}

          <ProductForm values={values.product_attributes} onChange={this.handleProductChange} categories={categories}/>
          <form className={styles.variant_form} onSubmit={this.handleSubmit}>

                    <input disabled={values.sale} type="checkbox" name="latest" checked={values.latest} onChange={this.handleInputChange} />
                      new

                    <input disabled={values.latest} type="checkbox" name="sale" checked={values.sale} onChange={this.handleInputChange} />
                      sale

                    <input type="checkbox" name="pinned" checked={values.pinned} onChange={this.handleInputChange} />
                      Закреплен

                Цвет

                <select name="color_id" onChange={this.handleInputChange} value={values.color_id}>
                  <option />
                  {colors.map(color =>
                    <option key={color.id} value={color.id}>{color.title}</option>
                  )}
                </select>

              {stores.map((store, _) =>
                <>

                        Доступные размеры для {store.title}

                        {sizes.sort((a, b) => a.size.weight - b.size.weight).map((size, _) =>

 s.size_id == size.id && s.store_id == store.id && !s._destroy)})} onClick={() => this.handleSizesChange(store.id, size.id)}>{size.size}

                        )}

                  {values.availabilities_attributes && values.availabilities_attributes.filter(s => s.store_id == store.id )&&
                    values.availabilities_attributes.filter(s => s.store_id == store.id && !s._destroy).sort((a, b) => a.weight - b.weight).map((size, key) =>

                          Количество размера "{sizes.find(s => s.id == size.size_id).size}"

                          <input type="text" value={size.quantity} name={`size[${size.size_id}]`} onChange={this.handleQuantityChange(store.id, size.size_id)} />

                    )
                  }
                </>
              )}

                Стоимость

                <input type="text" value={values.price} name="price" onChange={this.handleInputChange} />

                Стоимость со скидкой

                <input type="text" value={values.price_last} name="price_last" onChange={this.handleInputChange} />

                Описание

                <textarea name="desc" value={values.desc} rows="2" onChange={this.handleInputChange} />

                Как ухаживать

                <textarea name="comp" value={values.comp} rows="2" onChange={this.handleInputChange} />

                    Статус

                      <input type="radio" name="state" checked={'active' == values.state} value="active" onChange={this.handleInputChange} />
                      Активный

                      <input type="radio" name="state" checked={'archived' == values.state} value="archived" onChange={this.handleInputChange} />
                      Архив

                Изображения

              <Images images={values.images} onImagesChange={this.handleImagesChange}/>

              <input className={buttons.main} type="submit" value="Сохранить" disabled={!this.canSubmit()} />

          </form>

    )
  }

  _loadAsyncData(id) {
    this._asyncRequest = axios.CancelToken.source();

    axios.get(path('edit_variant_path', {id: id, format: 'json' }), { authenticity_token: document.querySelector('[name="csrf-token"]').content})
      .then(res => {
        this.setState({
          variant: res.data.variant,
          values: {
            color_id: res.data.variant.color_id,
            state: res.data.variant.state,
            created_at: res.data.variant.created_at,
            latest: res.data.variant.latest,
            sale: res.data.variant.sale,
            pinned: res.data.variant.pinned,
            desc: res.data.variant.desc,
            comp: res.data.variant.comp,
            price: res.data.variant.price,
            price_last: res.data.variant.price_last,
            product_id: res.data.variant.product_attributes.id,
            product_attributes: res.data.variant.product_attributes,
            availabilities_attributes: res.data.variant.availabilities_attributes,
            images: res.data.variant.images,
            image_ids: res.data.variant.images.map(i => i.id)
          }
        });

        this._asyncRequest = null;
      });
  }

  handleInputChange = event => {
    const target = event.target
    const value = target.type === 'checkbox' ? target.checked : target.value
    const name = target.name

    this.setState(state => ({
      values: { ...state.values, [name]: value }
    }))
  }

  handleQuantityChange = (store, size) => (event) => {
    const target = event.target
    const value = target.value

    let key = Object.keys(this.state.values.availabilities_attributes).find(s => this.state.values.availabilities_attributes[s].size_id == size && this.state.values.availabilities_attributes[s].store_id == store);

    const availabilities = update(this.state.values.availabilities_attributes, {
      [key]: {
        quantity: {
          $set: value
        }
      }
    });

    this.setState(state => ({
      values: { ...state.values,
        availabilities_attributes: availabilities
      }
    }))
  }

  handleSubmit = event => {
    if (this.props.id) {
      this._handleUpdate()
    }
    else {
      this._handleCreate()
    }
    event.preventDefault()
  }

  canSubmit = () => {
    return (
      this.state.values.product_attributes &&
      this.state.values.color_id &&
      this.state.values.price &&
      this.state.values.state
    );
  }

  _handleUpdate = async () => {
    const res = await axios.patch(
      path('variant_path', { id: this.props.id }),
      { variant: this.state.values, authenticity_token: document.querySelector('[name="csrf-token"]').content }
    )

    if(res.headers.location) window.location = res.headers.location
  }

  _handleCreate = async () => {
    const res = await axios.post(
      path('variants_path'),
      { variant: this.state.values, authenticity_token: document.querySelector('[name="csrf-token"]').content }
    )

    if(res.headers.location) window.location = res.headers.location
  }

  handleProductChange = (product) => {
    this.setState(state => ({
      values: { ...state.values,
        product_attributes: product,
        product_id: product.id,
      }
    }))
  }

  handleSizesChange = (store, size) => {
    if (this.state.values.availabilities_attributes.find(s => s.size_id == size && s.store_id == store)) {
      let key = Object.keys(this.state.values.availabilities_attributes).find(s => this.state.values.availabilities_attributes[s].size_id == size && this.state.values.availabilities_attributes[s].store_id == store);

      const availabilities = update(this.state.values.availabilities_attributes, {
        [key]: {
          _destroy: {
            $set: !this.state.values.availabilities_attributes[key]._destroy
          }
        }
      });

      this.setState(state => ({
        values: { ...state.values,
          availabilities_attributes: availabilities
        }
      }))
    }
    else {
      this.setState(state => ({
        values: { ...state.values,
          availabilities_attributes: [ ...state.values.availabilities_attributes, {
            store_id: store,
            size_id: size,
            variant_id: this.props.id,
            quantity: 0,
            weight: this.state.dictionaries.sizes.find(s => s.id == size).weight,
            _destroy: false
          }]
        }
      }))
    }
  }

  handleImagesChange = (images) => {
    this.setState(state => ({
      values: { ...state.values,
        image_ids: images.map(i => i.id),
        images_attributes: images.map((i, index) => ({id:i.id, weight: index+1}))
      }
    }))
  }
}

export default Form
<commit_msg>Правильная сортировка размеров<commit_after>import React, { Component } from 'react'
import axios from 'axios'
import classNames from 'classnames'
import update from 'immutability-helper'

import { path } from '../Routes'

import ProductForm from './ProductForm'
import Images from '../Images/Images'

import buttons from '../Buttons.module.css'
import page from '../Page.module.css'
import form from '../Form.module.css'
import styles from './Variant.module.css'

class Form extends React.Component {
  state = {
    variant: null,
    values: {
      color_id: '',
      state: '',
      created_at: null,
      latest: false,
      sale: false,
      pinned: false,
      desc: '',
      comp: '',
      price: '',
      price_last: '',
      product_id: this.props.product ? this.props.product.id : [],
      product_attributes: this.props.product || null,
      availabilities_attributes: [],
      images: [],
      image_ids: null,
      images_attributes: [],

    },
    dictionaries: {
      colors: this.props.colors || [],
      stores: this.props.stores || [],
      sizes: this.props.sizes || [],
      categories: this.props.categories || [],
    },
  }

  componentDidMount() {
    if (this.props.id) {
      this._loadAsyncData(this.props.id);
    }
  }

  render () {
    const { title, id } = this.props
    const { values, variant} = this.state
    const { colors, stores, sizes, categories } = this.state.dictionaries

    return (

{variant ? `Редактирование: ${title}` : 'Новый товар'}

          <ProductForm values={values.product_attributes} onChange={this.handleProductChange} categories={categories}/>
          <form className={styles.variant_form} onSubmit={this.handleSubmit}>

                    <input disabled={values.sale} type="checkbox" name="latest" checked={values.latest} onChange={this.handleInputChange} />
                      new

                    <input disabled={values.latest} type="checkbox" name="sale" checked={values.sale} onChange={this.handleInputChange} />
                      sale

                    <input type="checkbox" name="pinned" checked={values.pinned} onChange={this.handleInputChange} />
                      Закреплен

                Цвет

                <select name="color_id" onChange={this.handleInputChange} value={values.color_id}>
                  <option />
                  {colors.map(color =>
                    <option key={color.id} value={color.id}>{color.title}</option>
                  )}
                </select>

              {stores.map((store, _) =>
                <>

                        Доступные размеры для {store.title}

                        {sizes.sort((a, b) => a.weight - b.weight).map((size, _) =>

 s.size_id == size.id && s.store_id == store.id && !s._destroy)})} onClick={() => this.handleSizesChange(store.id, size.id)}>{size.size}

                        )}

                  {values.availabilities_attributes && values.availabilities_attributes.filter(s => s.store_id == store.id )&&
                    values.availabilities_attributes.filter(s => s.store_id == store.id && !s._destroy).sort((a, b) => a.weight - b.weight).map((size, key) =>

                          Количество размера "{sizes.find(s => s.id == size.size_id).size}"

                          <input type="text" value={size.quantity} name={`size[${size.size_id}]`} onChange={this.handleQuantityChange(store.id, size.size_id)} />

                    )
                  }
                </>
              )}

                Стоимость

                <input type="text" value={values.price} name="price" onChange={this.handleInputChange} />

                Стоимость со скидкой

                <input type="text" value={values.price_last} name="price_last" onChange={this.handleInputChange} />

                Описание

                <textarea name="desc" value={values.desc} rows="2" onChange={this.handleInputChange} />

                Как ухаживать

                <textarea name="comp" value={values.comp} rows="2" onChange={this.handleInputChange} />

                    Статус

                      <input type="radio" name="state" checked={'active' == values.state} value="active" onChange={this.handleInputChange} />
                      Активный

                      <input type="radio" name="state" checked={'archived' == values.state} value="archived" onChange={this.handleInputChange} />
                      Архив

                Изображения

              <Images images={values.images} onImagesChange={this.handleImagesChange}/>

              <input className={buttons.main} type="submit" value="Сохранить" disabled={!this.canSubmit()} />

          </form>

    )
  }

  _loadAsyncData(id) {
    this._asyncRequest = axios.CancelToken.source();

    axios.get(path('edit_variant_path', {id: id, format: 'json' }), { authenticity_token: document.querySelector('[name="csrf-token"]').content})
      .then(res => {
        this.setState({
          variant: res.data.variant,
          values: {
            color_id: res.data.variant.color_id,
            state: res.data.variant.state,
            created_at: res.data.variant.created_at,
            latest: res.data.variant.latest,
            sale: res.data.variant.sale,
            pinned: res.data.variant.pinned,
            desc: res.data.variant.desc,
            comp: res.data.variant.comp,
            price: res.data.variant.price,
            price_last: res.data.variant.price_last,
            product_id: res.data.variant.product_attributes.id,
            product_attributes: res.data.variant.product_attributes,
            availabilities_attributes: res.data.variant.availabilities_attributes,
            images: res.data.variant.images,
            image_ids: res.data.variant.images.map(i => i.id)
          }
        });

        this._asyncRequest = null;
      });
  }

  handleInputChange = event => {
    const target = event.target
    const value = target.type === 'checkbox' ? target.checked : target.value
    const name = target.name

    this.setState(state => ({
      values: { ...state.values, [name]: value }
    }))
  }

  handleQuantityChange = (store, size) => (event) => {
    const target = event.target
    const value = target.value

    let key = Object.keys(this.state.values.availabilities_attributes).find(s => this.state.values.availabilities_attributes[s].size_id == size && this.state.values.availabilities_attributes[s].store_id == store);

    const availabilities = update(this.state.values.availabilities_attributes, {
      [key]: {
        quantity: {
          $set: value
        }
      }
    });

    this.setState(state => ({
      values: { ...state.values,
        availabilities_attributes: availabilities
      }
    }))
  }

  handleSubmit = event => {
    if (this.props.id) {
      this._handleUpdate()
    }
    else {
      this._handleCreate()
    }
    event.preventDefault()
  }

  canSubmit = () => {
    return (
      this.state.values.product_attributes &&
      this.state.values.color_id &&
      this.state.values.price &&
      this.state.values.state
    );
  }

  _handleUpdate = async () => {
    const res = await axios.patch(
      path('variant_path', { id: this.props.id }),
      { variant: this.state.values, authenticity_token: document.querySelector('[name="csrf-token"]').content }
    )

    if(res.headers.location) window.location = res.headers.location
  }

  _handleCreate = async () => {
    const res = await axios.post(
      path('variants_path'),
      { variant: this.state.values, authenticity_token: document.querySelector('[name="csrf-token"]').content }
    )

    if(res.headers.location) window.location = res.headers.location
  }

  handleProductChange = (product) => {
    this.setState(state => ({
      values: { ...state.values,
        product_attributes: product,
        product_id: product.id,
      }
    }))
  }

  handleSizesChange = (store, size) => {
    if (this.state.values.availabilities_attributes.find(s => s.size_id == size && s.store_id == store)) {
      let key = Object.keys(this.state.values.availabilities_attributes).find(s => this.state.values.availabilities_attributes[s].size_id == size && this.state.values.availabilities_attributes[s].store_id == store);

      const availabilities = update(this.state.values.availabilities_attributes, {
        [key]: {
          _destroy: {
            $set: !this.state.values.availabilities_attributes[key]._destroy
          }
        }
      });

      this.setState(state => ({
        values: { ...state.values,
          availabilities_attributes: availabilities
        }
      }))
    }
    else {
      this.setState(state => ({
        values: { ...state.values,
          availabilities_attributes: [ ...state.values.availabilities_attributes, {
            store_id: store,
            size_id: size,
            variant_id: this.props.id,
            quantity: 0,
            weight: this.state.dictionaries.sizes.find(s => s.id == size).weight,
            _destroy: false
          }]
        }
      }))
    }
  }

  handleImagesChange = (images) => {
    this.setState(state => ({
      values: { ...state.values,
        image_ids: images.map(i => i.id),
        images_attributes: images.map((i, index) => ({id:i.id, weight: index+1}))
      }
    }))
  }
}

export default Form
<|endoftext|>