<commit_before>"use strict";

CSLEDIT = CSLEDIT || {};

CSLEDIT.infoPropertyPanel = (function () {
	var panel, infoNode, inputTimeout;

	var layout = [
		{ name : "Title", node : "title" },
		{ name : "Title (short)", node : "title-short" },
		{ name : "Id", node : "id" },
		{ name : "Summary", node : "summary" },
		{ name : "Rights", node : "rights" },
		{ name : "Published", node : "published" },
		{ name : "ISSNL", node : "issnl" },
		{ name : "eISSN", node : "eissn" },
		{ name : "ISSN", node : "issn" },
		{ name : "Link", node : "link" },
		{ name : "Author", node : "author" },
		{ name : "Contributor", node : "contributor" },
		{ name : "Category", node : "category" }
	];

	var pluralise = function (noun) {
		if (noun[noun.length - 1] === "y") {
			return noun.replace(/y$/, "ies");
		} else {
			return noun + "s";
		}
	};

	// TODO: could probably get some of this info from the schema
	var multipleNodes = ["link","author","contributor","category"];
	var attributeNodes = ["link", "category"];
	var nameNodes = ["author", "contributor"];

	var attributeEditorRow = function (item, node, schemaAttributes) {
		var thisRow = $('

');
		$.each(schemaAttributes, function (name) {
			var input, attributeValue;
			thisRow.append(' ' + name + ' ');

			attributeValue = new CSLEDIT.CslNode(node).getAttr(name);

			input = createInput(item.node, node, name, attributeValue);
			thisRow.append(input);
		});
		return thisRow;
	};

	var createInput = function (nodeName, node, type, value, parentCslId) {
		var input = $('<input></input>');
		if (typeof node === "undefined" || node === null) {
			if (typeof parentCslId === "undefined") {
				input.attr("parentcslid", infoNode.cslId);
			} else {
				input.attr("parentcslid", parentCslId);
			}
		} else {
			input.attr("cslid", node.cslId);
		}
		input.attr("type", type);
		input.attr("nodename", nodeName);
		input.val(value);
		input.on('input', onInput);

		return input;
	};

	var onInput = function () {
		var $this = $(this);

		clearTimeout(inputTimeout);
		inputTimeout = setTimeout(function () {
			var cslId,
				parentId,
				type,
				nodeName,
				thisNode,
				index,
				parentNode,
				numNodesInParent;

			cslId = parseInt($this.attr("cslid"));
			parentId = parseInt($this.attr("parentcslid"));
			type = $this.attr("type");
			nodeName = $this.attr("nodename");

			thisNode = new CSLEDIT.CslNode(nodeName);
			if (!isNaN(cslId)) {
				thisNode.copy(CSLEDIT.data.getNode(cslId));
			}

			if (type === "textValue") {
				thisNode.textValue = $this.val();
			} else {
				thisNode.setAttr(type, $this.val());
			}

			if (isNaN(cslId)) {
				CSLEDIT.viewController.setSuppressSelectNode(true);
				CSLEDIT.controller.exec('addNode', [parentId, "last", thisNode]);
				CSLEDIT.viewController.setSuppressSelectNode(false);
				parentNode = CSLEDIT.data.getNode(parentId);
				numNodesInParent = CSLEDIT.data.numNodes(parentNode);

				//setupPanel(panel);
				// update all cslids
				$.each(["cslid", "parentcslid"], function (i, attribute) {
					panel.find('input[' + attribute + ']').each(function() {
						var $this = $(this),
							cslId;

						cslId = parseInt($this.attr(attribute));

						if (cslId >= parentId + numNodesInParent) {
							$this.attr(attribute, cslId + 1);
						}
					});
				});

				// set added node cslid
				$this.removeAttr("parentcslid");
				$this.attr("cslid", parentId + numNodesInParent);
			} else {
				CSLEDIT.controller.exec('amendNode', [cslId, thisNode]);
			}
		}, 500);
	};

	var textValueEditorRow = function (item, node) {
		var thisRow, value = "";

		thisRow = $('

');
		thisRow.append(' ' + item.name + ' ');

		if (typeof node !== "undefined") {
			value = node.textValue;
		}
		thisRow.append(createInput(item.node, node, "textValue", value));
		return thisRow;
	};

	var nameEditorRow = function (item, cslNode) {
		var thisRow, children, input, cslChildren;

		thisRow = $('

');
		children = CSLEDIT.schema.childElements("info/author");

		cslChildren = {};
		$.each(cslNode.children, function (i, actualChild) {
			cslChildren[actualChild.name] = actualChild;
		});

		$.each(children, function (child, unused) {
			var value = "";

			thisRow.append(' ' + child + ' ');
			//input = $('<input class="' + item.node + '-' + child + '"></input>');

			if (child in cslChildren) {
				value = cslChildren[child].textValue;
			}
			input = createInput(child, cslChildren[child], "textValue", value, cslNode.cslId);
			thisRow.append(input);
		});
		return thisRow;
	};

	var editorRow = function (item, node, schemaAttributes) {
		if (attributeNodes.indexOf(item.node) >= 0) {
			return attributeEditorRow(item, node, schemaAttributes);
		} else if (nameNodes.indexOf(item.node) >=0) {
			return nameEditorRow(item, node);
		} else {
			return textValueEditorRow(item, node);
		}
	};

	// It's assumed that infoNode will always refer to the correct node
	// while the panel is visible
	var setupPanel = function (_panel) {
		panel = _panel;
		infoNode = CSLEDIT.data.getNodesFromPath("style/info");
		assertEqual(infoNode.length, 1);
		infoNode = infoNode[0];

		panel.children().remove();
		//panel.append('
Style Info
');

		$.each(layout, function (i, item) {
			var nodes = CSLEDIT.data.getNodesFromPath("info/" + item.node, infoNode),
				schemaAttributes, deleteButton, addButton, value, thisRow,
				table,
				titleRow, inputRow;

			if (multipleNodes.indexOf(item.node) >= 0) {
				schemaAttributes = CSLEDIT.schema.attributes("info/" + item.node);
				panel.append('
' + pluralise(item.name) + '
');
				table = $("

");
				$.each(nodes, function (i, node) {
					thisRow = editorRow(item, node, schemaAttributes);

					// convert 1st thisRow into table title
					if (typeof titleRow === "undefined") {
						titleRow = $('

');
						thisRow.find('label').each(function () {
							titleRow.append($('	').append($(this)));
						});
						table.append(titleRow);
					}

					// convert thisRow into table row
					inputRow = $('

');
					thisRow.find('input').each(function () {
						inputRow.append($('	').append($(this)));
					});

					deleteButton = $('<button>Delete</button>');
					deleteButton.on('click', function () {
						CSLEDIT.controller.exec("deleteNode", [node.cslId]);
						setupPanel(panel);
					});

					inputRow.append($('	').append(deleteButton));
					table.append(inputRow);
				});

				panel.append(table);

				addButton = $('<button>Add ' + item.name + '</button>');
				panel.append(addButton);

				addButton.on('click', function () {
					CSLEDIT.viewController.setSuppressSelectNode(true);
					CSLEDIT.controller.exec("addNode",
						[infoNode.cslId, "last", new CSLEDIT.CslNode(item.node)]);
					CSLEDIT.viewController.setSuppressSelectNode(false);
					setupPanel(panel);
				});
				panel.append('

');
			} else {
				assert(nodes.length < 2);
				thisRow = editorRow(item, nodes[0], null);

				// TODO: do this in less hacky way
				panel.css("position", "relative");
				thisRow.children('input').eq(0).css("position", "absolute");
				thisRow.children('input').eq(0).css("left", "100px");
				thisRow.children('label').eq(0).css("line-height", "1.5");
				panel.append(thisRow);
			}
		});

	};

	return {
		setupPanel : setupPanel
	};
}());
<commit_msg>Show categories in the info property panel again.<commit_after>"use strict";

CSLEDIT = CSLEDIT || {};

CSLEDIT.infoPropertyPanel = (function () {
	var panel, infoNode, inputTimeout;

	var layout = [
		{ name : "Title", node : "title" },
		{ name : "Title (short)", node : "title-short" },
		{ name : "Id", node : "id" },
		{ name : "Summary", node : "summary" },
		{ name : "Rights", node : "rights" },
		{ name : "Published", node : "published" },
		{ name : "ISSNL", node : "issnl" },
		{ name : "eISSN", node : "eissn" },
		{ name : "ISSN", node : "issn" },
		{ name : "Link", node : "link" },
		{ name : "Author", node : "author" },
		{ name : "Contributor", node : "contributor" },
		{ name : "Category", node : "category" }
	];

	var pluralise = function (noun) {
		if (noun[noun.length - 1] === "y") {
			return noun.replace(/y$/, "ies");
		} else {
			return noun + "s";
		}
	};

	// TODO: could probably get some of this info from the schema
	var multipleNodes = ["link","author","contributor","category"];
	var attributeNodes = ["link", "category"];
	var nameNodes = ["author", "contributor"];

	var attributeEditorRow = function (item, node, schemaAttributes) {
		var thisRow = $('

');
		$.each(schemaAttributes, function (name) {
			var input, attributeValue;
			thisRow.append(' ' + name + ' ');

			attributeValue = new CSLEDIT.CslNode(node).getAttr(name);

			input = createInput(item.node, node, name, attributeValue);
			thisRow.append(input);
		});
		return thisRow;
	};

	var createInput = function (nodeName, node, type, value, parentCslId) {
		var input = $('<input></input>');
		if (typeof node === "undefined" || node === null) {
			if (typeof parentCslId === "undefined") {
				input.attr("parentcslid", infoNode.cslId);
			} else {
				input.attr("parentcslid", parentCslId);
			}
		} else {
			input.attr("cslid", node.cslId);
		}
		input.attr("type", type);
		input.attr("nodename", nodeName);
		input.val(value);
		input.on('input', onInput);

		return input;
	};

	var onInput = function () {
		var $this = $(this);

		clearTimeout(inputTimeout);
		inputTimeout = setTimeout(function () {
			var cslId,
				parentId,
				type,
				nodeName,
				thisNode,
				index,
				parentNode,
				numNodesInParent;

			cslId = parseInt($this.attr("cslid"));
			parentId = parseInt($this.attr("parentcslid"));
			type = $this.attr("type");
			nodeName = $this.attr("nodename");

			thisNode = new CSLEDIT.CslNode(nodeName);
			if (!isNaN(cslId)) {
				thisNode.copy(CSLEDIT.data.getNode(cslId));
			}

			if (type === "textValue") {
				thisNode.textValue = $this.val();
			} else {
				thisNode.setAttr(type, $this.val());
			}

			if (isNaN(cslId)) {
				CSLEDIT.viewController.setSuppressSelectNode(true);
				CSLEDIT.controller.exec('addNode', [parentId, "last", thisNode]);
				CSLEDIT.viewController.setSuppressSelectNode(false);
				parentNode = CSLEDIT.data.getNode(parentId);
				numNodesInParent = CSLEDIT.data.numNodes(parentNode);

				// update all cslids
				$.each(["cslid", "parentcslid"], function (i, attribute) {
					panel.find('input[' + attribute + ']').each(function() {
						var $this = $(this),
							cslId;

						cslId = parseInt($this.attr(attribute));

						if (cslId >= parentId + numNodesInParent) {
							$this.attr(attribute, cslId + 1);
						}
					});
				});

				// set added node cslid
				$this.removeAttr("parentcslid");
				$this.attr("cslid", parentId + numNodesInParent);
			} else {
				CSLEDIT.controller.exec('amendNode', [cslId, thisNode]);
			}
		}, 500);
	};

	var textValueEditorRow = function (item, node) {
		var thisRow, value = "";

		thisRow = $('

');
		thisRow.append(' ' + item.name + ' ');

		if (typeof node !== "undefined") {
			value = node.textValue;
		}
		thisRow.append(createInput(item.node, node, "textValue", value));
		return thisRow;
	};

	var nameEditorRow = function (item, cslNode) {
		var thisRow, children, input, cslChildren;

		thisRow = $('

');
		children = CSLEDIT.schema.childElements("info/author");

		cslChildren = {};
		$.each(cslNode.children, function (i, actualChild) {
			cslChildren[actualChild.name] = actualChild;
		});

		$.each(children, function (child, unused) {
			var value = "";

			thisRow.append(' ' + child + ' ');
			//input = $('<input class="' + item.node + '-' + child + '"></input>');

			if (child in cslChildren) {
				value = cslChildren[child].textValue;
			}
			input = createInput(child, cslChildren[child], "textValue", value, cslNode.cslId);
			thisRow.append(input);
		});
		return thisRow;
	};

	var editorRow = function (item, node, schemaAttributes) {
		if (attributeNodes.indexOf(item.node) >= 0) {
			return attributeEditorRow(item, node, schemaAttributes);
		} else if (nameNodes.indexOf(item.node) >=0) {
			return nameEditorRow(item, node);
		} else {
			return textValueEditorRow(item, node);
		}
	};

	// It's assumed that infoNode will always refer to the correct node
	// while the panel is visible
	var setupPanel = function (_panel) {
		panel = _panel;
		infoNode = CSLEDIT.data.getNodesFromPath("style/info");
		assertEqual(infoNode.length, 1);
		infoNode = infoNode[0];

		panel.children().remove();

		$.each(layout, function (i, item) {
			var nodes = CSLEDIT.data.getNodesFromPath("info/" + item.node, infoNode),
				schemaAttributes, deleteButton, addButton, value, thisRow,
				table,
				titleRow, inputRow;

			if (multipleNodes.indexOf(item.node) >= 0) {
				schemaAttributes = {};
				$.each(CSLEDIT.schema.attributes("info/" + item.node), function (attrName, attr) {
					schemaAttributes[attrName] = attr;
				});

				// add choices to attributes
				// TODO: correct this to treat choices as mutaully exclusive
				//       as they should be
				$.each(CSLEDIT.schema.choices("info/" + item.node), function (i, choice) {
					$.each(choice, function (attrName, attr) {
						schemaAttributes[attrName] = attr;
					});
				});

				panel.append('
' + pluralise(item.name) + '
');
				table = $("

");
				$.each(nodes, function (i, node) {
					thisRow = editorRow(item, node, schemaAttributes);

					// convert 1st thisRow into table title
					if (typeof titleRow === "undefined") {
						titleRow = $('

');
						thisRow.find('label').each(function () {
							titleRow.append($('	').append($(this)));
						});
						table.append(titleRow);
					}

					// convert thisRow into table row
					inputRow = $('

');
					thisRow.find('input').each(function () {
						inputRow.append($('	').append($(this)));
					});

					deleteButton = $('<button>Delete</button>');
					deleteButton.on('click', function () {
						CSLEDIT.controller.exec("deleteNode", [node.cslId]);
						setupPanel(panel);
					});

					inputRow.append($('	').append(deleteButton));
					table.append(inputRow);
				});

				panel.append(table);

				addButton = $('<button>Add ' + item.name + '</button>');
				panel.append(addButton);

				addButton.on('click', function () {
					CSLEDIT.viewController.setSuppressSelectNode(true);
					CSLEDIT.controller.exec("addNode",
						[infoNode.cslId, "last", new CSLEDIT.CslNode(item.node)]);
					CSLEDIT.viewController.setSuppressSelectNode(false);
					setupPanel(panel);
				});
				panel.append('

');
			} else {
				assert(nodes.length < 2);
				thisRow = editorRow(item, nodes[0], null);

				// TODO: do this in less hacky way
				panel.css("position", "relative");
				thisRow.children('input').eq(0).css("position", "absolute");
				thisRow.children('input').eq(0).css("left", "100px");
				thisRow.children('label').eq(0).css("line-height", "1.5");
				panel.append(thisRow);
			}
		});

	};

	return {
		setupPanel : setupPanel
	};
}());
<|endoftext|>


<commit_before>/** @jsx React.DOM */

var React = require('react');
var SerializablePageDiff = require('../livescript/components/SerializablePageDiff.ls');
var IframeUtil = require('../livescript/components/IframeUtil.ls');
var ElementDifference = require('../livescript/components/ElementDifference.ls');
var Scroller = require('exports?Scroller!../../vendor/bower_components/scroller/src/Scroller.js');

console.log('React', React);
console.log('chrome', chrome);

// Make React Debugging Chrome extension happy
window.React = React;

var cropSize, setCropSize = function(){
  cropSize = parseInt(window.getComputedStyle(document.getElementById('fakecrop')).getPropertyValue('width'), 10);
};
window.addEventListener('resize', setCropSize);
setCropSize();

var ReportApp = React.createClass({
  render: function(){
    return (

        <ReportHeader />
        <DiffList />

    );
  }
});

var ReportHeader = React.createClass({
  getInitialState: function(){
    return {
      isInitial: true,
      isLoading: false,
      total: 0,
      done: 0
    };
  },
  componentDidMount: function(){
    // Message handling
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      switch(message.type){
      case "PROCESS_START":
        that.setState({
          isLoading: true,
          isInitial: false,
          total: message.data.total,
          done: 0,
          startTimestamp: Date.now()
        });
        break;

      case "PROCESS_END":
        that.setState({
          isLoading: false,
          processingTime: (Date.now() - that.state.startTimestamp) / 1000
        });
        break;

      case "PAGE_DIFF":
        that.setState({
          done: that.state.done + 1
        });
      }
    });
  },
  render: function(){
    if(this.state.isInitial){
      return (

SeeSS

Start recording some interaction! :)

      );
    }else{
      if(this.state.isLoading){
        return (

Loading... ({this.state.done} / {this.state.total})

        );
      }else{
        return (

Done in {this.state.processingTime} seconds.({this.state.done}/{this.state.total})

        );
      }
    }
  }
});

var DiffList = React.createClass({

  getInitialState: function(){
    return {data: []};
  },

  componentDidMount: function(){
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      var pageDiff;
      switch(message.type){

      // Background script starts processing a CSS or HTML change.
      //
      case "PROCESS_START":
        console.log("<Message> Background script processing started");
        that.setState({data: []});
        break;

      // The background script has done all processing.
      //
      case "PROCESS_END":
        console.log("<Message> Background script processing end");
        break;

      // The background script passes a SerializablePageDiff instance here.
      case "PAGE_DIFF":
        if(message.data === null){
          return;
        }
        pageDiff = new SerializablePageDiff(message.data);
        console.log("<Message> Data arrived from background script", pageDiff);

        var diff, i, pageDiffLength = pageDiff.diffs.length;
        // Write diff id inside diff
        for(i=0; i<pageDiffLength; i+=1){
          pageDiff.diffs[i].id = i;
        }

        // Merge the overlapping bounding boxes
        //
        pageDiff.diffs.sort(function(di, dj){
          return di.boundingBox.top - dj.boundingBox.top;
        });

        var mergedDiffs = [],
            currentMergedDiff = new MergedDiff();

        for(i=0; i<pageDiffLength; i+=1){
          diff = pageDiff.diffs[i];

          if( !currentMergedDiff.isEmpty() &&
              !currentMergedDiff.isOverlapping(diff) ){
            // Not overlapping, open up a new MergedDiff instance
            //
            mergedDiffs.push(currentMergedDiff);
            currentMergedDiff = new MergedDiff();
          }

          currentMergedDiff.add(diff);
        }
        mergedDiffs.push(currentMergedDiff);

        // Prepare the diff list state data
        //
        var newData = that.state.data;
        for(i = 0; i < mergedDiffs.length; i+=1){
          newData.push({
            domWidth: pageDiff.width,
            domHeight: pageDiff.height,
            mergedDiff: mergedDiffs[i],
            dom: pageDiff.dom(),
            url: pageDiff.url,
            doctype: pageDiff.doctype
          });
        }

        that.setState({data: newData});
      }
    });
  },

  render: function(){
    var diffList = this.state.data.map(function(packed){
      var boxWidth = packed.mergedDiff.box.right - packed.mergedDiff.box.left,
          boxHeight = packed.mergedDiff.box.bottom - packed.mergedDiff.box.top;

      return (<Diff domWidth={packed.domWidth} domHeight={packed.domHeight}
                    boxWidth={boxWidth} boxHeight={boxHeight}
                    boxLeft={packed.mergedDiff.box.left} boxTop={packed.mergedDiff.box.top}
                    dom={packed.dom} url={packed.url}
                    doctype={packed.doctype}
                    diffs={packed.mergedDiff.diffs}></Diff>);
    });

    return (

        {diffList}

    );
  }
});

var Diff = React.createClass({
  getInitialState: function(){

    // Calculate the iframe scrollTop needed to reveal the bottom edge of the
    // merged bounding box
    //
    var iframeScrollNeeded = 0;
    if(this.props.boxTop + this.props.boxHeight > this.props.domHeight){
      iframeScrollNeeded = this.props.boxTop + this.props.boxHeight - this.props.domHeight + 15; // 15: horizontal scrollbar width, if any
    }

    return {
      isZoomed: false,
      iframeScrollNeeded: iframeScrollNeeded
    };
  },
  componentDidMount: function(){
    var that = this,
        iframe = this.refs.iframeElem.getDOMNode(),
        diffs = this.props.diffs,
        iframeDoc = iframe.contentDocument,
        styleElem = iframeDoc.createElement('style');

    IframeUtil.setDocument(iframeDoc, this.props.dom.cloneNode(true), this.props.doctype);

    IframeUtil.waitForAssets(iframe.contentDocument).then(function(){
      // Create animated repositioning hint
      //
      diffs.forEach(function(diff){
        var diffId = diff.id,
            hintElem,
            currentRect,
            beforeRulesData, beforeRules, afterRules,
            elem = iframeDoc.querySelector('['+SerializablePageDiff.DIFF_ID_ATTR+'~="'+diffId+'"]');

        if(diff.type === ElementDifference.TYPE_MOD){
          if(diff.rect){
            currentRect = elem.getBoundingClientRect();

            hintElem = iframeDoc.createElement('div');
            hintElem.id = "SEESS_POSITION_ANIMATE_"+diffId;

            beforeRuleData = {
              left: (diff.rect.left && diff.rect.left.before) || currentRect.left,
              top: (diff.rect.top && diff.rect.top.before) || currentRect.top,
              width: (diff.rect.width && diff.rect.width.before) || currentRect.width,
              height: (diff.rect.height && diff.rect.height.before) || currentRect.height
            };

            beforeRules =
              "left: " + beforeRuleData.left + 'px;' +
              "top: " + beforeRuleData.top + 'px;' +
              "width: " + beforeRuleData.width + 'px;' +
              "height: " + beforeRuleData.height + 'px;';

            afterRules =
              "transform: translate(" + (currentRect.left-beforeRuleData.left) + 'px,' +
                                      (currentRect.top-beforeRuleData.top) + 'px) ' +
                         "scale(" + (currentRect.width / beforeRuleData.width) + ',' +
                                    (currentRect.height / beforeRuleData.height) + ');';

            styleElem.innerHTML += "#" + hintElem.id + "{" +
                "z-index: 99999; box-sizing: border-box; position: absolute; " +
                "border: 1px dashed red; background:rgba(255,0,0,0.1);" +
                "transform-origin: left top; opacity: 1;" +
                "-webkit-animation: SEESS_POSITION_" + diffId + " 3s ease-in-out 0s infinite;" +
                "will-change: transform; " + beforeRules +
              "}\n" +
              "@-webkit-keyframes SEESS_POSITION_" + diffId + " {to {"+afterRules+"opacity: 0.5;}}\n";

            iframeDoc.body.appendChild(hintElem);
          }
        }
      });
      iframeDoc.body.appendChild(styleElem);

      // Scroll the iframe the merged bounding box is submerged below the fold.
      // Scrolling affects getBoundingClientRect() calls so scrolling must
      // be done after that.
      //
      console.log('Scrolling to ', that.state.iframeScrollNeeded);
      iframe.contentWindow.scrollTo(0, that.state.iframeScrollNeeded);
    });
  },
  render: function(){
    var that = this;

    console.log('BBwidth: ' + this.props.boxWidth + '\nBBheight: ' + this.props.boxHeight);

    // Calculate scale
    //
    var scale = '';
    var scaleX = cropSize / this.props.boxWidth;
    var scaleY = cropSize / this.props.boxHeight;

    if(scaleX < 1 || scaleY < 1){
      var scaleMin;
      if(scaleX < scaleY){
        scaleMin = scaleX;
      }
      else{
        scaleMin = scaleY;
      }
      scale = 'scale(' + scaleMin + ',' + scaleMin + ')';
    }

    // Calculate translation & origin
    //
    var translate = '', origin = '',
        originLeft = this.props.boxLeft + this.props.boxWidth/2,
        originTop  = this.props.boxTop - this.state.iframeScrollNeeded + this.props.boxHeight/2;

    translate = 'translate(' + (cropSize/2-originLeft) + 'px,' + (cropSize/2-originTop) + 'px)';
    origin = originLeft + 'px ' + originTop + 'px';

    // Render
    //
    var iframeStyle= {
      width: this.props.domWidth,
      height: this.props.domHeight,
      transform: translate + ' ' + scale,
      transformOrigin: origin
    };

    var diffClasses = 'diff' + (this.state.isZoomed ? ' is-zoomed' : '');

    return (

{this.props.url}

    );

  },
  setZoom: function(){
    if(this.state.isZoomed){ return; }

    var that = this,
        isDragging = false,
        iframe = this.refs.iframeElem.getDOMNode(),
        iframeContainer = this.refs.iframeContainer.getDOMNode(),
        originalTransform = iframe.style.transform,
        scroller = new Scroller(function(left, top){
          iframe.style.setProperty("transform", "translate("+(-left)+"px,"+(-top)+"px)");
        }, {zooming: false, animating: false, bouncing: false, locking: false}),
        escCallback = function(e){
          if(e.which !== 27) { return; }
          window.removeEventListener('keydown', escCallback);
          iframeContainer.removeEventListener('mousedown', mousedownCallback);
          iframeContainer.removeEventListener('mouseup', mouseupCallback);
          iframeContainer.removeEventListener('mouseleave', mouseupCallback);
          iframeContainer.removeEventListener('mousemove', mousemoveCallback);
          iframe.style.removeProperty("transition");
          iframe.style.setProperty('transform', originalTransform);
          that.setState({isZoomed: false});
        },
        mousedownCallback = function(e){
          isDragging = true;
          scroller.doTouchStart([e], e.timeStamp);
          e.preventDefault();
          iframe.style.setProperty("transition", "none");
        },
        mouseupCallback = function(e){
          isDragging = false;
          scroller.doTouchEnd(e.timeStamp);
        },
        mousemoveCallback = function(e){
          if(isDragging){
            scroller.doTouchMove([e], e.timeStamp);
          }
        };

    scroller.setDimensions(window.innerWidth, window.innerHeight, iframe.clientWidth, iframe.clientHeight);
    scroller.setPosition(0, 0);

    window.addEventListener('keydown', escCallback);
    iframeContainer.addEventListener('mousedown', mousedownCallback);
    iframeContainer.addEventListener('mouseup', mouseupCallback);
    iframeContainer.addEventListener('mouseleave', mouseupCallback);
    iframeContainer.addEventListener('mousemove', mousemoveCallback);

    this.setState({isZoomed: true});
  }
});

React.renderComponent(<ReportApp />, document.getElementById('body'));

function MergedDiff(){
  this.diffs = [];

  // A default that will update to the given diff by #add(diff) method
  //
  this.box = {
    left: Infinity,
    top: Infinity,
    right: -Infinity,
    bottom: -Infinity
  };
}

MergedDiff.prototype.isEmpty = function(){
  return this.diffs.length === 0;
};

MergedDiff.prototype.add = function(diff){
  // Merge diff and update the bounding box of merged diffs
  //

  this.box.left = (diff.boundingBox.left < this.box.left) ? diff.boundingBox.left : this.box.left;
  this.box.right = (diff.boundingBox.right > this.box.right) ? diff.boundingBox.right : this.box.right;
  this.box.top = (diff.boundingBox.top < this.box.top) ? diff.boundingBox.top : this.box.top;
  this.box.bottom = (diff.boundingBox.bottom > this.box.bottom) ? diff.boundingBox.bottom : this.box.bottom;

  this.diffs.push(diff);
};

MergedDiff.prototype.isOverlapping = function(diff){
  // Return if the merged diff overlaps with a diff
  //
  var leftMostBox, topMostBox, anotherBox;

  if(this.box.left <= diff.boundingBox.left){
    leftMostBox = this.box;
    anotherBox = diff.boundingBox;
  }else{
    leftMostBox = diff.boundingBox;
    anotherBox = this.box;
  }

  // Check if horizontally detached
  if(leftMostBox.right < anotherBox.left){
    return false;
  }

  if(this.box.top <= diff.boundingBox.top){
    topMostBox = this.box;
    anotherBox = diff.boundingBox;
  }else{
    topMostBox = diff.boundingBox;
    anotherBox = this.box;
  }

  // Check if vertically detached
  if(topMostBox.bottom < anotherBox.top){
    return false;
  }

  return true;
};<commit_msg>Add tolerance threshold when calculating merged bounding box<commit_after>/** @jsx React.DOM */

var React = require('react');
var SerializablePageDiff = require('../livescript/components/SerializablePageDiff.ls');
var IframeUtil = require('../livescript/components/IframeUtil.ls');
var ElementDifference = require('../livescript/components/ElementDifference.ls');
var Scroller = require('exports?Scroller!../../vendor/bower_components/scroller/src/Scroller.js');

console.log('React', React);
console.log('chrome', chrome);

// Make React Debugging Chrome extension happy
window.React = React;

var cropSize, setCropSize = function(){
  cropSize = parseInt(window.getComputedStyle(document.getElementById('fakecrop')).getPropertyValue('width'), 10);
};
window.addEventListener('resize', setCropSize);
setCropSize();

var ReportApp = React.createClass({
  render: function(){
    return (

        <ReportHeader />
        <DiffList />

    );
  }
});

var ReportHeader = React.createClass({
  getInitialState: function(){
    return {
      isInitial: true,
      isLoading: false,
      total: 0,
      done: 0
    };
  },
  componentDidMount: function(){
    // Message handling
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      switch(message.type){
      case "PROCESS_START":
        that.setState({
          isLoading: true,
          isInitial: false,
          total: message.data.total,
          done: 0,
          startTimestamp: Date.now()
        });
        break;

      case "PROCESS_END":
        that.setState({
          isLoading: false,
          processingTime: (Date.now() - that.state.startTimestamp) / 1000
        });
        break;

      case "PAGE_DIFF":
        that.setState({
          done: that.state.done + 1
        });
      }
    });
  },
  render: function(){
    if(this.state.isInitial){
      return (

SeeSS

Start recording some interaction! :)

      );
    }else{
      if(this.state.isLoading){
        return (

Loading... ({this.state.done} / {this.state.total})

        );
      }else{
        return (

Done in {this.state.processingTime} seconds.({this.state.done}/{this.state.total})

        );
      }
    }
  }
});

var DiffList = React.createClass({

  getInitialState: function(){
    return {data: []};
  },

  componentDidMount: function(){
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      var pageDiff;
      switch(message.type){

      // Background script starts processing a CSS or HTML change.
      //
      case "PROCESS_START":
        console.log("<Message> Background script processing started");
        that.setState({data: []});
        break;

      // The background script has done all processing.
      //
      case "PROCESS_END":
        console.log("<Message> Background script processing end");
        break;

      // The background script passes a SerializablePageDiff instance here.
      case "PAGE_DIFF":
        if(message.data === null){
          return;
        }
        pageDiff = new SerializablePageDiff(message.data);
        console.log("<Message> Data arrived from background script", pageDiff);

        var diff, i, pageDiffLength = pageDiff.diffs.length;
        // Write diff id inside diff
        for(i=0; i<pageDiffLength; i+=1){
          pageDiff.diffs[i].id = i;
        }

        // Merge the overlapping bounding boxes
        //
        pageDiff.diffs.sort(function(di, dj){
          return di.boundingBox.top - dj.boundingBox.top;
        });

        var mergedDiffs = [],
            currentMergedDiff = new MergedDiff();

        for(i=0; i<pageDiffLength; i+=1){
          diff = pageDiff.diffs[i];

          if( !currentMergedDiff.isEmpty() &&
              !currentMergedDiff.isOverlapping(diff) ){
            // Not overlapping, open up a new MergedDiff instance
            //
            mergedDiffs.push(currentMergedDiff);
            currentMergedDiff = new MergedDiff();
          }

          currentMergedDiff.add(diff);
        }
        mergedDiffs.push(currentMergedDiff);

        // Prepare the diff list state data
        //
        var newData = that.state.data;
        for(i = 0; i < mergedDiffs.length; i+=1){
          newData.push({
            domWidth: pageDiff.width,
            domHeight: pageDiff.height,
            mergedDiff: mergedDiffs[i],
            dom: pageDiff.dom(),
            url: pageDiff.url,
            doctype: pageDiff.doctype
          });
        }

        that.setState({data: newData});
      }
    });
  },

  render: function(){
    var diffList = this.state.data.map(function(packed){
      var boxWidth = packed.mergedDiff.box.right - packed.mergedDiff.box.left,
          boxHeight = packed.mergedDiff.box.bottom - packed.mergedDiff.box.top;

      return (<Diff domWidth={packed.domWidth} domHeight={packed.domHeight}
                    boxWidth={boxWidth} boxHeight={boxHeight}
                    boxLeft={packed.mergedDiff.box.left} boxTop={packed.mergedDiff.box.top}
                    dom={packed.dom} url={packed.url}
                    doctype={packed.doctype}
                    diffs={packed.mergedDiff.diffs}></Diff>);
    });

    return (

        {diffList}

    );
  }
});

var Diff = React.createClass({
  getInitialState: function(){

    // Calculate the iframe scrollTop needed to reveal the bottom edge of the
    // merged bounding box
    //
    var iframeScrollNeeded = 0;
    if(this.props.boxTop + this.props.boxHeight > this.props.domHeight){
      iframeScrollNeeded = this.props.boxTop + this.props.boxHeight - this.props.domHeight + 15; // 15: horizontal scrollbar width, if any
    }

    return {
      isZoomed: false,
      iframeScrollNeeded: iframeScrollNeeded
    };
  },
  componentDidMount: function(){
    var that = this,
        iframe = this.refs.iframeElem.getDOMNode(),
        diffs = this.props.diffs,
        iframeDoc = iframe.contentDocument,
        styleElem = iframeDoc.createElement('style');

    IframeUtil.setDocument(iframeDoc, this.props.dom.cloneNode(true), this.props.doctype);

    IframeUtil.waitForAssets(iframe.contentDocument).then(function(){
      // Create animated repositioning hint
      //
      diffs.forEach(function(diff){
        var diffId = diff.id,
            hintElem,
            currentRect,
            beforeRulesData, beforeRules, afterRules,
            elem = iframeDoc.querySelector('['+SerializablePageDiff.DIFF_ID_ATTR+'~="'+diffId+'"]');

        if(diff.type === ElementDifference.TYPE_MOD){
          if(diff.rect){
            currentRect = elem.getBoundingClientRect();

            hintElem = iframeDoc.createElement('div');
            hintElem.id = "SEESS_POSITION_ANIMATE_"+diffId;

            beforeRuleData = {
              left: (diff.rect.left && diff.rect.left.before) || currentRect.left,
              top: (diff.rect.top && diff.rect.top.before) || currentRect.top,
              width: (diff.rect.width && diff.rect.width.before) || currentRect.width,
              height: (diff.rect.height && diff.rect.height.before) || currentRect.height
            };

            beforeRules =
              "left: " + beforeRuleData.left + 'px;' +
              "top: " + beforeRuleData.top + 'px;' +
              "width: " + beforeRuleData.width + 'px;' +
              "height: " + beforeRuleData.height + 'px;';

            afterRules =
              "transform: translate(" + (currentRect.left-beforeRuleData.left) + 'px,' +
                                      (currentRect.top-beforeRuleData.top) + 'px) ' +
                         "scale(" + (currentRect.width / beforeRuleData.width) + ',' +
                                    (currentRect.height / beforeRuleData.height) + ');';

            styleElem.innerHTML += "#" + hintElem.id + "{" +
                "z-index: 99999; box-sizing: border-box; position: absolute; " +
                "border: 1px dashed red; background:rgba(255,0,0,0.1);" +
                "transform-origin: left top; opacity: 1;" +
                "-webkit-animation: SEESS_POSITION_" + diffId + " 3s ease-in-out 0s infinite;" +
                "will-change: transform; " + beforeRules +
              "}\n" +
              "@-webkit-keyframes SEESS_POSITION_" + diffId + " {to {"+afterRules+"opacity: 0.5;}}\n";

            iframeDoc.body.appendChild(hintElem);
          }
        }
      });
      iframeDoc.body.appendChild(styleElem);

      // Scroll the iframe the merged bounding box is submerged below the fold.
      // Scrolling affects getBoundingClientRect() calls so scrolling must
      // be done after that.
      //
      console.log('Scrolling to ', that.state.iframeScrollNeeded);
      iframe.contentWindow.scrollTo(0, that.state.iframeScrollNeeded);
    });
  },
  render: function(){
    var that = this;

    console.log('BBwidth: ' + this.props.boxWidth + '\nBBheight: ' + this.props.boxHeight);

    // Calculate scale
    //
    var scale = '';
    var scaleX = cropSize / this.props.boxWidth;
    var scaleY = cropSize / this.props.boxHeight;

    if(scaleX < 1 || scaleY < 1){
      var scaleMin;
      if(scaleX < scaleY){
        scaleMin = scaleX;
      }
      else{
        scaleMin = scaleY;
      }
      scale = 'scale(' + scaleMin + ',' + scaleMin + ')';
    }

    // Calculate translation & origin
    //
    var translate = '', origin = '',
        originLeft = this.props.boxLeft + this.props.boxWidth/2,
        originTop  = this.props.boxTop - this.state.iframeScrollNeeded + this.props.boxHeight/2;

    translate = 'translate(' + (cropSize/2-originLeft) + 'px,' + (cropSize/2-originTop) + 'px)';
    origin = originLeft + 'px ' + originTop + 'px';

    // Render
    //
    var iframeStyle= {
      width: this.props.domWidth,
      height: this.props.domHeight,
      transform: translate + ' ' + scale,
      transformOrigin: origin
    };

    var diffClasses = 'diff' + (this.state.isZoomed ? ' is-zoomed' : '');

    return (

{this.props.url}

    );

  },
  setZoom: function(){
    if(this.state.isZoomed){ return; }

    var that = this,
        isDragging = false,
        iframe = this.refs.iframeElem.getDOMNode(),
        iframeContainer = this.refs.iframeContainer.getDOMNode(),
        originalTransform = iframe.style.transform,
        scroller = new Scroller(function(left, top){
          iframe.style.setProperty("transform", "translate("+(-left)+"px,"+(-top)+"px)");
        }, {zooming: false, animating: false, bouncing: false, locking: false}),
        escCallback = function(e){
          if(e.which !== 27) { return; }
          window.removeEventListener('keydown', escCallback);
          iframeContainer.removeEventListener('mousedown', mousedownCallback);
          iframeContainer.removeEventListener('mouseup', mouseupCallback);
          iframeContainer.removeEventListener('mouseleave', mouseupCallback);
          iframeContainer.removeEventListener('mousemove', mousemoveCallback);
          iframe.style.removeProperty("transition");
          iframe.style.setProperty('transform', originalTransform);
          that.setState({isZoomed: false});
        },
        mousedownCallback = function(e){
          isDragging = true;
          scroller.doTouchStart([e], e.timeStamp);
          e.preventDefault();
          iframe.style.setProperty("transition", "none");
        },
        mouseupCallback = function(e){
          isDragging = false;
          scroller.doTouchEnd(e.timeStamp);
        },
        mousemoveCallback = function(e){
          if(isDragging){
            scroller.doTouchMove([e], e.timeStamp);
          }
        };

    scroller.setDimensions(window.innerWidth, window.innerHeight, iframe.clientWidth, iframe.clientHeight);
    scroller.setPosition(0, 0);

    window.addEventListener('keydown', escCallback);
    iframeContainer.addEventListener('mousedown', mousedownCallback);
    iframeContainer.addEventListener('mouseup', mouseupCallback);
    iframeContainer.addEventListener('mouseleave', mouseupCallback);
    iframeContainer.addEventListener('mousemove', mousemoveCallback);

    this.setState({isZoomed: true});
  }
});

React.renderComponent(<ReportApp />, document.getElementById('body'));

function MergedDiff(){
  this.diffs = [];

  // A default that will update to the given diff by #add(diff) method
  //
  this.box = {
    left: Infinity,
    top: Infinity,
    right: -Infinity,
    bottom: -Infinity
  };
}

// Neighbor threshold
//
MergedDiff.THRESHOLD = 25;

MergedDiff.prototype.isEmpty = function(){
  return this.diffs.length === 0;
};

MergedDiff.prototype.add = function(diff){
  // Merge diff and update the bounding box of merged diffs
  //

  var enlargedDiffBox = {
    left: diff.boundingBox.left - MergedDiff.THRESHOLD,
    right: diff.boundingBox.right + MergedDiff.THRESHOLD,
    top: diff.boundingBox.top - MergedDiff.THRESHOLD,
    bottom: diff.boundingBox.bottom + MergedDiff.THRESHOLD
  };

  this.box.left = (enlargedDiffBox.left < this.box.left) ? enlargedDiffBox.left : this.box.left;
  this.box.right = (enlargedDiffBox.right > this.box.right) ? enlargedDiffBox.right : this.box.right;
  this.box.top = (enlargedDiffBox.top < this.box.top) ? enlargedDiffBox.top : this.box.top;
  this.box.bottom = (enlargedDiffBox.bottom > this.box.bottom) ? enlargedDiffBox.bottom : this.box.bottom;

  this.diffs.push(diff);
};

MergedDiff.prototype.isOverlapping = function(diff){
  // Return if the merged diff overlaps with a diff
  //
  var leftMostBox, topMostBox, anotherBox;

  if(this.box.left <= diff.boundingBox.left){
    leftMostBox = this.box;
    anotherBox = diff.boundingBox;
  }else{
    leftMostBox = diff.boundingBox;
    anotherBox = this.box;
  }

  // Check if horizontally detached
  if(leftMostBox.right < anotherBox.left){
    return false;
  }

  if(this.box.top <= diff.boundingBox.top){
    topMostBox = this.box;
    anotherBox = diff.boundingBox;
  }else{
    topMostBox = diff.boundingBox;
    anotherBox = this.box;
  }

  // Check if vertically detached
  if(topMostBox.bottom < anotherBox.top){
    return false;
  }

  return true;
};<|endoftext|>


<commit_before>/** @jsx React.DOM */

var React = require('react');
var SerializablePageDiff = require('../livescript/components/SerializablePageDiff.ls');
var IframeUtil = require('../livescript/components/IframeUtil.ls');
var ElementDifference = require('../livescript/components/ElementDifference.ls');

console.log('React', React);
console.log('chrome', chrome);

// Make React Debugging Chrome extension happy
window.React = React;

var ReportApp = React.createClass({
  render: function(){
    return (

        <ReportHeader />
        <DiffList />

    );
  }
});

var ReportHeader = React.createClass({
  getInitialState: function(){
    return {
      isInitial: true,
      isLoading: false,
      total: 0,
      done: 0
    };
  },
  componentDidMount: function(){
    // Message handling
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      switch(message.type){
      case "PROCESS_START":
        that.setState({
          isLoading: true,
          isInitial: false,
          total: message.data.total,
          done: 0,
          startTimestamp: Date.now()
        });
        break;

      case "PROCESS_END":
        that.setState({
          isLoading: false,
          processingTime: (Date.now() - that.state.startTimestamp) / 1000
        });
        break;

      case "PAGE_DIFF":
        that.setState({
          done: that.state.done + 1
        });
      }
    });
  },
  render: function(){
    if(this.state.isInitial){
      return (

SeeSS

Start recording some interaction! :)

      );
    }else{
      if(this.state.isLoading){
        return (

Loading... ({this.state.done} / {this.state.total})

        );
      }else{
        return (

Done in {this.state.processingTime} seconds.({this.state.done}/{this.state.total})

        );
      }
    }
  }
});

var DiffList = React.createClass({

  getInitialState: function(){
    return {data: []};
  },

  componentDidMount: function(){
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      var pageDiff;
      switch(message.type){

      // Background script starts processing a CSS or HTML change.
      //
      case "PROCESS_START":
        console.log("<Message> Background script processing started");
        that.setState({data: []});
        break;

      // The background script has done all processing.
      //
      case "PROCESS_END":
        console.log("<Message> Background script processing end");
        break;

      // The background script passes a SerializablePageDiff instance here.
      case "PAGE_DIFF":
        if(message.data === null){
          return;
        }
        pageDiff = new SerializablePageDiff(message.data);
        console.log("<Message> Data arrived from background script", pageDiff);

        var newData = that.state.data;

        var diff, i;
        // Write diff id inside diff
        for(i=0; i<pageDiff.diffs.length; i+=1){
          pageDiff.diffs[i].id = i;
        }

        for(i = 0; i < pageDiff.diffs.length; i+=1){
          diff = pageDiff.diffs[i];

          //Calculate BoundingBox, decide which is to push

          var diffPack={
            domWidth: pageDiff.width,
            domHeight: pageDiff.height,
            diffs: [diff],
            boxWidth: diff.boundingBox.right - diff.boundingBox.left,
            boxHeight: diff.boundingBox.bottom - diff.boundingBox.top,
            boxLeft: diff.boundingBox.left,
            boxTop: diff.boundingBox.top,
            dom: pageDiff.dom(),
            url: pageDiff.url,
            doctype: pageDiff.doctype
          };

          newData.push(diffPack);
        }

        that.setState({data: newData});
      }
    });
  },

  render: function(){
    var DiffArray = this.state.data.map(function(diff){
        return (<Diff domWidth={diff.domWidth} domHeight={diff.domHeight}
                      boxWidth={diff.boxWidth} boxHeight={diff.boxHeight}
                      boxLeft={diff.boxLeft} boxTop={diff.boxTop}
                      dom={diff.dom} url={diff.url}
                      doctype={diff.doctype}
                      diffs={diff.diffs}></Diff>);

    });

    return (

        {DiffArray}

    );
  }
});

var Diff = React.createClass({
  componentDidMount: function(){
    var iframe = this.refs.iframeElem.getDOMNode(),
        diffs = this.props.diffs,
        iframeDoc = iframe.contentDocument,
        styleElem = iframeDoc.createElement('style');

    // Create animated repositioning hint
    //
    IframeUtil.waitForAssets(iframe.contentDocument).then(function(){
      diffs.forEach(function(diff){
        var diffId = diff.id,
            hintElem,
            currentRect,
            beforeRulesData, beforeRules, afterRules,
            elem = iframeDoc.querySelector('['+SerializablePageDiff.DIFF_ID_ATTR+'~="'+diffId+'"]');

        if(diff.type === ElementDifference.TYPE_MOD){
          if(diff.rect){
            currentRect = elem.getBoundingClientRect();

            hintElem = iframeDoc.createElement('div');
            hintElem.id = "SEESS_POSITION_ANIMATE";

            beforeRuleData = {
              left: (diff.rect.left && diff.rect.left.before) || currentRect.left,
              top: (diff.rect.top && diff.rect.top.before) || currentRect.top,
              width: (diff.rect.width && diff.rect.width.before) || currentRect.width,
              height: (diff.rect.height && diff.rect.height.before) || currentRect.height
            };

            beforeRules =
              "left: " + beforeRuleData.left + 'px;' +
              "top: " + beforeRuleData.top + 'px;' +
              "width: " + beforeRuleData.width + 'px;' +
              "height: " + beforeRuleData.height + 'px;';

            afterRules =
              "transform: translate(" + (currentRect.left-beforeRuleData.left) + 'px,' +
                                      (currentRect.top-beforeRuleData.top) + 'px) ' +
                         "scale(" + (currentRect.width / beforeRuleData.width) + ',' +
                                    (currentRect.height / beforeRuleData.height) + ');';

            styleElem.innerHTML += "#SEESS_POSITION_ANIMATE{" +
                "z-index: 99999; box-sizing: border-box; position: fixed; " +
                "border: 1px dashed red; background:rgba(255,0,0,0.1);" +
                "transform-origin: left top;" +
                "-webkit-animation: SEESS_POSITION_" + diffId + " 3s ease-in-out 0s infinite;" +
                "will-change: transform; " + beforeRules +
              "}\n" +
              "@-webkit-keyframes SEESS_POSITION_" + diffId + " {to {"+afterRules+"}}";

            iframeDoc.body.appendChild(hintElem);
          }
        }
      });
      iframeDoc.body.appendChild(styleElem);
    });
    IframeUtil.setDocument(iframeDoc, this.props.dom.cloneNode(true), this.props.doctype);
  },
  render: function(){
    //var cropSize = document.getElementById('fakecrop').style.width;
    var cropSize = window.getComputedStyle(document.getElementById('fakecrop')).getPropertyValue('width');
    cropSize = parseInt(cropSize, 10);

    console.log('BBwidth: ' + this.props.boxWidth + '\nBBheight: ' + this.props.boxHeight);

    var scale = '', translate = '', origin = '';
    var scaleX = cropSize / this.props.boxWidth;
    var scaleY = cropSize / this.props.boxHeight;

    if(scaleX < 1 || scaleY < 1){
      var scaleMin;
      if(scaleX < scaleY){
        scaleMin = scaleX;
      }
      else{
        scaleMin = scaleY;
      }
      scale = 'scale(' + scaleMin + ',' + scaleMin + ')';
    }

    var originLeft = this.props.boxLeft + this.props.boxWidth/2,
        originTop  = this.props.boxTop + this.props.boxHeight/2;

    translate = 'translate(' + (cropSize/2-originLeft) + 'px,' + (cropSize/2-originTop) + 'px)';
    origin = originLeft + 'px ' + originTop + 'px';

    var iframeStyle= {
      width: this.props.domWidth,
      height: this.props.domHeight,
      transform: translate + ' ' + scale,
      transformOrigin: origin
    };

    return (

 {this.props.url}

    );

  }
});

React.renderComponent(<ReportApp />, document.getElementById('body'));

// For debug.
// TODO: Remove when going live!
//

<commit_msg>Opacify the repositioning hint a bit<commit_after>/** @jsx React.DOM */

var React = require('react');
var SerializablePageDiff = require('../livescript/components/SerializablePageDiff.ls');
var IframeUtil = require('../livescript/components/IframeUtil.ls');
var ElementDifference = require('../livescript/components/ElementDifference.ls');

console.log('React', React);
console.log('chrome', chrome);

// Make React Debugging Chrome extension happy
window.React = React;

var ReportApp = React.createClass({
  render: function(){
    return (

        <ReportHeader />
        <DiffList />

    );
  }
});

var ReportHeader = React.createClass({
  getInitialState: function(){
    return {
      isInitial: true,
      isLoading: false,
      total: 0,
      done: 0
    };
  },
  componentDidMount: function(){
    // Message handling
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      switch(message.type){
      case "PROCESS_START":
        that.setState({
          isLoading: true,
          isInitial: false,
          total: message.data.total,
          done: 0,
          startTimestamp: Date.now()
        });
        break;

      case "PROCESS_END":
        that.setState({
          isLoading: false,
          processingTime: (Date.now() - that.state.startTimestamp) / 1000
        });
        break;

      case "PAGE_DIFF":
        that.setState({
          done: that.state.done + 1
        });
      }
    });
  },
  render: function(){
    if(this.state.isInitial){
      return (

SeeSS

Start recording some interaction! :)

      );
    }else{
      if(this.state.isLoading){
        return (

Loading... ({this.state.done} / {this.state.total})

        );
      }else{
        return (

Done in {this.state.processingTime} seconds.({this.state.done}/{this.state.total})

        );
      }
    }
  }
});

var DiffList = React.createClass({

  getInitialState: function(){
    return {data: []};
  },

  componentDidMount: function(){
    var that = this;
    chrome.runtime.onMessage.addListener(function(message){
      var pageDiff;
      switch(message.type){

      // Background script starts processing a CSS or HTML change.
      //
      case "PROCESS_START":
        console.log("<Message> Background script processing started");
        that.setState({data: []});
        break;

      // The background script has done all processing.
      //
      case "PROCESS_END":
        console.log("<Message> Background script processing end");
        break;

      // The background script passes a SerializablePageDiff instance here.
      case "PAGE_DIFF":
        if(message.data === null){
          return;
        }
        pageDiff = new SerializablePageDiff(message.data);
        console.log("<Message> Data arrived from background script", pageDiff);

        var newData = that.state.data;

        var diff, i;
        // Write diff id inside diff
        for(i=0; i<pageDiff.diffs.length; i+=1){
          pageDiff.diffs[i].id = i;
        }

        for(i = 0; i < pageDiff.diffs.length; i+=1){
          diff = pageDiff.diffs[i];

          //Calculate BoundingBox, decide which is to push

          var diffPack={
            domWidth: pageDiff.width,
            domHeight: pageDiff.height,
            diffs: [diff],
            boxWidth: diff.boundingBox.right - diff.boundingBox.left,
            boxHeight: diff.boundingBox.bottom - diff.boundingBox.top,
            boxLeft: diff.boundingBox.left,
            boxTop: diff.boundingBox.top,
            dom: pageDiff.dom(),
            url: pageDiff.url,
            doctype: pageDiff.doctype
          };

          newData.push(diffPack);
        }

        that.setState({data: newData});
      }
    });
  },

  render: function(){
    var DiffArray = this.state.data.map(function(diff){
        return (<Diff domWidth={diff.domWidth} domHeight={diff.domHeight}
                      boxWidth={diff.boxWidth} boxHeight={diff.boxHeight}
                      boxLeft={diff.boxLeft} boxTop={diff.boxTop}
                      dom={diff.dom} url={diff.url}
                      doctype={diff.doctype}
                      diffs={diff.diffs}></Diff>);

    });

    return (

        {DiffArray}

    );
  }
});

var Diff = React.createClass({
  componentDidMount: function(){
    var iframe = this.refs.iframeElem.getDOMNode(),
        diffs = this.props.diffs,
        iframeDoc = iframe.contentDocument,
        styleElem = iframeDoc.createElement('style');

    // Create animated repositioning hint
    //
    IframeUtil.waitForAssets(iframe.contentDocument).then(function(){
      diffs.forEach(function(diff){
        var diffId = diff.id,
            hintElem,
            currentRect,
            beforeRulesData, beforeRules, afterRules,
            elem = iframeDoc.querySelector('['+SerializablePageDiff.DIFF_ID_ATTR+'~="'+diffId+'"]');

        if(diff.type === ElementDifference.TYPE_MOD){
          if(diff.rect){
            currentRect = elem.getBoundingClientRect();

            hintElem = iframeDoc.createElement('div');
            hintElem.id = "SEESS_POSITION_ANIMATE";

            beforeRuleData = {
              left: (diff.rect.left && diff.rect.left.before) || currentRect.left,
              top: (diff.rect.top && diff.rect.top.before) || currentRect.top,
              width: (diff.rect.width && diff.rect.width.before) || currentRect.width,
              height: (diff.rect.height && diff.rect.height.before) || currentRect.height
            };

            beforeRules =
              "left: " + beforeRuleData.left + 'px;' +
              "top: " + beforeRuleData.top + 'px;' +
              "width: " + beforeRuleData.width + 'px;' +
              "height: " + beforeRuleData.height + 'px;';

            afterRules =
              "transform: translate(" + (currentRect.left-beforeRuleData.left) + 'px,' +
                                      (currentRect.top-beforeRuleData.top) + 'px) ' +
                         "scale(" + (currentRect.width / beforeRuleData.width) + ',' +
                                    (currentRect.height / beforeRuleData.height) + ');';

            styleElem.innerHTML += "#SEESS_POSITION_ANIMATE{" +
                "z-index: 99999; box-sizing: border-box; position: fixed; " +
                "border: 1px dashed red; background:rgba(255,0,0,0.1);" +
                "transform-origin: left top; opacity: 1;" +
                "-webkit-animation: SEESS_POSITION_" + diffId + " 3s ease-in-out 0s infinite;" +
                "will-change: transform; " + beforeRules +
              "}\n" +
              "@-webkit-keyframes SEESS_POSITION_" + diffId + " {to {"+afterRules+"opacity: 0.5;}}";

            iframeDoc.body.appendChild(hintElem);
          }
        }
      });
      iframeDoc.body.appendChild(styleElem);
    });
    IframeUtil.setDocument(iframeDoc, this.props.dom.cloneNode(true), this.props.doctype);
  },
  render: function(){
    //var cropSize = document.getElementById('fakecrop').style.width;
    var cropSize = window.getComputedStyle(document.getElementById('fakecrop')).getPropertyValue('width');
    cropSize = parseInt(cropSize, 10);

    console.log('BBwidth: ' + this.props.boxWidth + '\nBBheight: ' + this.props.boxHeight);

    var scale = '', translate = '', origin = '';
    var scaleX = cropSize / this.props.boxWidth;
    var scaleY = cropSize / this.props.boxHeight;

    if(scaleX < 1 || scaleY < 1){
      var scaleMin;
      if(scaleX < scaleY){
        scaleMin = scaleX;
      }
      else{
        scaleMin = scaleY;
      }
      scale = 'scale(' + scaleMin + ',' + scaleMin + ')';
    }

    var originLeft = this.props.boxLeft + this.props.boxWidth/2,
        originTop  = this.props.boxTop + this.props.boxHeight/2;

    translate = 'translate(' + (cropSize/2-originLeft) + 'px,' + (cropSize/2-originTop) + 'px)';
    origin = originLeft + 'px ' + originTop + 'px';

    var iframeStyle= {
      width: this.props.domWidth,
      height: this.props.domHeight,
      transform: translate + ' ' + scale,
      transformOrigin: origin
    };

    return (

 {this.props.url}

    );

  }
});

React.renderComponent(<ReportApp />, document.getElementById('body'));

// For debug.
// TODO: Remove when going live!
//

<|endoftext|>


<commit_before>this.mmooc=this.mmooc||{};

//https://medium.com/@pointbmusic/youtube-api-checklist-c195e9abaff1
this.mmooc.youtube = function() {
	var hrefPrefix = "https://video.google.com/timedtext?v=";
	var transcriptIdPrefix = "videoTranscript";
	var transcriptArr = [];
	var initialized = false;

	function transcript(transcriptId, language, name)
	{
		var transcriptId = transcriptId;
		var videoId = transcriptId.split(transcriptIdPrefix)[1];

		var href = hrefPrefix + videoId;
		if(language != "")
		{
		   href = href + "&lang=" + language;
		}
		if(name != "")
		{
		  href = href + "&name=" + name;
		}

			//Array of captions in video
		var captionsLoaded = false;

		//Timeout for next caption
		var captionTimeout = null;

		var captions = null;

		//Keep track of which captions we are showing
		var currentCaptionIndex = 0;
		var nextCaptionIndex = 0;

		this.player = new YT.Player(videoId, {
		  videoId: videoId,
		  events: {
			'onReady': onPlayerReady,
			'onStateChange': onPlayerStateChange
		  }
  	    });

		var findCaptionIndexFromTimestamp = function(timeStamp)
		{
			var start = 0;
			var duration = 0;
			for (var i = 0, il = captions.length; i < il; i++) {
				start = Number(getStartTimeFromCaption(i));
				duration = Number(getDurationFromCaption(i));

				//Return the first caption if the timeStamp is smaller than the first caption start time.
				if(timeStamp < start)
				{
					break;
				}

				//Check if the timestamp is in the interval of this caption.
				if((timeStamp >= start) && (timeStamp < (start + duration)))
				{
					break;
				}
			}
			return i;
		}

		var clearCurrentHighlighting = function()
		{
			var timeStampId = getTimeIdFromTimestampIndex(currentCaptionIndex);
			$("#"+timeStampId).css('background-color', '');
		}

		var highlightNextCaption = function ()
		{
			var timestampId = getTimeIdFromTimestampIndex(nextCaptionIndex);
			$("#"+timestampId).css('background-color', 'yellow');
		}

		var calculateTimeout = function (currentTime)
		{
			var startTime = Number(getStartTimeFromCaption(currentCaptionIndex));
			var duration = Number(getDurationFromCaption(currentCaptionIndex));
			var timeoutValue = startTime - currentTime + duration;
			return timeoutValue;
		}

		this.setCaptionTimeout = function (timeoutValue)
		{
			if(timeoutValue < 0)
			{
				return;
			}

			clearTimeout(captionTimeout);

			var transcript = this;

			captionTimeout = setTimeout(function() {
				transcript.highlightCaptionAndPrepareForNext();
			}, timeoutValue*1000)
		}

		var getStartTimeFromCaption = function(i)
		{
			if(i >= captions.length)
			{
				return -1;
			}
			return captions[i].getAttribute('start');
		}
		var getDurationFromCaption = function(i)
		{
			if(i >= captions.length)
			{
				return -1;
			}
			return captions[i].getAttribute('dur');
		}
		var getTimeIdFromTimestampIndex = function(i)
		{
			var strTimestamp = "" + i;
			return "t" + strTimestamp;
		}

		//////////////////
		//Public functions
		/////////////////

		//This function highlights the next caption in the list and
		//sets a timeout for the next one after that.
		//It must be public as it is called from a timer.
		this.highlightCaptionAndPrepareForNext = function ()
		{
			clearCurrentHighlighting();
			highlightNextCaption();
			currentCaptionIndex = nextCaptionIndex;
			nextCaptionIndex++;

			var currentTime = this.player.getCurrentTime();
			var timeoutValue = calculateTimeout(currentTime);

			if(nextCaptionIndex <= captions.length)
			{
				this.setCaptionTimeout(timeoutValue);
			}
		}

		//Called if the user has dragged the slider to somewhere in the video.
		this.highlightCaptionFromTimestamp = function(timeStamp)
		{
			clearCurrentHighlighting();
			nextCaptionIndex = findCaptionIndexFromTimestamp(timeStamp);
			currentCaptionIndex = nextCaptionIndex;

			var startTime = Number(getStartTimeFromCaption(currentCaptionIndex));

			var timeoutValue = -1;
			if(timeStamp < startTime)
			{
				timeoutValue = startTime - currentTime;
			}
			else
			{
				highlightNextCaption();
				timeoutValue = calculateTimeout(currentTime);
			}
			this.setCaptionTimeout(timeoutValue);
		}

		this.transcriptLoaded = function(transcript) {
			var start = 0;
			captions = transcript.getElementsByTagName('text');
			var srt_output = "
0:00
";

			for (var i = 0, il = captions.length; i < il; i++) {
				start =+ getStartTimeFromCaption(i);

				captionText = captions[i].textContent.replace(/</g, '<').replace(/>/g, '>');
				var timestampId = getTimeIdFromTimestampIndex(i);
				srt_output += "" + captionText + " ";
			};

			$("#videoTranscript" + videoId).append(srt_output);
			captionsLoaded = true;
		}

		this.getTranscriptId = function()
		{
			return transcriptId;
		}
		this.getVideoId = function()
		{
			return videoId;
		}

		this.getTranscript = function()
		{
			var oTranscript = this;
			$.ajax({
				url: href,
				type: 'GET',
				data: {},
				success: function(response) {
					oTranscript.transcriptLoaded(response);
				},
				error: function(XMLHttpRequest, textStatus, errorThrown) {
					console.log("Error during GET");
				}
			});
		}

		this.playerPlaying = function()
		{
			if(!captionsLoaded)
			{
				return;
			}

		    currentTime = this.player.getCurrentTime();
		    this.highlightCaptionFromTimestamp(currentTime);
		}
		this.playerNotPlaying = function (transcript)
		{
			if(!captionsLoaded)
			{
				return;
			}
			clearTimeout(captionTimeout);
		}
	}

	//Called when user clicks somewhere in the transcript.
	$(function() {
		$(document).on('click', '.btnSeek', function() {
			var seekToTime = $(this).data('seek');
			var transcript = mmooc.youtube.getTranscriptFromTranscriptId($(this).parent().attr("id"));
			transcript.player.seekTo(seekToTime, true);
			transcript.player.playVideo();
		});
	});

	//These functions must be global as YouTube API will call them.
	var previousOnYouTubePlayerAPIReady = window.onYouTubePlayerAPIReady;
	window.onYouTubePlayerAPIReady = function() {
		if(previousOnYouTubePlayerAPIReady)
		{
			previousOnYouTubePlayerAPIReady();
		}
		mmooc.youtube.APIReady();
	};

	// The API will call this function when the video player is ready.
	// It can be used to auto start the video f.ex.
	window.onPlayerReady = function(event) {
	}

	// The API calls this function when the player's state changes.
	//    The function indicates that when playing a video (state=1),
	//    the player should play for six seconds and then stop.
	window.onPlayerStateChange = function(event) {
		console.log("onPlayerStateChange " + event.data);
		var transcript = this.mmooc.youtube.getTranscriptFromVideoId(event.target.getIframe().id);
		if (event.data == YT.PlayerState.PLAYING) {
			transcript.playerPlaying();
		}
		else
		{
			transcript.playerNotPlaying();
		}
	}

	return {
		getTranscriptFromTranscriptId(transcriptId)
		{
			for (index = 0; index < transcriptArr.length; ++index) {
				if(transcriptArr[index].getTranscriptId() == transcriptId)
				{
					return transcriptArr[index];
				}
			}
			return null;
		},
	    getTranscriptFromVideoId(videoId)
	    {
			for (index = 0; index < transcriptArr.length; ++index) {
				if(transcriptArr[index].getVideoId() == videoId)
				{
					return transcriptArr[index];
				}
			}
			return null;
	    },

		APIReady : function ()
		{
			if(!initialized)
			{
				$(".mmocVideoTranscript" ).each(function( i ) {
					var language = $(this).data('language');
					var name = $(this).data('name');
					var oTranscript = new transcript(this.id, language, name);
					oTranscript.getTranscript();
					transcriptArr.push(oTranscript);
				});
				initialized = true;
			}
		},
		init : function ()
		{
			this.APIReady();
		}
	}

}();
//Everything is ready, load the youtube iframe_api
$.getScript("https://www.youtube.com/iframe_api");

<commit_msg>Fix incorrect function declaration that made internet explorer complain.<commit_after>this.mmooc=this.mmooc||{};

//https://medium.com/@pointbmusic/youtube-api-checklist-c195e9abaff1
this.mmooc.youtube = function() {
	var hrefPrefix = "https://video.google.com/timedtext?v=";
	var transcriptIdPrefix = "videoTranscript";
	var transcriptArr = [];
	var initialized = false;

	function transcript(transcriptId, language, name)
	{
		var transcriptId = transcriptId;
		var videoId = transcriptId.split(transcriptIdPrefix)[1];

		var href = hrefPrefix + videoId;
		if(language != "")
		{
		   href = href + "&lang=" + language;
		}
		if(name != "")
		{
		  href = href + "&name=" + name;
		}

			//Array of captions in video
		var captionsLoaded = false;

		//Timeout for next caption
		var captionTimeout = null;

		var captions = null;

		//Keep track of which captions we are showing
		var currentCaptionIndex = 0;
		var nextCaptionIndex = 0;

		this.player = new YT.Player(videoId, {
		  videoId: videoId,
		  events: {
			'onReady': onPlayerReady,
			'onStateChange': onPlayerStateChange
		  }
  	    });

		var findCaptionIndexFromTimestamp = function(timeStamp)
		{
			var start = 0;
			var duration = 0;
			for (var i = 0, il = captions.length; i < il; i++) {
				start = Number(getStartTimeFromCaption(i));
				duration = Number(getDurationFromCaption(i));

				//Return the first caption if the timeStamp is smaller than the first caption start time.
				if(timeStamp < start)
				{
					break;
				}

				//Check if the timestamp is in the interval of this caption.
				if((timeStamp >= start) && (timeStamp < (start + duration)))
				{
					break;
				}
			}
			return i;
		}

		var clearCurrentHighlighting = function()
		{
			var timeStampId = getTimeIdFromTimestampIndex(currentCaptionIndex);
			$("#"+timeStampId).css('background-color', '');
		}

		var highlightNextCaption = function ()
		{
			var timestampId = getTimeIdFromTimestampIndex(nextCaptionIndex);
			$("#"+timestampId).css('background-color', 'yellow');
		}

		var calculateTimeout = function (currentTime)
		{
			var startTime = Number(getStartTimeFromCaption(currentCaptionIndex));
			var duration = Number(getDurationFromCaption(currentCaptionIndex));
			var timeoutValue = startTime - currentTime + duration;
			return timeoutValue;
		}

		this.setCaptionTimeout = function (timeoutValue)
		{
			if(timeoutValue < 0)
			{
				return;
			}

			clearTimeout(captionTimeout);

			var transcript = this;

			captionTimeout = setTimeout(function() {
				transcript.highlightCaptionAndPrepareForNext();
			}, timeoutValue*1000)
		}

		var getStartTimeFromCaption = function(i)
		{
			if(i >= captions.length)
			{
				return -1;
			}
			return captions[i].getAttribute('start');
		}
		var getDurationFromCaption = function(i)
		{
			if(i >= captions.length)
			{
				return -1;
			}
			return captions[i].getAttribute('dur');
		}
		var getTimeIdFromTimestampIndex = function(i)
		{
			var strTimestamp = "" + i;
			return "t" + strTimestamp;
		}

		//////////////////
		//Public functions
		/////////////////

		//This function highlights the next caption in the list and
		//sets a timeout for the next one after that.
		//It must be public as it is called from a timer.
		this.highlightCaptionAndPrepareForNext = function ()
		{
			clearCurrentHighlighting();
			highlightNextCaption();
			currentCaptionIndex = nextCaptionIndex;
			nextCaptionIndex++;

			var currentTime = this.player.getCurrentTime();
			var timeoutValue = calculateTimeout(currentTime);

			if(nextCaptionIndex <= captions.length)
			{
				this.setCaptionTimeout(timeoutValue);
			}
		}

		//Called if the user has dragged the slider to somewhere in the video.
		this.highlightCaptionFromTimestamp = function(timeStamp)
		{
			clearCurrentHighlighting();
			nextCaptionIndex = findCaptionIndexFromTimestamp(timeStamp);
			currentCaptionIndex = nextCaptionIndex;

			var startTime = Number(getStartTimeFromCaption(currentCaptionIndex));

			var timeoutValue = -1;
			if(timeStamp < startTime)
			{
				timeoutValue = startTime - currentTime;
			}
			else
			{
				highlightNextCaption();
				timeoutValue = calculateTimeout(currentTime);
			}
			this.setCaptionTimeout(timeoutValue);
		}

		this.transcriptLoaded = function(transcript) {
			var start = 0;
			captions = transcript.getElementsByTagName('text');
			var srt_output = "
0:00
";

			for (var i = 0, il = captions.length; i < il; i++) {
				start =+ getStartTimeFromCaption(i);

				captionText = captions[i].textContent.replace(/</g, '<').replace(/>/g, '>');
				var timestampId = getTimeIdFromTimestampIndex(i);
				srt_output += "" + captionText + " ";
			};

			$("#videoTranscript" + videoId).append(srt_output);
			captionsLoaded = true;
		}

		this.getTranscriptId = function()
		{
			return transcriptId;
		}
		this.getVideoId = function()
		{
			return videoId;
		}

		this.getTranscript = function()
		{
			var oTranscript = this;
			$.ajax({
				url: href,
				type: 'GET',
				data: {},
				success: function(response) {
					oTranscript.transcriptLoaded(response);
				},
				error: function(XMLHttpRequest, textStatus, errorThrown) {
					console.log("Error during GET");
				}
			});
		}

		this.playerPlaying = function()
		{
			if(!captionsLoaded)
			{
				return;
			}

		    currentTime = this.player.getCurrentTime();
		    this.highlightCaptionFromTimestamp(currentTime);
		}
		this.playerNotPlaying = function (transcript)
		{
			if(!captionsLoaded)
			{
				return;
			}
			clearTimeout(captionTimeout);
		}
	}

	//Called when user clicks somewhere in the transcript.
	$(function() {
		$(document).on('click', '.btnSeek', function() {
			var seekToTime = $(this).data('seek');
			var transcript = mmooc.youtube.getTranscriptFromTranscriptId($(this).parent().attr("id"));
			transcript.player.seekTo(seekToTime, true);
			transcript.player.playVideo();
		});
	});

	//These functions must be global as YouTube API will call them.
	var previousOnYouTubePlayerAPIReady = window.onYouTubePlayerAPIReady;
	window.onYouTubePlayerAPIReady = function() {
		if(previousOnYouTubePlayerAPIReady)
		{
			previousOnYouTubePlayerAPIReady();
		}
		mmooc.youtube.APIReady();
	};

	// The API will call this function when the video player is ready.
	// It can be used to auto start the video f.ex.
	window.onPlayerReady = function(event) {
	}

	// The API calls this function when the player's state changes.
	//    The function indicates that when playing a video (state=1),
	//    the player should play for six seconds and then stop.
	window.onPlayerStateChange = function(event) {
		console.log("onPlayerStateChange " + event.data);
		var transcript = this.mmooc.youtube.getTranscriptFromVideoId(event.target.getIframe().id);
		if (event.data == YT.PlayerState.PLAYING) {
			transcript.playerPlaying();
		}
		else
		{
			transcript.playerNotPlaying();
		}
	}

	return {
		getTranscriptFromTranscriptId : function(transcriptId)
		{
			for (index = 0; index < transcriptArr.length; ++index) {
				if(transcriptArr[index].getTranscriptId() == transcriptId)
				{
					return transcriptArr[index];
				}
			}
			return null;
		},
	    getTranscriptFromVideoId : function(videoId)
	    {
			for (index = 0; index < transcriptArr.length; ++index) {
				if(transcriptArr[index].getVideoId() == videoId)
				{
					return transcriptArr[index];
				}
			}
			return null;
	    },

		APIReady : function ()
		{
			if(!initialized)
			{
				$(".mmocVideoTranscript" ).each(function( i ) {
					var language = $(this).data('language');
					var name = $(this).data('name');
					var oTranscript = new transcript(this.id, language, name);
					oTranscript.getTranscript();
					transcriptArr.push(oTranscript);
				});
				initialized = true;
			}
		},
		init : function ()
		{
			this.APIReady();
		}
	}

}();
//Everything is ready, load the youtube iframe_api
$.getScript("https://www.youtube.com/iframe_api");

<|endoftext|>


<commit_before>/*
  Copyright 2017 Processwall Limited

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

  Company: Processwall Limited
  Address: The Winnowing House, Mill Lane, Askham Richard, York, YO23 3NW, United Kingdom
  Tel:     +44 113 815 3440
  Web:     http://www.processwall.com
  Email:   support@processwall.com
*/

define([
	'dojo/_base/declare',
	'dojo/_base/lang',
	'dojo/_base/array',
	'./_Grid',
	'dgrid/util/misc',
	'dijit/layout/BorderContainer',
	'./Control',
	'./CellEditors/Boolean',
	'./CellEditors/Date',
	'./CellEditors/Decimal',
	'./CellEditors/Float',
	'./CellEditors/Integer',
	'./CellEditors/Item',
	'./CellEditors/List',
	'./CellEditors/String',
	'./CellEditors/Text',
	'./Column',
	'./Row',
	'./Cell',
], function(declare, lang, array, _Grid, GridMisc, BorderContainer, Control, BooleanEditor, DateEditor, DecimalEditor, FloatEditor, IntegerEditor, ItemEditor, ListEditor, StringEditor, TextEditor, Column, Row, Cell) {

	return declare('Aras.View.Grid', [BorderContainer, Control], {

		_grid: null,

		_rowsHandle: null,

		_columnsHandle: null,

		SelectedRows: null,

		baseClass: 'dijitContentPaneNoPadding',

		ShowHeader: null,

		Columns: null,

		Rows: null,

		constructor: function() {

			this.Columns = [];
			this.Rows = [];
		},

		startup: function() {
			this.inherited(arguments);

			// Call Control Startup
			this._startup();

			// Create Grid
			this._grid = new _Grid({ region: 'center', selectionMode: 'extended', showHeader: this.ShowHeader });
			this.addChild(this._grid);

			// Process Grid Select Event
			this._grid.on('dgrid-select', lang.hitch(this, function(event) {

				if ((this.ViewModel != null) && (this.ViewModel.Select.CanExecute))
				{
					var Parameters = [];

					array.forEach(event.rows, function(row) {
						Parameters.push(this.ViewModel.Rows[row.data.id].ID);
					}, this);

					this.ViewModel.Select.Execute(Parameters);
				}
			}));

			// Process Grid Editor Show Event
			this._grid.on('dgrid-editor-show', lang.hitch(this, function(event) {

				if (this.ViewModel != null)
				{
					// Get Cell ViewModel
					var rowid = event.cell.row.id;
					var colname = event.cell.column.id;
					var rowviewmodel = this.ViewModel.Rows[rowid];
					var colindex = -1;

					for (var j=0; j<this.ViewModel.Columns.length; j++)
					{
						if (this.ViewModel.Columns[j].Name == colname)
						{
							colindex = j;
							break;
						}
					}

					// Set Editor ViewModel
					event.editor.set('ViewModel', this.ViewModel.Rows[rowid].Cells[colindex]);
				}

			}));

			// Process Grid Editor Changed Event
			this._grid.on('dgrid-datachange', lang.hitch(this, function(event) {

				// Cancel Event
				event.preventDefault();

				if (this.ViewModel != null)
				{
					// Get Cell ViewModel
					var rowid = event.cell.row.id;
					var colname = event.cell.column.id;
					var rowviewmodel = this.ViewModel.Rows[rowid];
					var colindex = -1;

					for (var j=0; j<this.ViewModel.Columns.length; j++)
					{
						if (this.ViewModel.Columns[j].Name == colname)
						{
							colindex = j;
							break;
						}
					}

					// Write New Value
					this.ViewModel.Rows[rowid].Cells[colindex].Write();
				}

			}));

			// Watch for Changes in Columns
			this._columnsHandle = this.ViewModel.watch("Columns", lang.hitch(this, this._updateColumns));

			// Watch for Changes in Rows
			this._rowsHandle = this.ViewModel.watch("Rows", lang.hitch(this, this._updateRows));

			// Update Rows
			this._updateColumns();
		},

		destroy: function() {
			this.inherited(arguments);

			// Call Control Destroy
			this._destroy();

			if (this._rowsHandle)
			{
				this._rowsHandle.unwatch();
			}

			if (this._columnsHandle)
			{
				this._columnsHandle.unwatch();
			}
		},

		OnViewModelChanged: function(name, oldValue, newValue) {
			this.inherited(arguments);

			// Unwatch for changes in Columns
			if (this._columnsHandle)
			{
				this._columnsHandle.unwatch();
			}

			// Unwatch for Changes in Rows
			if (this._rowsHandle)
			{
				this._rowsHandle.unwatch();
			}

			// Watch for Changes in Columns
			this._columnsHandle = this.ViewModel.watch("Columns", lang.hitch(this, this._updateColumns));

			// Watch for Changes in Rows
			this._rowsHandle = this.ViewModel.watch("Rows", lang.hitch(this, this._updateRows));

			// Update Columns
			this._updateColumns();
		},

		_updateColumns: function() {

			// Reset Columns
			array.forEach(this.Columns, function(column) {
				column.Visible = false;
			}, this);

			// Create Columns
			var gridcolumns = {};

			array.forEach(this.ViewModel.Columns, function(columnviewmodel, i) {

				if (this.Columns[i])
				{
					this.Columns[i].Visible = true;
				}
				else
				{
					this.Columns[i] = new Column({ Grid: this});
					this.Columns[i].startup();
				}

				this.Columns[i].ViewModel = columnviewmodel;

				gridcolumns[columnviewmodel.Name] = {};
				gridcolumns[columnviewmodel.Name].label = columnviewmodel.Label;
				gridcolumns[columnviewmodel.Name].sortable = false;
				gridcolumns[columnviewmodel.Name].editOn = 'click';
				gridcolumns[columnviewmodel.Name].dismissOnEnter = false;

				if (columnviewmodel.Editable)
				{
					// Set canEdit to allow row specific editing
					gridcolumns[columnviewmodel.Name].canEdit = lang.hitch(this, this._canEditCell);

					switch(columnviewmodel.Type)
					{
						case "Aras.View.Columns.Boolean":
							gridcolumns[columnviewmodel.Name].editor = BooleanEditor;
							break;

						case "Aras.View.Columns.Date":
							gridcolumns[columnviewmodel.Name].editor = DateEditor;
							gridcolumns[columnviewmodel.Name].editorArgs = { style: 'width:' + (columnviewmodel.Width - 5) + 'px;' };
							break;

						case "Aras.View.Columns.Decimal":
							gridcolumns[columnviewmodel.Name].editor = DecimalEditor;
							break;

						case "Aras.View.Columns.Float":
							gridcolumns[columnviewmodel.Name].editor = FloatEditor;
							break;

						case "Aras.View.Columns.Integer":
							gridcolumns[columnviewmodel.Name].editor = IntegerEditor;
							break;

						case "Aras.View.Columns.Item":
							gridcolumns[columnviewmodel.Name].editor = ItemEditor;
							break;

						case "Aras.View.Columns.List":
							gridcolumns[columnviewmodel.Name].editor = ListEditor;
							break;

						case "Aras.View.Columns.String":
							gridcolumns[columnviewmodel.Name].editor = StringEditor;
							break;

						case "Aras.View.Columns.Text":
							gridcolumns[columnviewmodel.Name].editor = TextEditor;
							break;

						default:
							gridcolumns[columnviewmodel.Name].editor = undefined;
							break;
					}
				}
				else
				{
					gridcolumns[columnviewmodel.Name].editor = undefined;
				}

				// Cell Formatter
				gridcolumns[columnviewmodel.Name].formatter = lang.hitch(this, this._formatCell);

				// Set Column Width
				GridMisc.addCssRule('#' + GridMisc.escapeCssIdentifier(this._grid.domNode.id) +
						' .dgrid-column-' + GridMisc.escapeCssIdentifier(columnviewmodel.Name, '-'),
						'width: ' + columnviewmodel.Width + 'px;');

			}, this);

			// Set Grid Columns
			this._grid._setColumns(gridcolumns);

			// Update Rows
			this._updateRows();
		},

		_canEditCell(Grid, Cell) {

			if (Cell.ViewModel != null)
			{
				return Cell.ViewModel.Editable;
			}
			else
			{
				return false;
			}
		},

		_updateRows: function() {

			// Reset Rows
			array.forEach(this.Rows, function(row) {
				row.Visible = false;
			}, this);

			// Render Grid
			var rowdata = [];

			array.forEach(this.ViewModel.Rows, function(rowviewmodel, i) {

				if (this.Rows[i])
				{
					this.Rows[i].Visible = true;
				}
				else
				{
					this.Rows[i] = new Row({ Grid: this });
					this.Rows[i].startup();
				}

				this.Rows[i].ViewModel = rowviewmodel;
				this.Rows[i].ID = i;

				rowdata[i] = new Object();
				rowdata[i]['id'] = i;

				for (var j=0; j<this.ViewModel.Columns.length; j++)
				{
					if (!this.Rows[i].Cells[j])
					{
						this.Rows[i].Cells[j] = new Cell({ Row: this.Rows[i], Column: this.Columns[j]});
						this.Rows[i].Cells[j].startup();
					}

					this.Rows[i].Cells[j].set('ViewModel', rowviewmodel.Cells[j]);

					rowdata[i][this.ViewModel.Columns[j].Name] = this.Rows[i].Cells[j];
				}

			}, this);

			// Clear Grid
			this._grid.refresh();

			// Render Grid
			this._grid.renderArray(rowdata);
		},

		_formatCell: function(value, object) {

			switch(value.Column.ViewModel.Type)
			{
				case "Aras.View.Columns.Boolean":

					if (value.ViewModel.Value == '1')
					{
						return '<input type="checkbox" disabled checked="checked">';
					}
					else
					{
						return '<input type="checkbox" disabled>';
					}
				case "Aras.View.Columns.List":

					if (value.ViewModel.Selected != null)
					{
						return value.ViewModel.Selected.Label;
					}
					else
					{
						return '';
					}

				default:

					if (value.ViewModel.Value != null)
					{
						return value.ViewModel.Value;
					}
					else
					{
						return '';
					}
			}

		}

	});
});<commit_msg>Fixed IE issue in Grid<commit_after>/*
  Copyright 2017 Processwall Limited

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

  Company: Processwall Limited
  Address: The Winnowing House, Mill Lane, Askham Richard, York, YO23 3NW, United Kingdom
  Tel:     +44 113 815 3440
  Web:     http://www.processwall.com
  Email:   support@processwall.com
*/

define([
	'dojo/_base/declare',
	'dojo/_base/lang',
	'dojo/_base/array',
	'./_Grid',
	'dgrid/util/misc',
	'dijit/layout/BorderContainer',
	'./Control',
	'./CellEditors/Boolean',
	'./CellEditors/Date',
	'./CellEditors/Decimal',
	'./CellEditors/Float',
	'./CellEditors/Integer',
	'./CellEditors/Item',
	'./CellEditors/List',
	'./CellEditors/String',
	'./CellEditors/Text',
	'./Column',
	'./Row',
	'./Cell',
], function(declare, lang, array, _Grid, GridMisc, BorderContainer, Control, BooleanEditor, DateEditor, DecimalEditor, FloatEditor, IntegerEditor, ItemEditor, ListEditor, StringEditor, TextEditor, Column, Row, Cell) {

	return declare('Aras.View.Grid', [BorderContainer, Control], {

		_grid: null,

		_rowsHandle: null,

		_columnsHandle: null,

		SelectedRows: null,

		baseClass: 'dijitContentPaneNoPadding',

		ShowHeader: null,

		Columns: null,

		Rows: null,

		constructor: function() {

			this.Columns = [];
			this.Rows = [];
		},

		startup: function() {
			this.inherited(arguments);

			// Call Control Startup
			this._startup();

			// Create Grid
			this._grid = new _Grid({ region: 'center', selectionMode: 'extended', showHeader: this.ShowHeader });
			this.addChild(this._grid);

			// Process Grid Select Event
			this._grid.on('dgrid-select', lang.hitch(this, function(event) {

				if ((this.ViewModel != null) && (this.ViewModel.Select.CanExecute))
				{
					var Parameters = [];

					array.forEach(event.rows, function(row) {
						Parameters.push(this.ViewModel.Rows[row.data.id].ID);
					}, this);

					this.ViewModel.Select.Execute(Parameters);
				}
			}));

			// Process Grid Editor Show Event
			this._grid.on('dgrid-editor-show', lang.hitch(this, function(event) {

				if (this.ViewModel != null)
				{
					// Get Cell ViewModel
					var rowid = event.cell.row.id;
					var colname = event.cell.column.id;
					var rowviewmodel = this.ViewModel.Rows[rowid];
					var colindex = -1;

					for (var j=0; j<this.ViewModel.Columns.length; j++)
					{
						if (this.ViewModel.Columns[j].Name == colname)
						{
							colindex = j;
							break;
						}
					}

					// Set Editor ViewModel
					event.editor.set('ViewModel', this.ViewModel.Rows[rowid].Cells[colindex]);
				}

			}));

			// Process Grid Editor Changed Event
			this._grid.on('dgrid-datachange', lang.hitch(this, function(event) {

				// Cancel Event
				event.preventDefault();

				if (this.ViewModel != null)
				{
					// Get Cell ViewModel
					var rowid = event.cell.row.id;
					var colname = event.cell.column.id;
					var rowviewmodel = this.ViewModel.Rows[rowid];
					var colindex = -1;

					for (var j=0; j<this.ViewModel.Columns.length; j++)
					{
						if (this.ViewModel.Columns[j].Name == colname)
						{
							colindex = j;
							break;
						}
					}

					// Write New Value
					this.ViewModel.Rows[rowid].Cells[colindex].Write();
				}

			}));

			// Watch for Changes in Columns
			this._columnsHandle = this.ViewModel.watch("Columns", lang.hitch(this, this._updateColumns));

			// Watch for Changes in Rows
			this._rowsHandle = this.ViewModel.watch("Rows", lang.hitch(this, this._updateRows));

			// Update Rows
			this._updateColumns();
		},

		destroy: function() {
			this.inherited(arguments);

			// Call Control Destroy
			this._destroy();

			if (this._rowsHandle)
			{
				this._rowsHandle.unwatch();
			}

			if (this._columnsHandle)
			{
				this._columnsHandle.unwatch();
			}
		},

		OnViewModelChanged: function(name, oldValue, newValue) {
			this.inherited(arguments);

			// Unwatch for changes in Columns
			if (this._columnsHandle)
			{
				this._columnsHandle.unwatch();
			}

			// Unwatch for Changes in Rows
			if (this._rowsHandle)
			{
				this._rowsHandle.unwatch();
			}

			// Watch for Changes in Columns
			this._columnsHandle = this.ViewModel.watch("Columns", lang.hitch(this, this._updateColumns));

			// Watch for Changes in Rows
			this._rowsHandle = this.ViewModel.watch("Rows", lang.hitch(this, this._updateRows));

			// Update Columns
			this._updateColumns();
		},

		_updateColumns: function() {

			// Reset Columns
			array.forEach(this.Columns, function(column) {
				column.Visible = false;
			}, this);

			// Create Columns
			var gridcolumns = {};

			array.forEach(this.ViewModel.Columns, function(columnviewmodel, i) {

				if (this.Columns[i])
				{
					this.Columns[i].Visible = true;
				}
				else
				{
					this.Columns[i] = new Column({ Grid: this});
					this.Columns[i].startup();
				}

				this.Columns[i].ViewModel = columnviewmodel;

				gridcolumns[columnviewmodel.Name] = {};
				gridcolumns[columnviewmodel.Name].label = columnviewmodel.Label;
				gridcolumns[columnviewmodel.Name].sortable = false;
				gridcolumns[columnviewmodel.Name].editOn = 'click';
				gridcolumns[columnviewmodel.Name].dismissOnEnter = false;

				if (columnviewmodel.Editable)
				{
					// Set canEdit to allow row specific editing
					gridcolumns[columnviewmodel.Name].canEdit = lang.hitch(this, this._canEditCell);

					switch(columnviewmodel.Type)
					{
						case "Aras.View.Columns.Boolean":
							gridcolumns[columnviewmodel.Name].editor = BooleanEditor;
							break;

						case "Aras.View.Columns.Date":
							gridcolumns[columnviewmodel.Name].editor = DateEditor;
							gridcolumns[columnviewmodel.Name].editorArgs = { style: 'width:' + (columnviewmodel.Width - 5) + 'px;' };
							break;

						case "Aras.View.Columns.Decimal":
							gridcolumns[columnviewmodel.Name].editor = DecimalEditor;
							break;

						case "Aras.View.Columns.Float":
							gridcolumns[columnviewmodel.Name].editor = FloatEditor;
							break;

						case "Aras.View.Columns.Integer":
							gridcolumns[columnviewmodel.Name].editor = IntegerEditor;
							break;

						case "Aras.View.Columns.Item":
							gridcolumns[columnviewmodel.Name].editor = ItemEditor;
							break;

						case "Aras.View.Columns.List":
							gridcolumns[columnviewmodel.Name].editor = ListEditor;
							break;

						case "Aras.View.Columns.String":
							gridcolumns[columnviewmodel.Name].editor = StringEditor;
							break;

						case "Aras.View.Columns.Text":
							gridcolumns[columnviewmodel.Name].editor = TextEditor;
							break;

						default:
							gridcolumns[columnviewmodel.Name].editor = undefined;
							break;
					}
				}
				else
				{
					gridcolumns[columnviewmodel.Name].editor = undefined;
				}

				// Cell Formatter
				gridcolumns[columnviewmodel.Name].formatter = lang.hitch(this, this._formatCell);

				// Set Column Width
				GridMisc.addCssRule('#' + GridMisc.escapeCssIdentifier(this._grid.domNode.id) +
						' .dgrid-column-' + GridMisc.escapeCssIdentifier(columnviewmodel.Name, '-'),
						'width: ' + columnviewmodel.Width + 'px;');

			}, this);

			// Set Grid Columns
			this._grid._setColumns(gridcolumns);

			// Update Rows
			this._updateRows();
		},

		_canEditCell: function(Grid, Cell) {

			if (Cell.ViewModel != null)
			{
				return Cell.ViewModel.Editable;
			}
			else
			{
				return false;
			}
		},

		_updateRows: function() {

			// Reset Rows
			array.forEach(this.Rows, function(row) {
				row.Visible = false;
			}, this);

			// Render Grid
			var rowdata = [];

			array.forEach(this.ViewModel.Rows, function(rowviewmodel, i) {

				if (this.Rows[i])
				{
					this.Rows[i].Visible = true;
				}
				else
				{
					this.Rows[i] = new Row({ Grid: this });
					this.Rows[i].startup();
				}

				this.Rows[i].ViewModel = rowviewmodel;
				this.Rows[i].ID = i;

				rowdata[i] = new Object();
				rowdata[i]['id'] = i;

				for (var j=0; j<this.ViewModel.Columns.length; j++)
				{
					if (!this.Rows[i].Cells[j])
					{
						this.Rows[i].Cells[j] = new Cell({ Row: this.Rows[i], Column: this.Columns[j]});
						this.Rows[i].Cells[j].startup();
					}

					this.Rows[i].Cells[j].set('ViewModel', rowviewmodel.Cells[j]);

					rowdata[i][this.ViewModel.Columns[j].Name] = this.Rows[i].Cells[j];
				}

			}, this);

			// Clear Grid
			this._grid.refresh();

			// Render Grid
			this._grid.renderArray(rowdata);
		},

		_formatCell: function(value, object) {

			switch(value.Column.ViewModel.Type)
			{
				case "Aras.View.Columns.Boolean":

					if (value.ViewModel.Value == '1')
					{
						return '<input type="checkbox" disabled checked="checked">';
					}
					else
					{
						return '<input type="checkbox" disabled>';
					}
				case "Aras.View.Columns.List":

					if (value.ViewModel.Selected != null)
					{
						return value.ViewModel.Selected.Label;
					}
					else
					{
						return '';
					}

				default:

					if (value.ViewModel.Value != null)
					{
						return value.ViewModel.Value;
					}
					else
					{
						return '';
					}
			}

		}

	});
});<|endoftext|>