<commit_before>(function( jQuery ) {

jQuery.support = (function() {

	var div = document.createElement( "div" ),
		all,
		a,
		select,
		opt,
		input,
		support,
		fragment,
		body,
		bodyStyle,
		tds,
		events,
		eventName,
		i,
		isSupported;

	// Preliminary tests
	div.innerHTML = "   <link/>

a<input type='checkbox'/>";

	all = div.getElementsByTagName( "*" );
	a = div.getElementsByTagName( "a" )[ 0 ];

	// Can't get basic test support
	if ( !all || !all.length || !a ) {
		return {};
	}

	// First batch of supports tests
	select = document.createElement( "select" );
	opt = select.appendChild( document.createElement("option") );
	input = div.getElementsByTagName( "input" )[ 0 ];

	support = {
		// IE strips leading whitespace when .innerHTML is used
		leadingWhitespace: ( div.firstChild.nodeType === 3 ),

		// Make sure that tbody elements aren't automatically inserted
		// IE will insert them into empty tables
		tbody: !div.getElementsByTagName( "tbody" ).length,

		// Make sure that link elements get serialized correctly by innerHTML
		// This requires a wrapper element in IE
		htmlSerialize: !!div.getElementsByTagName( "link" ).length,

		// Get the style information from getAttribute
		// (IE uses .cssText instead)
		style: /red/.test( a.getAttribute("style") ),

		// Make sure that URLs aren't manipulated
		// (IE normalizes it by default)
		hrefNormalized: ( a.getAttribute( "href" ) === "/a" ),

		// Make sure that element opacity exists
		// (IE uses filter instead)
		// Use a regex to work around a WebKit issue. See #5145
		opacity: /^0.55$/.test( a.style.opacity ),

		// Verify style float existence
		// (IE uses styleFloat instead of cssFloat)
		cssFloat: !!a.style.cssFloat,

		// Make sure that if no value is specified for a checkbox
		// that it defaults to "on".
		// (WebKit defaults to "" instead)
		checkOn: input.value === "on",

		// Make sure that a selected-by-default option has a working selected property.
		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
		optSelected: opt.selected,

		// Will be defined later
		submitBubbles: true,
		changeBubbles: true,
		deleteExpando: true,
		noCloneEvent: true,
		inlineBlockNeedsLayout: false,
		shrinkWrapBlocks: false,
		reliableMarginRight: true
	};

	// Make sure checked status is properly cloned
	input.checked = true;
	support.noCloneChecked = input.cloneNode( true ).checked;

	// Make sure that the options inside disabled selects aren't marked as disabled
	// (WebKit marks them as disabled)
	select.disabled = true;
	support.optDisabled = !opt.disabled;

	// Test to see if it's possible to delete an expando from an element
	// Fails in Internet Explorer
	try {
		delete div.test;
	} catch( e ) {
		support.deleteExpando = false;
	}

	if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
		div.attachEvent( "onclick", function click() {
			// Cloning a node shouldn't copy over any
			// bound event handlers (IE does this)
			support.noCloneEvent = false;
			div.detachEvent( "onclick", click );
		});
		div.cloneNode( true ).fireEvent( "onclick" );
	}

	div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>";

	fragment = document.createDocumentFragment();
	fragment.appendChild( div.firstChild );

	// WebKit doesn't clone checked state correctly in fragments
	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;

	div.innerHTML = "";

	// Figure out if the W3C box model works as expected
	div.style.width = div.style.paddingLeft = "1px";

	// We use our own, invisible, body
	body = document.createElement( "body" );
	bodyStyle = {
		visibility: "hidden",
		width: 0,
		height: 0,
		border: 0,
		margin: 0
	};
	for ( i in bodyStyle ) {
		body.style[ i ] = bodyStyle[ i ];
	}
	body.appendChild( div );
	document.documentElement.appendChild( body );

	support.boxModel = div.offsetWidth === 2;

	if ( "zoom" in div.style ) {
		// Check if natively block-level elements act like inline-block
		// elements when setting their display to 'inline' and giving
		// them layout
		// (IE < 8 does this)
		div.style.display = "inline";
		div.style.zoom = 1;
		support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 );

		// Check if elements with layout shrink-wrap their children
		// (IE 6 does this)
		div.style.display = "";
		div.innerHTML = "

";
		support.shrinkWrapBlocks = ( div.offsetWidth !== 2 );
	}

	div.innerHTML = "

	t

";
	tds = div.getElementsByTagName( "td" );

	// Check if table cells still have offsetWidth/Height when they are set
	// to display:none and there are still other visible table cells in a
	// table row; if so, offsetWidth/Height are not reliable for use when
	// determining if an element has been hidden directly using
	// display:none (it is still safe to use offsets if a parent element is
	// hidden; don safety goggles and see bug #4512 for more information).
	// (only IE 8 fails this test)
	isSupported = ( tds[ 0 ].offsetHeight === 0 );

	tds[ 0 ].style.display = "";
	tds[ 1 ].style.display = "none";

	// Check if empty table cells still have offsetWidth/Height
	// (IE < 8 fail this test)
	support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
	div.innerHTML = "";

	// Check if div with explicit width and no margin-right incorrectly
	// gets computed margin-right based on width of container. For more
	// info see bug #3333
	// Fails in WebKit before Feb 2011 nightlies
	// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
	if ( document.defaultView && document.defaultView.getComputedStyle ) {
		div.style.width = "1px";
		div.style.marginRight = "0";
		support.reliableMarginRight =
			( parseInt( document.defaultView.getComputedStyle(div).marginRight, 10 ) || 0 ) === 0;
	}

	// Remove the body element we added
	document.documentElement.removeChild( body );

	// Technique from Juriy Zaytsev
	// http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
	// We only care about the case where non-standard event systems
	// are used, namely in IE. Short-circuiting here helps us to
	// avoid an eval call (in setAttribute) which can cause CSP
	// to go haywire. See: https://developer.mozilla.org/en/Security/CSP
	if ( div.attachEvent ) {
		for( i in {
			submit: 1,
			change: 1,
			focusin: 1
		} ) {
			eventName = "on" + i;
			isSupported = ( eventName in div );
			if ( !isSupported ) {
				div.setAttribute( eventName, "return;" );
				isSupported = ( typeof div[ eventName ] === "function" );
			}
			support[ i + "Bubbles" ] = isSupported;
		}
	}

	// release memory in IE
	body = div = all = a = tds = undefined;

	return support;
})();

// Keep track of boxModel
jQuery.boxModel = jQuery.support.boxModel;

})( jQuery );
<commit_msg>Adds a default falsy value for focusinBubbles + minor style consistency regarding test expression in support map initialization.<commit_after>(function( jQuery ) {

jQuery.support = (function() {

	var div = document.createElement( "div" ),
		all,
		a,
		select,
		opt,
		input,
		support,
		fragment,
		body,
		bodyStyle,
		tds,
		events,
		eventName,
		i,
		isSupported;

	// Preliminary tests
	div.innerHTML = "   <link/>

a<input type='checkbox'/>";

	all = div.getElementsByTagName( "*" );
	a = div.getElementsByTagName( "a" )[ 0 ];

	// Can't get basic test support
	if ( !all || !all.length || !a ) {
		return {};
	}

	// First batch of supports tests
	select = document.createElement( "select" );
	opt = select.appendChild( document.createElement("option") );
	input = div.getElementsByTagName( "input" )[ 0 ];

	support = {
		// IE strips leading whitespace when .innerHTML is used
		leadingWhitespace: ( div.firstChild.nodeType === 3 ),

		// Make sure that tbody elements aren't automatically inserted
		// IE will insert them into empty tables
		tbody: !div.getElementsByTagName( "tbody" ).length,

		// Make sure that link elements get serialized correctly by innerHTML
		// This requires a wrapper element in IE
		htmlSerialize: !!div.getElementsByTagName( "link" ).length,

		// Get the style information from getAttribute
		// (IE uses .cssText instead)
		style: /red/.test( a.getAttribute("style") ),

		// Make sure that URLs aren't manipulated
		// (IE normalizes it by default)
		hrefNormalized: ( a.getAttribute( "href" ) === "/a" ),

		// Make sure that element opacity exists
		// (IE uses filter instead)
		// Use a regex to work around a WebKit issue. See #5145
		opacity: /^0.55$/.test( a.style.opacity ),

		// Verify style float existence
		// (IE uses styleFloat instead of cssFloat)
		cssFloat: !!a.style.cssFloat,

		// Make sure that if no value is specified for a checkbox
		// that it defaults to "on".
		// (WebKit defaults to "" instead)
		checkOn: ( input.value === "on" ),

		// Make sure that a selected-by-default option has a working selected property.
		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
		optSelected: opt.selected,

		// Will be defined later
		submitBubbles: true,
		changeBubbles: true,
		focusinBubbles: false,
		deleteExpando: true,
		noCloneEvent: true,
		inlineBlockNeedsLayout: false,
		shrinkWrapBlocks: false,
		reliableMarginRight: true
	};

	// Make sure checked status is properly cloned
	input.checked = true;
	support.noCloneChecked = input.cloneNode( true ).checked;

	// Make sure that the options inside disabled selects aren't marked as disabled
	// (WebKit marks them as disabled)
	select.disabled = true;
	support.optDisabled = !opt.disabled;

	// Test to see if it's possible to delete an expando from an element
	// Fails in Internet Explorer
	try {
		delete div.test;
	} catch( e ) {
		support.deleteExpando = false;
	}

	if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
		div.attachEvent( "onclick", function click() {
			// Cloning a node shouldn't copy over any
			// bound event handlers (IE does this)
			support.noCloneEvent = false;
			div.detachEvent( "onclick", click );
		});
		div.cloneNode( true ).fireEvent( "onclick" );
	}

	div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>";

	fragment = document.createDocumentFragment();
	fragment.appendChild( div.firstChild );

	// WebKit doesn't clone checked state correctly in fragments
	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;

	div.innerHTML = "";

	// Figure out if the W3C box model works as expected
	div.style.width = div.style.paddingLeft = "1px";

	// We use our own, invisible, body
	body = document.createElement( "body" );
	bodyStyle = {
		visibility: "hidden",
		width: 0,
		height: 0,
		border: 0,
		margin: 0
	};
	for ( i in bodyStyle ) {
		body.style[ i ] = bodyStyle[ i ];
	}
	body.appendChild( div );
	document.documentElement.appendChild( body );

	support.boxModel = div.offsetWidth === 2;

	if ( "zoom" in div.style ) {
		// Check if natively block-level elements act like inline-block
		// elements when setting their display to 'inline' and giving
		// them layout
		// (IE < 8 does this)
		div.style.display = "inline";
		div.style.zoom = 1;
		support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 );

		// Check if elements with layout shrink-wrap their children
		// (IE 6 does this)
		div.style.display = "";
		div.innerHTML = "

";
		support.shrinkWrapBlocks = ( div.offsetWidth !== 2 );
	}

	div.innerHTML = "

	t

";
	tds = div.getElementsByTagName( "td" );

	// Check if table cells still have offsetWidth/Height when they are set
	// to display:none and there are still other visible table cells in a
	// table row; if so, offsetWidth/Height are not reliable for use when
	// determining if an element has been hidden directly using
	// display:none (it is still safe to use offsets if a parent element is
	// hidden; don safety goggles and see bug #4512 for more information).
	// (only IE 8 fails this test)
	isSupported = ( tds[ 0 ].offsetHeight === 0 );

	tds[ 0 ].style.display = "";
	tds[ 1 ].style.display = "none";

	// Check if empty table cells still have offsetWidth/Height
	// (IE < 8 fail this test)
	support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
	div.innerHTML = "";

	// Check if div with explicit width and no margin-right incorrectly
	// gets computed margin-right based on width of container. For more
	// info see bug #3333
	// Fails in WebKit before Feb 2011 nightlies
	// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
	if ( document.defaultView && document.defaultView.getComputedStyle ) {
		div.style.width = "1px";
		div.style.marginRight = "0";
		support.reliableMarginRight =
			( parseInt( document.defaultView.getComputedStyle(div).marginRight, 10 ) || 0 ) === 0;
	}

	// Remove the body element we added
	document.documentElement.removeChild( body );

	// Technique from Juriy Zaytsev
	// http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
	// We only care about the case where non-standard event systems
	// are used, namely in IE. Short-circuiting here helps us to
	// avoid an eval call (in setAttribute) which can cause CSP
	// to go haywire. See: https://developer.mozilla.org/en/Security/CSP
	if ( div.attachEvent ) {
		for( i in {
			submit: 1,
			change: 1,
			focusin: 1
		} ) {
			eventName = "on" + i;
			isSupported = ( eventName in div );
			if ( !isSupported ) {
				div.setAttribute( eventName, "return;" );
				isSupported = ( typeof div[ eventName ] === "function" );
			}
			support[ i + "Bubbles" ] = isSupported;
		}
	}

	// release memory in IE
	body = div = all = a = tds = undefined;

	return support;
})();

// Keep track of boxModel
jQuery.boxModel = jQuery.support.boxModel;

})( jQuery );
<|endoftext|>


<commit_before>"use strict";

var _ = require("underscore");
var $ = window.$;

var View = require("substance-application").View;
var util = require("substance-util");

// Note: Surface errors have codes between 500-599
var SurfaceError = util.errors.define("SurfaceError", 500);
var SelectionError = util.errors.define("SelectionError", 501, SurfaceError);

// Substance.Surface
// ==========================================================================

var Surface = function(docCtrl, renderer) {
  View.call(this);

  // Rename docCtrl to surfaceCtrl ?
  this.docCtrl = docCtrl;
  this.renderer = renderer;
  this.document = docCtrl.session.document;

  // Pull out the registered nodetypes on the written article
  this.nodeTypes = this.document.nodeTypes;
  this.nodeViews = this.renderer.nodeViews;

  this.$el.addClass('surface');

  this.listenTo(this.document, "property:updated", this.onUpdateView);
  this.listenTo(this.document, "graph:reset", this.reset);

  // bind a DOM blur handler so that we can fire a node blur event
  this.$el.blur(this.onBlur.bind(this));

  this.__lastFocussed = null;
};

Surface.Prototype = function() {

  var _selectionOptions = { "source": "surface" };

  // Private helpers
  // ---------------

  var _extractPath = function(el) {
    var path = [];
    var current = el;

    while(current) {

      // if available extract a path fragment
      if (current.getAttribute) {
        // Stop when we find an element which has been made read-only
        if (current.getAttribute("contenteditable") === "false") {
          return null;
        }

        // if there is a path attibute we collect it
        var p = current.getAttribute("data-path");
        if (p) path.unshift(p);
      }

      // node-views
      if ($(current).is(".content-node")) {
        var id = current.getAttribute("id");
        if (!id) {
          throw new Error("Every element with class 'content-node' must have an 'id' attribute.");
        }
        path.unshift(id);

        // STOP here
        return path;
      }

      current = current.parentElement;
    }

    return null;
  };

  var _mapDOMCoordinates = function(el, offset) {
    var pos, charPos;

    var container = this.docCtrl.container;

    // extract a path by looking for ".content-node" and ".node-property"
    var elementPath = _extractPath(el);

    if (!elementPath) {
      return null;
    }

    // get the position from the container
    var component = container.lookup(elementPath);
    if (!component) return null;

    // TODO rethink when it is a good time to attach the view to the node surface
    // FIXME: here we have a problem now. The TextSurface depends on the TextView
    // which can not be retrieved easily.
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    if (!component.surface.hasView()) {
      throw new Error("NodeView.attachView() must propagate down to child views.");
    }

    pos = component.pos;
    charPos = component.surface.getCharPosition(el, offset);

    return [pos, charPos];
  };

  this.getCoordinateForPosition = function(range) {
    return _mapDOMCoordinates.call(this, range.startContainer, range.startOffset);
  };

  // Read out current DOM selection and update selection in the model
  // ---------------

  this.updateSelection = function(/*e*/) {
    // console.log("Surface.updateSelection()", this.docCtrl.container.name);

    try {
      var wSel = window.getSelection();

      // HACK: sometimes it happens that the selection anchor node is undefined.
      // Try to understand and fix someday.
      if (wSel.anchorNode === null) {
        // invalid selection.
        // This happens if you click something strange
        // Decided to take the user serious and invalidate the selection
        this.clearModelSelection();
        return;
      }

      // Set selection to the cursor if clicked on the cursor.
      if ($(wSel.anchorNode.parentElement).is(".cursor")) {
        this.docCtrl.selection.collapse("cursor", _selectionOptions);
        return;
      }

      var wRange = wSel.getRangeAt(0);
      var wStartPos;
      var wEndPos;

      // Note: there are three different cases:
      // 1. selection started at startContainer (regular)
      // 2. selection started at endContainer (reverse)
      // 3. selection done via double click (anchor in different to range boundaries)
      // In cases 1. + 3. the range is used as given, in case 2. reversed.

      wStartPos = [wRange.startContainer, wRange.startOffset];
      wEndPos = [wRange.endContainer, wRange.endOffset];

      if (wRange.endContainer === wSel.anchorNode && wRange.endOffset === wSel.anchorOffset) {
        var tmp = wStartPos;
        wStartPos = wEndPos;
        wEndPos = tmp;
      }

      // Note: we clear the selection whenever we can not map the window selection
      // to model coordinates.

      var startPos = _mapDOMCoordinates.call(this, wStartPos[0], wStartPos[1]);
      if (!startPos) {
        // console.log("Surface.updateSelection(): no valid start position. Clearing the selection");
        wSel.removeAllRanges();
        this.clearModelSelection();
        return;
      }

      var endPos;
      if (wRange.collapsed) {
        endPos = startPos;
      } else {
        endPos = _mapDOMCoordinates.call(this, wEndPos[0], wEndPos[1]);
        if (!endPos) {
          // console.log("Surface.updateSelection(): no valid end position. Clearing the selection");
          wSel.removeAllRanges();
          this.clearModelSelection();
          return;
        }
      }

      try {
        this._emitFocusAndBlur(startPos[0] === endPos[0], startPos[0]);
      } catch (err) {
        console.error(err);
      }

      // console.log("Surface.updateSelection()", startPos, endPos);
      this.docCtrl.selection.set({start: startPos, end: endPos}, _selectionOptions);

    } catch (error) {
      // On errors clear the selection and report
      console.error(error);
      util.printStackTrace(error);

      var err = new SelectionError("Could not map to model cordinates.", error);
      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  // Renders the current selection
  // --------
  //

  var _mapModelCoordinates = function(pos) {
    var container = this.docCtrl.container;
    var component = container.getComponent(pos[0]);
    return this.getPositionFromComponent(component, offset);
  };

  this.getPositionFromCoordinate = function(path, offset) {
    var container = this.docCtrl.container;
    var component = container.lookup(path);
    return this.getPositionFromComponent(component, offset);
  };

  this.getPositionFromComponent = function(component, offset) {
    // TODO rethink when it is a good time to attach the view to the node surface
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    var wCoor = component.surface.getDOMPosition(offset);
    return wCoor;
  };

  this._attachViewToNodeSurface = function(component) {
    var nodeId = component.root.id;
    var topLevelSurface = component.surface.surfaceProvider.getNodeSurface(nodeId);
    var topLevelView = this.nodeViews[nodeId];
    topLevelSurface.attachView(topLevelView);
  };

  // HACK: putting this in renderSelection before the cycle guard
  // did lead to some strange infinite recursion
  // so this method is used from both places updateSelection and renderSelection
  this._emitFocusAndBlur = function(is_collapsed, pos) {
    if (is_collapsed) {
      var component = this.docCtrl.container.getComponent(pos);
      if (!component.surface.hasView()) {
        this._attachViewToNodeSurface(component);
      }
      var nodeView = component.surface.view;
      if (nodeView !== this.__lastFocussedView) {
        if (this.__lastFocussedView) this.__lastFocussedView.onBlur();
        nodeView.onFocus();
        this.__lastFocussedView = nodeView;
      }
    } else if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.onBlur = function() {
    if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.renderSelection = function(range, options) {
    // console.log("Surface.renderSelection()", this.docCtrl.container.name);

    try {

      var sel = this.docCtrl.selection;
      if (sel.isCollapsed()) {
        var cursorPos = sel.getCursorPosition();
        try {
          this._emitFocusAndBlur("is-collapsed", cursorPos[0]);
        } catch (err) {
          console.error(err);
        }
      } else {
        this._emitFocusAndBlur();
      }

      if (options && (options["source"] === "surface" || options["silent"] === true)){
        this.scrollToCursor();
        return;
      }

      var wSel = window.getSelection();
      // console.log("Clearing window selection.");
      wSel.removeAllRanges();

      if (sel.isNull()) {
        return;
      }

      var wRange = window.document.createRange();
      var wStartPos = _mapModelCoordinates.call(this, sel.start);
      wRange.setStart(wStartPos.startContainer, wStartPos.startOffset);

      // TODO: is there a better way to manipulate the current selection?
      // console.log("Setting window selection.");
      wSel.addRange(wRange);

      // Move the caret to the end position
      // Note: this is the only way to get reversed selections.
      if (!sel.isCollapsed()) {
        var wEndPos = _mapModelCoordinates.call(this, [sel.cursor.pos, sel.cursor.charPos]);
        wSel.extend(wEndPos.endContainer, wEndPos.endOffset);
      }

      this.scrollToCursor();

    } catch (error) {
      console.error(error);
      util.printStackTrace(error);

      // On errors clear the selection and report
      var err = new SelectionError("Could not map to DOM cordinates.", error);

      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  this.clearModelSelection = function() {
    // leave a mark that the surface will not handle the returning selection update
    this.docCtrl.selection.clear(_selectionOptions);
  };

  this.scrollToCursor = function() {
    var sel = this.docCtrl.selection;

    // Not exactly beautiful but ensures the cursor stays in view
    // E.g. when hitting enter on the lower document bound
    if (sel.isCollapsed()) {
      var that = this;

      // Wait for next DOM iteration
      window.setTimeout(function() {
        // Look up parent node if startContainer is a text node
        var topCorrection = $(that.el).offset().top;
        var wSel = window.getSelection();

        // avoid errors due to non existing DOM selection.
        if (wSel.rangeCount === 0 ) {
          return;
        }

        var range = wSel.getRangeAt(0);
        var bounds = range.getClientRects()[0];

        if (!bounds) {
          // This happens when the cursor is in an empty node
          // However, that is not a problem as we can use the container then
          var $content = $(range.startContainer).parents('.content');
          // do not proceed if the cursor is not in a node view
          if ($content.length === 0) return;
          bounds = $content.offset();
        }

        var topOffset = bounds.top - topCorrection;
        var surfaceHeight = $(that.el).height();

        var scrollTop = $(that.el).scrollTop();
        var lineHeight = 50;

        var targetScroll;
        if (topOffset>surfaceHeight) {
          targetScroll = scrollTop + topOffset - surfaceHeight + lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else if (topOffset < 0) {
          targetScroll = scrollTop + topOffset - 3*lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else {
          // console.log("Not scrolling ...", topOffset, surfaceHeight);
        }
      // NOTE: 0 millis was not enough sometimes. However, 5 millis is probably not the solution
      // E.g., after inserting an image, it would be necessary to wait for it being loaded...
      }, 5);
    }
  };

  // Render it
  // --------
  //
  // input.image-files
  // .controls
  // .nodes
  //   .content-node.paragraph
  //   .content-node.heading
  //   ...
  // .cursor

  this.render = function() {

    // var controls = window.document.createElement('div');
    // controls.className = "controls";
    var nodes = window.document.createElement('div');
    nodes.className = "nodes";

    // var cursor = window.document.createElement('div');
    // cursor.className = "cursor";

    // this.el.appendChild(controls);
    this.el.appendChild(nodes);
    // this.el.appendChild(cursor);

    // Actual content goes here
    // --------
    //
    // We get back a document fragment from the renderer

    nodes.appendChild(this.renderer.render());

    // TODO: fixme
    this.$('input.image-files').hide();
    this.$cursor = this.$('.cursor');
    this.$cursor.hide();

    // keep the nodes for later access
    this._nodesEl = nodes;

    return this;
  };

  this.reset = function() {
    _.each(this.nodeViews, function(nodeView) {
      nodeView.dispose();
    });
    this.render();
  };

  // Cleanup view before removing it
  // --------
  //

  this.dispose = function() {
    this.stopListening();
    _.each(this.nodeViews, function(n) {
      n.dispose();
    }, this);
    if (this.keyboard) this.keyboard.disconnect(this.el);
  };

  // HACK: used by outline
  // TODO: meditate on the Surface's API
  this.getContainer = function() {
    return this.docCtrl.container;
  };

  // TODO: we could factor this out into something like a ContainerView?

  function insertOrAppend(container, pos, el) {
    var childs = container.childNodes;
    if (pos < childs.length) {
      var refNode = childs[pos];
      container.insertBefore(el, refNode);
    } else {
      container.appendChild(el);
    }
  }

  this.onUpdateView = function(path, diff) {
    if (path.length !== 2 || path[0] !== this.docCtrl.session.container.name || path[1] !== "nodes") return;

    var nodeId, node;
    var container = this._nodesEl;

    var children, el;

    if (diff.isInsert()) {
      // Create a view and insert render it into the nodes container element.
      nodeId = diff.val;
      node = this.document.get(nodeId);

      if (this.nodeTypes[node.type]) {
        // TODO: createView is misleading as returns a cached instance
        // or creates a new one
        var nodeView = this.renderer.createView(node);
        this.nodeViews[nodeId] = nodeView;
        el = nodeView.render().el;
        insertOrAppend(container, diff.pos, el);
      }
    }
    else if (diff.isDelete()) {
      // Dispose the view and remove its element from the nodes container
      nodeId = diff.val;
      if (this.nodeViews[nodeId]) {
        this.nodeViews[nodeId].dispose();
      }
      delete this.nodeViews[nodeId];
      children = container.children;
      container.removeChild(children[diff.pos]);
    }
    else if (diff.isMove()) {
      children = container.children;
      el = children[diff.pos];
      container.removeChild(el);
      insertOrAppend(container, diff.target, el);
    } else if (diff.type === "NOP") {
    } else {
      throw new Error("Illegal state.");
    }
  };

  this.getNodeView = function(nodeId) {
    return this.renderer.getView(nodeId);
  };

};

_.extend(Surface.Prototype, util.Events.Listener);

Surface.Prototype.prototype = View.prototype;
Surface.prototype = new Surface.Prototype();

Object.defineProperties(Surface.prototype, {
  "name": {
    get: function() {
      return this.docCtrl.session.container.name;
    }
  }
});

module.exports = Surface;
<commit_msg>Fix regression.<commit_after>"use strict";

var _ = require("underscore");
var $ = window.$;

var View = require("substance-application").View;
var util = require("substance-util");

// Note: Surface errors have codes between 500-599
var SurfaceError = util.errors.define("SurfaceError", 500);
var SelectionError = util.errors.define("SelectionError", 501, SurfaceError);

// Substance.Surface
// ==========================================================================

var Surface = function(docCtrl, renderer) {
  View.call(this);

  // Rename docCtrl to surfaceCtrl ?
  this.docCtrl = docCtrl;
  this.renderer = renderer;
  this.document = docCtrl.session.document;

  // Pull out the registered nodetypes on the written article
  this.nodeTypes = this.document.nodeTypes;
  this.nodeViews = this.renderer.nodeViews;

  this.$el.addClass('surface');

  this.listenTo(this.document, "property:updated", this.onUpdateView);
  this.listenTo(this.document, "graph:reset", this.reset);

  // bind a DOM blur handler so that we can fire a node blur event
  this.$el.blur(this.onBlur.bind(this));

  this.__lastFocussed = null;
};

Surface.Prototype = function() {

  var _selectionOptions = { "source": "surface" };

  // Private helpers
  // ---------------

  var _extractPath = function(el) {
    var path = [];
    var current = el;

    while(current) {

      // if available extract a path fragment
      if (current.getAttribute) {
        // Stop when we find an element which has been made read-only
        if (current.getAttribute("contenteditable") === "false") {
          return null;
        }

        // if there is a path attibute we collect it
        var p = current.getAttribute("data-path");
        if (p) path.unshift(p);
      }

      // node-views
      if ($(current).is(".content-node")) {
        var id = current.getAttribute("id");
        if (!id) {
          throw new Error("Every element with class 'content-node' must have an 'id' attribute.");
        }
        path.unshift(id);

        // STOP here
        return path;
      }

      current = current.parentElement;
    }

    return null;
  };

  var _mapDOMCoordinates = function(el, offset) {
    var pos, charPos;

    var container = this.docCtrl.container;

    // extract a path by looking for ".content-node" and ".node-property"
    var elementPath = _extractPath(el);

    if (!elementPath) {
      return null;
    }

    // get the position from the container
    var component = container.lookup(elementPath);
    if (!component) return null;

    // TODO rethink when it is a good time to attach the view to the node surface
    // FIXME: here we have a problem now. The TextSurface depends on the TextView
    // which can not be retrieved easily.
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    if (!component.surface.hasView()) {
      throw new Error("NodeView.attachView() must propagate down to child views.");
    }

    pos = component.pos;
    charPos = component.surface.getCharPosition(el, offset);

    return [pos, charPos];
  };

  this.getCoordinateForPosition = function(range) {
    return _mapDOMCoordinates.call(this, range.startContainer, range.startOffset);
  };

  // Read out current DOM selection and update selection in the model
  // ---------------

  this.updateSelection = function(/*e*/) {
    // console.log("Surface.updateSelection()", this.docCtrl.container.name);

    try {
      var wSel = window.getSelection();

      // HACK: sometimes it happens that the selection anchor node is undefined.
      // Try to understand and fix someday.
      if (wSel.anchorNode === null) {
        // invalid selection.
        // This happens if you click something strange
        // Decided to take the user serious and invalidate the selection
        this.clearModelSelection();
        return;
      }

      // Set selection to the cursor if clicked on the cursor.
      if ($(wSel.anchorNode.parentElement).is(".cursor")) {
        this.docCtrl.selection.collapse("cursor", _selectionOptions);
        return;
      }

      var wRange = wSel.getRangeAt(0);
      var wStartPos;
      var wEndPos;

      // Note: there are three different cases:
      // 1. selection started at startContainer (regular)
      // 2. selection started at endContainer (reverse)
      // 3. selection done via double click (anchor in different to range boundaries)
      // In cases 1. + 3. the range is used as given, in case 2. reversed.

      wStartPos = [wRange.startContainer, wRange.startOffset];
      wEndPos = [wRange.endContainer, wRange.endOffset];

      if (wRange.endContainer === wSel.anchorNode && wRange.endOffset === wSel.anchorOffset) {
        var tmp = wStartPos;
        wStartPos = wEndPos;
        wEndPos = tmp;
      }

      // Note: we clear the selection whenever we can not map the window selection
      // to model coordinates.

      var startPos = _mapDOMCoordinates.call(this, wStartPos[0], wStartPos[1]);
      if (!startPos) {
        // console.log("Surface.updateSelection(): no valid start position. Clearing the selection");
        wSel.removeAllRanges();
        this.clearModelSelection();
        return;
      }

      var endPos;
      if (wRange.collapsed) {
        endPos = startPos;
      } else {
        endPos = _mapDOMCoordinates.call(this, wEndPos[0], wEndPos[1]);
        if (!endPos) {
          // console.log("Surface.updateSelection(): no valid end position. Clearing the selection");
          wSel.removeAllRanges();
          this.clearModelSelection();
          return;
        }
      }

      try {
        this._emitFocusAndBlur(startPos[0] === endPos[0], startPos[0]);
      } catch (err) {
        console.error(err);
      }

      // console.log("Surface.updateSelection()", startPos, endPos);
      this.docCtrl.selection.set({start: startPos, end: endPos}, _selectionOptions);

    } catch (error) {
      // On errors clear the selection and report
      console.error(error);
      util.printStackTrace(error);

      var err = new SelectionError("Could not map to model cordinates.", error);
      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  // Renders the current selection
  // --------
  //

  var _mapModelCoordinates = function(pos) {
    var container = this.docCtrl.container;
    var component = container.getComponent(pos[0]);
    return this.getPositionFromComponent(component, pos[1]);
  };

  this.getPositionFromCoordinate = function(path, offset) {
    var container = this.docCtrl.container;
    var component = container.lookup(path);
    return this.getPositionFromComponent(component, offset);
  };

  this.getPositionFromComponent = function(component, offset) {
    // TODO rethink when it is a good time to attach the view to the node surface
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    var wCoor = component.surface.getDOMPosition(offset);
    return wCoor;
  };

  this._attachViewToNodeSurface = function(component) {
    var nodeId = component.root.id;
    var topLevelSurface = component.surface.surfaceProvider.getNodeSurface(nodeId);
    var topLevelView = this.nodeViews[nodeId];
    topLevelSurface.attachView(topLevelView);
  };

  // HACK: putting this in renderSelection before the cycle guard
  // did lead to some strange infinite recursion
  // so this method is used from both places updateSelection and renderSelection
  this._emitFocusAndBlur = function(is_collapsed, pos) {
    if (is_collapsed) {
      var component = this.docCtrl.container.getComponent(pos);
      if (!component.surface.hasView()) {
        this._attachViewToNodeSurface(component);
      }
      var nodeView = component.surface.view;
      if (nodeView !== this.__lastFocussedView) {
        if (this.__lastFocussedView) this.__lastFocussedView.onBlur();
        nodeView.onFocus();
        this.__lastFocussedView = nodeView;
      }
    } else if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.onBlur = function() {
    if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.renderSelection = function(range, options) {
    // console.log("Surface.renderSelection()", this.docCtrl.container.name);

    try {

      var sel = this.docCtrl.selection;
      if (sel.isCollapsed()) {
        var cursorPos = sel.getCursorPosition();
        try {
          this._emitFocusAndBlur("is-collapsed", cursorPos[0]);
        } catch (err) {
          console.error(err);
        }
      } else {
        this._emitFocusAndBlur();
      }

      if (options && (options["source"] === "surface" || options["silent"] === true)){
        this.scrollToCursor();
        return;
      }

      var wSel = window.getSelection();
      // console.log("Clearing window selection.");
      wSel.removeAllRanges();

      if (sel.isNull()) {
        return;
      }

      var wRange = window.document.createRange();
      var wStartPos = _mapModelCoordinates.call(this, sel.start);
      wRange.setStart(wStartPos.startContainer, wStartPos.startOffset);

      // TODO: is there a better way to manipulate the current selection?
      // console.log("Setting window selection.");
      wSel.addRange(wRange);

      // Move the caret to the end position
      // Note: this is the only way to get reversed selections.
      if (!sel.isCollapsed()) {
        var wEndPos = _mapModelCoordinates.call(this, [sel.cursor.pos, sel.cursor.charPos]);
        wSel.extend(wEndPos.endContainer, wEndPos.endOffset);
      }

      this.scrollToCursor();

    } catch (error) {
      console.error(error);
      util.printStackTrace(error);

      // On errors clear the selection and report
      var err = new SelectionError("Could not map to DOM cordinates.", error);

      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  this.clearModelSelection = function() {
    // leave a mark that the surface will not handle the returning selection update
    this.docCtrl.selection.clear(_selectionOptions);
  };

  this.scrollToCursor = function() {
    var sel = this.docCtrl.selection;

    // Not exactly beautiful but ensures the cursor stays in view
    // E.g. when hitting enter on the lower document bound
    if (sel.isCollapsed()) {
      var that = this;

      // Wait for next DOM iteration
      window.setTimeout(function() {
        // Look up parent node if startContainer is a text node
        var topCorrection = $(that.el).offset().top;
        var wSel = window.getSelection();

        // avoid errors due to non existing DOM selection.
        if (wSel.rangeCount === 0 ) {
          return;
        }

        var range = wSel.getRangeAt(0);
        var bounds = range.getClientRects()[0];

        if (!bounds) {
          // This happens when the cursor is in an empty node
          // However, that is not a problem as we can use the container then
          var $content = $(range.startContainer).parents('.content');
          // do not proceed if the cursor is not in a node view
          if ($content.length === 0) return;
          bounds = $content.offset();
        }

        var topOffset = bounds.top - topCorrection;
        var surfaceHeight = $(that.el).height();

        var scrollTop = $(that.el).scrollTop();
        var lineHeight = 50;

        var targetScroll;
        if (topOffset>surfaceHeight) {
          targetScroll = scrollTop + topOffset - surfaceHeight + lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else if (topOffset < 0) {
          targetScroll = scrollTop + topOffset - 3*lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else {
          // console.log("Not scrolling ...", topOffset, surfaceHeight);
        }
      // NOTE: 0 millis was not enough sometimes. However, 5 millis is probably not the solution
      // E.g., after inserting an image, it would be necessary to wait for it being loaded...
      }, 5);
    }
  };

  // Render it
  // --------
  //
  // input.image-files
  // .controls
  // .nodes
  //   .content-node.paragraph
  //   .content-node.heading
  //   ...
  // .cursor

  this.render = function() {

    // var controls = window.document.createElement('div');
    // controls.className = "controls";
    var nodes = window.document.createElement('div');
    nodes.className = "nodes";

    // var cursor = window.document.createElement('div');
    // cursor.className = "cursor";

    // this.el.appendChild(controls);
    this.el.appendChild(nodes);
    // this.el.appendChild(cursor);

    // Actual content goes here
    // --------
    //
    // We get back a document fragment from the renderer

    nodes.appendChild(this.renderer.render());

    // TODO: fixme
    this.$('input.image-files').hide();
    this.$cursor = this.$('.cursor');
    this.$cursor.hide();

    // keep the nodes for later access
    this._nodesEl = nodes;

    return this;
  };

  this.reset = function() {
    _.each(this.nodeViews, function(nodeView) {
      nodeView.dispose();
    });
    this.render();
  };

  // Cleanup view before removing it
  // --------
  //

  this.dispose = function() {
    this.stopListening();
    _.each(this.nodeViews, function(n) {
      n.dispose();
    }, this);
    if (this.keyboard) this.keyboard.disconnect(this.el);
  };

  // HACK: used by outline
  // TODO: meditate on the Surface's API
  this.getContainer = function() {
    return this.docCtrl.container;
  };

  // TODO: we could factor this out into something like a ContainerView?

  function insertOrAppend(container, pos, el) {
    var childs = container.childNodes;
    if (pos < childs.length) {
      var refNode = childs[pos];
      container.insertBefore(el, refNode);
    } else {
      container.appendChild(el);
    }
  }

  this.onUpdateView = function(path, diff) {
    if (path.length !== 2 || path[0] !== this.docCtrl.session.container.name || path[1] !== "nodes") return;

    var nodeId, node;
    var container = this._nodesEl;

    var children, el;

    if (diff.isInsert()) {
      // Create a view and insert render it into the nodes container element.
      nodeId = diff.val;
      node = this.document.get(nodeId);

      if (this.nodeTypes[node.type]) {
        // TODO: createView is misleading as returns a cached instance
        // or creates a new one
        var nodeView = this.renderer.createView(node);
        this.nodeViews[nodeId] = nodeView;
        el = nodeView.render().el;
        insertOrAppend(container, diff.pos, el);
      }
    }
    else if (diff.isDelete()) {
      // Dispose the view and remove its element from the nodes container
      nodeId = diff.val;
      if (this.nodeViews[nodeId]) {
        this.nodeViews[nodeId].dispose();
      }
      delete this.nodeViews[nodeId];
      children = container.children;
      container.removeChild(children[diff.pos]);
    }
    else if (diff.isMove()) {
      children = container.children;
      el = children[diff.pos];
      container.removeChild(el);
      insertOrAppend(container, diff.target, el);
    } else if (diff.type === "NOP") {
    } else {
      throw new Error("Illegal state.");
    }
  };

  this.getNodeView = function(nodeId) {
    return this.renderer.getView(nodeId);
  };

};

_.extend(Surface.Prototype, util.Events.Listener);

Surface.Prototype.prototype = View.prototype;
Surface.prototype = new Surface.Prototype();

Object.defineProperties(Surface.prototype, {
  "name": {
    get: function() {
      return this.docCtrl.session.container.name;
    }
  }
});

module.exports = Surface;
<|endoftext|>


<commit_before>"use strict";

var _ = require("underscore");
var $ = window.$;

var View = require("substance-application").View;
var util = require("substance-util");

// Note: Surface errors have codes between 500-599
var SurfaceError = util.errors.define("SurfaceError", 500);
var SelectionError = util.errors.define("SelectionError", 501, SurfaceError);

// Substance.Surface
// ==========================================================================

var Surface = function(docCtrl, renderer) {
  View.call(this);

  // Rename docCtrl to surfaceCtrl ?
  this.docCtrl = docCtrl;
  this.renderer = renderer;
  this.document = docCtrl.session.document;

  // Pull out the registered nodetypes on the written article
  this.nodeTypes = this.document.nodeTypes;
  this.nodeViews = this.renderer.nodeViews;

  this.$el.addClass('surface');

  this.listenTo(this.document, "property:updated", this.onUpdateView);
  this.listenTo(this.document, "graph:reset", this.reset);

  // bind a DOM blur handler so that we can fire a node blur event
  this.$el.blur(this.onBlur.bind(this));

  this.__lastFocussed = null;
};

Surface.Prototype = function() {

  var _selectionOptions = { "source": "surface" };

  // Private helpers
  // ---------------

  var _extractPath = function(el) {
    var path = [];
    var current = el;

    while(current) {

      // if available extract a path fragment
      if (current.getAttribute) {
        // Stop when we find an element which has been made read-only
        if (current.getAttribute("contenteditable") === "false") {
          return null;
        }

        // if there is a path attibute we collect it
        var p = current.getAttribute("data-path");
        if (p) path.unshift(p);
      }

      // node-views
      if ($(current).is(".content-node")) {
        var id = current.getAttribute("id");
        if (!id) {
          throw new Error("Every element with class 'content-node' must have an 'id' attribute.");
        }
        path.unshift(id);

        // STOP here
        return path;
      }

      current = current.parentElement;
    }

    return null;
  };

  var _mapDOMCoordinates = function(el, offset) {
    var pos, charPos;

    var container = this.docCtrl.container;

    // extract a path by looking for ".content-node" and ".node-property"
    var elementPath = _extractPath(el);

    if (!elementPath) {
      return null;
    }

    // get the position from the container
    var component = container.lookup(elementPath);
    if (!component) return null;

    // TODO rethink when it is a good time to attach the view to the node surface
    // FIXME: here we have a problem now. The TextSurface depends on the TextView
    // which can not be retrieved easily.
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    if (!component.surface.hasView()) {
      throw new Error("NodeView.attachView() must propagate down to child views.");
    }

    pos = component.pos;
    charPos = component.surface.getCharPosition(el, offset);

    return [pos, charPos];
  };

  // Read out current DOM selection and update selection in the model
  // ---------------

  this.updateSelection = function(/*e*/) {
    // console.log("Surface.updateSelection()", this.docCtrl.container.name);

    try {
      var wSel = window.getSelection();

      // HACK: sometimes it happens that the selection anchor node is undefined.
      // Try to understand and fix someday.
      if (wSel.anchorNode === null) {
        // invalid selection.
        // This happens if you click something strange
        // Decided to take the user serious and invalidate the selection
        this.clearModelSelection();
        return;
      }

      // Set selection to the cursor if clicked on the cursor.
      if ($(wSel.anchorNode.parentElement).is(".cursor")) {
        this.docCtrl.selection.collapse("cursor", _selectionOptions);
        return;
      }

      var wRange = wSel.getRangeAt(0);
      var wStartPos;
      var wEndPos;

      // Note: there are three different cases:
      // 1. selection started at startContainer (regular)
      // 2. selection started at endContainer (reverse)
      // 3. selection done via double click (anchor in different to range boundaries)
      // In cases 1. + 3. the range is used as given, in case 2. reversed.

      wStartPos = [wRange.startContainer, wRange.startOffset];
      wEndPos = [wRange.endContainer, wRange.endOffset];

      if (wRange.endContainer === wSel.anchorNode && wRange.endOffset === wSel.anchorOffset) {
        var tmp = wStartPos;
        wStartPos = wEndPos;
        wEndPos = tmp;
      }

      // Note: we clear the selection whenever we can not map the window selection
      // to model coordinates.

      var startPos = _mapDOMCoordinates.call(this, wStartPos[0], wStartPos[1]);
      if (!startPos) {
        // console.log("Surface.updateSelection(): no valid start position. Clearing the selection");
        wSel.removeAllRanges();
        this.clearModelSelection();
        return;
      }

      var endPos;
      if (wRange.collapsed) {
        endPos = startPos;
      } else {
        endPos = _mapDOMCoordinates.call(this, wEndPos[0], wEndPos[1]);
        if (!endPos) {
          // console.log("Surface.updateSelection(): no valid end position. Clearing the selection");
          wSel.removeAllRanges();
          this.clearModelSelection();
          return;
        }
      }

      try {
        this._emitFocusAndBlur(startPos[0] === endPos[0], startPos[0]);
      } catch (err) {
        console.error(err);
      }

      // console.log("Surface.updateSelection()", startPos, endPos);
      this.docCtrl.selection.set({start: startPos, end: endPos}, _selectionOptions);

    } catch (error) {
      // On errors clear the selection and report
      console.error(error);
      util.printStackTrace(error);

      var err = new SelectionError("Could not map to model cordinates.", error);
      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  // Renders the current selection
  // --------
  //

  var _mapModelCoordinates = function(pos) {
    var container = this.docCtrl.container;
    var component = container.getComponent(pos[0]);
    // TODO rethink when it is a good time to attach the view to the node surface
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    var wCoor = component.surface.getDOMPosition(pos[1]);
    return wCoor;
  };

  this._attachViewToNodeSurface = function(component) {
    var nodeId = component.root.id;
    var topLevelSurface = component.surface.surfaceProvider.getNodeSurface(nodeId);
    var topLevelView = this.nodeViews[nodeId];
    topLevelSurface.attachView(topLevelView);
  };

  // HACK: putting this in renderSelection before the cycle guard
  // did lead to some strange infinite recursion
  // so this method is used from both places updateSelection and renderSelection
  this._emitFocusAndBlur = function(is_collapsed, pos) {
    if (is_collapsed) {
      var component = this.docCtrl.container.getComponent(pos);
      if (!component.surface.hasView()) {
        this._attachViewToNodeSurface(component);
      }
      var nodeView = component.surface.view;
      if (nodeView !== this.__lastFocussedView) {
        if (this.__lastFocussedView) this.__lastFocussedView.onBlur();
        nodeView.onFocus();
        this.__lastFocussedView = nodeView;
      }
    } else if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.onBlur = function() {
    if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.renderSelection = function(range, options) {
    // console.log("Surface.renderSelection()", this.docCtrl.container.name);

    try {

      var sel = this.docCtrl.selection;
      if (sel.isCollapsed()) {
        var cursorPos = sel.getCursorPosition();
        try {
          this._emitFocusAndBlur("is-collapsed", cursorPos[0]);
        } catch (err) {
          console.error(err);
        }
      } else {
        this._emitFocusAndBlur();
      }

      if (options && (options["source"] === "surface" || options["silent"] === true)){
        this.scrollToCursor();
        return;
      }

      var wSel = window.getSelection();
      // console.log("Clearing window selection.");
      wSel.removeAllRanges();

      if (sel.isNull()) {
        return;
      }

      var wRange = window.document.createRange();
      var wStartPos = _mapModelCoordinates.call(this, sel.start);
      wRange.setStart(wStartPos.startContainer, wStartPos.startOffset);

      // TODO: is there a better way to manipulate the current selection?
      // console.log("Setting window selection.");
      wSel.addRange(wRange);

      // Move the caret to the end position
      // Note: this is the only way to get reversed selections.
      if (!sel.isCollapsed()) {
        var wEndPos = _mapModelCoordinates.call(this, [sel.cursor.pos, sel.cursor.charPos]);
        wSel.extend(wEndPos.endContainer, wEndPos.endOffset);
      }

      this.scrollToCursor();

    } catch (error) {
      console.error(error);
      util.printStackTrace(error);

      // On errors clear the selection and report
      var err = new SelectionError("Could not map to DOM cordinates.", error);

      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  this.clearModelSelection = function() {
    // leave a mark that the surface will not handle the returning selection update
    this.docCtrl.selection.clear(_selectionOptions);
  };

  this.scrollToCursor = function() {
    var sel = this.docCtrl.selection;

    // Not exactly beautiful but ensures the cursor stays in view
    // E.g. when hitting enter on the lower document bound
    if (sel.isCollapsed()) {
      var that = this;

      // Wait for next DOM iteration
      window.setTimeout(function() {
        // Look up parent node if startContainer is a text node
        var topCorrection = $(that.el).offset().top;
        var wSel = window.getSelection();

        // avoid errors due to non existing DOM selection.
        if (wSel.rangeCount === 0 ) {
          return;
        }

        var range = wSel.getRangeAt(0);
        var bounds = range.getClientRects()[0];

        if (!bounds) {
          // This happens when the cursor is in an empty node
          // However, that is not a problem as we can use the container then
          var $content = $(range.startContainer).parents('.content');
          // do not proceed if the cursor is not in a node view
          if ($content.length === 0) return;
          bounds = $content.offset();
        }

        var topOffset = bounds.top - topCorrection;
        var surfaceHeight = $(that.el).height();

        var scrollTop = $(that.el).scrollTop();
        var lineHeight = 50;

        var targetScroll;
        if (topOffset>surfaceHeight) {
          targetScroll = scrollTop + topOffset - surfaceHeight + lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else if (topOffset < 0) {
          targetScroll = scrollTop + topOffset - 3*lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else {
          // console.log("Not scrolling ...", topOffset, surfaceHeight);
        }
      // NOTE: 0 millis was not enough sometimes. However, 5 millis is probably not the solution
      // E.g., after inserting an image, it would be necessary to wait for it being loaded...
      }, 5);
    }
  };

  // Render it
  // --------
  //
  // input.image-files
  // .controls
  // .nodes
  //   .content-node.paragraph
  //   .content-node.heading
  //   ...
  // .cursor

  this.render = function() {

    // var controls = window.document.createElement('div');
    // controls.className = "controls";
    var nodes = window.document.createElement('div');
    nodes.className = "nodes";

    // var cursor = window.document.createElement('div');
    // cursor.className = "cursor";

    // this.el.appendChild(controls);
    this.el.appendChild(nodes);
    // this.el.appendChild(cursor);

    // Actual content goes here
    // --------
    //
    // We get back a document fragment from the renderer

    nodes.appendChild(this.renderer.render());

    // TODO: fixme
    this.$('input.image-files').hide();
    this.$cursor = this.$('.cursor');
    this.$cursor.hide();

    // keep the nodes for later access
    this._nodesEl = nodes;

    return this;
  };

  this.reset = function() {
    _.each(this.nodeViews, function(nodeView) {
      nodeView.dispose();
    });
    this.render();
  };

  // Cleanup view before removing it
  // --------
  //

  this.dispose = function() {
    this.stopListening();
    _.each(this.nodeViews, function(n) {
      n.dispose();
    }, this);
    if (this.keyboard) this.keyboard.disconnect(this.el);
  };

  // HACK: used by outline
  // TODO: meditate on the Surface's API
  this.getContainer = function() {
    return this.docCtrl.container;
  };

  // TODO: we could factor this out into something like a ContainerView?

  function insertOrAppend(container, pos, el) {
    var childs = container.childNodes;
    if (pos < childs.length) {
      var refNode = childs[pos];
      container.insertBefore(el, refNode);
    } else {
      container.appendChild(el);
    }
  }

  this.onUpdateView = function(path, diff) {
    if (path.length !== 2 || path[0] !== this.docCtrl.session.container.name || path[1] !== "nodes") return;

    var nodeId, node;
    var container = this._nodesEl;

    var children, el;

    if (diff.isInsert()) {
      // Create a view and insert render it into the nodes container element.
      nodeId = diff.val;
      node = this.document.get(nodeId);

      if (this.nodeTypes[node.type]) {
        // TODO: createView is misleading as returns a cached instance
        // or creates a new one
        var nodeView = this.renderer.createView(node);
        this.nodeViews[nodeId] = nodeView;
        el = nodeView.render().el;
        insertOrAppend(container, diff.pos, el);
      }
    }
    else if (diff.isDelete()) {
      // Dispose the view and remove its element from the nodes container
      nodeId = diff.val;
      if (this.nodeViews[nodeId]) {
        this.nodeViews[nodeId].dispose();
      }
      delete this.nodeViews[nodeId];
      children = container.children;
      container.removeChild(children[diff.pos]);
    }
    else if (diff.isMove()) {
      children = container.children;
      el = children[diff.pos];
      container.removeChild(el);
      insertOrAppend(container, diff.target, el);
    } else if (diff.type === "NOP") {
    } else {
      throw new Error("Illegal state.");
    }
  };

  this.getNodeView = function(nodeId) {
    return this.renderer.getView(nodeId);
  };

};

_.extend(Surface.Prototype, util.Events.Listener);

Surface.Prototype.prototype = View.prototype;
Surface.prototype = new Surface.Prototype();

Object.defineProperties(Surface.prototype, {
  "name": {
    get: function() {
      return this.docCtrl.session.container.name;
    }
  }
});

module.exports = Surface;
<commit_msg>Added some helpers to map between screen coordinates to model coordinates.<commit_after>"use strict";

var _ = require("underscore");
var $ = window.$;

var View = require("substance-application").View;
var util = require("substance-util");

// Note: Surface errors have codes between 500-599
var SurfaceError = util.errors.define("SurfaceError", 500);
var SelectionError = util.errors.define("SelectionError", 501, SurfaceError);

// Substance.Surface
// ==========================================================================

var Surface = function(docCtrl, renderer) {
  View.call(this);

  // Rename docCtrl to surfaceCtrl ?
  this.docCtrl = docCtrl;
  this.renderer = renderer;
  this.document = docCtrl.session.document;

  // Pull out the registered nodetypes on the written article
  this.nodeTypes = this.document.nodeTypes;
  this.nodeViews = this.renderer.nodeViews;

  this.$el.addClass('surface');

  this.listenTo(this.document, "property:updated", this.onUpdateView);
  this.listenTo(this.document, "graph:reset", this.reset);

  // bind a DOM blur handler so that we can fire a node blur event
  this.$el.blur(this.onBlur.bind(this));

  this.__lastFocussed = null;
};

Surface.Prototype = function() {

  var _selectionOptions = { "source": "surface" };

  // Private helpers
  // ---------------

  var _extractPath = function(el) {
    var path = [];
    var current = el;

    while(current) {

      // if available extract a path fragment
      if (current.getAttribute) {
        // Stop when we find an element which has been made read-only
        if (current.getAttribute("contenteditable") === "false") {
          return null;
        }

        // if there is a path attibute we collect it
        var p = current.getAttribute("data-path");
        if (p) path.unshift(p);
      }

      // node-views
      if ($(current).is(".content-node")) {
        var id = current.getAttribute("id");
        if (!id) {
          throw new Error("Every element with class 'content-node' must have an 'id' attribute.");
        }
        path.unshift(id);

        // STOP here
        return path;
      }

      current = current.parentElement;
    }

    return null;
  };

  var _mapDOMCoordinates = function(el, offset) {
    var pos, charPos;

    var container = this.docCtrl.container;

    // extract a path by looking for ".content-node" and ".node-property"
    var elementPath = _extractPath(el);

    if (!elementPath) {
      return null;
    }

    // get the position from the container
    var component = container.lookup(elementPath);
    if (!component) return null;

    // TODO rethink when it is a good time to attach the view to the node surface
    // FIXME: here we have a problem now. The TextSurface depends on the TextView
    // which can not be retrieved easily.
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    if (!component.surface.hasView()) {
      throw new Error("NodeView.attachView() must propagate down to child views.");
    }

    pos = component.pos;
    charPos = component.surface.getCharPosition(el, offset);

    return [pos, charPos];
  };

  this.getCoordinateForPosition = function(range) {
    return _mapDOMCoordinates.call(this, range.startContainer, range.startOffset);
  };

  // Read out current DOM selection and update selection in the model
  // ---------------

  this.updateSelection = function(/*e*/) {
    // console.log("Surface.updateSelection()", this.docCtrl.container.name);

    try {
      var wSel = window.getSelection();

      // HACK: sometimes it happens that the selection anchor node is undefined.
      // Try to understand and fix someday.
      if (wSel.anchorNode === null) {
        // invalid selection.
        // This happens if you click something strange
        // Decided to take the user serious and invalidate the selection
        this.clearModelSelection();
        return;
      }

      // Set selection to the cursor if clicked on the cursor.
      if ($(wSel.anchorNode.parentElement).is(".cursor")) {
        this.docCtrl.selection.collapse("cursor", _selectionOptions);
        return;
      }

      var wRange = wSel.getRangeAt(0);
      var wStartPos;
      var wEndPos;

      // Note: there are three different cases:
      // 1. selection started at startContainer (regular)
      // 2. selection started at endContainer (reverse)
      // 3. selection done via double click (anchor in different to range boundaries)
      // In cases 1. + 3. the range is used as given, in case 2. reversed.

      wStartPos = [wRange.startContainer, wRange.startOffset];
      wEndPos = [wRange.endContainer, wRange.endOffset];

      if (wRange.endContainer === wSel.anchorNode && wRange.endOffset === wSel.anchorOffset) {
        var tmp = wStartPos;
        wStartPos = wEndPos;
        wEndPos = tmp;
      }

      // Note: we clear the selection whenever we can not map the window selection
      // to model coordinates.

      var startPos = _mapDOMCoordinates.call(this, wStartPos[0], wStartPos[1]);
      if (!startPos) {
        // console.log("Surface.updateSelection(): no valid start position. Clearing the selection");
        wSel.removeAllRanges();
        this.clearModelSelection();
        return;
      }

      var endPos;
      if (wRange.collapsed) {
        endPos = startPos;
      } else {
        endPos = _mapDOMCoordinates.call(this, wEndPos[0], wEndPos[1]);
        if (!endPos) {
          // console.log("Surface.updateSelection(): no valid end position. Clearing the selection");
          wSel.removeAllRanges();
          this.clearModelSelection();
          return;
        }
      }

      try {
        this._emitFocusAndBlur(startPos[0] === endPos[0], startPos[0]);
      } catch (err) {
        console.error(err);
      }

      // console.log("Surface.updateSelection()", startPos, endPos);
      this.docCtrl.selection.set({start: startPos, end: endPos}, _selectionOptions);

    } catch (error) {
      // On errors clear the selection and report
      console.error(error);
      util.printStackTrace(error);

      var err = new SelectionError("Could not map to model cordinates.", error);
      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  // Renders the current selection
  // --------
  //

  var _mapModelCoordinates = function(pos) {
    var container = this.docCtrl.container;
    var component = container.getComponent(pos[0]);
    return this.getPositionFromComponent(component, offset);
  };

  this.getPositionFromCoordinate = function(path, offset) {
    var container = this.docCtrl.container;
    var component = container.lookup(path);
    return this.getPositionFromComponent(component, offset);
  };

  this.getPositionFromComponent = function(component, offset) {
    // TODO rethink when it is a good time to attach the view to the node surface
    if (!component.surface.hasView()) {
      this._attachViewToNodeSurface(component);
    }
    var wCoor = component.surface.getDOMPosition(offset);
    return wCoor;
  };

  this._attachViewToNodeSurface = function(component) {
    var nodeId = component.root.id;
    var topLevelSurface = component.surface.surfaceProvider.getNodeSurface(nodeId);
    var topLevelView = this.nodeViews[nodeId];
    topLevelSurface.attachView(topLevelView);
  };

  // HACK: putting this in renderSelection before the cycle guard
  // did lead to some strange infinite recursion
  // so this method is used from both places updateSelection and renderSelection
  this._emitFocusAndBlur = function(is_collapsed, pos) {
    if (is_collapsed) {
      var component = this.docCtrl.container.getComponent(pos);
      if (!component.surface.hasView()) {
        this._attachViewToNodeSurface(component);
      }
      var nodeView = component.surface.view;
      if (nodeView !== this.__lastFocussedView) {
        if (this.__lastFocussedView) this.__lastFocussedView.onBlur();
        nodeView.onFocus();
        this.__lastFocussedView = nodeView;
      }
    } else if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.onBlur = function() {
    if (this.__lastFocussedView) {
      this.__lastFocussedView.onBlur();
      this.__lastFocussedView = null;
    }
  };

  this.renderSelection = function(range, options) {
    // console.log("Surface.renderSelection()", this.docCtrl.container.name);

    try {

      var sel = this.docCtrl.selection;
      if (sel.isCollapsed()) {
        var cursorPos = sel.getCursorPosition();
        try {
          this._emitFocusAndBlur("is-collapsed", cursorPos[0]);
        } catch (err) {
          console.error(err);
        }
      } else {
        this._emitFocusAndBlur();
      }

      if (options && (options["source"] === "surface" || options["silent"] === true)){
        this.scrollToCursor();
        return;
      }

      var wSel = window.getSelection();
      // console.log("Clearing window selection.");
      wSel.removeAllRanges();

      if (sel.isNull()) {
        return;
      }

      var wRange = window.document.createRange();
      var wStartPos = _mapModelCoordinates.call(this, sel.start);
      wRange.setStart(wStartPos.startContainer, wStartPos.startOffset);

      // TODO: is there a better way to manipulate the current selection?
      // console.log("Setting window selection.");
      wSel.addRange(wRange);

      // Move the caret to the end position
      // Note: this is the only way to get reversed selections.
      if (!sel.isCollapsed()) {
        var wEndPos = _mapModelCoordinates.call(this, [sel.cursor.pos, sel.cursor.charPos]);
        wSel.extend(wEndPos.endContainer, wEndPos.endOffset);
      }

      this.scrollToCursor();

    } catch (error) {
      console.error(error);
      util.printStackTrace(error);

      // On errors clear the selection and report
      var err = new SelectionError("Could not map to DOM cordinates.", error);

      this.clearModelSelection();
      this.docCtrl.trigger("error", err);
    }
  };

  this.clearModelSelection = function() {
    // leave a mark that the surface will not handle the returning selection update
    this.docCtrl.selection.clear(_selectionOptions);
  };

  this.scrollToCursor = function() {
    var sel = this.docCtrl.selection;

    // Not exactly beautiful but ensures the cursor stays in view
    // E.g. when hitting enter on the lower document bound
    if (sel.isCollapsed()) {
      var that = this;

      // Wait for next DOM iteration
      window.setTimeout(function() {
        // Look up parent node if startContainer is a text node
        var topCorrection = $(that.el).offset().top;
        var wSel = window.getSelection();

        // avoid errors due to non existing DOM selection.
        if (wSel.rangeCount === 0 ) {
          return;
        }

        var range = wSel.getRangeAt(0);
        var bounds = range.getClientRects()[0];

        if (!bounds) {
          // This happens when the cursor is in an empty node
          // However, that is not a problem as we can use the container then
          var $content = $(range.startContainer).parents('.content');
          // do not proceed if the cursor is not in a node view
          if ($content.length === 0) return;
          bounds = $content.offset();
        }

        var topOffset = bounds.top - topCorrection;
        var surfaceHeight = $(that.el).height();

        var scrollTop = $(that.el).scrollTop();
        var lineHeight = 50;

        var targetScroll;
        if (topOffset>surfaceHeight) {
          targetScroll = scrollTop + topOffset - surfaceHeight + lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else if (topOffset < 0) {
          targetScroll = scrollTop + topOffset - 3*lineHeight;
          $(that.el).scrollTop(targetScroll);
          // console.log("Scrolling to", targetScroll);
        } else {
          // console.log("Not scrolling ...", topOffset, surfaceHeight);
        }
      // NOTE: 0 millis was not enough sometimes. However, 5 millis is probably not the solution
      // E.g., after inserting an image, it would be necessary to wait for it being loaded...
      }, 5);
    }
  };

  // Render it
  // --------
  //
  // input.image-files
  // .controls
  // .nodes
  //   .content-node.paragraph
  //   .content-node.heading
  //   ...
  // .cursor

  this.render = function() {

    // var controls = window.document.createElement('div');
    // controls.className = "controls";
    var nodes = window.document.createElement('div');
    nodes.className = "nodes";

    // var cursor = window.document.createElement('div');
    // cursor.className = "cursor";

    // this.el.appendChild(controls);
    this.el.appendChild(nodes);
    // this.el.appendChild(cursor);

    // Actual content goes here
    // --------
    //
    // We get back a document fragment from the renderer

    nodes.appendChild(this.renderer.render());

    // TODO: fixme
    this.$('input.image-files').hide();
    this.$cursor = this.$('.cursor');
    this.$cursor.hide();

    // keep the nodes for later access
    this._nodesEl = nodes;

    return this;
  };

  this.reset = function() {
    _.each(this.nodeViews, function(nodeView) {
      nodeView.dispose();
    });
    this.render();
  };

  // Cleanup view before removing it
  // --------
  //

  this.dispose = function() {
    this.stopListening();
    _.each(this.nodeViews, function(n) {
      n.dispose();
    }, this);
    if (this.keyboard) this.keyboard.disconnect(this.el);
  };

  // HACK: used by outline
  // TODO: meditate on the Surface's API
  this.getContainer = function() {
    return this.docCtrl.container;
  };

  // TODO: we could factor this out into something like a ContainerView?

  function insertOrAppend(container, pos, el) {
    var childs = container.childNodes;
    if (pos < childs.length) {
      var refNode = childs[pos];
      container.insertBefore(el, refNode);
    } else {
      container.appendChild(el);
    }
  }

  this.onUpdateView = function(path, diff) {
    if (path.length !== 2 || path[0] !== this.docCtrl.session.container.name || path[1] !== "nodes") return;

    var nodeId, node;
    var container = this._nodesEl;

    var children, el;

    if (diff.isInsert()) {
      // Create a view and insert render it into the nodes container element.
      nodeId = diff.val;
      node = this.document.get(nodeId);

      if (this.nodeTypes[node.type]) {
        // TODO: createView is misleading as returns a cached instance
        // or creates a new one
        var nodeView = this.renderer.createView(node);
        this.nodeViews[nodeId] = nodeView;
        el = nodeView.render().el;
        insertOrAppend(container, diff.pos, el);
      }
    }
    else if (diff.isDelete()) {
      // Dispose the view and remove its element from the nodes container
      nodeId = diff.val;
      if (this.nodeViews[nodeId]) {
        this.nodeViews[nodeId].dispose();
      }
      delete this.nodeViews[nodeId];
      children = container.children;
      container.removeChild(children[diff.pos]);
    }
    else if (diff.isMove()) {
      children = container.children;
      el = children[diff.pos];
      container.removeChild(el);
      insertOrAppend(container, diff.target, el);
    } else if (diff.type === "NOP") {
    } else {
      throw new Error("Illegal state.");
    }
  };

  this.getNodeView = function(nodeId) {
    return this.renderer.getView(nodeId);
  };

};

_.extend(Surface.Prototype, util.Events.Listener);

Surface.Prototype.prototype = View.prototype;
Surface.prototype = new Surface.Prototype();

Object.defineProperties(Surface.prototype, {
  "name": {
    get: function() {
      return this.docCtrl.session.container.name;
    }
  }
});

module.exports = Surface;
<|endoftext|>


<commit_before>"use strict";

$(document).ready(function () {
	// These will all be populated in parseValues()
	var businessInputs = {},
		countryInputs = {},
		output = {},
		xe = {};

	// Parse numerical value from the <input> element with the given name
	var parse = function (name) {
		var element = $('[name=' + name + ']');
		var result = parseFloat(element.val());

		if (isNaN(result)) {
			element.addClass('invalid');
			return;
		}
		element.removeClass('invalid');
		return result;
	};

	var parseBands = function (name) {
		var element = $('[name=' + name + ']');
		var text = element.val();
		var lineRegExp = new RegExp('^\\s*([0-9\\.]+)\\s*-\\s*([0-9\\.]+)\\s*:\\s*([0-9\\.]+)$');
		var result = [];

		var parsedOk = _.every(text.split('\n'), function (line) {
			console.log('parsing line: ', line);
			var match = lineRegExp.exec(line);

			if (match === null) {
				if (line.trim().length > 0) {
					// couldn't parse line
					return false;
				} else {
					// line empty - just ignore
					return true;
				}
			}

			result.push({
				from: match[1],
				to:   match[2],
				rate: match[3]
			});
			return true;
		});

		if (!parsedOk) {
			element.addClass('invalid');
			return;
		}
		element.removeClass('invalid');
		return result;
	};

	var clearError = function (message) {
		$('.error-message').hide();
		$('.results').show();
	};

	var displayError = function (message) {
		$('.error-message')
			.text(message)
			.fadeIn();

		$('.results').hide();
	};

	var parseValues = function () {
		businessInputs.b2cRevenue = parse('b2c-revenue') || 0;
		businessInputs.b2bRevenue = parse('b2b-revenue') || 0;
		businessInputs.expenses   = parse('expenses') || 0;

		_.each(businessInputs, function (value) {
			if (value > 10000000) {
				displayError('Come on, now you\'re just being silly. We\'re taking about sole traders and not companies remember.');
			}
		});

		_.each(['gb', 'es'], function (country) {
			countryInputs[country] = {};
			countryInputs[country].code = country;
			countryInputs[country].vat = parse(country + '-vat');
			countryInputs[country].vatThreshold = parse(country + '-vat-threshold');
			countryInputs[country].taxFreeAllowance = parse(country + '-tax-free-allowance');
			countryInputs[country].taxRates = parseBands(country + '-tax-rates');
			countryInputs[country].socialSecurityFixed = parse(country + '-social-security-fixed');
			countryInputs[country].socialSecurityRates = parseBands(country + '-social-security-rates');
			countryInputs[country].socialSecurityIsExpense =
				$('input[name="' + country + '-social-security-is-expense"]').is(':checked');
		});
		countryInputs.gb.name = "the UK";
		countryInputs.es.name = "Spain";

		xe.pounds = 1;
		xe.euros = parseFloat($('input[name="eurosPerPound"]').val());
	};

	var applyRates = function (brackets, input) {
		return _.reduce(brackets, function (memo, bracket) {
			var inputWithinBracket = Math.max(0, Math.min(input - bracket.from, bracket.to - bracket.from));
			return memo + inputWithinBracket * bracket.rate;
		}, 0);
	};

	var roundCurrency = function (number) {
		return Math.round(number * 100) / 100;
	};

	// Calculate all deductions
	//
	// # Business Inputs (all monthly)
	// business.b2cRevenue         : Total sales to consumers, including VAT
	// business.b2bRevenue         : Total sales to business, excluding VAT
	// business.expenses           : Total expenses
	//
	// # Country Inputs (all yearly)
	// country.vat                 : Percentage of VAT
	// country.vatThreshold        : VAT registration threshold
	// country.taxFreeAllowance    : Tax free allowance for income tax
	// country.taxRates            : Income tax rates (array)
	// country.socialSecurityFixed : Fixed social security payment
	// country.socialSecurityRates : Income dependent social security rates (array)
	// country.socialSecurityIsExpense : True if social security payment is taken before tax
	//
	// # Output (all monthly)
	// vat
	// preTaxProfit
	// socialSecurity
	// incomeTax
	// totalDeductions
	// netProfit
	var calcDeductions = function (business, country) {
		var output = {};

		// VAT
		if (businessInputs.b2cRevenue * 12 < country.vatThreshold) {
			output.vat = 0;
		} else {
			output.vat = business.b2cRevenue * (1 - 100 / (100 + country.vat));
		}

		output.preTaxProfit =
			business.b2cRevenue + business.b2bRevenue - business.expenses - output.vat;

		var yearlyPreTaxProfit = output.preTaxProfit * 12;

		// Social Security
		var yearlySocialSecurity = country.socialSecurityFixed;
		yearlySocialSecurity += applyRates(country.socialSecurityRates, yearlyPreTaxProfit);
		output.socialSecurity = yearlySocialSecurity / 12;

		// Income Tax
		var yearlyTaxableIncome = yearlyPreTaxProfit;
		if (country.socialSecurityIsExpense) {
			yearlyTaxableIncome = yearlyPreTaxProfit - yearlySocialSecurity;
		}
		yearlyTaxableIncome = Math.max(0, yearlyTaxableIncome - country.taxFreeAllowance);

		console.log(country.code + ' - taxable yearly: ', yearlyTaxableIncome);

		output.incomeTax = applyRates(country.taxRates, yearlyTaxableIncome) / 12;
		output.totalDeductions = output.vat + output.socialSecurity + output.incomeTax;
		output.netProfit = output.preTaxProfit - output.socialSecurity - output.incomeTax;

		return output;
	};

	var scaleValues = function (inputs, factor) {
		var output = {};

		_.each(inputs, function (value, key) {
			output[key] = value * factor;
		});

		return output;
	};

	// Display a friendly message explaining how they'll be better off in one
	// country or another
	var displayBubble = function () {
		var betterCountry,
			worseCountry,
			difference,
			percentage,
			message;

		if (output.gb.netProfit > output.es.netProfit) {
			betterCountry = 'gb';
			worseCountry = 'es';
		} else {
			betterCountry = 'es';
			worseCountry = 'gb';
		}

		var betterProfit = output[betterCountry].netProfit,
			worseProfit = output[worseCountry].netProfit,
			betterName = countryInputs[betterCountry].name,
			worseName = countryInputs[worseCountry].name;

		difference = betterProfit - worseProfit;
		percentage = 100 * difference / worseProfit;

		if (worseProfit < 0 && betterProfit < 0) {
			message = 'You will lose €' + Math.round(difference) + ' / month more in ' + worseName;
		} else if (worseProfit < 0) {

			message = 'You will make €' + Math.round(betterProfit) + ' in ' + betterName +
				' instead of losing €' + Math.round(-worseProfit) + ' in ' + worseName;
		} else {
			message = 'You will make a net profit ' + Math.round(percentage) + '% (€' +
					Math.round(difference) + ') higher in ' + betterName;
		}

		$('.compare-net-profit').text(message);
	};

	var calculateEverything = function () {
		clearError();

		// parse inputs
		parseValues();

		// calculate all deductions
		var businessInputsPounds = scaleValues(businessInputs, xe.pounds / xe.euros);
		var outputsPounds = calcDeductions(businessInputsPounds, countryInputs.gb);
		output.gb = scaleValues(outputsPounds, xe.euros / xe.pounds);
		output.es = calcDeductions(businessInputs, countryInputs.es);

		console.log('output: ', JSON.stringify(output, null, '\t'));

		// write output
		_.each(output, function (country, countryCode) {
			_.each(country, function (value, key) {
				$('.' + countryCode + '-output-' + key)
					.text('€' + roundCurrency(value))
					.attr('title', '£' + roundCurrency(value * xe.pounds / xe.euros));
			});
		});

		displayBubble();

	};

	$('input').keyup(calculateEverything);
	$('textarea').keyup(calculateEverything);
	$('input[type="checkbox"]').change(calculateEverything);

	calculateEverything();
});

<commit_msg>tweak message<commit_after>"use strict";

$(document).ready(function () {
	// These will all be populated in parseValues()
	var businessInputs = {},
		countryInputs = {},
		output = {},
		xe = {};

	// Parse numerical value from the <input> element with the given name
	var parse = function (name) {
		var element = $('[name=' + name + ']');
		var result = parseFloat(element.val());

		if (isNaN(result)) {
			element.addClass('invalid');
			return;
		}
		element.removeClass('invalid');
		return result;
	};

	var parseBands = function (name) {
		var element = $('[name=' + name + ']');
		var text = element.val();
		var lineRegExp = new RegExp('^\\s*([0-9\\.]+)\\s*-\\s*([0-9\\.]+)\\s*:\\s*([0-9\\.]+)$');
		var result = [];

		var parsedOk = _.every(text.split('\n'), function (line) {
			console.log('parsing line: ', line);
			var match = lineRegExp.exec(line);

			if (match === null) {
				if (line.trim().length > 0) {
					// couldn't parse line
					return false;
				} else {
					// line empty - just ignore
					return true;
				}
			}

			result.push({
				from: match[1],
				to:   match[2],
				rate: match[3]
			});
			return true;
		});

		if (!parsedOk) {
			element.addClass('invalid');
			return;
		}
		element.removeClass('invalid');
		return result;
	};

	var clearError = function (message) {
		$('.error-message').hide();
		$('.results').show();
	};

	var displayError = function (message) {
		$('.error-message')
			.text(message)
			.fadeIn();

		$('.results').hide();
	};

	var parseValues = function () {
		businessInputs.b2cRevenue = parse('b2c-revenue') || 0;
		businessInputs.b2bRevenue = parse('b2b-revenue') || 0;
		businessInputs.expenses   = parse('expenses') || 0;

		_.each(businessInputs, function (value) {
			if (value > 10000000) {
				displayError('Come on, now you\'re just being silly. We\'re taking about sole traders remember.');
			}
		});

		_.each(['gb', 'es'], function (country) {
			countryInputs[country] = {};
			countryInputs[country].code = country;
			countryInputs[country].vat = parse(country + '-vat');
			countryInputs[country].vatThreshold = parse(country + '-vat-threshold');
			countryInputs[country].taxFreeAllowance = parse(country + '-tax-free-allowance');
			countryInputs[country].taxRates = parseBands(country + '-tax-rates');
			countryInputs[country].socialSecurityFixed = parse(country + '-social-security-fixed');
			countryInputs[country].socialSecurityRates = parseBands(country + '-social-security-rates');
			countryInputs[country].socialSecurityIsExpense =
				$('input[name="' + country + '-social-security-is-expense"]').is(':checked');
		});
		countryInputs.gb.name = "the UK";
		countryInputs.es.name = "Spain";

		xe.pounds = 1;
		xe.euros = parseFloat($('input[name="eurosPerPound"]').val());
	};

	var applyRates = function (brackets, input) {
		return _.reduce(brackets, function (memo, bracket) {
			var inputWithinBracket = Math.max(0, Math.min(input - bracket.from, bracket.to - bracket.from));
			return memo + inputWithinBracket * bracket.rate;
		}, 0);
	};

	var roundCurrency = function (number) {
		return Math.round(number * 100) / 100;
	};

	// Calculate all deductions
	//
	// # Business Inputs (all monthly)
	// business.b2cRevenue         : Total sales to consumers, including VAT
	// business.b2bRevenue         : Total sales to business, excluding VAT
	// business.expenses           : Total expenses
	//
	// # Country Inputs (all yearly)
	// country.vat                 : Percentage of VAT
	// country.vatThreshold        : VAT registration threshold
	// country.taxFreeAllowance    : Tax free allowance for income tax
	// country.taxRates            : Income tax rates (array)
	// country.socialSecurityFixed : Fixed social security payment
	// country.socialSecurityRates : Income dependent social security rates (array)
	// country.socialSecurityIsExpense : True if social security payment is taken before tax
	//
	// # Output (all monthly)
	// vat
	// preTaxProfit
	// socialSecurity
	// incomeTax
	// totalDeductions
	// netProfit
	var calcDeductions = function (business, country) {
		var output = {};

		// VAT
		if (businessInputs.b2cRevenue * 12 < country.vatThreshold) {
			output.vat = 0;
		} else {
			output.vat = business.b2cRevenue * (1 - 100 / (100 + country.vat));
		}

		output.preTaxProfit =
			business.b2cRevenue + business.b2bRevenue - business.expenses - output.vat;

		var yearlyPreTaxProfit = output.preTaxProfit * 12;

		// Social Security
		var yearlySocialSecurity = country.socialSecurityFixed;
		yearlySocialSecurity += applyRates(country.socialSecurityRates, yearlyPreTaxProfit);
		output.socialSecurity = yearlySocialSecurity / 12;

		// Income Tax
		var yearlyTaxableIncome = yearlyPreTaxProfit;
		if (country.socialSecurityIsExpense) {
			yearlyTaxableIncome = yearlyPreTaxProfit - yearlySocialSecurity;
		}
		yearlyTaxableIncome = Math.max(0, yearlyTaxableIncome - country.taxFreeAllowance);

		console.log(country.code + ' - taxable yearly: ', yearlyTaxableIncome);

		output.incomeTax = applyRates(country.taxRates, yearlyTaxableIncome) / 12;
		output.totalDeductions = output.vat + output.socialSecurity + output.incomeTax;
		output.netProfit = output.preTaxProfit - output.socialSecurity - output.incomeTax;

		return output;
	};

	var scaleValues = function (inputs, factor) {
		var output = {};

		_.each(inputs, function (value, key) {
			output[key] = value * factor;
		});

		return output;
	};

	// Display a friendly message explaining how they'll be better off in one
	// country or another
	var displayBubble = function () {
		var betterCountry,
			worseCountry,
			difference,
			percentage,
			message;

		if (output.gb.netProfit > output.es.netProfit) {
			betterCountry = 'gb';
			worseCountry = 'es';
		} else {
			betterCountry = 'es';
			worseCountry = 'gb';
		}

		var betterProfit = output[betterCountry].netProfit,
			worseProfit = output[worseCountry].netProfit,
			betterName = countryInputs[betterCountry].name,
			worseName = countryInputs[worseCountry].name;

		difference = betterProfit - worseProfit;
		percentage = 100 * difference / worseProfit;

		if (worseProfit < 0 && betterProfit < 0) {
			message = 'You will lose €' + Math.round(difference) + ' / month more in ' + worseName;
		} else if (worseProfit < 0) {

			message = 'You will make €' + Math.round(betterProfit) + ' in ' + betterName +
				' instead of losing €' + Math.round(-worseProfit) + ' in ' + worseName;
		} else {
			message = 'You will make a net profit ' + Math.round(percentage) + '% (€' +
					Math.round(difference) + ') higher in ' + betterName;
		}

		$('.compare-net-profit').text(message);
	};

	var calculateEverything = function () {
		clearError();

		// parse inputs
		parseValues();

		// calculate all deductions
		var businessInputsPounds = scaleValues(businessInputs, xe.pounds / xe.euros);
		var outputsPounds = calcDeductions(businessInputsPounds, countryInputs.gb);
		output.gb = scaleValues(outputsPounds, xe.euros / xe.pounds);
		output.es = calcDeductions(businessInputs, countryInputs.es);

		console.log('output: ', JSON.stringify(output, null, '\t'));

		// write output
		_.each(output, function (country, countryCode) {
			_.each(country, function (value, key) {
				$('.' + countryCode + '-output-' + key)
					.text('€' + roundCurrency(value))
					.attr('title', '£' + roundCurrency(value * xe.pounds / xe.euros));
			});
		});

		displayBubble();

	};

	$('input').keyup(calculateEverything);
	$('textarea').keyup(calculateEverything);
	$('input[type="checkbox"]').change(calculateEverything);

	calculateEverything();
});

<|endoftext|>


<commit_before>$(function() {
$.fn.toolgif = function( options ) {

var settings = $.extend({
width:200,
height:100,
speed:200,
borderRadius:5,
textColor:"initial",
gifChange:true
}, options );

return  this.each( function() {
if ( $( this ).data( 'toolgif' ) ){
return false;
}
$( this ).data( 'toolgif', true );

var span=$(this);
//Create a img
var $img = $("<img>" , {class: "toolgif-gif"});
$(this).append($img);

//Create an arrow
var $arrow=$("<i>",{class:"arrow"});
$(this).append($arrow);
//Arrow css
$arrow.css("position","absolute");
$arrow.css("width",0);
$arrow.css("height",0);
$arrow.css("left","50%");
$arrow.css("margin-left",-4);

//Span css
span.css("color",settings.textColor)
span.css("position","relative");
span.css("top",0);
span.css("left",0);

//Img css
$img.css("width",settings.width);
$img.css("marginLeft",-settings.width/2);

$img.css("height",settings.height);

$img.css("position","absolute");
$img.css("left","50%");

$img.css("z-index",9999999);
$img.css("border-radius",settings.borderRadius);

//Check data-place
if($.trim($(this).attr('data-place')) === "bottom") {
$img.css("bottom",-settings.height-2);

$arrow.css("bottom",-2);
$arrow.css("border-left","4px solid transparent");
$arrow.css("border-right","4px solid transparent");
$arrow.css("border-bottom","4px solid");
}else {
$img.css("top",-settings.height-2);

$arrow.css("top",-2);
$arrow.css("border-left","4px solid transparent");
$arrow.css("border-right","4px solid transparent ");
$arrow.css("border-top","4px solid");
}

var toolgifText= $(this).text();
//Replace spaces with + sign
toolgifText = toolgifText.split(' ').join('+');

//Giphy api
var host = "https://api.giphy.com";
var path = "/v1/gifs/random?";
var api_key = "&api_key=dc6zaTOxFJmzC";
var query = "&q=";
var tag = "&tag=";

//Check data-tag
if($.trim($(this).attr('data-tag'))) {
var dataTag= $(this).attr("data-tag");
dataTag=dataTag.split(' ').join('+');
var url = host + path + api_key + tag + dataTag;
}else {
var url = host + path + api_key + tag + toolgifText;
}

//Check data-url
if($.trim($(this).attr("data-url"))) {
var dataUrl= $(this).attr("data-url");
dataUrl=dataUrl.split(' ').join('+');
$.getJSON(url, function(mydata) {
$($img).attr("src",dataUrl);
});
}else {
function refreshUrl() {
$.getJSON(url, function(mydata) {
$($img).attr("src",mydata.data.image_url);
});
}
refreshUrl();
}

//Hide arrow and gif
$arrow.hide();
$img.hide();

//Hover
span.hover(

function () {
$img.stop(true).fadeIn(settings.speed);
$arrow.stop(true).fadeIn(settings.speed);
},

function () {
if(settings.gifChange === false) {
$img.stop(true).fadeOut(settings.speed);
$arrow.stop(true).fadeOut(settings.speed);
}else {
$img.stop(true).fadeOut(settings.speed);
$arrow.stop(true).fadeOut(settings.speed);
refreshUrl();
}
}

);

});
};
})(jQuery);

<commit_msg>Fix Uncaught TypeError: $(...) is not a function<commit_after>(function() {
$.fn.toolgif = function( options ) {

var settings = $.extend({
width:200,
height:100,
speed:200,
borderRadius:5,
textColor:"initial",
gifChange:true
}, options );

return  this.each( function() {
if ( $( this ).data( 'toolgif' ) ){
return false;
}
$( this ).data( 'toolgif', true );

var span=$(this);
//Create a img
var $img = $("<img>" , {class: "toolgif-gif"});
$(this).append($img);

//Create an arrow
var $arrow=$("<i>",{class:"arrow"});
$(this).append($arrow);
//Arrow css
$arrow.css("position","absolute");
$arrow.css("width",0);
$arrow.css("height",0);
$arrow.css("left","50%");
$arrow.css("margin-left",-4);

//Span css
span.css("color",settings.textColor)
span.css("position","relative");
span.css("top",0);
span.css("left",0);

//Img css
$img.css("width",settings.width);
$img.css("marginLeft",-settings.width/2);

$img.css("height",settings.height);

$img.css("position","absolute");
$img.css("left","50%");

$img.css("z-index",9999999);
$img.css("border-radius",settings.borderRadius);

//Check data-place
if($.trim($(this).attr('data-place')) === "bottom") {
$img.css("bottom",-settings.height-2);

$arrow.css("bottom",-2);
$arrow.css("border-left","4px solid transparent");
$arrow.css("border-right","4px solid transparent");
$arrow.css("border-bottom","4px solid");
}else {
$img.css("top",-settings.height-2);

$arrow.css("top",-2);
$arrow.css("border-left","4px solid transparent");
$arrow.css("border-right","4px solid transparent ");
$arrow.css("border-top","4px solid");
}

var toolgifText= $(this).text();
//Replace spaces with + sign
toolgifText = toolgifText.split(' ').join('+');

//Giphy api
var host = "https://api.giphy.com";
var path = "/v1/gifs/random?";
var api_key = "&api_key=dc6zaTOxFJmzC";
var query = "&q=";
var tag = "&tag=";

//Check data-tag
if($.trim($(this).attr('data-tag'))) {
var dataTag= $(this).attr("data-tag");
dataTag=dataTag.split(' ').join('+');
var url = host + path + api_key + tag + dataTag;
}else {
var url = host + path + api_key + tag + toolgifText;
}

//Check data-url
if($.trim($(this).attr("data-url"))) {
var dataUrl= $(this).attr("data-url");
dataUrl=dataUrl.split(' ').join('+');
$.getJSON(url, function(mydata) {
$($img).attr("src",dataUrl);
});
}else {
function refreshUrl() {
$.getJSON(url, function(mydata) {
$($img).attr("src",mydata.data.image_url);
});
}
refreshUrl();
}

//Hide arrow and gif
$arrow.hide();
$img.hide();

//Hover
span.hover(

function () {
$img.stop(true).fadeIn(settings.speed);
$arrow.stop(true).fadeIn(settings.speed);
},

function () {
if(settings.gifChange === false) {
$img.stop(true).fadeOut(settings.speed);
$arrow.stop(true).fadeOut(settings.speed);
}else {
$img.stop(true).fadeOut(settings.speed);
$arrow.stop(true).fadeOut(settings.speed);
refreshUrl();
}
}

);

});
};
})(jQuery);

<|endoftext|>