<commit_before>'use strict';

import { h, Component } from 'preact';
import { bind } from 'decko';
import Pikadayer from './Pikadayer.js';
import classNames from 'classnames';

const defaultPeriod = 'last-7-days';
const padZero = function(n){return n<10? '0'+n:''+n;}

let now = new Date();
window.setInterval(() => {
  now = new Date();
}, 60000 );

const availablePeriods = {
  '1d': {
    label: '1d',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
    end: function() {
      return this.start();
    },
  },
  '1w': {
    label: '1w',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-6);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
 },
 '4w': {
    label: '4w',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-4*7+1);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
 },
 'mtd': {
    label: 'Mtd',
    start: function() {
      return new Date(now.getFullYear(),  now.getMonth(), 1);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth()+1, 0);
    },
 },
'qtd': {
  label: 'Qtd',
  start: function() {
    let qs = Math.ceil((now.getMonth()+1) / 3) * 3 - 3;
    return new Date(now.getFullYear(), qs, 1);

  },
  end: function() {
    let start = this.start();
    return new Date(start.getFullYear(), start.getMonth() + 3, 0);
  },
 },
 'ytd': {
  label: 'Ytd',
  start: function() {
    return new Date(now.getFullYear(), 0, 1);
  },
  end: function() {
    return new Date(now.getFullYear()+1, 0, 0);
  },
 },
 'all': {
  label: 'All',
  start: function() {
    return new Date(2018, 6, 1);
  },
  end: function() {
    return new Date();
  },
 }
}

class DatePicker extends Component {
  constructor(props) {
    super(props)

    this.state = {
      period: window.location.hash.substring(2) || window.localStorage.getItem('period') || defaultPeriod,
      startDate: now,
      endDate: now,
      groupBy: 'day',
    }
    this.updateDatesFromPeriod(this.state.period)
  }

  componentDidMount() {
    window.addEventListener('keydown', this.handleKeyPress);
  }

  componentWillUnmount() {
    window.removeEventListener('keydown', this.handleKeyPress)
  }

  @bind
  updateDatesFromPeriod(period) {
    if(typeof(availablePeriods[period]) !== "object") {
      period = "1w";
    }
    let p = availablePeriods[period];
    this.setDateRange(p.start(), p.end(), period);
  }

  @bind
  setDateRange(start, end, period) {
    // don't update state if start > end. user may be busy picking dates.
    if(start > end) {
      return;
    }

    // include start & end day by forcing time
    start.setHours(0, 0, 0);
    end.setHours(23, 59, 59);

    let diff =  Math.round((end - start) / 1000 / 60 / 60 / 24)
    let groupBy = 'day';
    if(diff >= 31) {
      groupBy = 'month';
    } else if( diff < 2) {
      groupBy = 'hour';
    }

    this.setState({
      period: period || '',
      startDate: start,
      endDate: end,
      diff: diff,
      groupBy: groupBy,
    });

    // use slight delay for updating rest of application to allow this function to be called again
    if(!this.timeout) {
      this.timeout = window.setTimeout(() => {
        this.props.onChange(this.state);
        this.timeout = null;

        window.localStorage.setItem('period', this.state.period)
        window.history.replaceState(this.state, null, `#!${this.state.period}`)
      }, 5)
    }
  }

  @bind
  setPeriod(e) {
    e.preventDefault();

    let newPeriod = e.target.getAttribute('data-value');
    if( newPeriod === this.state.period) {
      return;
    }

    this.updateDatesFromPeriod(newPeriod);
  }

  dateValue(date) {
    return date.getFullYear() + '-' + padZero(date.getMonth() + 1) + '-' + padZero(date.getDate());
  }

  @bind
  setStartDate(date) {
    this.setDateRange(date, this.state.endDate, '')
  }

  @bind
  setEndDate(date) {
    this.setDateRange(this.state.startDate, date, '')
  }

  @bind
  handleKeyPress(evt) {
    // Don't handle input when the user is in a text field or text area.
    let tag = evt.target.tagName;
    if(tag === "INPUT" || tag === "TEXTAREA") {
      return;
    }

    // TODO: Account for leap years
    let diff = this.state.endDate - this.state.startDate + 1000;
    let newStartDate, newEndDate;

    switch(evt.which) {
      // left-arrow
      case 37:
        newStartDate = new Date(+this.state.startDate - diff)
        newEndDate = new Date(+this.state.endDate - diff)
        this.setDateRange(newStartDate, newEndDate)
      break;

      //right-arrow
      case 39:
      newStartDate = new Date(+this.state.startDate + diff)
      newEndDate = new Date(+this.state.endDate + diff)
      this.setDateRange(newStartDate, newEndDate)
      break;
    }
  }

  @bind
  setGroupBy(e) {
    this.setState({
      groupBy: e.target.getAttribute('data-value')
    })
    this.props.onChange(this.state);
  }

  render(props, state) {
    const presets = Object.keys(availablePeriods).map((id) => {
      let p = availablePeriods[id];
      return (

          {p.label}

      );
    });

    return (

          {presets}

<Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} /> › <Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate}  />

         {state.diff < 31 ? (
Hourly
) : ''}

Daily

         {state.diff >= 31 ? (
Monthly
) : ''}

    )

    /*

Daily

Monthly

    */

  }
}

export default DatePicker
<commit_msg>set default period correctly<commit_after>'use strict';

import { h, Component } from 'preact';
import { bind } from 'decko';
import Pikadayer from './Pikadayer.js';
import classNames from 'classnames';

const padZero = (n) => n < 10 ? '0'+n : ''+n;

let now = new Date();
window.setInterval(() => {
  now = new Date();
}, 60000 );

const availablePeriods = {
  '1d': {
    label: '1d',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
    end: function() {
      return this.start();
    },
  },
  '1w': {
    label: '1w',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-6);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
 },
 '4w': {
    label: '4w',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-4*7+1);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
 },
 'mtd': {
    label: 'Mtd',
    start: function() {
      return new Date(now.getFullYear(),  now.getMonth(), 1);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth()+1, 0);
    },
 },
'qtd': {
  label: 'Qtd',
  start: function() {
    let qs = Math.ceil((now.getMonth()+1) / 3) * 3 - 3;
    return new Date(now.getFullYear(), qs, 1);

  },
  end: function() {
    let start = this.start();
    return new Date(start.getFullYear(), start.getMonth() + 3, 0);
  },
 },
 'ytd': {
  label: 'Ytd',
  start: function() {
    return new Date(now.getFullYear(), 0, 1);
  },
  end: function() {
    return new Date(now.getFullYear()+1, 0, 0);
  },
 },
 'all': {
  label: 'All',
  start: function() {
    return new Date(2018, 6, 1);
  },
  end: function() {
    return new Date();
  },
 }
}

class DatePicker extends Component {
  constructor(props) {
    super(props)

    this.state = {
      period: window.location.hash.substring(2) || window.localStorage.getItem('period') || '1w',
      startDate: now,
      endDate: now,
      groupBy: 'day',
    }
    this.updateDatesFromPeriod(this.state.period)
  }

  componentDidMount() {
    window.addEventListener('keydown', this.handleKeyPress);
  }

  componentWillUnmount() {
    window.removeEventListener('keydown', this.handleKeyPress)
  }

  @bind
  updateDatesFromPeriod(period) {
    if(typeof(availablePeriods[period]) !== "object") {
      period = "1w";
    }
    let p = availablePeriods[period];
    this.setDateRange(p.start(), p.end(), period);
  }

  @bind
  setDateRange(start, end, period) {
    // don't update state if start > end. user may be busy picking dates.
    if(start > end) {
      return;
    }

    // include start & end day by forcing time
    start.setHours(0, 0, 0);
    end.setHours(23, 59, 59);

    let diff =  Math.round((end - start) / 1000 / 60 / 60 / 24)
    let groupBy = 'day';
    if(diff >= 31) {
      groupBy = 'month';
    } else if( diff < 2) {
      groupBy = 'hour';
    }

    this.setState({
      period: period || '',
      startDate: start,
      endDate: end,
      diff: diff,
      groupBy: groupBy,
    });

    // use slight delay for updating rest of application to allow this function to be called again
    if(!this.timeout) {
      this.timeout = window.setTimeout(() => {
        this.props.onChange(this.state);
        this.timeout = null;

        window.localStorage.setItem('period', this.state.period)
        window.history.replaceState(this.state, null, `#!${this.state.period}`)
      }, 5)
    }
  }

  @bind
  setPeriod(e) {
    e.preventDefault();

    let newPeriod = e.target.getAttribute('data-value');
    if( newPeriod === this.state.period) {
      return;
    }

    this.updateDatesFromPeriod(newPeriod);
  }

  dateValue(date) {
    return date.getFullYear() + '-' + padZero(date.getMonth() + 1) + '-' + padZero(date.getDate());
  }

  @bind
  setStartDate(date) {
    this.setDateRange(date, this.state.endDate, '')
  }

  @bind
  setEndDate(date) {
    this.setDateRange(this.state.startDate, date, '')
  }

  @bind
  handleKeyPress(evt) {
    // Don't handle input when the user is in a text field or text area.
    let tag = evt.target.tagName;
    if(tag === "INPUT" || tag === "TEXTAREA") {
      return;
    }

    // TODO: Account for leap years
    let diff = this.state.endDate - this.state.startDate + 1000;
    let newStartDate, newEndDate;

    switch(evt.which) {
      // left-arrow
      case 37:
        newStartDate = new Date(+this.state.startDate - diff)
        newEndDate = new Date(+this.state.endDate - diff)
        this.setDateRange(newStartDate, newEndDate)
      break;

      //right-arrow
      case 39:
      newStartDate = new Date(+this.state.startDate + diff)
      newEndDate = new Date(+this.state.endDate + diff)
      this.setDateRange(newStartDate, newEndDate)
      break;
    }
  }

  @bind
  setGroupBy(e) {
    this.setState({
      groupBy: e.target.getAttribute('data-value')
    })
    this.props.onChange(this.state);
  }

  render(props, state) {
    const presets = Object.keys(availablePeriods).map((id) => {
      let p = availablePeriods[id];
      return (

          {p.label}

      );
    });

    return (

          {presets}

<Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} /> › <Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate}  />

         {state.diff < 31 ? (
Hourly
) : ''}

Daily

         {state.diff >= 31 ? (
Monthly
) : ''}

    )

    /*

Daily

Monthly

    */

  }
}

export default DatePicker
<|endoftext|>


<commit_before>import { connect } from 'react-redux';
import PositionFrom from './presenter';
import { changePositionFrom } from '../../modules/sequence';

const mapStateToProps = (state) => {
  const sequence = state.sequence;

  return {
    value: sequence.positionFrom,
  };
};

const mapDispatchToProps = (dispatch) => ({
  onChange: (event) => {
    let position = parseInt(event.target.value, 10);

    if (isNaN(position) || position < 1) {
      position = 1;
    }

    dispatch(changePositionFrom(position));
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(PositionFrom);
<commit_msg>Fix lint warning<commit_after>import { connect } from 'react-redux';
import PositionFrom from './presenter';
import { changePositionFrom } from '../../modules/sequence';

const mapStateToProps = (state) => {
  const sequence = state.sequence;

  return {
    value: sequence.positionFrom,
  };
};

const mapDispatchToProps = (dispatch) => ({
  onChange: (event) => {
    let position = parseInt(event.target.value, 10);

    if (isNaN(position) || 1 > position) {
      position = 1;
    }

    dispatch(changePositionFrom(position));
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(PositionFrom);
<|endoftext|>


<commit_before>/*global mockForms2, mockInstances*/

/**
 * [FormDataController description]
 * @param {{instanceId: string, entityId: string}} params [description]
 * @constructor
 */
function FormDataController(params){
	params = params || {};
	var originalInstanceId = params.instanceId || null;
	var formDataRepository = new enketo.FormDataRepository();
    var controller = new enketo.FormDataController(
        new enketo.EntityRelationshipLoader(),
        new enketo.FormDefinitionLoader(),
        new enketo.FormModelMapper(formDataRepository, new enketo.SQLQueryBuilder(formDataRepository), new enketo.IdFactory(new enketo.IdFactoryBridge())),
        formDataRepository);

    /**
	 * Gets instance as JSON from Dristhi DB - Should this be asynchronous?
	 * @return {?*} Form Data JSON object
	 */
    this.get = function(){
		return controller.get(params) || null;
	};

	/**
	 * Passes instance as JSON to store in Dristhi DB - Should this be asynchronous?
	 * @param  {string} instanceId	the new instanceID of the record
	 * @param  {*}		data		Form Data JSON object
	 * @return {boolean}
	 */
	this.save = function(data, status){
		controller.save(data, status);
	};

	this.remove = function(instanceId){

	};
}

/**
 * Class maintaining a Drishti JSON Data Definition and deal with JSON <-> XML transformation
 * @param {FormDataJSON} data  Drishti Data Definition JSON
 * @constructor
 */
function JData(data){

	if (!data){
		recordError('No instance query parameter provided!');
	}

	var unboundSubformInstanceProps = ['id'];

	/**
	 * Transforms JSON to an XML string
	 * NOTE: alternatively, we could could overwrite Form.init() to use JSON data instead of XML for instantiation
	 * @return {?string}			XML string
	 */
	this.toXML = function(){
		var i, j, k, field, path, value, subForm, repeatInstance, defaultPath, repeatNodeName,
			$instance = $($.parseXML('<root />'));

		if (typeof data !== 'object'){
			recordError("error: no JSON object provided during instantiation");
		}
		//main form:
		for (i = 0; i<data.form.fields.length; i++){
			defaultPath = defaultPathFixed(data.form.default_bind_path);
			field = data.form.fields[i];
			//we only have to concern ourselves with fields that have a value (incl empty string)
			if (typeof field.value !== 'undefined'){
				path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
				value = field.value;
				addXMLNodeAndValue($instance, path, value);
				//console.log('added path: '+path+' with value: "'+value+'"');
			}
		}
		//repeats:
		if (data.form.sub_forms){
			for (i = 0; i<data.form.sub_forms.length; i++){
				subForm = data.form.sub_forms[i];
				defaultPath = defaultPathFixed(subForm.default_bind_path);
				repeatNodeName = defaultPath.match(/.*\/([^\/]*)\/$/)[1];
				if (!subForm.bind_type){
					recordError('Repeat (subform) is missing bind_type.');
				}
				else{
					for (j = 0; j < subForm.instances.length; j++){
						repeatInstance = subForm.instances[j];
						for (k = 0; k < subForm.fields.length; k++){
							field = subForm.fields[k];
							if (typeof repeatInstance[field.name] !== 'undefined' && unboundSubformInstanceProps.indexOf(field.name) == -1){
								path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
								value = repeatInstance[field.name];
								//note: also if the value is empty it is added!
								addXMLNodeAndValue($instance, path, value, {name: repeatNodeName, index: j});
								console.log('added path: '+path+' with value: "'+value+'", repeat NodeName: '+repeatNodeName+' and repeat index: '+j);
							}
						}
					}
				}
			}
		}
		return (new XMLSerializer()).serializeToString($instance.find('instance>*:first')[0]);
	};

	/**
	 * Gets current state of form instance in JSON format.
	 */
	this.get = function(){
		var $repeatLeaves, origInstances,
			subFormsStarted = [],
			$mainLeaves = this.getInstanceXML(false).find('*').filter(function(){
				return $(this).children().length === 0;
			}),
			/*
			 * issue: this relies on a template node to be present, which is not required for repeats in OpenRosa
			 * but thankfully is guaranteed in formhub-hosted forms.
			 * Nested repeats are not supported!
			 */
			//$repeats = form.getDataO().$.find('instance:first [template]').siblings().filter(function(){
			$repeats = this.getInstanceXML(true).find('[template]').siblings().filter(function(){
				var nodeName = $(this).prop('nodeName');
				return $(this).siblings(nodeName+'[template]').length > 0;
			});

		//this code has to be refactored, as the unboundSubformInstanceProps stuff was bolted on when the JSON spec changed.
		$repeats.each(function(){
			var i, prop, bindPath, subForm, instanceIndex,
				subForms = [],
				instance = {},
				$repeat = $(this);
			//identifier for subform in JSON format is default_bind_path
			bindPath = '/model/instance'+$repeat.getXPath();
			if (data.form.sub_forms) {
				subForms = $.grep(data.form.sub_forms, function(subfrm){
					//be flexible with trailing slash
					return (subfrm.default_bind_path === bindPath || subfrm.default_bind_path === bindPath+'/');
				});
			}
			if (subForms.length === 0){
				recordError('Repeat definition not found (no subform with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else if (subForms.length > 1){
				recordError('Multiple repeat definititions found (multiple subforms with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else{
				subForm = subForms[0];
				if ($.inArray(bindPath, subFormsStarted) === -1){
					subFormsStarted.push(bindPath);
					origInstances = jQuery.extend(true, {}, subForm.instances);
					subForm.instances = [];
				}
				$repeatLeaves = $repeat.find('*').filter(function(){
					return $(this).children().length === 0;
				});
				$mainLeaves = $mainLeaves.takeOut($repeatLeaves);
				$repeatLeaves.each(function(){
					var props = getNodeProps($(this)),
						field = getField(props.path, subForm.fields, subForm.default_bind_path);
					if (field){
						//adding value even if it's empty without checking what it was previously
						instance[field.name] = props.value;
					}
				});
				subForm.instances.push(instance);
				//add subform instance properties that have no equivalent in XML - a quick and dirty addition
				instanceIndex = subForm.instances.indexOf(instance);
				for (i=0; i<unboundSubformInstanceProps.length; i++){
					prop = unboundSubformInstanceProps[i];
					if (origInstances[instanceIndex] && typeof origInstances[instanceIndex][prop] !== 'undefined'){
						//console.log('adding unbound property '+prop+'to repeat instance with index '+instanceIndex);
						instance[prop] = origInstances[instanceIndex][prop];
					}
				}
			}
		});

		$mainLeaves.each(function(){
			var props = getNodeProps($(this)),
				field = getField(props.path, data.form.fields, data.form.default_bind_path);
			if (field && (field.value || props.value.length > 0) ){
				field.value = props.value;
			}
		});
		return data;
	};

	/**
	 * gets XML from form wrapped as a JQuery XML object
	 * @param  {boolean} includeTemplates [description]
	 * @return {jQuery}                  [description]
	 */
	this.getInstanceXML = function(includeTemplates){
		return $($.parseXML(form.getDataStr(includeTemplates)));
	};

	/**
	 * given an XML path, obtains a field from a field array
	 * @param  {string} path
	 * @param  {Array.<{name:string, source: string, value: string, bind: string}>} fieldArr    array of field objects
	 * @param  {string} defaultPath																default bind path of form or subform
	 * @return {?{name:string, source: string, value: string, bind: string}}					field object
	 */
	function getField(path, fieldArr, defaultPath){
		var fields = $.grep(fieldArr, function(field){
			return (typeof field.bind  === 'undefined' && (defaultPath + field.name) === path) ||
				(typeof field.bind !== 'undefined' && field.bind === path);
		});

		if (fields.length > 1){
			recordError('Multiple fields found (multiple nodes with path: '+path+' found in JSON format.');
			return null;
		}
		/*else if (fields.length === 0 && path.indexOf('/meta/deprecatedID') !== -1){
			return null;
		}*/
		else if (fields.length === 0){
			recordError('Field not found (node with path: '+path+' was missing from JSON format).');
			return null;
		}
		else {
			console.debug('found field with path '+path+' in JSON Form Data');
			return fields[0];
		}
	}

	function getNodeProps($node){
		var partialPath = $node.getXPath('model'),
			fullPath = (partialPath.indexOf('/model/instance') === -1) ? '/model/instance'+partialPath : partialPath;
		return {
			nodeName: $node.prop('nodeName'),
			value: $node.text(),
			path: fullPath
		};
	}

	/**
	 * [addXMLNode description]
	 * @param {jQuery} $doc								jQuery doc with root element to add nodes to
	 * @param {string} path								path of node to be added when not present starting with /
	 * @param {string} value							value of node
	 * @param {{name: string, index: number}=} repeatO	repeatObject with repeat nodeName and 0-based index of repeat parent of node to be added
	 * @return {jQuery}									jQuery doc with added node and value
	 */
	function addXMLNodeAndValue ($doc, path, value, repeatO){
		var j, $node,
			$current = $doc.find('root'),
			nodeNames = path.substring(1).split('/'),
			r = repeatO || {};

		for (j = 0; j<nodeNames.length ; j++){
			if ($current.children(nodeNames[j]).length === 0 ||
				(r.name && r.index && nodeNames[j] === r.name && $current.children(nodeNames[j]).eq(r.index).length === 0)){
				//console.log('nodeName does not exist, going to create it as child of ', $current[0]);
				$node = $($.parseXML('<'+nodeNames[j]+'/>').documentElement).clone();
				$current.append($node);
				$current = $node;
			}
			else{
				$current = ( r.index && nodeNames[j] === r.name ) ? $current.children(nodeNames[j]).eq(r.index) : $current.children(nodeNames[j]);
			}

			if (j === (nodeNames.length - 1)){
				$current.text(value);
			}
		}
		return $doc;
	}

	//if the default path does not have a trailing slash, add one
	function defaultPathFixed (path){
        if(!path) return path;
 		return (path.lastIndexOf('/') !== path.length -1) ? path + '/' : path;
	}

	function recordError(errorMsg){
		if (typeof data.errors == 'undefined') data.errors = [];
		data.errors.push(errorMsg);
		console.error(errorMsg);
	}
}

/**
 * Plugin that removes from a collection the nodes in the parameter that have the same Path
 */
(function($){
	$.fn.takeOut = function($nodes){
		return this.filter(function(){
			for (var i = 0 ; i<$nodes.length ; i++){
				if ($(this).getXPath() === $nodes.eq(i).getXPath()){
					//console.log('took out node: ', $(this));
					return false;
				}
			}
			return true;
		});
	};
})(jQuery);<commit_msg>Caught the unhandled exception<commit_after>/*global mockForms2, mockInstances*/

/**
 * [FormDataController description]
 * @param {{instanceId: string, entityId: string}} params [description]
 * @constructor
 */
function FormDataController(params){
	params = params || {};
	var originalInstanceId = params.instanceId || null;
	var formDataRepository = new enketo.FormDataRepository();
    var controller = new enketo.FormDataController(
        new enketo.EntityRelationshipLoader(),
        new enketo.FormDefinitionLoader(),
        new enketo.FormModelMapper(formDataRepository, new enketo.SQLQueryBuilder(formDataRepository), new enketo.IdFactory(new enketo.IdFactoryBridge())),
        formDataRepository);

    /**
	 * Gets instance as JSON from Dristhi DB - Should this be asynchronous?
	 * @return {?*} Form Data JSON object
	 */
    this.get = function(){
		return controller.get(params) || null;
	};

	/**
	 * Passes instance as JSON to store in Dristhi DB - Should this be asynchronous?
	 * @param  {string} instanceId	the new instanceID of the record
	 * @param  {*}		data		Form Data JSON object
	 * @return {boolean}
	 */
	this.save = function(data, status){
		controller.save(data, status);
	};

	this.remove = function(instanceId){

	};
}

/**
 * Class maintaining a Drishti JSON Data Definition and deal with JSON <-> XML transformation
 * @param {FormDataJSON} data  Drishti Data Definition JSON
 * @constructor
 */
function JData(data){

	if (!data){
		recordError('No instance query parameter provided!');
	}

	var unboundSubformInstanceProps = ['id'];

	/**
	 * Transforms JSON to an XML string
	 * NOTE: alternatively, we could could overwrite Form.init() to use JSON data instead of XML for instantiation
	 * @return {?string}			XML string
	 */
	this.toXML = function(){
		var i, j, k, field, path, value, subForm, repeatInstance, defaultPath, repeatNodeName,
			$instance = $($.parseXML('<root />'));

		if (typeof data !== 'object'){
			recordError("error: no JSON object provided during instantiation");
		}
		//main form:
		for (i = 0; i<data.form.fields.length; i++){
			defaultPath = defaultPathFixed(data.form.default_bind_path);
			field = data.form.fields[i];
			//we only have to concern ourselves with fields that have a value (incl empty string)
			if (typeof field.value !== 'undefined'){
				path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
				value = field.value;
				addXMLNodeAndValue($instance, path, value);
				//console.log('added path: '+path+' with value: "'+value+'"');
			}
		}
		//repeats:
		if (data.form.sub_forms){
			for (i = 0; i<data.form.sub_forms.length; i++){
				subForm = data.form.sub_forms[i];
				defaultPath = defaultPathFixed(subForm.default_bind_path);
				repeatNodeName = defaultPath.match(/.*\/([^\/]*)\/$/)[1];
				if (!subForm.bind_type){
					recordError('Repeat (subform) is missing bind_type.');
				}
				else{
					for (j = 0; j < subForm.instances.length; j++){
						repeatInstance = subForm.instances[j];
						for (k = 0; k < subForm.fields.length; k++){
							field = subForm.fields[k];
							if (typeof repeatInstance[field.name] !== 'undefined' && unboundSubformInstanceProps.indexOf(field.name) == -1){
								path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
								value = repeatInstance[field.name];
								//note: also if the value is empty it is added!
								addXMLNodeAndValue($instance, path, value, {name: repeatNodeName, index: j});
								console.log('added path: '+path+' with value: "'+value+'", repeat NodeName: '+repeatNodeName+' and repeat index: '+j);
							}
						}
					}
				}
			}
		}
        try {
            var elementLookUp = $instance.find('instance>*:first')[0];
            var serializeToString = (new XMLSerializer()).serializeToString(elementLookUp);
        } catch (err) {
            console.log(err);
        }
        return  serializeToString;
	};

	/**
	 * Gets current state of form instance in JSON format.
	 */
	this.get = function(){
		var $repeatLeaves, origInstances,
			subFormsStarted = [],
			$mainLeaves = this.getInstanceXML(false).find('*').filter(function(){
				return $(this).children().length === 0;
			}),
			/*
			 * issue: this relies on a template node to be present, which is not required for repeats in OpenRosa
			 * but thankfully is guaranteed in formhub-hosted forms.
			 * Nested repeats are not supported!
			 */
			//$repeats = form.getDataO().$.find('instance:first [template]').siblings().filter(function(){
			$repeats = this.getInstanceXML(true).find('[template]').siblings().filter(function(){
				var nodeName = $(this).prop('nodeName');
				return $(this).siblings(nodeName+'[template]').length > 0;
			});

		//this code has to be refactored, as the unboundSubformInstanceProps stuff was bolted on when the JSON spec changed.
		$repeats.each(function(){
			var i, prop, bindPath, subForm, instanceIndex,
				subForms = [],
				instance = {},
				$repeat = $(this);
			//identifier for subform in JSON format is default_bind_path
			bindPath = '/model/instance'+$repeat.getXPath();
			if (data.form.sub_forms) {
				subForms = $.grep(data.form.sub_forms, function(subfrm){
					//be flexible with trailing slash
					return (subfrm.default_bind_path === bindPath || subfrm.default_bind_path === bindPath+'/');
				});
			}
			if (subForms.length === 0){
				recordError('Repeat definition not found (no subform with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else if (subForms.length > 1){
				recordError('Multiple repeat definititions found (multiple subforms with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else{
				subForm = subForms[0];
				if ($.inArray(bindPath, subFormsStarted) === -1){
					subFormsStarted.push(bindPath);
					origInstances = jQuery.extend(true, {}, subForm.instances);
					subForm.instances = [];
				}
				$repeatLeaves = $repeat.find('*').filter(function(){
					return $(this).children().length === 0;
				});
				$mainLeaves = $mainLeaves.takeOut($repeatLeaves);
				$repeatLeaves.each(function(){
					var props = getNodeProps($(this)),
						field = getField(props.path, subForm.fields, subForm.default_bind_path);
					if (field){
						//adding value even if it's empty without checking what it was previously
						instance[field.name] = props.value;
					}
				});
				subForm.instances.push(instance);
				//add subform instance properties that have no equivalent in XML - a quick and dirty addition
				instanceIndex = subForm.instances.indexOf(instance);
				for (i=0; i<unboundSubformInstanceProps.length; i++){
					prop = unboundSubformInstanceProps[i];
					if (origInstances[instanceIndex] && typeof origInstances[instanceIndex][prop] !== 'undefined'){
						//console.log('adding unbound property '+prop+'to repeat instance with index '+instanceIndex);
						instance[prop] = origInstances[instanceIndex][prop];
					}
				}
			}
		});

		$mainLeaves.each(function(){
			var props = getNodeProps($(this)),
				field = getField(props.path, data.form.fields, data.form.default_bind_path);
			if (field && (field.value || props.value.length > 0) ){
				field.value = props.value;
			}
		});
		return data;
	};

	/**
	 * gets XML from form wrapped as a JQuery XML object
	 * @param  {boolean} includeTemplates [description]
	 * @return {jQuery}                  [description]
	 */
	this.getInstanceXML = function(includeTemplates){
		return $($.parseXML(form.getDataStr(includeTemplates)));
	};

	/**
	 * given an XML path, obtains a field from a field array
	 * @param  {string} path
	 * @param  {Array.<{name:string, source: string, value: string, bind: string}>} fieldArr    array of field objects
	 * @param  {string} defaultPath																default bind path of form or subform
	 * @return {?{name:string, source: string, value: string, bind: string}}					field object
	 */
	function getField(path, fieldArr, defaultPath){
		var fields = $.grep(fieldArr, function(field){
			return (typeof field.bind  === 'undefined' && (defaultPath + field.name) === path) ||
				(typeof field.bind !== 'undefined' && field.bind === path);
		});

		if (fields.length > 1){
			recordError('Multiple fields found (multiple nodes with path: '+path+' found in JSON format.');
			return null;
		}
		/*else if (fields.length === 0 && path.indexOf('/meta/deprecatedID') !== -1){
			return null;
		}*/
		else if (fields.length === 0){
			recordError('Field not found (node with path: '+path+' was missing from JSON format).');
			return null;
		}
		else {
			console.debug('found field with path '+path+' in JSON Form Data');
			return fields[0];
		}
	}

	function getNodeProps($node){
		var partialPath = $node.getXPath('model'),
			fullPath = (partialPath.indexOf('/model/instance') === -1) ? '/model/instance'+partialPath : partialPath;
		return {
			nodeName: $node.prop('nodeName'),
			value: $node.text(),
			path: fullPath
		};
	}

	/**
	 * [addXMLNode description]
	 * @param {jQuery} $doc								jQuery doc with root element to add nodes to
	 * @param {string} path								path of node to be added when not present starting with /
	 * @param {string} value							value of node
	 * @param {{name: string, index: number}=} repeatO	repeatObject with repeat nodeName and 0-based index of repeat parent of node to be added
	 * @return {jQuery}									jQuery doc with added node and value
	 */
	function addXMLNodeAndValue ($doc, path, value, repeatO){
		var j, $node,
			$current = $doc.find('root'),
			nodeNames = path.substring(1).split('/'),
			r = repeatO || {};

		for (j = 0; j<nodeNames.length ; j++){
			if ($current.children(nodeNames[j]).length === 0 ||
				(r.name && r.index && nodeNames[j] === r.name && $current.children(nodeNames[j]).eq(r.index).length === 0)){
				//console.log('nodeName does not exist, going to create it as child of ', $current[0]);
				$node = $($.parseXML('<'+nodeNames[j]+'/>').documentElement).clone();
				$current.append($node);
				$current = $node;
			}
			else{
				$current = ( r.index && nodeNames[j] === r.name ) ? $current.children(nodeNames[j]).eq(r.index) : $current.children(nodeNames[j]);
			}

			if (j === (nodeNames.length - 1)){
				$current.text(value);
			}
		}
		return $doc;
	}

	//if the default path does not have a trailing slash, add one
	function defaultPathFixed (path){
        if(!path) return path;
 		return (path.lastIndexOf('/') !== path.length -1) ? path + '/' : path;
	}

	function recordError(errorMsg){
		if (typeof data.errors == 'undefined') data.errors = [];
		data.errors.push(errorMsg);
		console.error(errorMsg);
	}
}

/**
 * Plugin that removes from a collection the nodes in the parameter that have the same Path
 */
(function($){
	$.fn.takeOut = function($nodes){
		return this.filter(function(){
			for (var i = 0 ; i<$nodes.length ; i++){
				if ($(this).getXPath() === $nodes.eq(i).getXPath()){
					//console.log('took out node: ', $(this));
					return false;
				}
			}
			return true;
		});
	};
})(jQuery);<|endoftext|>


<commit_before>import Ember from 'ember';

import ClickOutsideComponent from '../mixins/click-outside-component';
import Filterable from '../mixins/filterable';

export default Ember.Component.extend(Filterable, ClickOutsideComponent, {

    /** Bind conditional classes. */
    classNameBindings: ['hasSelected:has-success', 'isInvalid:has-error', '!removeCreate:input-group'],

    /** Setup component css classes. */
    classNames: [
        'autocomplete-input', 'dropdown-group'
    ],

    /** Update when item being updated from outside. */
    didUpdateAttrs() {
        this._super(...arguments);
        const item = this.get('item');

        if (item && item !== this.get('hasSelected')) {
            this.set('hasSelected', item);
        }
    },

    /** Enable multi select.
     *
     * When multi select is enabled the textfield will not be
     * set with the selected value.
     *
     */
    multiSelect: false,

    /** Setting to disable create button. */
    disableCreate: true,

    /** Setting to hide the create button. */
    removeCreate: true,

    /** Injected services. */
    store: Ember.inject.service('store'),

    /** Name of the store to fetch data from. */
    storeName: null,

    /** Value of the input field */
    value: '',

    /** Limit number of responses. */
    limit: 40,

    /** Has valid selection. */
    hasSelected: false,

    /** Highligted index in dropdown. */
    highlightedIndex: -1,

    /** Update dropdown list when index change. */
    onHighlightedIndexChange: function () {
        if (this.get('hasFocus')) {
            let listItem = this.$('li[data-index='+ this.get('highlightedIndex') +']');

            if (listItem) {
                listItem.addClass('active').siblings().removeClass('active');
            }
        }
    }.observes('highlightedIndex', 'hasFocus'),

    /** Handle item select. */
    onSelected: function () {
        if (this.get('hasSelected')) {
            this.set(
                'value', this.get('hasSelected').get(this.get('displayField', 'name'))
            );

            this.set('hasFocus', false);
        }
    }.observes('hasSelected'),

    /** Return if input is invalid. */
    isInvalid: Ember.computed('hasSelected', 'hasFocus', 'value', function () {
        const isInvalid = (
            (this.get('value') && this.get('value').length > 0 && !this.get('multiSelect')) &&
            !this.get('hasFocus') && !this.get('hasSelected')
        );

        return isInvalid;
    }),

    /** Data to display in preview dropdown. */
    previewData: [],

    /** Does the input field have focus. */
    hasFocus: false,

    /** Field to use in autocomplete search */
    filterField: null,

    /** Timeout between keyup events. */
    keyupTimeout: 150,

    /** Return if preview dropdown is visible. */
    isDropdownVisible: Ember.computed('previewData', 'hasFocus', function () {
        const visible = (this.get('previewData').length || this.get('value.length')) && this.get('hasFocus');
        return visible;
    }),

    /** Override init. */
    init () {

        /** If using local data decrease the timeout. */
        if (this.localData) {
            this.keyupTimeout = 10;
        }

        return this._super(...arguments);
    },

    /** Fetch data stored in client store with out request.
     *
     * Using this method it's up to the application to keep the data up to date.
     *
     */
    _fetchLocalData (filterField, filterValue, limit) {
        var result = this.get('store').peekAll(this.storeName).filter((item) => {
            let fieldValue = item.get(filterField).toLowerCase();

            return fieldValue.indexOf(filterValue.toLowerCase()) !== -1;
        }).sortBy(filterField).slice(0, limit + 1);

        this.set('previewData', result);
    },

    /** Fetch data from remote server. */
    _fetchRemoteData (filterField, filterValue, limit) {
        var queryParams = {
            search: true,
            limit: limit,
            orderby: filterField
        };

        queryParams[filterField] = filterValue;

        Ember.$.extend(queryParams, this.get('filters'));

        this.get('store').query(this.storeName, queryParams).then((response) => {
            this.set('previewData', response);
        }).catch((reason) => {
            this.set('previewData', []);

            console.warn('Invalid response from server: ', reason);
        });
    },

    /**
     * Fetch data based on search value and update store.
     */
    fetchData () {
        if (this.localData) {
            this._fetchLocalData(
                this.get('filterField'), this.get('value'), this.get('limit')
            );
        } else {
            this._fetchRemoteData(
                this.get('filterField'), this.get('value'), this.get('limit')
            );
        }
    },

    /** Handle event when user clicks outside of component. */
    onOutsideClick () {
        this.set('hasFocus', false);
    },

    /**
     * Handle keyup events in input field.
     */
    _onKeyup () {
        if (!this.get('value').length) {
            this.set('previewData', []);
        } else {
            this.fetchData();
        }

        // Set item as undefined.
        if (!this.get('multiSelect')) {
            this.get('itemSelected')();
            this.set('hasSelected', false);
        }

        this.set('highlightedIndex', -1);
    },

    actions: {

        /**
        * Handle Keydown events in input field.
        *
        * Triggers private method to be able to delay execution.
        *
        */
        onKeyDown (value, event) {
            this.set('hasFocus', true);

            if ([38, 40, 13, 27, 9].indexOf(event.keyCode) !== -1) {
                let index = this.get('highlightedIndex');

                // Arrow key up.
                if (event.keyCode === 38) {
                    index -= 1;
                // Arrow key down
                } else if (event.keyCode === 40) {
                    index += 1;

                // Enter key
                } else if (event.keyCode === 13) {
                    this.send(
                        'onItemClick', this.get('previewData').objectAt(index)
                    );

                    return;

                // ESC and TAB key should remove focus.
                } else if ([27, 9].indexOf(event.keyCode) !== -1) {
                    this.set('hasFocus', false);
                }

                if (index < 0) {
                    index = 0;
                } else if (index > this.get('previewData').length - 1) {
                    index = this.get('previewData').length - 1;
                }

                this.set('highlightedIndex', index);

                return false;
            }

            // Debounce keyup event if user types fast.
            Ember.run.debounce(this, this._onKeyup, this.keyupTimeout);
        },

        /**
         * Handle focus events from input field.
         */
        onFocus () {
            this.set('hasFocus', true);
        },

        /**
         * Handle click event in dropdown list.
         */
        onItemClick (item) {
            // Should be set from parent.
            this.attrs.itemSelected(item);

            if (!this.get('multiSelect')) {
                this.set('hasSelected', item);

            // If multi select clear the value, close the dropdown and give focus to
            // input field.
            } else {
                this.set('value', '');
                this.set('previewData', []);
                this.$('input').focus();
            }
        }
    }
});
<commit_msg>Refactor autocomplete to remove internal state<commit_after>import Ember from 'ember';

import ClickOutsideComponent from '../mixins/click-outside-component';
import Filterable from '../mixins/filterable';

export default Ember.Component.extend(Filterable, ClickOutsideComponent, {

    /** Setup component css classes. */
    classNames: [
        'autocomplete-input', 'dropdown-group'
    ],

    /** Update when item being updated from outside. */
    didReceiveAttrs() {
        this._super(...arguments);
        const value = this.get(`item.${this.get('displayField')}`);

        if (value) {
            if (value !== this.get('value')) {
                this.set('value', value);
            }
        }
    },

    /** Enable multi select.
     *
     * When multi select is enabled the textfield will not be
     * set with the selected value.
     *
     */
    multiSelect: false,

    /** Injected services. */
    store: Ember.inject.service('store'),

    /** Name of the store to fetch data from. */
    storeName: null,

    /** Value of the input field */
    value: '',

    /** Limit number of responses. */
    limit: 40,

    /** Highligted index in dropdown. */
    highlightedIndex: -1,

    /** Update dropdown list when index change. */
    onHighlightedIndexChange: function () {
        if (this.get('hasFocus')) {
            let listItem = this.$('li[data-index='+ this.get('highlightedIndex') +']');

            if (listItem) {
                listItem.addClass('active').siblings().removeClass('active');
            }
        }
    }.observes('highlightedIndex', 'hasFocus'),

    /** Data to display in preview dropdown. */
    previewData: [],

    /** Does the input field have focus. */
    hasFocus: false,

    /** Field to use in autocomplete search */
    filterField: null,

    /** Timeout between keyup events. */
    keyupTimeout: 150,

    /** Return if preview dropdown is visible. */
    isDropdownVisible: Ember.computed('previewData', 'hasFocus', function () {
        const visible = (
            (this.get('previewData').length || this.get('value.length')) &&
            this.get('hasFocus')
        );
        return visible;
    }),

    /** Override init. */
    init () {

        /** If using local data decrease the timeout. */
        if (this.localData) {
            this.keyupTimeout = 10;
        }

        return this._super(...arguments);
    },

    /** Fetch data stored in client store with out request.
     *
     * Using this method it's up to the application to keep the data up to date.
     *
     */
    _fetchLocalData (filterField, filterValue, limit) {
        var result = this.get('store').peekAll(this.storeName).filter((item) => {
            let fieldValue = item.get(filterField).toLowerCase();

            return fieldValue.indexOf(filterValue.toLowerCase()) !== -1;
        }).sortBy(filterField).slice(0, limit + 1);

        this.set('previewData', result);
    },

    /** Fetch data from remote server. */
    _fetchRemoteData (filterField, filterValue, limit) {
        var queryParams = {
            search: true,
            limit: limit,
            orderby: filterField
        };

        queryParams[filterField] = filterValue;

        Ember.$.extend(queryParams, this.get('filters'));

        this.get('store').query(this.storeName, queryParams).then((response) => {
            this.set('previewData', response);
        }).catch((reason) => {
            this.set('previewData', []);

            console.warn('Invalid response from server: ', reason);
        });
    },

    /**
     * Fetch data based on search value and update store.
     */
    fetchData () {
        if (this.localData) {
            this._fetchLocalData(
                this.get('filterField'), this.get('value'), this.get('limit')
            );
        } else {
            this._fetchRemoteData(
                this.get('filterField'), this.get('value'), this.get('limit')
            );
        }
    },

    /** Handle event when user clicks outside of component. */
    onOutsideClick () {
        this.set('hasFocus', false);
    },

    /**
     * Handle keyup events in input field.
     */
    _onKeyup () {
        if (!this.get('value').length) {
            this.set('previewData', []);
        } else {
            this.fetchData();
        }

        // Set item as undefined.
        if (!this.get('multiSelect')) {
            this.get('itemSelected')();
        }

        this.set('highlightedIndex', -1);
    },

    actions: {

        /**
        * Handle Keydown events in input field.
        *
        * Triggers private method to be able to delay execution.
        *
        */
        onKeyDown (value, event) {
            this.set('hasFocus', true);

            if ([38, 40, 13, 27, 9].indexOf(event.keyCode) !== -1) {
                let index = this.get('highlightedIndex');

                // Arrow key up.
                if (event.keyCode === 38) {
                    index -= 1;
                // Arrow key down
                } else if (event.keyCode === 40) {
                    index += 1;

                // Enter key
                } else if (event.keyCode === 13) {
                    this.send(
                        'onItemClick', this.get('previewData').objectAt(index)
                    );

                    return;

                // ESC and TAB key should remove focus.
                } else if ([27, 9].indexOf(event.keyCode) !== -1) {
                    this.set('hasFocus', false);
                }

                if (index < 0) {
                    index = 0;
                } else if (index > this.get('previewData').length - 1) {
                    index = this.get('previewData').length - 1;
                }

                this.set('highlightedIndex', index);

                return false;
            }

            // Debounce keyup event if user types fast.
            Ember.run.debounce(this, this._onKeyup, this.keyupTimeout);
        },

        /**
         * Handle focus events from input field.
         */
        onFocus () {
            this.set('hasFocus', true);
        },

        /**
         * Handle click event in dropdown list.
         */
        onItemClick (item) {
            if (!this.get('multiSelect')) {
                this.$('input').blur();
                this.set('hasFocus', false);

            // If multi select clear the value,
            // close the dropdown and give focus to
            // input field.
            } else {
                this.set('value', '');
                this.set('previewData', []);
                this.$('input').focus();
            }

            // Should be set from parent.
            this.attrs.itemSelected(item);
        }
    }
});
<|endoftext|>


<commit_before>'use strict';

var blueprint = require ('@onehilltech/blueprint')
  , mongodb = require ('@onehilltech/blueprint-mongodb')
  , ResourceController = mongodb.ResourceController
  , _ = require ('lodash')
  ;

var Message = require ('../models/Message')
  , User    = require ('../models/User')
  ;

function MessageController () {
  ResourceController.call (this, {name: 'message', model: Message});
}

MessageController.prototype.create = function () {
  var opts = {
    on: {
      preCreate: function (req, doc, callback) {
        var token = req.headers.authorization.split(' ')[1];

        User.findOne({token: token}, function (err, user) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }

          doc.org_id = user.org_id;
          doc.sender = user.username;
          return callback(null, doc);
        });
      }
    }
  };

  return mongodb.ResourceController.prototype.create.call (this, opts);
};

MessageController.prototype.getAll = function () {
  var opts = {
    on: {
      authorize: function (req, callback) {
        var role = req.user.role;

        if (role != 'admin') {
          var queries = req.query;

          if (_.isEmpty (queries)) {
            return callback ('unauthorized');
          }

          var whitelist = ['sender', 'receiver'];

          for (var query in queries) {
            if (!_.includes (whitelist, query)) {
              return callback ('unauthorized query parameter');
            }
          }
        }

        return callback ();
      }
    }
  };

  return mongodb.ResourceController.prototype.getAll.call (this, opts);
};

blueprint.controller (MessageController, ResourceController);

module.exports = MessageController;<commit_msg>updated authorize method to fix codeclimate issues<commit_after>'use strict';

var blueprint = require ('@onehilltech/blueprint')
  , mongodb = require ('@onehilltech/blueprint-mongodb')
  , ResourceController = mongodb.ResourceController
  , _ = require ('lodash')
  ;

var Message = require ('../models/Message')
  , User    = require ('../models/User')
  ;

function MessageController () {
  ResourceController.call (this, {name: 'message', model: Message});
}

MessageController.prototype.create = function () {
  var opts = {
    on: {
      preCreate: function (req, doc, callback) {
        var token = req.headers.authorization.split(' ')[1];

        User.findOne({token: token}, function (err, user) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }

          doc.org_id = user.org_id;
          doc.sender = user.username;
          return callback(null, doc);
        });
      }
    }
  };

  return mongodb.ResourceController.prototype.create.call (this, opts);
};

MessageController.prototype.getAll = function () {
  var opts = {
    on: {
      authorize: function (req, callback) {
        var role = req.user.role;

        if (role !== 'admin') {
          var queries = req.query;

          if (_.isEmpty (queries)) {
            return callback ('unauthorized');
          }

          var whitelist = ['sender', 'receiver'];

          for (var query in queries) {
            if (!_.includes (whitelist, query)) {
              return callback ('unauthorized query parameter');
            }
          }
        }

        return callback ();
      }
    }
  };

  return mongodb.ResourceController.prototype.getAll.call (this, opts);
};

blueprint.controller (MessageController, ResourceController);

module.exports = MessageController;<|endoftext|>


<commit_before>/* global complete, desc, directory, fail, jake, task */
"use strict";

var BUILD_DIR = "build";
var TEST_TEMP_DIR = BUILD_DIR + "/test";
var lint_runner = require("./src/build/lint/lint_runner.js");

function getCommonJSHintOptions() {
  return {
    bitwise: false,
    curly: true,
    eqeqeq: true,
    forin: true,
    immed: true,
    latedef: true,
    newcap: true,
    noarg: true,
    nonew: true,
    undef: true,
    unused: true,
    strict: true,
    trailing: true,
    maxstatements: 15,
    maxcomplexity: 10
  };
}

function getJSHintOptionsForServerSideCode() {
  var options = getCommonJSHintOptions();
  options.node = true;
  return options;
}

function getJSHintOptionsForBrowsers() {
  var options = getCommonJSHintOptions();
  options.browser = true;
  return  options;
}

desc("Integration");
task("integrate", ["default"], function() {
  console.log("Steps for the commit tango...");
  console.log("1. Verify the build on the master branch, locally.\n   yourbox$ ./jake.sh\n");
  console.log("2. Pull to the Integation Station and verify the build there.\n   integration$ git pull && ./jake.sh\n");
  console.log("3. If all good, merge changes into 'last-known-good'\n   yourbox$ merge-to-integration.sh\n");
});

directory(BUILD_DIR);
directory(TEST_TEMP_DIR);

desc("Runs server-side unit tests.");
task("test.unit.server", [TEST_TEMP_DIR], function() {
  // CAP-0002
  var reporter = require('nodeunit').reporters.default;
  var allJavaScriptTests = new jake.FileList();
  allJavaScriptTests.include("src/specs/server/*.js");
  allJavaScriptTests.include("src/tests/smoke_test.js");
  allJavaScriptTests.exclude("node_modules");  // assuming these are properly linted, already.

  reporter.run(allJavaScriptTests.toArray(), null, function(failureOccurred) {
    if(failureOccurred) { fail("Task 'test.unit.server' failed (see above)."); }
    complete();
  });
}, {async: true});

desc("Runs client-side unit tests.");
task("test.unit.client", [], function() {
  var config = {};
  require("karma/lib/runner").run(config, function(errorCode) {
    if(errorCode) {
      fail("Task 'test.unit.client' failed with error code = " + errorCode + " (see above).");
    }
    complete();
  });
}, {async: true});

desc("Runs all unit tests");
task("test.unit", ["test.unit.server", "test.unit.client"]);

desc("Runs production verification tests.");
task("test.prod", [TEST_TEMP_DIR], function() {
  // CAP-0002
  var reporter = require('nodeunit').reporters.default;
  var productionTests = new jake.FileList();
  productionTests.include("src/tests/production_smoke_test.js");

  reporter.run(productionTests.toArray(), null, function(failureOccurred) {
    if(failureOccurred) { fail("Task 'test.prod' failed (see above)."); }
    complete();
  });
}, {async: true});

desc("Runs JSLint on everything");
task("lint", ["lint.server", "lint.client"]);

task("lint.server", function() {
  // CAP-0002
  console.log("\nLinting server-side code...");

  var serverSideJavaScriptFiles = new jake.FileList();
  serverSideJavaScriptFiles.include("src/**/server/**/*.js");
  serverSideJavaScriptFiles.include("src/tests/smoke_test.js");
  serverSideJavaScriptFiles.include("src/build/**/*.js");
  serverSideJavaScriptFiles.include("*.js");
  serverSideJavaScriptFiles.exclude("karma.conf.js");  // won't be proper stand-alone js, it's JSON config file.
  var passesLint = lint_runner.validateFileList(serverSideJavaScriptFiles, getJSHintOptionsForServerSideCode());

  if(!passesLint) { fail("Task 'lint.server' failed (see above)."); }
});

task("lint.client", function() {
  // CAP-0002
  console.log("\nLinting client-side code...");

  var clientSideJavaScriptFiles = new jake.FileList();
  clientSideJavaScriptFiles.include("src/**/client/**/*.js");
  clientSideJavaScriptFiles.include("src/code/web/**/*.js");
  var passesLint = lint_runner.validateFileList(clientSideJavaScriptFiles, getJSHintOptionsForBrowsers());

  if(!passesLint) { fail("Task 'lint.client' failed (see above)."); }
});

desc("Purges the build output directory (i.e. " + BUILD_DIR + ").");
task("clean", function() {
  jake.rmRf(BUILD_DIR);
});

task("check node version", function() {
  // TODO: obtain node version from the package.json definition.
  var expectedNodeVersion = "v0.10.8";
  var actualNodeVersion = process.version;

  // since node.js is changes quite frequently, making this an exact match, for now.
  if(actualNodeVersion !== expectedNodeVersion) {
    fail(actualNodeVersion + " is an unsupported version of node.js.  Must be " + expectedNodeVersion);
  }
});

desc("Full build.");
task("default", ["check node version", "clean", "lint", "test.unit"]);

<commit_msg>(minor) removed 'CAP' -- these two are sufficiently different with no need to refactor, now.<commit_after>/* global complete, desc, directory, fail, jake, task */
"use strict";

var BUILD_DIR = "build";
var TEST_TEMP_DIR = BUILD_DIR + "/test";
var lint_runner = require("./src/build/lint/lint_runner.js");

function getCommonJSHintOptions() {
  return {
    bitwise: false,
    curly: true,
    eqeqeq: true,
    forin: true,
    immed: true,
    latedef: true,
    newcap: true,
    noarg: true,
    nonew: true,
    undef: true,
    unused: true,
    strict: true,
    trailing: true,
    maxstatements: 15,
    maxcomplexity: 10
  };
}

function getJSHintOptionsForServerSideCode() {
  var options = getCommonJSHintOptions();
  options.node = true;
  return options;
}

function getJSHintOptionsForBrowsers() {
  var options = getCommonJSHintOptions();
  options.browser = true;
  return  options;
}

desc("Integration");
task("integrate", ["default"], function() {
  console.log("Steps for the commit tango...");
  console.log("1. Verify the build on the master branch, locally.\n   yourbox$ ./jake.sh\n");
  console.log("2. Pull to the Integation Station and verify the build there.\n   integration$ git pull && ./jake.sh\n");
  console.log("3. If all good, merge changes into 'last-known-good'\n   yourbox$ merge-to-integration.sh\n");
});

directory(BUILD_DIR);
directory(TEST_TEMP_DIR);

desc("Runs server-side unit tests.");
task("test.unit.server", [TEST_TEMP_DIR], function() {
  // CAP-0002
  var reporter = require('nodeunit').reporters.default;
  var allJavaScriptTests = new jake.FileList();
  allJavaScriptTests.include("src/specs/server/*.js");
  allJavaScriptTests.include("src/tests/smoke_test.js");
  allJavaScriptTests.exclude("node_modules");  // assuming these are properly linted, already.

  reporter.run(allJavaScriptTests.toArray(), null, function(failureOccurred) {
    if(failureOccurred) { fail("Task 'test.unit.server' failed (see above)."); }
    complete();
  });
}, {async: true});

desc("Runs client-side unit tests.");
task("test.unit.client", [], function() {
  var config = {};
  require("karma/lib/runner").run(config, function(errorCode) {
    if(errorCode) {
      fail("Task 'test.unit.client' failed with error code = " + errorCode + " (see above).");
    }
    complete();
  });
}, {async: true});

desc("Runs all unit tests");
task("test.unit", ["test.unit.server", "test.unit.client"]);

desc("Runs production verification tests.");
task("test.prod", [TEST_TEMP_DIR], function() {
  // CAP-0002
  var reporter = require('nodeunit').reporters.default;
  var productionTests = new jake.FileList();
  productionTests.include("src/tests/production_smoke_test.js");

  reporter.run(productionTests.toArray(), null, function(failureOccurred) {
    if(failureOccurred) { fail("Task 'test.prod' failed (see above)."); }
    complete();
  });
}, {async: true});

desc("Runs JSLint on everything");
task("lint", ["lint.server", "lint.client"]);

task("lint.server", function() {
  console.log("\nLinting server-side code...");

  var serverSideJavaScriptFiles = new jake.FileList();
  serverSideJavaScriptFiles.include("src/**/server/**/*.js");
  serverSideJavaScriptFiles.include("src/tests/smoke_test.js");
  serverSideJavaScriptFiles.include("src/build/**/*.js");
  serverSideJavaScriptFiles.include("*.js");
  serverSideJavaScriptFiles.exclude("karma.conf.js");  // won't be proper stand-alone js, it's JSON config file.
  var passesLint = lint_runner.validateFileList(serverSideJavaScriptFiles, getJSHintOptionsForServerSideCode());

  if(!passesLint) { fail("Task 'lint.server' failed (see above)."); }
});

task("lint.client", function() {
  console.log("\nLinting client-side code...");

  var clientSideJavaScriptFiles = new jake.FileList();
  clientSideJavaScriptFiles.include("src/**/client/**/*.js");
  clientSideJavaScriptFiles.include("src/code/web/**/*.js");
  var passesLint = lint_runner.validateFileList(clientSideJavaScriptFiles, getJSHintOptionsForBrowsers());

  if(!passesLint) { fail("Task 'lint.client' failed (see above)."); }
});

desc("Purges the build output directory (i.e. " + BUILD_DIR + ").");
task("clean", function() {
  jake.rmRf(BUILD_DIR);
});

task("check node version", function() {
  // TODO: obtain node version from the package.json definition.
  var expectedNodeVersion = "v0.10.8";
  var actualNodeVersion = process.version;

  // since node.js is changes quite frequently, making this an exact match, for now.
  if(actualNodeVersion !== expectedNodeVersion) {
    fail(actualNodeVersion + " is an unsupported version of node.js.  Must be " + expectedNodeVersion);
  }
});

desc("Full build.");
task("default", ["check node version", "clean", "lint", "test.unit"]);

<|endoftext|>


<commit_before>define(['jquery', 'backbone', 'underscore'], function($, Backbone, _){
    var InfiniteScrollView = Backbone.View.extend({
        // Generic InfiniteScrollView implementation
        scrollCallbacks: [function(){ console.log("Scrolled!"); }],
        infiniteScrollEnabled: true,

        // Used to capture once a scroll has occured and on which element
        elScrolled: false,
        windowScrolled: false,

        handleScroll: function() {
            // Are we enabled? Has anything been scrolled?
            if ((this.infiniteScrollEnabled) && (this.windowScrolled || this.elScrolled)) {
                this.infiniteScrollEnabled = false; // Infinite scroll disabled for duration of handling 1 event
                var thresholdSurpassed = false;
                if (this.scrollThreshold) {
                    if (this.elScrolled) {
                        thresholdSurpassed = ((this.scrollElement.scrollTop / this.scrollElement.scrollHeight) > this.scrollThreshold);
                    } else {
                        thresholdSurpassed = (($(document).scrollTop() / $(document).height()) > this.scrollThreshold);
                    }
                }
                // Call our callbacks with the correct `this` context
                if ((this.scrollThreshold && thresholdSurpassed) || !this.scrollThreshold) {
                    _.each(this.scrollCallbacks, _.bind(function(cb) { cb.apply(this); }, this));
                }
                this.infiniteScrollEnabled = true;
                this.windowScrolled = false;
                this.elScrolled = false;
            }
        },

        initScroll: function(options) {
            // Initialises the scroll event handlers and sets an interval to observe them
            // The options object can contain any or none of the following:
            //  * `windowScroll` should be a boolean saying if we want to listen to window.scroll events
            //  * `scrollElement` is an optional DOM element we want to listen to scroll events for
            //  * `intervalPeriod` time in ms which we should check if the user has scrolled
            //  * `scrollThreshold` a floating point integer between 0 and 1 - The ratio representating
            //                      how far down a page scroll should the `scrollCallbacks` be called.
            this.scrollThreshold = options.scrollThreshold ? options.scrollThreshold : null;
            if (options.windowScroll) {
                $(window).scroll(
                    _.bind(function(){ this.windowScrolled = true; }, this)
                );
            }
            if (options.scrollElement) {
                this.scrollElement = options.scrollElement;
                $(options.scrollElement).scroll(
                    _.bind(function(){ this.elScrolled = true; }, this)
                );
            }
            var intervalPeriod = options.intervalPeriod ? options.intervalPeriod : 250; // default of 250ms per Mr. Resig's suggestion
            if (options.windowScroll || options.scrollElement) {
                this.scrollInterval = window.setInterval(_.bind(this.handleScroll, this), intervalPeriod);
            }
        },

        onClose: function() {
            if (this.scrollInterval) {
                // Stop looking for scroll events
                window.clearInterval(this.scrollInterval);
            }
        }
    });
    return InfiniteScrollView;
});
<commit_msg>Fixed bug where scrollTop didn't include clientHeight<commit_after>define(['jquery', 'backbone', 'underscore'], function($, Backbone, _){
    var InfiniteScrollView = Backbone.View.extend({
        // Generic InfiniteScrollView implementation
        scrollCallbacks: [function(){ console.log("Scrolled!"); }],
        infiniteScrollEnabled: true,

        // Used to capture once a scroll has occured and on which element
        elScrolled: false,
        windowScrolled: false,

        handleScroll: function() {
            // Are we enabled? Has anything been scrolled?
            if ((this.infiniteScrollEnabled) && (this.windowScrolled || this.elScrolled)) {
                this.infiniteScrollEnabled = false; // Infinite scroll disabled for duration of handling 1 event
                var thresholdSurpassed = false;
                if (this.scrollThreshold) {
                    if (this.elScrolled) {
                        thresholdSurpassed = (((this.scrollElement.scrollTop + this.scrollElement.clientHeight) / this.scrollElement.scrollHeight) > this.scrollThreshold);
                    } else {
                        thresholdSurpassed = (($(document).scrollTop() / $(document).height()) > this.scrollThreshold);
                    }
                }
                // Call our callbacks with the correct `this` context
                if ((this.scrollThreshold && thresholdSurpassed) || !this.scrollThreshold) {
                    _.each(this.scrollCallbacks, _.bind(function(cb) { cb.apply(this); }, this));
                }
                this.infiniteScrollEnabled = true;
                this.windowScrolled = false;
                this.elScrolled = false;
            }
        },

        initScroll: function(options) {
            // Initialises the scroll event handlers and sets an interval to observe them
            // The options object can contain any or none of the following:
            //  * `windowScroll` should be a boolean saying if we want to listen to window.scroll events
            //  * `scrollElement` is an optional DOM element we want to listen to scroll events for
            //  * `intervalPeriod` time in ms which we should check if the user has scrolled
            //  * `scrollThreshold` a floating point integer between 0 and 1 - The ratio representating
            //                      how far down a page scroll should the `scrollCallbacks` be called.
            this.scrollThreshold = options.scrollThreshold ? options.scrollThreshold : null;
            if (options.windowScroll) {
                $(window).scroll(
                    _.bind(function(){ this.windowScrolled = true; }, this)
                );
            }
            if (options.scrollElement) {
                this.scrollElement = options.scrollElement;
                $(options.scrollElement).scroll(
                    _.bind(function(){ this.elScrolled = true; }, this)
                );
            }
            var intervalPeriod = options.intervalPeriod ? options.intervalPeriod : 250; // default of 250ms per Mr. Resig's suggestion
            if (options.windowScroll || options.scrollElement) {
                this.scrollInterval = window.setInterval(_.bind(this.handleScroll, this), intervalPeriod);
            }
        },

        onClose: function() {
            if (this.scrollInterval) {
                // Stop looking for scroll events
                window.clearInterval(this.scrollInterval);
            }
        }
    });
    return InfiniteScrollView;
});
<|endoftext|>


<commit_before>/* globals jsPDF */
/*
 * svgToPdf.js
 *
 * Copyright 2012-2014 Florian Hülsmann <fh@cbix.de>
 * Copyright 2014 Ben Gribaudo <www.bengribaudo.com>
 *
 * This script is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This script is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this file.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import RGBColor from '../canvg/rgbcolor.js';

const jsPDFAPI = jsPDF.API;
const pdfSvgAttr = {
  // allowed attributes. all others are removed from the preview.
  g: ['stroke', 'fill', 'stroke-width'],
  line: ['x1', 'y1', 'x2', 'y2', 'stroke', 'stroke-width'],
  rect: ['x', 'y', 'width', 'height', 'stroke', 'fill', 'stroke-width'],
  ellipse: ['cx', 'cy', 'rx', 'ry', 'stroke', 'fill', 'stroke-width'],
  circle: ['cx', 'cy', 'r', 'stroke', 'fill', 'stroke-width'],
  polygon: ['points', 'stroke', 'fill', 'stroke-width'],
  // polyline attributes are the same as those of polygon
  text: ['x', 'y', 'font-size', 'font-family', 'text-anchor', 'font-weight', 'font-style', 'fill']
};

const attributeIsNotEmpty = function (node, attr) {
  const attVal = attr ? node.getAttribute(attr) : node;
  return attVal !== '' && attVal !== null;
};

const nodeIs = function (node, possible) {
  return possible.includes(node.tagName.toLowerCase());
};

const removeAttributes = function (node, attributes) {
  const toRemove = [];
  [].forEach.call(node.attributes, function (a) {
    if (attributeIsNotEmpty(a) && !attributes.includes(a.name.toLowerCase())) {
      toRemove.push(a.name);
    }
  });

  toRemove.forEach((a) => {
    node.removeAttribute(a.name);
  });
};

const numRgx = /[+-]?(?:\d+\.\d*|\d+|\.\d+)(?:[eE]\d+|[eE][+-]\d+|)/g;
const getLinesOptionsOfPoly = function (node) {
  let nums = node.getAttribute('points');
  nums = (nums && nums.match(numRgx)) || [];
  if (nums && nums.length) {
    nums = nums.map((n) => Number(n));
    if (nums.length % 2) {
      nums.length--;
    }
  }
  if (nums.length < 4) {
    console.log('invalid points attribute:', node); // eslint-disable-line no-console
    return undefined;
  }
  const [x, y] = nums, lines = [];
  for (let i = 2; i < nums.length; i += 2) {
    lines.push([nums[i] - nums[i - 2], nums[i + 1] - nums[i - 1]]);
  }
  return {x, y, lines};
};

const svgElementToPdf = function (element, pdf, options) {
  // pdf is a jsPDF object
  // console.log('options =', options);
  const remove = (options.removeInvalid === undefined ? false : options.removeInvalid);
  const k = (options.scale === undefined ? 1.0 : options.scale);
  let colorMode = null;
  [].forEach.call(element.children, function (node) {
    // console.log('passing: ', node);
    // let hasStrokeColor = false;
    let hasFillColor = false;
    let fillRGB;
    if (nodeIs(node, ['g', 'line', 'rect', 'ellipse', 'circle', 'polygon', 'polyline', 'text'])) {
      const fillColor = node.getAttribute('fill');
      if (attributeIsNotEmpty(fillColor)) {
        fillRGB = new RGBColor(fillColor);
        if (fillRGB.ok) {
          hasFillColor = true;
          colorMode = 'F';
        } else {
          colorMode = null;
        }
      }
    }
    if (nodeIs(node, ['g', 'line', 'rect', 'ellipse', 'circle', 'polygon', 'polyline'])) {
      if (hasFillColor) {
        pdf.setFillColor(fillRGB.r, fillRGB.g, fillRGB.b);
      }
      if (attributeIsNotEmpty(node, 'stroke-width')) {
        pdf.setLineWidth(k * parseInt(node.getAttribute('stroke-width')));
      }
      const strokeColor = node.getAttribute('stroke');
      if (attributeIsNotEmpty(strokeColor)) {
        const strokeRGB = new RGBColor(strokeColor);
        if (strokeRGB.ok) {
          // hasStrokeColor = true;
          pdf.setDrawColor(strokeRGB.r, strokeRGB.g, strokeRGB.b);
          if (colorMode === 'F') {
            colorMode = 'FD';
          } else {
            colorMode = 'S';
          }
        } else {
          colorMode = null;
        }
      }
    }
    const tag = node.tagName.toLowerCase();
    switch (tag) {
    case 'svg':
    case 'a':
    case 'g':
      svgElementToPdf(node, pdf, options);
      removeAttributes(node, pdfSvgAttr.g);
      break;
    case 'line':
      pdf.line(
        k * parseInt(node.getAttribute('x1')),
        k * parseInt(node.getAttribute('y1')),
        k * parseInt(node.getAttribute('x2')),
        k * parseInt(node.getAttribute('y2'))
      );
      removeAttributes(node, pdfSvgAttr.line);
      break;
    case 'rect':
      pdf.rect(
        k * parseInt(node.getAttribute('x')),
        k * parseInt(node.getAttribute('y')),
        k * parseInt(node.getAttribute('width')),
        k * parseInt(node.getAttribute('height')),
        colorMode
      );
      removeAttributes(node, pdfSvgAttr.rect);
      break;
    case 'ellipse':
      pdf.ellipse(
        k * parseInt(node.getAttribute('cx')),
        k * parseInt(node.getAttribute('cy')),
        k * parseInt(node.getAttribute('rx')),
        k * parseInt(node.getAttribute('ry')),
        colorMode
      );
      removeAttributes(node, pdfSvgAttr.ellipse);
      break;
    case 'circle':
      pdf.circle(
        k * parseInt(node.getAttribute('cx')),
        k * parseInt(node.getAttribute('cy')),
        k * parseInt(node.getAttribute('r')),
        colorMode
      );
      removeAttributes(node, pdfSvgAttr.circle);
      break;
    case 'polygon':
    case 'polyline': {
      const linesOptions = getLinesOptionsOfPoly(node);
      if (linesOptions) {
        pdf.lines(
          linesOptions.lines,
          k * linesOptions.x,
          k * linesOptions.y,
          [k, k],
          colorMode,
          tag === 'polygon' // polygon is closed, polyline is not closed
        );
      }
      removeAttributes(node, pdfSvgAttr.polygon);
      break;
    // TODO: path
    } case 'text': {
      if (node.hasAttribute('font-family')) {
        switch ((node.getAttribute('font-family') || '').toLowerCase()) {
        case 'serif': pdf.setFont('times'); break;
        case 'monospace': pdf.setFont('courier'); break;
        default:
          node.setAttribute('font-family', 'sans-serif');
          pdf.setFont('helvetica');
        }
      }
      if (hasFillColor) {
        pdf.setTextColor(fillRGB.r, fillRGB.g, fillRGB.b);
      }
      let fontType = '';
      if (node.hasAttribute('font-weight')) {
        if (node.getAttribute('font-weight') === 'bold') {
          fontType = 'bold';
        } else {
          node.removeAttribute('font-weight');
        }
      }
      if (node.hasAttribute('font-style')) {
        if (node.getAttribute('font-style') === 'italic') {
          fontType += 'italic';
        } else {
          node.removeAttribute('font-style');
        }
      }
      pdf.setFontType(fontType);
      const pdfFontSize = node.hasAttribute('font-size')
        ? parseInt(node.getAttribute('font-size'), 10)
        : 16;

      /**
       *
       * @param {Element} elem
       * @returns {Float}
       */
      const getWidth = (elem) => {
        let box;
        try {
          box = elem.getBBox(); // Firefox on MacOS will raise error here
        } catch (err) {
          // copy and append to body so that getBBox is available
          const nodeCopy = elem.cloneNode(true);
          const svg = elem.ownerSVGElement.cloneNode(false);
          svg.appendChild(nodeCopy);
          document.body.appendChild(svg);
          try {
            box = nodeCopy.getBBox();
          } catch (error) {
            box = {width: 0};
          }
          document.body.removeChild(svg);
        }
        return box.width;
      };
      // TODO: use more accurate positioning!!
      let x, y, xOffset = 0;
      if (node.hasAttribute('text-anchor')) {
        switch (node.getAttribute('text-anchor')) {
        case 'end': xOffset = getWidth(node); break;
        case 'middle': xOffset = getWidth(node) / 2; break;
        case 'start': break;
        case 'default': node.setAttribute('text-anchor', 'start'); break;
        }
        x = parseInt(node.getAttribute('x'), 10) - xOffset;
        y = parseInt(node.getAttribute('y'), 10);
      }
      // console.log('fontSize:', pdfFontSize, 'text:', node.textContent);
      pdf.setFontSize(pdfFontSize).text(
        k * x,
        k * y,
        node.textContent
      );
      removeAttributes(node, pdfSvgAttr.text);
      break;
    // TODO: image
    } default:
      if (remove) {
        console.log("can't translate to pdf:", node); // eslint-disable-line no-console
        node.remove();
      }
    }
  });
  return pdf;
};

jsPDFAPI.addSVG = function (element, x, y, options) {
  options = (options === undefined ? {} : options);
  options.x_offset = x;
  options.y_offset = y;

  if (typeof element === 'string') {
    element = new DOMParser().parseFromString(element, 'text/xml').documentElement;
  }
  svgElementToPdf(element, this, options);
  return this;
};
<commit_msg>- Linting (ESLint): Revert addition of radix which is not needed in ES6 dev. environment (Babel to handle)<commit_after>/* globals jsPDF */
/*
 * svgToPdf.js
 *
 * Copyright 2012-2014 Florian Hülsmann <fh@cbix.de>
 * Copyright 2014 Ben Gribaudo <www.bengribaudo.com>
 *
 * This script is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This script is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this file.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import RGBColor from '../canvg/rgbcolor.js';

const jsPDFAPI = jsPDF.API;
const pdfSvgAttr = {
  // allowed attributes. all others are removed from the preview.
  g: ['stroke', 'fill', 'stroke-width'],
  line: ['x1', 'y1', 'x2', 'y2', 'stroke', 'stroke-width'],
  rect: ['x', 'y', 'width', 'height', 'stroke', 'fill', 'stroke-width'],
  ellipse: ['cx', 'cy', 'rx', 'ry', 'stroke', 'fill', 'stroke-width'],
  circle: ['cx', 'cy', 'r', 'stroke', 'fill', 'stroke-width'],
  polygon: ['points', 'stroke', 'fill', 'stroke-width'],
  // polyline attributes are the same as those of polygon
  text: ['x', 'y', 'font-size', 'font-family', 'text-anchor', 'font-weight', 'font-style', 'fill']
};

const attributeIsNotEmpty = function (node, attr) {
  const attVal = attr ? node.getAttribute(attr) : node;
  return attVal !== '' && attVal !== null;
};

const nodeIs = function (node, possible) {
  return possible.includes(node.tagName.toLowerCase());
};

const removeAttributes = function (node, attributes) {
  const toRemove = [];
  [].forEach.call(node.attributes, function (a) {
    if (attributeIsNotEmpty(a) && !attributes.includes(a.name.toLowerCase())) {
      toRemove.push(a.name);
    }
  });

  toRemove.forEach((a) => {
    node.removeAttribute(a.name);
  });
};

const numRgx = /[+-]?(?:\d+\.\d*|\d+|\.\d+)(?:[eE]\d+|[eE][+-]\d+|)/g;
const getLinesOptionsOfPoly = function (node) {
  let nums = node.getAttribute('points');
  nums = (nums && nums.match(numRgx)) || [];
  if (nums && nums.length) {
    nums = nums.map((n) => Number(n));
    if (nums.length % 2) {
      nums.length--;
    }
  }
  if (nums.length < 4) {
    console.log('invalid points attribute:', node); // eslint-disable-line no-console
    return undefined;
  }
  const [x, y] = nums, lines = [];
  for (let i = 2; i < nums.length; i += 2) {
    lines.push([nums[i] - nums[i - 2], nums[i + 1] - nums[i - 1]]);
  }
  return {x, y, lines};
};

const svgElementToPdf = function (element, pdf, options) {
  // pdf is a jsPDF object
  // console.log('options =', options);
  const remove = (options.removeInvalid === undefined ? false : options.removeInvalid);
  const k = (options.scale === undefined ? 1.0 : options.scale);
  let colorMode = null;
  [].forEach.call(element.children, function (node) {
    // console.log('passing: ', node);
    // let hasStrokeColor = false;
    let hasFillColor = false;
    let fillRGB;
    if (nodeIs(node, ['g', 'line', 'rect', 'ellipse', 'circle', 'polygon', 'polyline', 'text'])) {
      const fillColor = node.getAttribute('fill');
      if (attributeIsNotEmpty(fillColor)) {
        fillRGB = new RGBColor(fillColor);
        if (fillRGB.ok) {
          hasFillColor = true;
          colorMode = 'F';
        } else {
          colorMode = null;
        }
      }
    }
    if (nodeIs(node, ['g', 'line', 'rect', 'ellipse', 'circle', 'polygon', 'polyline'])) {
      if (hasFillColor) {
        pdf.setFillColor(fillRGB.r, fillRGB.g, fillRGB.b);
      }
      if (attributeIsNotEmpty(node, 'stroke-width')) {
        pdf.setLineWidth(k * parseInt(node.getAttribute('stroke-width')));
      }
      const strokeColor = node.getAttribute('stroke');
      if (attributeIsNotEmpty(strokeColor)) {
        const strokeRGB = new RGBColor(strokeColor);
        if (strokeRGB.ok) {
          // hasStrokeColor = true;
          pdf.setDrawColor(strokeRGB.r, strokeRGB.g, strokeRGB.b);
          if (colorMode === 'F') {
            colorMode = 'FD';
          } else {
            colorMode = 'S';
          }
        } else {
          colorMode = null;
        }
      }
    }
    const tag = node.tagName.toLowerCase();
    switch (tag) {
    case 'svg':
    case 'a':
    case 'g':
      svgElementToPdf(node, pdf, options);
      removeAttributes(node, pdfSvgAttr.g);
      break;
    case 'line':
      pdf.line(
        k * parseInt(node.getAttribute('x1')),
        k * parseInt(node.getAttribute('y1')),
        k * parseInt(node.getAttribute('x2')),
        k * parseInt(node.getAttribute('y2'))
      );
      removeAttributes(node, pdfSvgAttr.line);
      break;
    case 'rect':
      pdf.rect(
        k * parseInt(node.getAttribute('x')),
        k * parseInt(node.getAttribute('y')),
        k * parseInt(node.getAttribute('width')),
        k * parseInt(node.getAttribute('height')),
        colorMode
      );
      removeAttributes(node, pdfSvgAttr.rect);
      break;
    case 'ellipse':
      pdf.ellipse(
        k * parseInt(node.getAttribute('cx')),
        k * parseInt(node.getAttribute('cy')),
        k * parseInt(node.getAttribute('rx')),
        k * parseInt(node.getAttribute('ry')),
        colorMode
      );
      removeAttributes(node, pdfSvgAttr.ellipse);
      break;
    case 'circle':
      pdf.circle(
        k * parseInt(node.getAttribute('cx')),
        k * parseInt(node.getAttribute('cy')),
        k * parseInt(node.getAttribute('r')),
        colorMode
      );
      removeAttributes(node, pdfSvgAttr.circle);
      break;
    case 'polygon':
    case 'polyline': {
      const linesOptions = getLinesOptionsOfPoly(node);
      if (linesOptions) {
        pdf.lines(
          linesOptions.lines,
          k * linesOptions.x,
          k * linesOptions.y,
          [k, k],
          colorMode,
          tag === 'polygon' // polygon is closed, polyline is not closed
        );
      }
      removeAttributes(node, pdfSvgAttr.polygon);
      break;
    // TODO: path
    } case 'text': {
      if (node.hasAttribute('font-family')) {
        switch ((node.getAttribute('font-family') || '').toLowerCase()) {
        case 'serif': pdf.setFont('times'); break;
        case 'monospace': pdf.setFont('courier'); break;
        default:
          node.setAttribute('font-family', 'sans-serif');
          pdf.setFont('helvetica');
        }
      }
      if (hasFillColor) {
        pdf.setTextColor(fillRGB.r, fillRGB.g, fillRGB.b);
      }
      let fontType = '';
      if (node.hasAttribute('font-weight')) {
        if (node.getAttribute('font-weight') === 'bold') {
          fontType = 'bold';
        } else {
          node.removeAttribute('font-weight');
        }
      }
      if (node.hasAttribute('font-style')) {
        if (node.getAttribute('font-style') === 'italic') {
          fontType += 'italic';
        } else {
          node.removeAttribute('font-style');
        }
      }
      pdf.setFontType(fontType);
      const pdfFontSize = node.hasAttribute('font-size')
        ? parseInt(node.getAttribute('font-size'))
        : 16;

      /**
       *
       * @param {Element} elem
       * @returns {Float}
       */
      const getWidth = (elem) => {
        let box;
        try {
          box = elem.getBBox(); // Firefox on MacOS will raise error here
        } catch (err) {
          // copy and append to body so that getBBox is available
          const nodeCopy = elem.cloneNode(true);
          const svg = elem.ownerSVGElement.cloneNode(false);
          svg.appendChild(nodeCopy);
          document.body.appendChild(svg);
          try {
            box = nodeCopy.getBBox();
          } catch (error) {
            box = {width: 0};
          }
          document.body.removeChild(svg);
        }
        return box.width;
      };
      // TODO: use more accurate positioning!!
      let x, y, xOffset = 0;
      if (node.hasAttribute('text-anchor')) {
        switch (node.getAttribute('text-anchor')) {
        case 'end': xOffset = getWidth(node); break;
        case 'middle': xOffset = getWidth(node) / 2; break;
        case 'start': break;
        case 'default': node.setAttribute('text-anchor', 'start'); break;
        }
        x = parseInt(node.getAttribute('x')) - xOffset;
        y = parseInt(node.getAttribute('y'));
      }
      // console.log('fontSize:', pdfFontSize, 'text:', node.textContent);
      pdf.setFontSize(pdfFontSize).text(
        k * x,
        k * y,
        node.textContent
      );
      removeAttributes(node, pdfSvgAttr.text);
      break;
    // TODO: image
    } default:
      if (remove) {
        console.log("can't translate to pdf:", node); // eslint-disable-line no-console
        node.remove();
      }
    }
  });
  return pdf;
};

jsPDFAPI.addSVG = function (element, x, y, options) {
  options = (options === undefined ? {} : options);
  options.x_offset = x;
  options.y_offset = y;

  if (typeof element === 'string') {
    element = new DOMParser().parseFromString(element, 'text/xml').documentElement;
  }
  svgElementToPdf(element, this, options);
  return this;
};
<|endoftext|>