<commit_before>// set up SVG for D3
var width  = 640,
    height = 480,
    colors = d3.scale.category10();

var svg = d3.select('#graph')
  .append('svg')
  .attr('width', width)
  .attr('height', height);

// set up initial nodes and links
//  - nodes are known by 'id', not by index in array.
//  - reflexive edges are indicated on the node (as a bold black circle).
//  - links are always source < target; edge directions are set by 'left' and 'right'.
var nodes = [
    {id: 0, reflexive: false, infected: false, threshold: 1},
    {id: 1, reflexive: false, infected: false, threshold: 1},
    {id: 2, reflexive: false, infected: false, threshold: 1}
  ],
  lastNodeId = 2,
  links = [
    {source: nodes[0], target: nodes[1], left: true, right: true },
    {source: nodes[1], target: nodes[2], left: true, right: true }
  ];

// init D3 force layout
var force = d3.layout.force()
    .nodes(nodes)
    .links(links)
    .size([width, height])
    .linkDistance(150)
    .charge(-500)
    .on('tick', tick)

// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
    .attr('id', 'end-arrow')
    .attr('viewBox', '0 -5 10 10')
    .attr('refX', 6)
    .attr('markerWidth', 3)
    .attr('markerHeight', 3)
    .attr('orient', 'auto')
  .append('svg:path')
    .attr('d', 'M0,-5L10,0L0,5')
    .attr('fill', '#000');

svg.append('svg:defs').append('svg:marker')
    .attr('id', 'start-arrow')
    .attr('viewBox', '0 -5 10 10')
    .attr('refX', 4)
    .attr('markerWidth', 3)
    .attr('markerHeight', 3)
    .attr('orient', 'auto')
  .append('svg:path')
    .attr('d', 'M10,-5L0,0L10,5')
    .attr('fill', '#000');

// line displayed when dragging new nodes
var drag_line = svg.append('svg:path')
  .attr('class', 'link dragline hidden')
  .attr('d', 'M0,0L0,0');

// handles to link and node element groups
var path = svg.append('svg:g').selectAll('path'),
    circle = svg.append('svg:g').selectAll('g');

// mouse event vars
var selected_node = null,
    selected_link = null,
    mousedown_link = null,
    mousedown_node = null,
    mouseup_node = null;

function resetMouseVars() {
  mousedown_node = null;
  mouseup_node = null;
  mousedown_link = null;
}

// update force layout (called automatically each iteration)
function tick() {
  // draw directed edges with proper padding from node centers
  path.attr('d', function(d) {
    var deltaX = d.target.x - d.source.x,
        deltaY = d.target.y - d.source.y,
        dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
        normX = deltaX / dist,
        normY = deltaY / dist,
        sourcePadding = d.left ? 17 : 12,
        targetPadding = d.right ? 17 : 12,
        sourceX = d.source.x + (sourcePadding * normX),
        sourceY = d.source.y + (sourcePadding * normY),
        targetX = d.target.x - (targetPadding * normX),
        targetY = d.target.y - (targetPadding * normY);
    return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
  });

  circle.attr('transform', function(d) {
    return 'translate(' + d.x + ',' + d.y + ')';
  });
}

// update graph (called when needed)
function restart() {
  // path (link) group
  path = path.data(links);

  // update existing links
  path.classed('selected', function(d) { return d === selected_link; })
    .style('marker-start', function(d) { return 'url(#start-arrow)'; })
    .style('marker-end', function(d) { return 'url(#end-arrow)'; });

  // add new links
  path.enter().append('svg:path')
    .attr('class', 'link')
    .classed('selected', function(d) { return d === selected_link; })
    .style('marker-start', function(d) { return 'url(#start-arrow)'; })
    .style('marker-end', function(d) { return 'url(#end-arrow)'; })
    .on('mousedown', function(d) {
      if(d3.event.ctrlKey) return;

      // select link
      mousedown_link = d;
      if(mousedown_link === selected_link) selected_link = null;
      else selected_link = mousedown_link;
      selected_node = null;
      restart();
    });

  // remove old links
  path.exit().remove();

  // circle (node) group
  // NB: the function arg is crucial here! nodes are known by id, not by index!
  circle = circle.data(nodes, function(d) { return d.id; });

  // update existing nodes (reflexive & selected visual states)
  circle.selectAll('circle')
    .style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
    .classed('reflexive', function(d) { return d.reflexive; });

  // add new nodes
  var g = circle.enter().append('svg:g');

  g.append('svg:circle')
    .attr('class', 'node')
    .attr('r', 12)
    .style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
    .style('stroke', function(d) { return d3.rgb(colors(d.id)).darker().toString(); })
    .classed('reflexive', function(d) { return d.reflexive; })
    .on('mouseover', function(d) {
      if(!mousedown_node || d === mousedown_node) return;
      // enlarge target node
      d3.select(this).attr('transform', 'scale(1.1)');
    })
    .on('mouseout', function(d) {
      if(!mousedown_node || d === mousedown_node) return;
      // unenlarge target node
      d3.select(this).attr('transform', '');
    })
    .on('mousedown', function(d) {
      if(d3.event.ctrlKey) return;

      // select node
      mousedown_node = d;
      if(mousedown_node === selected_node) selected_node = null;
      else selected_node = mousedown_node;
      selected_link = null;

      // reposition drag line
      drag_line
        .style('marker-end', 'url(#end-arrow)')
        .classed('hidden', false)
        .attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y);

      restart();
    })
    .on('mouseup', function(d) {
      if(!mousedown_node) return;

      // needed by FF
      drag_line
        .classed('hidden', true)
        .style('marker-end', 'url(#end-arrow)');

      // check for drag-to-self
      mouseup_node = d;
      if(mouseup_node === mousedown_node) { resetMouseVars(); return; }

      // unenlarge target node
      d3.select(this).attr('transform', '');

      // add link to graph (update if exists)
      // NB: links are strictly source < target; arrows separately specified by booleans
      var source, target, direction;
      if(mousedown_node.id < mouseup_node.id) {
        source = mousedown_node;
        target = mouseup_node;
        direction = 'right';
      } else {
        source = mouseup_node;
        target = mousedown_node;
        direction = 'left';
      }

      var link;
      link = links.filter(function(l) {
        return (l.source === source && l.target === target);
      })[0];

      if(link) {
        link[direction] = true;
      } else {
        link = {source: source, target: target, left: false, right: false};
        link[direction] = true;
        links.push(link);
      }

      // select new link
      selected_link = link;
      selected_node = null;
      restart();
    });

  // show node IDs
  g.append('svg:text')
      .attr('x', 0)
      .attr('y', 4)
      .attr('class', 'id')
      .text(function(d) { return d.id; });

  // remove old nodes
  circle.exit().remove();

  // set the graph in motion
  force.start();
}

function mousedown() {
  // prevent I-bar on drag
  //d3.event.preventDefault();

  // because :active only works in WebKit?
  svg.classed('active', true);

  if(d3.event.ctrlKey || mousedown_node || mousedown_link) return;

  // insert new node at point
  var point = d3.mouse(this),
      node = {id: ++lastNodeId, reflexive: false, infected: false, threshold: 1};
  node.x = point[0];
  node.y = point[1];
  nodes.push(node);

  restart();
}

function mousemove() {
  if(!mousedown_node) return;

  // update drag line
  drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);

  restart();
}

function mouseup() {
  if(mousedown_node) {
    // hide drag line
    drag_line
      .classed('hidden', true)
      .style('marker-end', 'url(#end-arrow)');
  }

  // because :active only works in WebKit?
  svg.classed('active', false);

  // clear mouse event vars
  resetMouseVars();

  selected_link = null;

  restart();
}

function spliceLinksForNode(node) {
  var toSplice = links.filter(function(l) {
    return (l.source === node || l.target === node);
  });
  toSplice.map(function(l) {
    links.splice(links.indexOf(l), 1);
  });
}

// only respond once per keydown
var lastKeyDown = -1;

function keydown() {
  d3.event.preventDefault();

  if(lastKeyDown !== -1) return;
  lastKeyDown = d3.event.keyCode;

  // ctrl
  if(d3.event.keyCode === 17) {
    circle.call(force.drag);
    svg.classed('ctrl', true);
  }

  if(!selected_node && !selected_link) return;
  switch(d3.event.keyCode) {
    case 8: // backspace
    case 46: // delete
      if(selected_node) {
        nodes.splice(nodes.indexOf(selected_node), 1);
        spliceLinksForNode(selected_node);
      } else if(selected_link) {
        links.splice(links.indexOf(selected_link), 1);
      }
      selected_link = null;
      selected_node = null;
      restart();
      break;
/*    case 66: // B
      if(selected_link) {
        // set link direction to both left and right
        selected_link.left = true;
        selected_link.right = true;
      }
      restart();
      break;
    case 76: // L
      if(selected_link) {
        // set link direction to left only
        selected_link.left = true;
        selected_link.right = false;
      }
      restart();
      break;
    case 82: // R
      if(selected_node) {
        // toggle node reflexivity
        selected_node.reflexive = !selected_node.reflexive;
      } else if(selected_link) {
        // set link direction to right only
        selected_link.left = false;
        selected_link.right = true;
      }
      restart();
      break;*/
  }
}

function keyup() {
  lastKeyDown = -1;

  // ctrl
  if(d3.event.keyCode === 17) {
    circle
      .on('mousedown.drag', null)
      .on('touchstart.drag', null);
    svg.classed('ctrl', false);
  }
}

// app starts here
svg.on('mousedown', mousedown)
  .on('mousemove', mousemove)
  .on('mouseup', mouseup);
d3.select(window)
  .on('keydown', keydown)
  .on('keyup', keyup);
restart();<commit_msg>Fixed a bug that prevent selecting an edge.<commit_after>// set up SVG for D3
var width  = 640,
    height = 480,
    colors = d3.scale.category10();

var svg = d3.select('#graph')
  .append('svg')
  .attr('width', width)
  .attr('height', height);

// set up initial nodes and links
//  - nodes are known by 'id', not by index in array.
//  - reflexive edges are indicated on the node (as a bold black circle).
//  - links are always source < target; edge directions are set by 'left' and 'right'.
var nodes = [
    {id: 0, reflexive: false, infected: false, threshold: 1},
    {id: 1, reflexive: false, infected: false, threshold: 1},
    {id: 2, reflexive: false, infected: false, threshold: 1}
  ],
  lastNodeId = 2,
  links = [
    {source: nodes[0], target: nodes[1], left: true, right: true },
    {source: nodes[1], target: nodes[2], left: true, right: true }
  ];

// init D3 force layout
var force = d3.layout.force()
    .nodes(nodes)
    .links(links)
    .size([width, height])
    .linkDistance(150)
    .charge(-500)
    .on('tick', tick)

// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
    .attr('id', 'end-arrow')
    .attr('viewBox', '0 -5 10 10')
    .attr('refX', 6)
    .attr('markerWidth', 3)
    .attr('markerHeight', 3)
    .attr('orient', 'auto')
  .append('svg:path')
    .attr('d', 'M0,-5L10,0L0,5')
    .attr('fill', '#000');

svg.append('svg:defs').append('svg:marker')
    .attr('id', 'start-arrow')
    .attr('viewBox', '0 -5 10 10')
    .attr('refX', 4)
    .attr('markerWidth', 3)
    .attr('markerHeight', 3)
    .attr('orient', 'auto')
  .append('svg:path')
    .attr('d', 'M10,-5L0,0L10,5')
    .attr('fill', '#000');

// line displayed when dragging new nodes
var drag_line = svg.append('svg:path')
  .attr('class', 'link dragline hidden')
  .attr('d', 'M0,0L0,0');

// handles to link and node element groups
var path = svg.append('svg:g').selectAll('path'),
    circle = svg.append('svg:g').selectAll('g');

// mouse event vars
var selected_node = null,
    selected_link = null,
    mousedown_link = null,
    mousedown_node = null,
    mouseup_node = null;

function resetMouseVars() {
  mousedown_node = null;
  mouseup_node = null;
  mousedown_link = null;
}

// update force layout (called automatically each iteration)
function tick() {
  // draw directed edges with proper padding from node centers
  path.attr('d', function(d) {
    var deltaX = d.target.x - d.source.x,
        deltaY = d.target.y - d.source.y,
        dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
        normX = deltaX / dist,
        normY = deltaY / dist,
        sourcePadding = d.left ? 17 : 12,
        targetPadding = d.right ? 17 : 12,
        sourceX = d.source.x + (sourcePadding * normX),
        sourceY = d.source.y + (sourcePadding * normY),
        targetX = d.target.x - (targetPadding * normX),
        targetY = d.target.y - (targetPadding * normY);
    return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
  });

  circle.attr('transform', function(d) {
    return 'translate(' + d.x + ',' + d.y + ')';
  });
}

// update graph (called when needed)
function restart() {
  // path (link) group
  path = path.data(links);

  // update existing links
  path.classed('selected', function(d) { return d === selected_link; })
    .style('marker-start', function(d) { return 'url(#start-arrow)'; })
    .style('marker-end', function(d) { return 'url(#end-arrow)'; });

  // add new links
  path.enter().append('svg:path')
    .attr('class', 'link')
    .classed('selected', function(d) { return d === selected_link; })
    .style('marker-start', function(d) { return 'url(#start-arrow)'; })
    .style('marker-end', function(d) { return 'url(#end-arrow)'; })
    .on('mousedown', function(d) {
      if(d3.event.ctrlKey) return;

      // select link
      mousedown_link = d;
      if(mousedown_link === selected_link) selected_link = null;
      else selected_link = mousedown_link;
      selected_node = null;
      restart();
    });

  // remove old links
  path.exit().remove();

  // circle (node) group
  // NB: the function arg is crucial here! nodes are known by id, not by index!
  circle = circle.data(nodes, function(d) { return d.id; });

  // update existing nodes (reflexive & selected visual states)
  circle.selectAll('circle')
    .style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
    .classed('reflexive', function(d) { return d.reflexive; });

  // add new nodes
  var g = circle.enter().append('svg:g');

  g.append('svg:circle')
    .attr('class', 'node')
    .attr('r', 12)
    .style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
    .style('stroke', function(d) { return d3.rgb(colors(d.id)).darker().toString(); })
    .classed('reflexive', function(d) { return d.reflexive; })
    .on('mouseover', function(d) {
      if(!mousedown_node || d === mousedown_node) return;
      // enlarge target node
      d3.select(this).attr('transform', 'scale(1.1)');
    })
    .on('mouseout', function(d) {
      if(!mousedown_node || d === mousedown_node) return;
      // unenlarge target node
      d3.select(this).attr('transform', '');
    })
    .on('mousedown', function(d) {
      if(d3.event.ctrlKey) return;

      // select node
      mousedown_node = d;
      if(mousedown_node === selected_node) selected_node = null;
      else selected_node = mousedown_node;
      selected_link = null;

      // reposition drag line
      drag_line
        .style('marker-end', 'url(#end-arrow)')
        .classed('hidden', false)
        .attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y);

      restart();
    })
    .on('mouseup', function(d) {
      if(!mousedown_node) return;

      // needed by FF
      drag_line
        .classed('hidden', true)
        .style('marker-end', 'url(#end-arrow)');

      // check for drag-to-self
      mouseup_node = d;
      if(mouseup_node === mousedown_node) { resetMouseVars(); return; }

      // unenlarge target node
      d3.select(this).attr('transform', '');

      // add link to graph (update if exists)
      // NB: links are strictly source < target; arrows separately specified by booleans
      var source, target, direction;
      if(mousedown_node.id < mouseup_node.id) {
        source = mousedown_node;
        target = mouseup_node;
        direction = 'right';
      } else {
        source = mouseup_node;
        target = mousedown_node;
        direction = 'left';
      }

      var link;
      link = links.filter(function(l) {
        return (l.source === source && l.target === target);
      })[0];

      if(link) {
        link[direction] = true;
      } else {
        link = {source: source, target: target, left: false, right: false};
        link[direction] = true;
        links.push(link);
      }

      // select new link
      selected_link = link;
      selected_node = null;
      restart();
    });

  // show node IDs
  g.append('svg:text')
      .attr('x', 0)
      .attr('y', 4)
      .attr('class', 'id')
      .text(function(d) { return d.id; });

  // remove old nodes
  circle.exit().remove();

  // set the graph in motion
  force.start();
}

function mousedown() {
  // prevent I-bar on drag
  //d3.event.preventDefault();

  // because :active only works in WebKit?
  svg.classed('active', true);

  if(d3.event.ctrlKey || mousedown_node || mousedown_link) return;

  // insert new node at point
  var point = d3.mouse(this),
      node = {id: ++lastNodeId, reflexive: false, infected: false, threshold: 1};
  node.x = point[0];
  node.y = point[1];
  nodes.push(node);

  restart();
}

function mousemove() {
  if(!mousedown_node) return;

  // update drag line
  drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);

  restart();
}

function mouseup() {
  if(mousedown_node) {
    // hide drag line
    drag_line
      .classed('hidden', true)
      .style('marker-end', 'url(#end-arrow)');
  }

  // because :active only works in WebKit?
  svg.classed('active', false);

  // clear mouse event vars
  resetMouseVars();

  restart();
}

function spliceLinksForNode(node) {
  var toSplice = links.filter(function(l) {
    return (l.source === node || l.target === node);
  });
  toSplice.map(function(l) {
    links.splice(links.indexOf(l), 1);
  });
}

// only respond once per keydown
var lastKeyDown = -1;

function keydown() {
  d3.event.preventDefault();

  if(lastKeyDown !== -1) return;
  lastKeyDown = d3.event.keyCode;

  // ctrl
  if(d3.event.keyCode === 17) {
    circle.call(force.drag);
    svg.classed('ctrl', true);
  }

  if(!selected_node && !selected_link) return;
  switch(d3.event.keyCode) {
    case 8: // backspace
    case 46: // delete
      if(selected_node) {
        nodes.splice(nodes.indexOf(selected_node), 1);
        spliceLinksForNode(selected_node);
      } else if(selected_link) {
        links.splice(links.indexOf(selected_link), 1);
      }
      selected_link = null;
      selected_node = null;
      restart();
      break;
/*    case 66: // B
      if(selected_link) {
        // set link direction to both left and right
        selected_link.left = true;
        selected_link.right = true;
      }
      restart();
      break;
    case 76: // L
      if(selected_link) {
        // set link direction to left only
        selected_link.left = true;
        selected_link.right = false;
      }
      restart();
      break;
    case 82: // R
      if(selected_node) {
        // toggle node reflexivity
        selected_node.reflexive = !selected_node.reflexive;
      } else if(selected_link) {
        // set link direction to right only
        selected_link.left = false;
        selected_link.right = true;
      }
      restart();
      break;*/
  }
}

function keyup() {
  lastKeyDown = -1;

  // ctrl
  if(d3.event.keyCode === 17) {
    circle
      .on('mousedown.drag', null)
      .on('touchstart.drag', null);
    svg.classed('ctrl', false);
  }
}

// app starts here
svg.on('mousedown', mousedown)
  .on('mousemove', mousemove)
  .on('mouseup', mouseup);
d3.select(window)
  .on('keydown', keydown)
  .on('keyup', keyup);
restart();<|endoftext|>


<commit_before>/* global DOMParser */

export default class Validation {

	static messages = {
		'min':        (val) => `This field needs at least ${val} items.`,
		'max':        (val) => `This field can't have more than ${val} items.`,
		'min_value':  (val) => `This number must be more than ${val}.`,
		'max_value':  (val) => `This number must be less than ${val}.`,
		'min_length': (val) => `This field must be at least ${val} characters long.`,
		'max_length': (val) => `This field can't be more than ${val} characters long.`
	};

	static transform(validationRule, message = null) {
		let
			[rule, value] = validationRule.split(/:(.*)/, 2),
			tranformedRule = {};

		if(
			[
				'min_value', 'max_value',
				'min_length', 'max_length',
				'min', 'max', 'length'
			].indexOf(rule) !== -1
		) {
			value = parseFloat(value, 2);
		}

		switch(rule) {
			case 'string':
				tranformedRule = { type: 'string' };
				break;

			case 'integer':
				tranformedRule = { type: 'integer' };
				break;

			case 'in':
				tranformedRule = {
					type: 'enum',
					enum: value.split(',').map(value => {
						const number = Number(value);
						return isNaN(number) ? value : number;
					})
				};
				break;

			case 'required':
			case 'present':
				tranformedRule = {
					required: true,
					message: 'This field is required.'
				};
				break;

			// These are possible client-side but don't currently
			// have a corresponding implementation server-side.

			// case 'number':
			// 	tranformedRule = { type: 'number' };
			// 	break;

			// case 'boolean':
			// 	tranformedRule = { type: 'boolean' };
			// 	break;

			// case 'float':
			// 	tranformedRule = { type: 'float' };
			// 	break;

			// case 'array':
			// 	tranformedRule = { type: 'array' };
			// 	break;

			// case 'object':
			// 	tranformedRule = { type: 'object' };
			// 	break;

			case 'min':
			case 'min_value':
			case 'min_length':
				tranformedRule = { min: value };
				break;

			case 'max':
			case 'max_value':
			case 'max_length':
				tranformedRule = { max: value };
				break;

			case 'length':
				tranformedRule = { len: value };
				break;

			case 'regex':
				tranformedRule = {
					regex: value,
					message: 'The format of this field is invalid.',

					validator(rule, value, cb) {
						if(
							value &&
							value.length &&
							!value.match(new RegExp(rule.regex))
						) {
							return cb(rule.message);
						}

						return cb();
					}
				};
				break;

			case 'slug':
				tranformedRule = {
					message: 'This field should only have lowercase alphanumeric characters (a-z and 0-9). Separate keywords with dashes.',

					validator(rule, value, cb) {
						if(
							value &&
							value.length &&
							!value.match(new RegExp('^[a-z0-9]+(?:-[a-z0-9]+)*$'))
						) {
							return cb(rule.message);
						}

						return cb();
					}
				};
				break;
		}

		if(message) {
			tranformedRule.message = message;
		}
		else if(Validation.messages[rule]) {
			tranformedRule.message = Validation.messages[rule](value);
		}

		return tranformedRule;
	}

}
<commit_msg>Add our client-side "max_length_without_html" rule that uses the browser's DOMParser API to strip HTML out before checking the length.<commit_after>/* global DOMParser */

export default class Validation {

	static messages = {
		'min':        (val) => `This field needs at least ${val} items.`,
		'max':        (val) => `This field can't have more than ${val} items.`,
		'min_value':  (val) => `This number must be more than ${val}.`,
		'max_value':  (val) => `This number must be less than ${val}.`,
		'min_length': (val) => `This field must be at least ${val} characters long.`,
		'max_length': (val) => `This field can't be more than ${val} characters long.`
	};

	static transform(validationRule, message = null) {
		let
			[rule, value] = validationRule.split(/:(.*)/, 2),
			tranformedRule = {};

		if(
			[
				'min_value', 'max_value',
				'min_length', 'max_length',
				'min', 'max', 'length'
			].indexOf(rule) !== -1
		) {
			value = parseFloat(value, 2);
		}

		switch(rule) {
			case 'string':
				tranformedRule = { type: 'string' };
				break;

			case 'integer':
				tranformedRule = { type: 'integer' };
				break;

			case 'in':
				tranformedRule = {
					type: 'enum',
					enum: value.split(',').map(value => {
						const number = Number(value);
						return isNaN(number) ? value : number;
					})
				};
				break;

			case 'required':
			case 'present':
				tranformedRule = {
					required: true,
					message: 'This field is required.'
				};
				break;

			// These are possible client-side but don't currently
			// have a corresponding implementation server-side.

			// case 'number':
			// 	tranformedRule = { type: 'number' };
			// 	break;

			// case 'boolean':
			// 	tranformedRule = { type: 'boolean' };
			// 	break;

			// case 'float':
			// 	tranformedRule = { type: 'float' };
			// 	break;

			// case 'array':
			// 	tranformedRule = { type: 'array' };
			// 	break;

			// case 'object':
			// 	tranformedRule = { type: 'object' };
			// 	break;

			case 'min':
			case 'min_value':
			case 'min_length':
				tranformedRule = { min: value };
				break;

			case 'max':
			case 'max_value':
			case 'max_length':
				tranformedRule = { max: value };
				break;

			case 'length':
				tranformedRule = { len: value };
				break;

			case 'regex':
				tranformedRule = {
					regex: value,
					message: 'The format of this field is invalid.',

					validator(rule, value, cb) {
						if(
							value &&
							value.length &&
							!value.match(new RegExp(rule.regex))
						) {
							return cb(rule.message);
						}

						return cb();
					}
				};
				break;

			case 'slug':
				tranformedRule = {
					message: 'This field should only have lowercase alphanumeric characters (a-z and 0-9). Separate keywords with dashes.',

					validator(rule, value, cb) {
						if(
							value &&
							value.length &&
							!value.match(new RegExp('^[a-z0-9]+(?:-[a-z0-9]+)*$'))
						) {
							return cb(rule.message);
						}

						return cb();
					}
				};
				break;

			case 'max_length_without_html': {
				const maxLength = parseFloat(value, 2);

				tranformedRule = {
					message: `This field should not be more than ${maxLength} characters.`,

					validator(rule, value, cb) {
						const html = new DOMParser().parseFromString(value || '', 'text/html');

						if(value && html.body.textContent.length > maxLength) {
							return cb(rule.message);
						}

						return cb();
					}
				};
				break;
			}
		}

		if(message) {
			tranformedRule.message = message;
		}
		else if(Validation.messages[rule]) {
			tranformedRule.message = Validation.messages[rule](value);
		}

		return tranformedRule;
	}

}
<|endoftext|>


<commit_before>'use strict'

if(typeof require !== 'undefined') {
    var EventDispatcher = require('./eventdispatcher')
}

function Reactant(value) {
    EventDispatcher.call(this)
    this.value = value
}
Reactant.prototype = {
    __proto__: EventDispatcher.prototype,
    setFunc: function(func, onMod) {
        var removal = this.setFuncSilent(func, onMod)
        this.proc()
        return removal
    },
    super_proc: EventDispatcher.prototype.proc,
    proc: function() {
        var prev = this.lastValue
        var curr = this.value
        if(this.equals(prev, curr)) return

        this.lastValue = curr // must be saved here because it might be altered during event propagation
        this.super_proc(prev, curr)
    },
    listen_pc: function(callback) {
        var result = this.listen(callback)  /// TODO: test for removal called during first call of callback
        callback(undefined, this.value)
        return result
    },
    and: function(other) {
        return this.compose(other, function(a, b) { return a && b })
    },
    or: function(other) {
        return this.compose(other, function(a, b) { return a || b })
    },
    transform: function(func) {
        var result = new Reactant()
        result.setFunc(function() { return func(this.value) }.bind(this))

        ////////////////////////////////////////////////////// TODO: This listener needs to be removed when `result` has none of its own listeners, and re-added when it does
        //var remove = this.listen(function() { result.proc() })
        var remove = this.listen(result.proc.bind(result))

        return result
    },
    onCondition: function(predicate) {
        return this.transform(predicate).on(true)
    },
    on: function(target) {
        var result = new EventDispatcher()

        ////////////////////////////////////////////////////// TODO: This listener needs to be removed when `result` has none of its own listeners, and re-added when it does
        this.listen(function(prev, curr) {
            if(curr === target) // Use ==, ===, or deepEquals?? Might change depending on task.
                result.proc()
        })
        return result
    },
    onNot: function(target) {
        var result = new EventDispatcher()

        ////////////////////////////////////////////////////// TODO: This listener needs to be removed when `result` has none of its own listeners, and re-added when it does
        this.listen(function(prev, curr) {
            if(curr !== target)
                result.proc()
        })
        return result
    },
    /////////////////////////////////////////////// TODO: reactant.filter

    setFuncSilent: function(func, onMod) {
        if(this.breakHierarchy) this.breakHierarchy()
        this.breakHierarchy = undefined

        this.valueFunc = func

        if(onMod)
            this.breakHierarchy = onMod.listen(this.proc.bind(this))
        return this.breakHierarchy
    },
    compose: function(b, func) {
        var a = this
        var result = new Reactant()
        result.setFunc(function() {
            return func(a.value, b.value)
        })

        ////////////////////////////////////////////////////// TODO: These listeners need to be removed when `result` has none of its own listeners, and re-added when it does
        var removeA = a.listen(result.proc.bind(result))
        var removeB = b.listen(result.proc.bind(result))

        return result
    },
    assignment: function(value) {
        this.setFunc(function() { return value })
    },
    equals: function(a, b) {
        if(a === b)
            return true
        if(a && a.equals)
            return a.equals(b)
        if(b && b.equals)
            return b.equals(a)
        return false
    }
}
Object.defineProperties(
    Reactant.prototype, {
        value: {
            get: function() {
                if(this.valueFunc) return this.valueFunc()
                return undefined
            },
            set: function(value) {
                this.assignment(value)
            }
        },
        not: {
            get: function() { return this.transform(function(a) { return !a }) }
        },
        negative: {
            get: function() { return this.transform(function(a) { return -a }) }
        }
    }
)

if(typeof module !== 'undefined') {
    module.exports = Reactant
}
<commit_msg>Made all reactant equality comparisons use the equals method and added anyTruthy/anyFalsy.<commit_after>'use strict'

if(typeof require !== 'undefined') {
    var EventDispatcher = require('./eventdispatcher')
}

function Reactant(value) {
    EventDispatcher.call(this)
    this.value = value
}
Reactant.prototype = {
    __proto__: EventDispatcher.prototype,
    setFunc: function(func, onMod) {
        var removal = this.setFuncSilent(func, onMod)
        this.proc()
        return removal
    },
    super_proc: EventDispatcher.prototype.proc,
    proc: function() {
        var prev = this.lastValue
        var curr = this.value
        if(this.equals(prev, curr)) return

        this.lastValue = curr // must be saved here because it might be altered during event propagation
        this.super_proc(prev, curr)
    },
    listen_pc: function(callback) {
        var result = this.listen(callback)  /// TODO: test for removal called during first call of callback
        callback(undefined, this.value)
        return result
    },
    and: function(other) {
        return this.compose(other, function(a, b) { return a && b })
    },
    or: function(other) {
        return this.compose(other, function(a, b) { return a || b })
    },
    transform: function(func) {
        var result = new Reactant()
        result.setFunc(function() { return func(this.value) }.bind(this))

        ////////////////////////////////////////////////////// TODO: This listener needs to be removed when `result` has none of its own listeners, and re-added when it does
        //var remove = this.listen(function() { result.proc() })
        var remove = this.listen(result.proc.bind(result))

        return result
    },
    onCondition: function(predicate) {
        return this.transform(predicate).on(true)
    },
    on: function(target) {
        var result = new EventDispatcher()

        ////////////////////////////////////////////////////// TODO: This listener needs to be removed when `result` has none of its own listeners, and re-added when it does
        this.listen(function(prev, curr) {
            if(this.equals(curr, target))
                result.proc()
        }.bind(this))
        return result
    },
    onNot: function(target) {
        var result = new EventDispatcher()

        ////////////////////////////////////////////////////// TODO: This listener needs to be removed when `result` has none of its own listeners, and re-added when it does
        this.listen(function(prev, curr) {
            if(!this.equals(curr, target))
                result.proc()
        }.bind(this))
        return result
    },
    /////////////////////////////////////////////// TODO: reactant.filter

    setFuncSilent: function(func, onMod) {
        if(this.breakHierarchy) this.breakHierarchy()
        this.breakHierarchy = undefined

        this.valueFunc = func

        if(onMod)
            this.breakHierarchy = onMod.listen(this.proc.bind(this))
        return this.breakHierarchy
    },
    compose: function(b, func) {
        var a = this
        var result = new Reactant()
        result.setFunc(function() {
            return func(a.value, b.value)
        })

        ////////////////////////////////////////////////////// TODO: These listeners need to be removed when `result` has none of its own listeners, and re-added when it does
        var removeA = a.listen(result.proc.bind(result))
        var removeB = b.listen(result.proc.bind(result))

        return result
    },
    assignment: function(value) {
        this.setFunc(function() { return value })
    },
    equals: function(a, b) {
        if(a === b)
            return true
        if(a && a.equals)
            return a.equals(b)
        if(b && b.equals)
            return b.equals(a)
        return false
    }
}
Object.defineProperties(
    Reactant.prototype, {
        value: {
            get: function() {
                if(this.valueFunc) return this.valueFunc()
                return undefined
            },
            set: function(value) {
                this.assignment(value)
            }
        },
        not: {
            get: function() { return this.transform(function(a) { return !a }) }
        },
        negative: {
            get: function() { return this.transform(function(a) { return -a }) }
        },
        anyTruthy: {
            get: function() {
                var result = new EventDispatcher()

                this.listen(function(prev, curr) {
                    if(curr)
                        result.proc()
                })
                return result
            }
        },
        anyFalsy: {
            get: function() {
                // TODO: EventDispatcher.transform
                var result = new EventDispatcher()

                this.listen(function(prev, curr) {
                    if(!curr)
                        result.proc()
                })
                return result
            }
        }
    }
)

if(typeof module !== 'undefined') {
    module.exports = Reactant
}
<|endoftext|>


<commit_before>(function () {
  "use strict";

  var rose = document.getElementById("rose");
  var positionCurrent = {
    lat: null,
    lng: null,
    hng: null
  };

  var debug = false;

  var positionLat = document.getElementById("position-lat");
  var positionLng = document.getElementById("position-lng");
  var positionHng = document.getElementById("position-hng");
  var debugOrientation = document.getElementById("debug-orientation");
  var debugOrientationDefault = document.getElementById("debug-orientation-default");

  var popupButtons = document.querySelectorAll(".btn-popup");
  var popup = document.getElementById("popup");
  var popupContents = document.getElementById("popup-contents");
  var popupInners = document.querySelectorAll(".poppup__iner");

  var btnLockOrientation = document.getElementById("btn-lock-orientation");
  var btnNightmode = document.getElementById("btn-nightmode");
  var btnMap = document.getElementById("btn-map");
  var btnInfo = document.getElementById("btn-info");

  var orientationWarningShown = false;

  var headingPrevious = 0;
  var rotations = 0;
  var isOrientationLockable = false;
  var isOrientationLocked;
  var isNightMode;

  var defaultOrientation;

  function getBrowserOrientation() {
    var orientation;
    if (screen.orientation && screen.orientation.type) {
      orientation = screen.orientation.type;
    } else {
      orientation = screen.orientation ||
                    screen.mozOrientation ||
                    screen.msOrientation;
    }

    return orientation;
  }

  function browserUnlockOrientation() {
    if (screen.orientation && screen.orientation.unlock) {
      screen.orientation.unlock();
    } else if (screen.unlockOrientation) {
      screen.unlockOrientation();
    } else if (screen.mozUnlockOrientation) {
      screen.mozUnlockOrientation();
    } else if (screen.msUnlockOrientation) {
      screen.msUnlockOrientation();
    }
  }

  function getBrowserFullscreenElement() {
    if (typeof document.fullscreenElement !== "undefined") {
      return document.fullscreenElement;
    } else if (typeof document.webkitFullscreenElement !== "undefined") {
      return document.webkitFullscreenElement;
    } else if (typeof document.mozFullScreenElement !== "undefined") {
      return document.mozFullScreenElement;
    } else if (typeof document.msFullscreenElement !== "undefined") {
      return document.msFullscreenElement;
    }
  }

  function browserRequestFullscreen() {
    if (document.documentElement.requestFullscreen) {
      document.documentElement.requestFullscreen();
    } else if (document.documentElement.webkitRequestFullscreen) {
      document.documentElement.webkitRequestFullscreen();
    } else if (document.documentElement.mozRequestFullScreen) {
      document.documentElement.mozRequestFullScreen();
    } else if (document.documentElement.msRequestFullscreen) {
      document.documentElement.msRequestFullscreen();
    }
  }

  function browserExitFullscreen() {
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }
  }

  function onOrientationChange(event) {
    var heading = event.alpha;
    var orientation = getBrowserOrientation();

    if (typeof heading !== "undefined" && typeof orientation !== "undefined") {
      var diff = Math.abs(heading - headingPrevious);

      if(diff > 300) {
        if(heading - headingPrevious < 0) {
          rotations++;
        } else {
          rotations--;
        }
      }

      if (debug) {
        debugOrientation.textContent = orientation;
      }

      var adjustment = 0;
      var currentOrientation = orientation.split("-");

      if (defaultOrientation === "landscape") {
        adjustment -= 90;
      }

      if (defaultOrientation !== currentOrientation[0]) {
        if (defaultOrientation === "landscape") {
          adjustment -= 270;
        } else {
          adjustment -= 90;
        }
      }

      if (currentOrientation[1] === "secondary") {
        adjustment -= 180;
      }

      headingPrevious = heading;

      positionCurrent.hng = heading + adjustment;

      var phase = positionCurrent.hng < 0 ? 360 + positionCurrent.hng : positionCurrent.hng;
      positionHng.textContent = (360 - phase | 0) + "°";

      if (typeof rose.style.transform !== "undefined") {
        rose.style.transform = "rotateZ(" + (positionCurrent.hng + rotations*360) + "deg)";
      } else if (typeof rose.style.webkitTransform !== "undefined") {
        rose.style.webkitTransform = "rotateZ(" + (positionCurrent.hng + rotations*360) + "deg)";
      }
    } else {
      positionHng.textContent = "n/a";
      showOrientationWarning();
    }
  }

  function showOrientationWarning() {
    if (!orientationWarningShown) {
      popupOpen("noorientation");
      orientationWarningShown = true;
    }
  }

  function onFullscreenChange() {
    if (isOrientationLockable && getBrowserFullscreenElement()) {
      if (screen.orientation && screen.orientation.lock) {
        screen.orientation.lock(getBrowserOrientation()).then(function () {
        }).catch(function () {
        });
      }
    } else {
      lockOrientationRequest(false);
    }
  }

  function toggleOrientationLockable(lockable) {
    isOrientationLockable = lockable;

    if (isOrientationLockable) {
      btnLockOrientation.classList.remove("btn--hide");

      btnNightmode.classList.add("column-25");
      btnNightmode.classList.remove("column-33");
      btnMap.classList.add("column-25");
      btnMap.classList.remove("column-33");
      btnInfo.classList.add("column-25");
      btnInfo.classList.remove("column-33");
    } else {
      btnLockOrientation.classList.add("btn--hide");

      btnNightmode.classList.add("column-33");
      btnNightmode.classList.remove("column-25");
      btnMap.classList.add("column-33");
      btnMap.classList.remove("column-25");
      btnInfo.classList.add("column-33");
      btnInfo.classList.remove("column-25");
    }
  }

  function checkLockable() {
    if (screen.orientation && screen.orientation.lock) {
      screen.orientation.lock(getBrowserOrientation()).then(function () {
        toggleOrientationLockable(true);
        browserUnlockOrientation(); //needed as chrome was locking orientation (even if not in fullscreen, bug??)
      }).catch(function (event) {
        if (event.code === 18) { // The page needs to be fullscreen in order to call lockOrientation(), but is lockable
          toggleOrientationLockable(true);
        } else {  // lockOrientation() is not available on this device (or other error)
          toggleOrientationLockable(false);
        }
      });
    } else {
      toggleOrientationLockable(false);
    }
  }

  function lockOrientationRequest(doLock) {
    if (isOrientationLockable) {
      if (doLock) {
        browserRequestFullscreen();
        lockOrientation(true);
      } else {
        browserUnlockOrientation();
        browserExitFullscreen();
        lockOrientation(false);
      }
    }
  }

  function lockOrientation(locked) {
    if (locked) {
      btnLockOrientation.classList.add("active");
    } else {
      btnLockOrientation.classList.remove("active");
    }

    isOrientationLocked = locked;
  }

  function toggleOrientationLock() {
    if (isOrientationLockable) {
      lockOrientationRequest(!isOrientationLocked);
    }
  }

  function locationUpdate(position) {
    positionCurrent.lat = position.coords.latitude;
    positionCurrent.lng = position.coords.longitude;

    positionLat.textContent = decimalToSexagesimal(positionCurrent.lat, "lat");
    positionLng.textContent = decimalToSexagesimal(positionCurrent.lng, "lng");
  }

  function locationUpdateFail(error) {
    positionLat.textContent = "n/a";
    positionLng.textContent = "n/a";
    console.log("location fail: ", error);
  }

  function setNightmode(on) {

    if (on) {
      btnNightmode.classList.add("active");
    } else {
      btnNightmode.classList.remove("active");
    }

    window.setTimeout(function() {
      if (on) {
        document.documentElement.classList.add("nightmode");
      } else {
        document.documentElement.classList.remove("nightmode");
      }
    }, 1);

    isNightMode = on;
  }

  function toggleNightmode() {
    setNightmode(!isNightMode);
  }

  function openMap() {
    window.open("https://www.google.com/maps/place/@" + positionCurrent.lat + "," + positionCurrent.lng + ",16z", "_blank");
  }

  function popupOpenFromClick(event) {
    popupOpen(event.currentTarget.dataset.name);
  }

  function popupOpen(name) {
    var i;
    for (i=0; i<popupInners.length; i++) {
      popupInners[i].classList.add("popup__inner--hide");
    }
    document.getElementById("popup-inner-" + name).classList.remove("popup__inner--hide");

    popup.classList.add("popup--show");
  }

  function popupClose() {
    popup.classList.remove("popup--show");
  }

  function popupContentsClick(event) {
    event.stopPropagation();
  }

  function decimalToSexagesimal(decimal, type) {
    var degrees = decimal | 0;
    var fraction = Math.abs(decimal - degrees);
    var minutes = (fraction * 60) | 0;
    var seconds = (fraction * 3600 - minutes * 60) | 0;

    var direction = "";
    var positive = degrees > 0;
    degrees = Math.abs(degrees);
    switch (type) {
      case "lat":
        direction = positive ? "N" : "S";
        break;
      case "lng":
        direction = positive ? "E" : "W";
        break;
    }

    return degrees + "° " + minutes + "' " + seconds + "\" " + direction;
  }

  if (screen.width > screen.height) {
    defaultOrientation = "landscape";
  } else {
    defaultOrientation = "portrait";
  }
  if (debug) {
    debugOrientationDefault.textContent = defaultOrientation;
  }

  window.addEventListener("deviceorientation", onOrientationChange);

  document.addEventListener("fullscreenchange", onFullscreenChange);
  document.addEventListener("webkitfullscreenchange", onFullscreenChange);
  document.addEventListener("mozfullscreenchange", onFullscreenChange);
  document.addEventListener("MSFullscreenChange", onFullscreenChange);

  btnLockOrientation.addEventListener("click", toggleOrientationLock);
  btnNightmode.addEventListener("click", toggleNightmode);
  btnMap.addEventListener("click", openMap);

  var i;
  for (i=0; i<popupButtons.length; i++) {
    popupButtons[i].addEventListener("click", popupOpenFromClick);
  }

  popup.addEventListener("click", popupClose);
  popupContents.addEventListener("click", popupContentsClick);

  navigator.geolocation.watchPosition(locationUpdate, locationUpdateFail, {
    enableHighAccuracy: false,
    maximumAge: 30000,
    timeout: 27000
  });

  setNightmode(false);
  checkLockable();

}());
<commit_msg>orientation check<commit_after>(function () {
  "use strict";

  var rose = document.getElementById("rose");
  var positionCurrent = {
    lat: null,
    lng: null,
    hng: null
  };

  var debug = false;

  var positionLat = document.getElementById("position-lat");
  var positionLng = document.getElementById("position-lng");
  var positionHng = document.getElementById("position-hng");
  var debugOrientation = document.getElementById("debug-orientation");
  var debugOrientationDefault = document.getElementById("debug-orientation-default");

  var popupButtons = document.querySelectorAll(".btn-popup");
  var popup = document.getElementById("popup");
  var popupContents = document.getElementById("popup-contents");
  var popupInners = document.querySelectorAll(".poppup__iner");

  var btnLockOrientation = document.getElementById("btn-lock-orientation");
  var btnNightmode = document.getElementById("btn-nightmode");
  var btnMap = document.getElementById("btn-map");
  var btnInfo = document.getElementById("btn-info");

  var orientationWarningShown = false;

  var headingPrevious = 0;
  var rotations = 0;
  var isOrientationLockable = false;
  var isOrientationLocked;
  var isNightMode;

  var defaultOrientation;

  function getBrowserOrientation() {
    var orientation;
    if (screen.orientation && screen.orientation.type) {
      orientation = screen.orientation.type;
    } else {
      orientation = screen.orientation ||
                    screen.mozOrientation ||
                    screen.msOrientation;
    }

    return orientation;
  }

  function browserUnlockOrientation() {
    if (screen.orientation && screen.orientation.unlock) {
      screen.orientation.unlock();
    } else if (screen.unlockOrientation) {
      screen.unlockOrientation();
    } else if (screen.mozUnlockOrientation) {
      screen.mozUnlockOrientation();
    } else if (screen.msUnlockOrientation) {
      screen.msUnlockOrientation();
    }
  }

  function getBrowserFullscreenElement() {
    if (typeof document.fullscreenElement !== "undefined") {
      return document.fullscreenElement;
    } else if (typeof document.webkitFullscreenElement !== "undefined") {
      return document.webkitFullscreenElement;
    } else if (typeof document.mozFullScreenElement !== "undefined") {
      return document.mozFullScreenElement;
    } else if (typeof document.msFullscreenElement !== "undefined") {
      return document.msFullscreenElement;
    }
  }

  function browserRequestFullscreen() {
    if (document.documentElement.requestFullscreen) {
      document.documentElement.requestFullscreen();
    } else if (document.documentElement.webkitRequestFullscreen) {
      document.documentElement.webkitRequestFullscreen();
    } else if (document.documentElement.mozRequestFullScreen) {
      document.documentElement.mozRequestFullScreen();
    } else if (document.documentElement.msRequestFullscreen) {
      document.documentElement.msRequestFullscreen();
    }
  }

  function browserExitFullscreen() {
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }
  }

  function onOrientationChange(event) {
    var heading = event.alpha;
    var orientation = getBrowserOrientation();

    if (typeof heading !== "undefined" && typeof orientation !== "undefined") {
      var diff = Math.abs(heading - headingPrevious);

      if(diff > 300) {
        if(heading - headingPrevious < 0) {
          rotations++;
        } else {
          rotations--;
        }
      }

      if (debug) {
        debugOrientation.textContent = orientation;
      }

      var adjustment = 0;
      var currentOrientation = orientation.split("-");

      if (defaultOrientation === "landscape") {
        adjustment -= 90;
      }

      if (defaultOrientation !== currentOrientation[0]) {
        if (defaultOrientation === "landscape") {
          adjustment -= 270;
        } else {
          adjustment -= 90;
        }
      }

      if (currentOrientation[1] === "secondary") {
        adjustment -= 180;
      }

      headingPrevious = heading;

      positionCurrent.hng = heading + adjustment;

      var phase = positionCurrent.hng < 0 ? 360 + positionCurrent.hng : positionCurrent.hng;
      positionHng.textContent = (360 - phase | 0) + "°";

      if (typeof rose.style.transform !== "undefined") {
        rose.style.transform = "rotateZ(" + (positionCurrent.hng + rotations*360) + "deg)";
      } else if (typeof rose.style.webkitTransform !== "undefined") {
        rose.style.webkitTransform = "rotateZ(" + (positionCurrent.hng + rotations*360) + "deg)";
      }
    } else {
      positionHng.textContent = "n/a";
      showOrientationWarning();
    }
  }

  function showOrientationWarning() {
    if (!orientationWarningShown) {
      popupOpen("noorientation");
      orientationWarningShown = true;
    }
  }

  function onFullscreenChange() {
    if (isOrientationLockable && getBrowserFullscreenElement()) {
      if (screen.orientation && screen.orientation.lock) {
        screen.orientation.lock(getBrowserOrientation()).then(function () {
        }).catch(function () {
        });
      }
    } else {
      lockOrientationRequest(false);
    }
  }

  function toggleOrientationLockable(lockable) {
    isOrientationLockable = lockable;

    if (isOrientationLockable) {
      btnLockOrientation.classList.remove("btn--hide");

      btnNightmode.classList.add("column-25");
      btnNightmode.classList.remove("column-33");
      btnMap.classList.add("column-25");
      btnMap.classList.remove("column-33");
      btnInfo.classList.add("column-25");
      btnInfo.classList.remove("column-33");
    } else {
      btnLockOrientation.classList.add("btn--hide");

      btnNightmode.classList.add("column-33");
      btnNightmode.classList.remove("column-25");
      btnMap.classList.add("column-33");
      btnMap.classList.remove("column-25");
      btnInfo.classList.add("column-33");
      btnInfo.classList.remove("column-25");
    }
  }

  function checkLockable() {
    if (screen.orientation && screen.orientation.lock) {
      screen.orientation.lock(getBrowserOrientation()).then(function () {
        toggleOrientationLockable(true);
        browserUnlockOrientation(); //needed as chrome was locking orientation (even if not in fullscreen, bug??)
      }).catch(function (event) {
        if (event.code === 18) { // The page needs to be fullscreen in order to call lockOrientation(), but is lockable
          toggleOrientationLockable(true);
          browserUnlockOrientation(); //needed as chrome was locking orientation (even if not in fullscreen, bug??)
        } else {  // lockOrientation() is not available on this device (or other error)
          toggleOrientationLockable(false);
        }
      });
    } else {
      toggleOrientationLockable(false);
    }
  }

  function lockOrientationRequest(doLock) {
    if (isOrientationLockable) {
      if (doLock) {
        browserRequestFullscreen();
        lockOrientation(true);
      } else {
        browserUnlockOrientation();
        browserExitFullscreen();
        lockOrientation(false);
      }
    }
  }

  function lockOrientation(locked) {
    if (locked) {
      btnLockOrientation.classList.add("active");
    } else {
      btnLockOrientation.classList.remove("active");
    }

    isOrientationLocked = locked;
  }

  function toggleOrientationLock() {
    if (isOrientationLockable) {
      lockOrientationRequest(!isOrientationLocked);
    }
  }

  function locationUpdate(position) {
    positionCurrent.lat = position.coords.latitude;
    positionCurrent.lng = position.coords.longitude;

    positionLat.textContent = decimalToSexagesimal(positionCurrent.lat, "lat");
    positionLng.textContent = decimalToSexagesimal(positionCurrent.lng, "lng");
  }

  function locationUpdateFail(error) {
    positionLat.textContent = "n/a";
    positionLng.textContent = "n/a";
    console.log("location fail: ", error);
  }

  function setNightmode(on) {

    if (on) {
      btnNightmode.classList.add("active");
    } else {
      btnNightmode.classList.remove("active");
    }

    window.setTimeout(function() {
      if (on) {
        document.documentElement.classList.add("nightmode");
      } else {
        document.documentElement.classList.remove("nightmode");
      }
    }, 1);

    isNightMode = on;
  }

  function toggleNightmode() {
    setNightmode(!isNightMode);
  }

  function openMap() {
    window.open("https://www.google.com/maps/place/@" + positionCurrent.lat + "," + positionCurrent.lng + ",16z", "_blank");
  }

  function popupOpenFromClick(event) {
    popupOpen(event.currentTarget.dataset.name);
  }

  function popupOpen(name) {
    var i;
    for (i=0; i<popupInners.length; i++) {
      popupInners[i].classList.add("popup__inner--hide");
    }
    document.getElementById("popup-inner-" + name).classList.remove("popup__inner--hide");

    popup.classList.add("popup--show");
  }

  function popupClose() {
    popup.classList.remove("popup--show");
  }

  function popupContentsClick(event) {
    event.stopPropagation();
  }

  function decimalToSexagesimal(decimal, type) {
    var degrees = decimal | 0;
    var fraction = Math.abs(decimal - degrees);
    var minutes = (fraction * 60) | 0;
    var seconds = (fraction * 3600 - minutes * 60) | 0;

    var direction = "";
    var positive = degrees > 0;
    degrees = Math.abs(degrees);
    switch (type) {
      case "lat":
        direction = positive ? "N" : "S";
        break;
      case "lng":
        direction = positive ? "E" : "W";
        break;
    }

    return degrees + "° " + minutes + "' " + seconds + "\" " + direction;
  }

  if (screen.width > screen.height) {
    defaultOrientation = "landscape";
  } else {
    defaultOrientation = "portrait";
  }
  if (debug) {
    debugOrientationDefault.textContent = defaultOrientation;
  }

  window.addEventListener("deviceorientation", onOrientationChange);

  document.addEventListener("fullscreenchange", onFullscreenChange);
  document.addEventListener("webkitfullscreenchange", onFullscreenChange);
  document.addEventListener("mozfullscreenchange", onFullscreenChange);
  document.addEventListener("MSFullscreenChange", onFullscreenChange);

  btnLockOrientation.addEventListener("click", toggleOrientationLock);
  btnNightmode.addEventListener("click", toggleNightmode);
  btnMap.addEventListener("click", openMap);

  var i;
  for (i=0; i<popupButtons.length; i++) {
    popupButtons[i].addEventListener("click", popupOpenFromClick);
  }

  popup.addEventListener("click", popupClose);
  popupContents.addEventListener("click", popupContentsClick);

  navigator.geolocation.watchPosition(locationUpdate, locationUpdateFail, {
    enableHighAccuracy: false,
    maximumAge: 30000,
    timeout: 27000
  });

  setNightmode(false);
  checkLockable();

}());
<|endoftext|>


<commit_before>// Populate Project
var projects = [];

function Project(opts){
  this.title = opts.title;
  this.image = opts.image;
  this.projectInfo = opts.projectInfo;
  this.projectUrl = opts.projectUrl;
};

Project.prototype.toHtml = function(){
  var $source = $('#projects-template').html();
  var template = Handlebars.compile($source);
  return template(this);
};

// Initial Hide
handleInitialHide = function() {
  $('#projects').hide();
};

// Switch Views
handleMainNav = function() {
  $('.mainNav').on('click touchstart', '.tab', function(){
    var val = $(this).attr('data-content');
    $('.tab-content').hide();
    $('.tab-content').each(function(index){
      if($(this).attr('id') === val){
        $(this).fadeIn('fast');
      }
    });
  });
};

handleMobileNav = function() {
  $('.mobilenav').on('click touchstart', '.tab', function(){
    var val = $(this).attr('data-content');
    $('.tab-content').hide();
    $('.tab-content').each(function(index){
      if($(this).attr('id') === val){
        $(this).fadeIn('fast');
      }
    });
    $('.mobilenav').fadeOut(500);
    $('.top-menu').removeClass('top-animate');
    $('body').removeClass('noscroll');
    $('.mid-menu').removeClass('mid-animate');
    $('.bottom-menu').removeClass('bottom-animate');
  });
};

// Handle JSON
Project.loadAll = function(dataWePassIn) {
  projects = JSON.parse(dataWePassIn).map(function(ele){
    return new Project(ele);
  });
};

Project.appendAll = function(dataToAppend) {
  // dataToAppend.forEach(function(p){
  //   $('#projects').append(p.toHtml());
  dataToAppend.map(function(p) {
    return $('#projects').append(p.toHtml());
  });
};

Project.fetchAll = function(){
  if (localStorage.data) {
    console.log('Local Storage DOES Exist');

    $.ajax({
      url: 'data/projects-data.json',
      success: function (data, message, xhr) {
        var eTag = xhr.getResponseHeader('eTag');
        console.log('Current eTag = ' + eTag);
        var compareETag = JSON.parse(localStorage.getItem('eTag1'));
        console.log('Compare eTag = ' + compareETag);
        // console.log(message, eTag);
        if (eTag !== compareETag) {
          console.log('Changes to JSON file detected');
          console.log('Retrieved data from JSON file...');
          Project.loadAll(JSON.stringify(data));
          Project.appendAll(projects);
          //Set Local Storage
          localStorage.setItem('data', JSON.stringify(projects));
          console.log('Setting Local Data...');
          Project.appendAll(projects);
        }
        else {
          console.log('NO change to JSON Data');
          var localData = localStorage.getItem('data');
          console.log('Retriving Local Data');
          Project.loadAll(localData);
          //Append to HTML
        }
        Project.appendAll(projects);
      }
    });
  }
  else {
    $.getJSON('data/projects-data.json', function(data){
      console.log('Retrieved data from JSON file...');
      Project.loadAll(JSON.stringify(data));
      Project.appendAll(projects);
      //Set Local Storage
      localStorage.setItem('data', JSON.stringify(projects));
      console.log('Locale Storage DOES NOT exist.');
      console.log('Setting Local Data...');
    }).success(function(data, message, xhr) {
      var eTag1 = xhr.getResponseHeader('eTag');
      localStorage.setItem('eTag1', JSON.stringify(eTag1));
    });
  }
};

// Call Functions
$(document).ready(function(){
  handleMainNav();
  handleMobileNav();
  handleInitialHide();
});
<commit_msg>refactored for loops<commit_after>// Populate Project
var projects = [];

function Project(opts){
  this.title = opts.title;
  this.image = opts.image;
  this.projectInfo = opts.projectInfo;
  this.projectUrl = opts.projectUrl;
};

Project.prototype.toHtml = function(){
  var $source = $('#projects-template').html();
  var template = Handlebars.compile($source);
  return template(this);
};

// Initial Hide
handleInitialHide = function() {
  $('#projects').hide();
};

// Switch Views
handleMainNav = function() {
  $('.mainNav').on('click touchstart', '.tab', function(){
    var val = $(this).attr('data-content');
    $('.tab-content').hide();
    $('.tab-content').map(function(index){
      if($(this).attr('id') === val){
        $(this).fadeIn('fast');
      }
    });
  });
};

handleMobileNav = function() {
  $('.mobilenav').on('click touchstart', '.tab', function(){
    var val = $(this).attr('data-content');
    $('.tab-content').hide();
    $('.tab-content').map(function(index){
      if($(this).attr('id') === val){
        $(this).fadeIn('fast');
      }
    });
    $('.mobilenav').fadeOut(500);
    $('.top-menu').removeClass('top-animate');
    $('body').removeClass('noscroll');
    $('.mid-menu').removeClass('mid-animate');
    $('.bottom-menu').removeClass('bottom-animate');
  });
};

// Handle JSON
Project.loadAll = function(dataWePassIn) {
  projects = JSON.parse(dataWePassIn).map(function(ele){
    return new Project(ele);
  });
};

Project.appendAll = function(dataToAppend) {
  dataToAppend.map(function(p) {
    return $('#projects').append(p.toHtml());
  });
};

Project.fetchAll = function(){
  if (localStorage.data) {
    console.log('Local Storage DOES Exist');

    $.ajax({
      url: 'data/projects-data.json',
      success: function (data, message, xhr) {
        var eTag = xhr.getResponseHeader('eTag');
        console.log('Current eTag = ' + eTag);
        var compareETag = JSON.parse(localStorage.getItem('eTag1'));
        console.log('Compare eTag = ' + compareETag);

        if (eTag !== compareETag) {
          console.log('Changes to JSON file detected');
          console.log('Retrieved data from JSON file...');
          Project.loadAll(JSON.stringify(data));
          Project.appendAll(projects);
          //Set Local Storage
          localStorage.setItem('data', JSON.stringify(projects));
          console.log('Setting Local Data...');
          Project.appendAll(projects);
        }
        else {
          console.log('NO change to JSON Data');
          var localData = localStorage.getItem('data');
          console.log('Retriving Local Data');
          Project.loadAll(localData);
          //Append to HTML
        }
        Project.appendAll(projects);
      }
    });
  }
  else {
    $.getJSON('data/projects-data.json', function(data){
      console.log('Retrieved data from JSON file...');
      Project.loadAll(JSON.stringify(data));
      Project.appendAll(projects);
      //Set Local Storage
      localStorage.setItem('data', JSON.stringify(projects));
      console.log('Locale Storage DOES NOT exist.');
      console.log('Setting Local Data...');
    }).success(function(data, message, xhr) {
      var eTag1 = xhr.getResponseHeader('eTag');
      localStorage.setItem('eTag1', JSON.stringify(eTag1));
    });
  }
};

// Call Functions
$(document).ready(function(){
  handleMainNav();
  handleMobileNav();
  handleInitialHide();
});
<|endoftext|>


<commit_before>var App = (function(md) {

  if(localStorage.getItem("__author_darkmode") == "true") {
    document.body.classList.add("dark");
  }

  var handleLightEvent = function(event) {
    if(event.value < 5) {
      document.body.classList.add("dark");
    } else {
      document.body.classList.remove("dark");
    }
  };

  window.addEventListener("devicelight", handleLightEvent);

  var content  = document.getElementById("content"),
         preview = document.getElementById("rendered");

   var body = content;
   var start = null;
   var currentFileName = null;
   var isPreviewing = false;

   var loadFile = function(fName) {
     var text= localStorage.getItem(fName);

     content.value = text;
   };

   var saveFile = function(fileName, content) {
     localStorage.setItem(fileName, content);
   };

   document.getElementById("save").addEventListener("click", function() {
     var name = currentFileName || window.prompt("Enter a filename");

     saveFile(name, content.value);
     currentFileName = name;
   });

   document.getElementById("open").addEventListener("click", function() {
     currentFileName = window.prompt("Open file", currentFileName);
     loadFile(currentFileName);
   });

   document.getElementById("new").addEventListener("click", function() {
     currentFileName = null;
     content.value = "";
   });

   document.getElementById("mode").addEventListener("click", function() {
     document.body.classList.toggle("dark");
     window.removeEventListener("devicelight", handleLightEvent);
     localStorage.setItem("__author_darkmode",
       !(localStorage.getItem("__author_darkmode") == "true"));
   });

   document.getElementById("preview").addEventListener("click", function() {
     isPreviewing = !isPreviewing;
       content.style.display = isPreviewing ? "none" : "block";
       preview.style.display = isPreviewing ? "block" : "none";
       preview.innerHTML = md.makeHtml(content.value);
   });
})(new Showdown.converter());
<commit_msg>Storing into remoteStorage, loading from local and remote<commit_after>var App = (function(md) {

  remoteStorage.displayWidget();

  if(localStorage.getItem("__author_darkmode") == "true") {
    document.body.classList.add("dark");
  }

  var handleLightEvent = function(event) {
    if(event.value < 5) {
      document.body.classList.add("dark");
    } else {
      document.body.classList.remove("dark");
    }
  };

  window.addEventListener("devicelight", handleLightEvent);

  var content  = document.getElementById("content"),
         preview = document.getElementById("rendered");

   var body = content;
   var start = null;
   var currentFileName = null;
   var isPreviewing = false;

   var loadFile = function(fName) {
    remoteStorage.scribbles.loadScribble(fName).then(function(scribble) {
      if(!scribble) {
        var text= localStorage.getItem(fName);
        content.value = text;
        return;
      }

      content.value = scribble.content;
    })
   };

   var saveFile = function(fileName, content) {
     remoteStorage.scribbles.saveScribble(fileName, content);
   };

   document.getElementById("save").addEventListener("click", function() {
     var name = currentFileName || window.prompt("Enter a filename");

     saveFile(name, content.value);
     currentFileName = name;
   });

   document.getElementById("open").addEventListener("click", function() {
     currentFileName = window.prompt("Open file", currentFileName);
     loadFile(currentFileName);
   });

   document.getElementById("new").addEventListener("click", function() {
     currentFileName = null;
     content.value = "";
   });

   document.getElementById("mode").addEventListener("click", function() {
     document.body.classList.toggle("dark");
     window.removeEventListener("devicelight", handleLightEvent);
     localStorage.setItem("__author_darkmode",
       !(localStorage.getItem("__author_darkmode") == "true"));
   });

   document.getElementById("preview").addEventListener("click", function() {
     isPreviewing = !isPreviewing;
       content.style.display = isPreviewing ? "none" : "block";
       preview.style.display = isPreviewing ? "block" : "none";
       preview.innerHTML = md.makeHtml(content.value);
   });
})(new Showdown.converter());
<|endoftext|>


<commit_before>// ---------------------------------------
// Test Environment Setup
// ---------------------------------------
import sinon from 'sinon'
import chai from 'chai'
import sinonChai from 'sinon-chai'
import chaiAsPromised from 'chai-as-promised'
import chaiEnzyme from 'chai-enzyme'

chai.use(sinonChai)
chai.use(chaiAsPromised)
chai.use(chaiEnzyme())

global.chai = chai
global.sinon = sinon
global.expect = chai.expect
global.should = chai.should()

// ---------------------------------------
// Require Tests
// ---------------------------------------
// for use with karma-webpack-with-fast-source-maps
const __karmaWebpackManifest__ = []; // eslint-disable-line
const inManifest = (path) => ~__karmaWebpackManifest__.indexOf(path)

// require all `tests/**/*.spec.js`
const testsContext = require.context('./', true, /\.spec\.js$/)

// only run tests that have changed after the first pass.
const testsToRun = testsContext.keys().filter(inManifest)
;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext)

// require all `src/**/*.js` except for `main.js` (for isparta coverage reporting)
if (__COVERAGE__) {
  const componentsContext = require.context('../src/', true, /^((?!main|reducers).)*\.js$/)
  componentsContext.keys().forEach(componentsContext)
}
<commit_msg>Remove unneeded coverage lines from tests bundler<commit_after>// ---------------------------------------
// Test Environment Setup
// ---------------------------------------
import sinon from 'sinon'
import chai from 'chai'
import sinonChai from 'sinon-chai'
import chaiAsPromised from 'chai-as-promised'
import chaiEnzyme from 'chai-enzyme'

chai.use(sinonChai)
chai.use(chaiAsPromised)
chai.use(chaiEnzyme())

global.chai = chai
global.sinon = sinon
global.expect = chai.expect
global.should = chai.should()

// ---------------------------------------
// Require Tests
// ---------------------------------------
// for use with karma-webpack-with-fast-source-maps
const __karmaWebpackManifest__ = []; // eslint-disable-line
const inManifest = (path) => ~__karmaWebpackManifest__.indexOf(path)

// require all `tests/**/*.spec.js`
const testsContext = require.context('./', true, /\.spec\.js$/)

// only run tests that have changed after the first pass.
const testsToRun = testsContext.keys().filter(inManifest)
;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext)
<|endoftext|>


<commit_before>var _ = require('underscore');
var queueAsync = require('d3-queue').queue;
var Backbone = require('backbone');
var analyses = require('builder/data/analyses');

/**
 * Analysis source options are not trivial to get and requires some coordination of events.
 *
 * It's intended to be fetched
 */
module.exports = Backbone.Model.extend({

  defaults: {
    fetching: false,
    nodes_options: []
  },

  initialize: function (attrs, opts) {
    if (!opts.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
    if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
    if (!opts.tablesCollection) throw new Error('tablesCollection is required');

    this._analysisDefinitionNodesCollection = opts.analysisDefinitionNodesCollection;
    this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
    this._tablesCollection = opts.tablesCollection;
  },

  fetch: function () {
    this.set({
      fetching: true,
      nodes_options: []
    });

    var queue = queueAsync();

    this._fetchTables(queue);
    this._createOptionsFromNodes(queue);

    queue.awaitAll(function () {
      this.set('fetching', false);
    }.bind(this));
  },

  /**
   * @param {String, Array<String>} requiredSimpleGeometryTypes
   */
  getSelectOptions: function (requiredSimpleGeometryTypes) {
    var nodeOptions = this.get('nodes_options');
    if (this.get('fetching') && nodeOptions.length === 0) {
      return nodeOptions;
    }

    if (_.isString(requiredSimpleGeometryTypes)) {
      requiredSimpleGeometryTypes = [requiredSimpleGeometryTypes];
    }

    return _.flatten([
      nodeOptions
        .filter(function (d) {
          return this._isValidGeometry(requiredSimpleGeometryTypes, d.simpleGeometryType);
        }, this)
        .map(function (d) {
          var node = this._analysisDefinitionNodesCollection.get(d.val);
          var layer = this._layerDefinitionsCollection.findOwnerOfAnalysisNode(node);
          var layerName = node.isSourceType()
            ? layer.getTableName()
            : layer.getName();

          return _.extend({
            layerName: layerName,
            nodeTitle: analyses.short_title(node),
            color: node.getColor(),
            type: 'node',
            isSourceType: node.isSourceType()
          }, d);
        }, this),
      this._getTablesSelectOptions(requiredSimpleGeometryTypes)
    ]);
  },

  /**
   * @param {String} val val from select option
   */
  createSourceNodeUnlessExisting: function (val) {
    var tableModel = this._tablesCollection.get(val);

    // If there is no table with given val as id, then it must be a layer source which already exist, so nothing to do
    if (tableModel) {
      var tableName = tableModel.get('name');
      this._analysisDefinitionNodesCollection.createSourceNode({
        id: tableName, // to avoid duplicats
        tableName: tableName
      });
    }
  },

  _isValidGeometry: function (requiredSimpleGeometryTypes, simpleGeometryType) {
    return _.contains(requiredSimpleGeometryTypes, simpleGeometryType) || requiredSimpleGeometryTypes[0] === '*';
  },

  _fetchTables: function (queue) {
    queue.defer(function (cb) {
      this._tablesCollection.fetch({
        data: {
          show_liked: false,
          show_stats: false,
          show_table_size_and_row_count: false,
          show_permission: false,
          show_synchronization: false,
          show_uses_builder_features: false,
          load_totals: false,
          per_page: 1000
        }
      });
      this._tablesCollection.once('sync error', function () {
        cb();
      });
    }.bind(this));
  },

  _createOptionsFromNodes: function (queue) {
    var self = this;

    this._analysisDefinitionNodesCollection
      .each(function (nodeDefModel) {
        var queryGeometryModel = nodeDefModel.queryGeometryModel;

        queue.defer(function (cb) {
          if (queryGeometryModel.isFetched()) {
            self._appendNodeOption(nodeDefModel);
            return cb();
          }

          if (!queryGeometryModel.canFetch()) {
            return cb();
          }

          var onStatusChange = function () {
            if (queryGeometryModel.get('status') === 'fetching') return;
            queryGeometryModel.off('change:status', onStatusChange);
            self._appendNodeOption(nodeDefModel);
            cb();
          };
          queryGeometryModel.on('change:status', onStatusChange);
          queryGeometryModel.fetch();
        });
      });
  },

  _appendNodeOption: function (nodeDefModel) {
    var geom = nodeDefModel.queryGeometryModel.get('simple_geom');
    // If a node doesn't belong to a layer, we should not append it as a node option
    // It could be an intersection node, for example.
    var belongsToLayer = this._layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);

    if (!geom || !belongsToLayer) return;

    this.get('nodes_options')
      .push({
        simpleGeometryType: geom,
        val: nodeDefModel.id,
        label: nodeDefModel.id
      });
  },

  _getTablesSelectOptions: function (requiredSimpleGeometryTypes) {
    return this._tablesCollection.reduce(function (memo, m) {
      var simpleGeometryType = m.getGeometryType()[0];

      if (this._isValidGeometry(requiredSimpleGeometryTypes, simpleGeometryType)) {
        memo.push({
          val: m.id,
          label: m.get('name'),
          type: 'dataset'
        });
      }

      return memo;
    }, [], this);
  }

});
<commit_msg>Set show_table to false<commit_after>var _ = require('underscore');
var queueAsync = require('d3-queue').queue;
var Backbone = require('backbone');
var analyses = require('builder/data/analyses');

/**
 * Analysis source options are not trivial to get and requires some coordination of events.
 *
 * It's intended to be fetched
 */
module.exports = Backbone.Model.extend({

  defaults: {
    fetching: false,
    nodes_options: []
  },

  initialize: function (attrs, opts) {
    if (!opts.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
    if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
    if (!opts.tablesCollection) throw new Error('tablesCollection is required');

    this._analysisDefinitionNodesCollection = opts.analysisDefinitionNodesCollection;
    this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
    this._tablesCollection = opts.tablesCollection;
  },

  fetch: function () {
    this.set({
      fetching: true,
      nodes_options: []
    });

    var queue = queueAsync();

    this._fetchTables(queue);
    this._createOptionsFromNodes(queue);

    queue.awaitAll(function () {
      this.set('fetching', false);
    }.bind(this));
  },

  /**
   * @param {String, Array<String>} requiredSimpleGeometryTypes
   */
  getSelectOptions: function (requiredSimpleGeometryTypes) {
    var nodeOptions = this.get('nodes_options');
    if (this.get('fetching') && nodeOptions.length === 0) {
      return nodeOptions;
    }

    if (_.isString(requiredSimpleGeometryTypes)) {
      requiredSimpleGeometryTypes = [requiredSimpleGeometryTypes];
    }

    return _.flatten([
      nodeOptions
        .filter(function (d) {
          return this._isValidGeometry(requiredSimpleGeometryTypes, d.simpleGeometryType);
        }, this)
        .map(function (d) {
          var node = this._analysisDefinitionNodesCollection.get(d.val);
          var layer = this._layerDefinitionsCollection.findOwnerOfAnalysisNode(node);
          var layerName = node.isSourceType()
            ? layer.getTableName()
            : layer.getName();

          return _.extend({
            layerName: layerName,
            nodeTitle: analyses.short_title(node),
            color: node.getColor(),
            type: 'node',
            isSourceType: node.isSourceType()
          }, d);
        }, this),
      this._getTablesSelectOptions(requiredSimpleGeometryTypes)
    ]);
  },

  /**
   * @param {String} val val from select option
   */
  createSourceNodeUnlessExisting: function (val) {
    var tableModel = this._tablesCollection.get(val);

    // If there is no table with given val as id, then it must be a layer source which already exist, so nothing to do
    if (tableModel) {
      var tableName = tableModel.get('name');
      this._analysisDefinitionNodesCollection.createSourceNode({
        id: tableName, // to avoid duplicats
        tableName: tableName
      });
    }
  },

  _isValidGeometry: function (requiredSimpleGeometryTypes, simpleGeometryType) {
    return _.contains(requiredSimpleGeometryTypes, simpleGeometryType) || requiredSimpleGeometryTypes[0] === '*';
  },

  _fetchTables: function (queue) {
    queue.defer(function (cb) {
      this._tablesCollection.fetch({
        data: {
          show_liked: false,
          show_stats: false,
          show_table_size_and_row_count: false,
          show_permission: false,
          show_table: false,
          show_synchronization: false,
          show_uses_builder_features: false,
          load_totals: false,
          per_page: 1000
        }
      });
      this._tablesCollection.once('sync error', function () {
        cb();
      });
    }.bind(this));
  },

  _createOptionsFromNodes: function (queue) {
    var self = this;

    this._analysisDefinitionNodesCollection
      .each(function (nodeDefModel) {
        var queryGeometryModel = nodeDefModel.queryGeometryModel;

        queue.defer(function (cb) {
          if (queryGeometryModel.isFetched()) {
            self._appendNodeOption(nodeDefModel);
            return cb();
          }

          if (!queryGeometryModel.canFetch()) {
            return cb();
          }

          var onStatusChange = function () {
            if (queryGeometryModel.get('status') === 'fetching') return;
            queryGeometryModel.off('change:status', onStatusChange);
            self._appendNodeOption(nodeDefModel);
            cb();
          };
          queryGeometryModel.on('change:status', onStatusChange);
          queryGeometryModel.fetch();
        });
      });
  },

  _appendNodeOption: function (nodeDefModel) {
    var geom = nodeDefModel.queryGeometryModel.get('simple_geom');
    // If a node doesn't belong to a layer, we should not append it as a node option
    // It could be an intersection node, for example.
    var belongsToLayer = this._layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);

    if (!geom || !belongsToLayer) return;

    this.get('nodes_options')
      .push({
        simpleGeometryType: geom,
        val: nodeDefModel.id,
        label: nodeDefModel.id
      });
  },

  _getTablesSelectOptions: function (requiredSimpleGeometryTypes) {
    return this._tablesCollection.reduce(function (memo, m) {
      var simpleGeometryType = m.getGeometryType()[0];

      if (this._isValidGeometry(requiredSimpleGeometryTypes, simpleGeometryType)) {
        memo.push({
          val: m.id,
          label: m.get('name'),
          type: 'dataset'
        });
      }

      return memo;
    }, [], this);
  }

});
<|endoftext|>