<commit_before>/*

	Ractive-events-tap
	==================

	Version <%= VERSION %>.

	<< description goes here... >>

	==========================

	Troubleshooting: If you're using a module system in your app (AMD or
	something more nodey) then you may need to change the paths below,
	where it says `require( 'ractive' )` or `define([ 'Ractive' ]...)`.

	==========================

	Usage: Include this file on your page below Ractive, e.g:

	Or, if you're using a module loader, require this module:

	    // requiring the plugin will 'activate' it - no need to use
	    // the return value
	    require( 'Ractive-events-tap' );

	<< more specific instructions for this plugin go here... >>

*/

(function ( global, factory ) {

	'use strict';

	// Common JS (i.e. browserify) environment
	if ( typeof module !== 'undefined' && module.exports && typeof require === 'function' ) {
		factory( require( 'ractive' ) );
	}

	// AMD?
	else if ( typeof define === 'function' && define.amd ) {
		define([ 'Ractive' ], factory );
	}

	// browser global
	else if ( global.Ractive ) {
		factory( global.Ractive );
	}

	else {
		throw new Error( 'Could not find Ractive! It must be loaded before the Ractive-events-tap plugin' );
	}

}( typeof window !== 'undefined' ? window : this, function ( Ractive ) {

	'use strict';

	var tap = function ( node, fire ) {
		var mousedown, touchstart, focusHandler, distanceThreshold, timeThreshold;

		distanceThreshold = 5; // maximum pixels pointer can move before cancel
		timeThreshold = 400;   // maximum milliseconds between down and up before cancel

		mousedown = function ( event ) {
			var currentTarget, x, y, pointerId, up, move, cancel;

			if ( event.which !== undefined && event.which !== 1 ) {
				return;
			}

			x = event.clientX;
			y = event.clientY;
			currentTarget = this;
			// This will be null for mouse events.
			pointerId = event.pointerId;

			up = function ( event ) {
				if ( event.pointerId != pointerId ) {
					return;
				}

				fire({
					node: currentTarget,
					original: event
				});

				cancel();
			};

			move = function ( event ) {
				if ( event.pointerId != pointerId ) {
					return;
				}

				if ( ( Math.abs( event.clientX - x ) >= distanceThreshold ) || ( Math.abs( event.clientY - y ) >= distanceThreshold ) ) {
					cancel();
				}
			};

			cancel = function () {
				node.removeEventListener( 'MSPointerUp', up, false );
				document.removeEventListener( 'MSPointerMove', move, false );
				document.removeEventListener( 'MSPointerCancel', cancel, false );
				node.removeEventListener( 'pointerup', up, false );
				document.removeEventListener( 'pointermove', move, false );
				document.removeEventListener( 'pointercancel', cancel, false );
				node.removeEventListener( 'click', up, false );
				document.removeEventListener( 'mousemove', move, false );
			};

			if ( window.navigator.pointerEnabled ) {
				node.addEventListener( 'pointerup', up, false );
				document.addEventListener( 'pointermove', move, false );
				document.addEventListener( 'pointercancel', cancel, false );
			} else if ( window.navigator.msPointerEnabled ) {
				node.addEventListener( 'MSPointerUp', up, false );
				document.addEventListener( 'MSPointerMove', move, false );
				document.addEventListener( 'MSPointerCancel', cancel, false );
			} else {
				node.addEventListener( 'click', up, false );
				document.addEventListener( 'mousemove', move, false );
			}

			setTimeout( cancel, timeThreshold );
		};

		if ( window.navigator.pointerEnabled ) {
			node.addEventListener( 'pointerdown', mousedown, false );
		} else if ( window.navigator.msPointerEnabled ) {
			node.addEventListener( 'MSPointerDown', mousedown, false );
		} else {
			node.addEventListener( 'mousedown', mousedown, false );
		}

		touchstart = function ( event ) {
			var currentTarget, x, y, touch, finger, move, up, cancel;

			if ( event.touches.length !== 1 ) {
				return;
			}

			touch = event.touches[0];

			x = touch.clientX;
			y = touch.clientY;
			currentTarget = this;

			finger = touch.identifier;

			up = function ( event ) {
				var touch;

				touch = event.changedTouches[0];
				if ( touch.identifier !== finger ) {
					cancel();
				}

				event.preventDefault();  // prevent compatibility mouse event
				fire({
					node: currentTarget,
					original: event
				});

				cancel();
			};

			move = function ( event ) {
				var touch;

				if ( event.touches.length !== 1 || event.touches[0].identifier !== finger ) {
					cancel();
				}

				touch = event.touches[0];
				if ( ( Math.abs( touch.clientX - x ) >= distanceThreshold ) || ( Math.abs( touch.clientY - y ) >= distanceThreshold ) ) {
					cancel();
				}
			};

			cancel = function () {
				node.removeEventListener( 'touchend', up, false );
				window.removeEventListener( 'touchmove', move, false );
				window.removeEventListener( 'touchcancel', cancel, false );
			};

			node.addEventListener( 'touchend', up, false );
			window.addEventListener( 'touchmove', move, false );
			window.addEventListener( 'touchcancel', cancel, false );

			setTimeout( cancel, timeThreshold );
		};

		node.addEventListener( 'touchstart', touchstart, false );

		// native buttons, and <input type='button'> elements, should fire a tap event
		// when the space key is pressed
		if ( node.tagName === 'BUTTON' || node.type === 'button' ) {
			focusHandler = function () {
				var blurHandler, keydownHandler;

				keydownHandler = function ( event ) {
					if ( event.which === 32 ) { // space key
						fire({
							node: node,
							original: event
						});
					}
				};

				blurHandler = function () {
					node.removeEventListener( 'keydown', keydownHandler, false );
					node.removeEventListener( 'blur', blurHandler, false );
				};

				node.addEventListener( 'keydown', keydownHandler, false );
				node.addEventListener( 'blur', blurHandler, false );
			};

			node.addEventListener( 'focus', focusHandler, false );
		}

		return {
			teardown: function () {
				node.removeEventListener( 'pointerdown', mousedown, false );
				node.removeEventListener( 'MSPointerDown', mousedown, false );
				node.removeEventListener( 'mousedown', mousedown, false );
				node.removeEventListener( 'touchstart', touchstart, false );
				node.removeEventListener( 'focus', focusHandler, false );
			}
		};
	};

	Ractive.events.tap = tap;

}));<commit_msg>added some inline docs<commit_after>/*

	Ractive-events-tap
	==================

	Version <%= VERSION %>.

	On mobile devices, using `on-click` isn't good enough. Tapping the
	touchscreen will fire a simulated click event, but only after a 300
	millisecond delay, which makes your app feel sluggish. It also
	causes the tapped area to highlight, which in most cases looks a
	bit messy.

	Instead, use `on-tap`. When you tap an area, the simulated click
	event will be prevented, and the user's action is responded to
	instantly. The `on-tap` event also differs from `on-click` in that
	the click event will (frankly rather bizarrely) fire even if you
	hold the mouse down over a single element for several seconds and
	waggle it about.

	Pointer events are also supported, as is pressing the spacebar when
	the relevant element is focused (which triggers a click event, and
	is good for accessibility).

	==========================

	Troubleshooting: If you're using a module system in your app (AMD or
	something more nodey) then you may need to change the paths below,
	where it says `require( 'ractive' )` or `define([ 'Ractive' ]...)`.

	==========================

	Usage: Include this file on your page below Ractive, e.g:

	Or, if you're using a module loader, require this module:

	    // requiring the plugin will 'activate' it - no need to use
	    // the return value
	    require( 'Ractive-events-tap' );

	Add a tap event in the normal fashion:

tap me!

	Then add a handler:

	    ractive.on( 'foo', function ( event ) {
	      alert( 'tapped' );
	    });

*/

(function ( global, factory ) {

	'use strict';

	// Common JS (i.e. browserify) environment
	if ( typeof module !== 'undefined' && module.exports && typeof require === 'function' ) {
		factory( require( 'ractive' ) );
	}

	// AMD?
	else if ( typeof define === 'function' && define.amd ) {
		define([ 'Ractive' ], factory );
	}

	// browser global
	else if ( global.Ractive ) {
		factory( global.Ractive );
	}

	else {
		throw new Error( 'Could not find Ractive! It must be loaded before the Ractive-events-tap plugin' );
	}

}( typeof window !== 'undefined' ? window : this, function ( Ractive ) {

	'use strict';

	var tap = function ( node, fire ) {
		var mousedown, touchstart, focusHandler, distanceThreshold, timeThreshold;

		distanceThreshold = 5; // maximum pixels pointer can move before cancel
		timeThreshold = 400;   // maximum milliseconds between down and up before cancel

		mousedown = function ( event ) {
			var currentTarget, x, y, pointerId, up, move, cancel;

			if ( event.which !== undefined && event.which !== 1 ) {
				return;
			}

			x = event.clientX;
			y = event.clientY;
			currentTarget = this;
			// This will be null for mouse events.
			pointerId = event.pointerId;

			up = function ( event ) {
				if ( event.pointerId != pointerId ) {
					return;
				}

				fire({
					node: currentTarget,
					original: event
				});

				cancel();
			};

			move = function ( event ) {
				if ( event.pointerId != pointerId ) {
					return;
				}

				if ( ( Math.abs( event.clientX - x ) >= distanceThreshold ) || ( Math.abs( event.clientY - y ) >= distanceThreshold ) ) {
					cancel();
				}
			};

			cancel = function () {
				node.removeEventListener( 'MSPointerUp', up, false );
				document.removeEventListener( 'MSPointerMove', move, false );
				document.removeEventListener( 'MSPointerCancel', cancel, false );
				node.removeEventListener( 'pointerup', up, false );
				document.removeEventListener( 'pointermove', move, false );
				document.removeEventListener( 'pointercancel', cancel, false );
				node.removeEventListener( 'click', up, false );
				document.removeEventListener( 'mousemove', move, false );
			};

			if ( window.navigator.pointerEnabled ) {
				node.addEventListener( 'pointerup', up, false );
				document.addEventListener( 'pointermove', move, false );
				document.addEventListener( 'pointercancel', cancel, false );
			} else if ( window.navigator.msPointerEnabled ) {
				node.addEventListener( 'MSPointerUp', up, false );
				document.addEventListener( 'MSPointerMove', move, false );
				document.addEventListener( 'MSPointerCancel', cancel, false );
			} else {
				node.addEventListener( 'click', up, false );
				document.addEventListener( 'mousemove', move, false );
			}

			setTimeout( cancel, timeThreshold );
		};

		if ( window.navigator.pointerEnabled ) {
			node.addEventListener( 'pointerdown', mousedown, false );
		} else if ( window.navigator.msPointerEnabled ) {
			node.addEventListener( 'MSPointerDown', mousedown, false );
		} else {
			node.addEventListener( 'mousedown', mousedown, false );
		}

		touchstart = function ( event ) {
			var currentTarget, x, y, touch, finger, move, up, cancel;

			if ( event.touches.length !== 1 ) {
				return;
			}

			touch = event.touches[0];

			x = touch.clientX;
			y = touch.clientY;
			currentTarget = this;

			finger = touch.identifier;

			up = function ( event ) {
				var touch;

				touch = event.changedTouches[0];
				if ( touch.identifier !== finger ) {
					cancel();
				}

				event.preventDefault();  // prevent compatibility mouse event
				fire({
					node: currentTarget,
					original: event
				});

				cancel();
			};

			move = function ( event ) {
				var touch;

				if ( event.touches.length !== 1 || event.touches[0].identifier !== finger ) {
					cancel();
				}

				touch = event.touches[0];
				if ( ( Math.abs( touch.clientX - x ) >= distanceThreshold ) || ( Math.abs( touch.clientY - y ) >= distanceThreshold ) ) {
					cancel();
				}
			};

			cancel = function () {
				node.removeEventListener( 'touchend', up, false );
				window.removeEventListener( 'touchmove', move, false );
				window.removeEventListener( 'touchcancel', cancel, false );
			};

			node.addEventListener( 'touchend', up, false );
			window.addEventListener( 'touchmove', move, false );
			window.addEventListener( 'touchcancel', cancel, false );

			setTimeout( cancel, timeThreshold );
		};

		node.addEventListener( 'touchstart', touchstart, false );

		// native buttons, and <input type='button'> elements, should fire a tap event
		// when the space key is pressed
		if ( node.tagName === 'BUTTON' || node.type === 'button' ) {
			focusHandler = function () {
				var blurHandler, keydownHandler;

				keydownHandler = function ( event ) {
					if ( event.which === 32 ) { // space key
						fire({
							node: node,
							original: event
						});
					}
				};

				blurHandler = function () {
					node.removeEventListener( 'keydown', keydownHandler, false );
					node.removeEventListener( 'blur', blurHandler, false );
				};

				node.addEventListener( 'keydown', keydownHandler, false );
				node.addEventListener( 'blur', blurHandler, false );
			};

			node.addEventListener( 'focus', focusHandler, false );
		}

		return {
			teardown: function () {
				node.removeEventListener( 'pointerdown', mousedown, false );
				node.removeEventListener( 'MSPointerDown', mousedown, false );
				node.removeEventListener( 'mousedown', mousedown, false );
				node.removeEventListener( 'touchstart', touchstart, false );
				node.removeEventListener( 'focus', focusHandler, false );
			}
		};
	};

	Ractive.events.tap = tap;

}));<|endoftext|>


<commit_before>import describe from 'tape';
import dom from 'cheerio';
import React from 'react';
import ReactDOMServer from 'react-dom/server';

import { withFeatures, configureFeature } from '../../../src/';

const render = ReactDOMServer.renderToStaticMarkup;

describe('test integration of withFeatures() & configureFeature()', ({ test }) => {
  test('...enabled feature', ({ end, deepEqual }) => {
    const initialFeatures = {
      comments: {
        enabled: false,
        dependencies: []
      },
      help: {
        enabled: true,
        dependencies: []
      },
      sorting: {
        enabled: false,
        dependencies: []
      }
    };

    const PresentationalComponent = ({children} = {}) =>
{children}
;
    const NotFoundComponent = () =>
No help for you today!
;
    const FeatureComponent = () =>
Need help? Call XXX-XXX-XXXX
;
    const ConfiguredFeature = configureFeature(NotFoundComponent)('help')(FeatureComponent);
    const Features = withFeatures({ initialFeatures })(PresentationalComponent);

    const $ = dom.load(
      render(
        <Features>
          <ConfiguredFeature />
        </Features>
      )
    );
    {
      const msg = 'it should not render NotFound component';
      const actual = $('.not-found').length;
      const expected = 0;
      deepEqual(actual, expected, msg);
    }
    {
      const msg = 'it should render the Feature component';
      const actual = $('.feature').length;
      const expected = 1;
      deepEqual(actual, expected, msg);
    }
    end();
  });

  test('...disabled feature', ({ end, deepEqual }) => {
    const initialFeatures = {
      comments: {
        enabled: true,
        dependencies: []
      },
      help: {
        enabled: false,
        dependencies: []
      },
      sorting: {
        enabled: false,
        dependencies: []
      }
    };

    const PresentationalComponent = ({children} = {}) =>
{children}
;
    const NotFoundComponent = () =>
No help for you today!
;
    const FeatureComponent = () =>
Need help? Call XXX-XXX-XXXX
;
    const ConfiguredFeature = configureFeature(NotFoundComponent)('help')(FeatureComponent);
    const Features = withFeatures({ initialFeatures })(PresentationalComponent);

    const $ = dom.load(
      render(
        <Features>
          <ConfiguredFeature />
        </Features>
      )
    );
    {
      const msg = 'it should render NotFound component';
      const actual = $('.not-found').length;
      const expected = 1;
      deepEqual(actual, expected, msg);
    }
    {
      const msg = 'it should not render the Feature component';
      const actual = $('.feature').length;
      const expected = 0;
      deepEqual(actual, expected, msg);
    }
    end();
  });
});
<commit_msg>fix integration test<commit_after>import describe from 'tape';
import dom from 'cheerio';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import createFeature from '../fixtures/createFeature';
import { withFeatures, configureFeature } from '../../../src/';

const render = ReactDOMServer.renderToStaticMarkup;

describe('test integration of withFeatures() & configureFeature()', ({ test }) => {
  test('...enabled feature', ({ end, deepEqual }) => {
    const initialFeatures = [
      createFeature({
        name: 'comments',
        enabled: false,
        dependencies: []
      }),
      createFeature({
        name: 'help',
        enabled: true,
        dependencies: []
      }),
      createFeature({
        name: 'sorting',
        enabled: false,
        dependencies: []
      })
    ];

    const PresentationalComponent = ({children} = {}) =>
{children}
;
    const NotFoundComponent = () =>
No help for you today!
;
    const FeatureComponent = () =>
Need help? Call XXX-XXX-XXXX
;
    const ConfiguredFeature = configureFeature(NotFoundComponent)('help')(FeatureComponent);
    const Features = withFeatures({ initialFeatures })(PresentationalComponent);

    const $ = dom.load(
      render(
        <Features>
          <ConfiguredFeature />
        </Features>
      )
    );
    {
      const msg = 'it should not render NotFound component';
      const actual = $('.not-found').length;
      const expected = 0;
      deepEqual(actual, expected, msg);
    }
    {
      const msg = 'it should render the Feature component';
      const actual = $('.feature').length;
      const expected = 1;
      deepEqual(actual, expected, msg);
    }
    end();
  });

  test('...disabled feature', ({ end, deepEqual }) => {
    const initialFeatures = [
      createFeature({
        name: 'comments',
        enabled: true,
        dependencies: []
      }),
      createFeature({
        name: 'help',
        enabled: false,
        dependencies: []
      }),
      createFeature({
        name: 'sorting',
        enabled: false,
        dependencies: []
      })
    ];

    const PresentationalComponent = ({children} = {}) =>
{children}
;
    const NotFoundComponent = () =>
No help for you today!
;
    const FeatureComponent = () =>
Need help? Call XXX-XXX-XXXX
;
    const ConfiguredFeature = configureFeature(NotFoundComponent)('help')(FeatureComponent);
    const Features = withFeatures({ initialFeatures })(PresentationalComponent);

    const $ = dom.load(
      render(
        <Features>
          <ConfiguredFeature />
        </Features>
      )
    );
    {
      const msg = 'it should render NotFound component';
      const actual = $('.not-found').length;
      const expected = 1;
      deepEqual(actual, expected, msg);
    }
    {
      const msg = 'it should not render the Feature component';
      const actual = $('.feature').length;
      const expected = 0;
      deepEqual(actual, expected, msg);
    }
    end();
  });
});
<|endoftext|>


<commit_before>var inlineEditRowContents = {};

function inlineEdit(rowName, options) {
	var tableRow = document.getElementById(rowName);
	inlineEditRowContents[rowName] = {};
	for (var i=0; i<tableRow.childElementCount; i++) {
		var cell = tableRow.children[i];
		inlineEditRowContents[rowName][i] = cell.innerHTML;
		if (options.hasOwnProperty("updateCell"))
			options.updateCell(cell, i, rowName, options);
		else
			inlineDefaultUpdateCell(cell, i, rowName, options);
	}
}

function inlineDefaultUpdateCell(cell, i, rowName, options) {
	var attributesFilter = ["inlineoptionsvalue", "inlineoptionstitle"];
	var cellContent = "";
	var key;
	if (i === 0) {
		cellContent += `<form id='${rowName}Form'></form>`;
	}
	switch (cell.dataset.inlinetype) {
		case "doneButton":
			cellContent += `<input type='submit' value='Finish' form='${rowName}Form'/>`;
			break;
		case "button":
			cellContent += inlineEditRowContents[rowName][i];
			break;
		case "text":
			cellContent += `<input type='text' value='${inlineEditRowContents[rowName][i]}' form='${rowName}Form'`;
			for (key in cell.dataset) {
				if (cell.dataset.hasOwnProperty(key) && key.substr(0, 6) == "inline" && attributesFilter.indexOf(key) == -1) {
					cellContent += ` ${key.substr(6)}='${cell.dataset[key]}'`;
				}
			}
			cellContent += "/>";
			break;
		case "select":
			cellContent += "<select";
			for (key in cell.dataset) {
				if (cell.dataset.hasOwnProperty(key) && key.substr(0, 6) == "inline" && attributesFilter.indexOf(key) == -1) {
					cellContent += ` ${key.substr(6)}='${cell.dataset[key]}'`;
				}
			}
			cellContent += ">";
			var optionsTitle = JSON.parse(cell.dataset.inlineoptionstitle);
			var optionsValue = cell.dataset.hasOwnProperty("inlineoptionsvalue") ? JSON.parse(cell.dataset.inlineoptionsvalue) : [];
			for (var j=0; j<optionsTitle.length; j++) {
				cellContent += "<option ";
				cellContent += ((optionsValue.length<=j) ? "" : `value='${optionsValue[j]}'`);
				cellContent += ((inlineEditRowContents[rowName][i] == optionsTitle[j]) ? " selected='selected'" : "");
				cellContent += ">";
				cellContent += optionsTitle[j];
				cellContent += "</option>";
			}
			cellContent += "</select>";
			break;
		case "textarea":
			cellContent += `<textarea form='${rowName}Form'`;
			for (key in cell.dataset) {
				if (cell.dataset.hasOwnProperty(key) && key.substr(0, 6) == "inline" && attributesFilter.indexOf(key) == -1) {
					cellContent += ` ${key.substr(6)}='${cell.dataset[key]}'`;
				}
			}
			cellContent += ">";
			cellContent += inlineEditRowContents[rowName][i];
			cellContent += "</textarea>";
			break;
	}
	cell.innerHTML = cellContent;
	if (i === 0) {
		// set the onsubmit function of the form of this row
		document.getElementById(rowName+"Form").onsubmit = function () {
			event.preventDefault();
			if (this.reportValidity()) {
				if (options.hasOwnProperty("finish"))
					options.finish(rowName, options);
				else
					inlineDefaultFinish(rowName, options);
			}
		};
	}
}

function inlineDefaultFinish(rowName, options) {
	var tableRow = document.getElementById(rowName);
	var rowData = {};
	for (var i=0; i<tableRow.childElementCount; i++) {
		var cell = tableRow.children[i];
		var getFromChildren = (i === 0) ? 1 : 0;
		switch (cell.dataset.inlinetype) {
			case "doneButton":
				break;
			case "button":
				break;
			case "text":
				rowData[cell.dataset.inlinename] = cell.children[getFromChildren].value;
				inlineEditRowContents[rowName][i] = cell.children[getFromChildren].value;
				break;
			case "select":
				rowData[cell.dataset.inlinename] = cell.children[getFromChildren].selectedIndex;
				inlineEditRowContents[rowName][i] = JSON.parse(cell.dataset.inlineoptionstitle)[cell.children[getFromChildren].selectedIndex];
				break;
			case "textarea":
				// TODO textarea value is \n not

				rowData[cell.dataset.inlinename] = cell.children[getFromChildren].value;
				inlineEditRowContents[rowName][i] = cell.children[getFromChildren].value;
				break;
		}
	}

	// do whatever ajax magic
	if (options.hasOwnProperty("finishCallback"))
		options.finishCallback(rowData, rowName);

	for (i=0; i<tableRow.childElementCount; i++) {
		var cell = tableRow.children[i];
		if (options.hasOwnProperty("finishCell")) {
			// return true invokes the default finishCell function
			if (options.finishCell(cell, i, rowName, inlineEditRowContents[rowName][i]) === true) {
				inlineDefaultFinishCell(cell, i, rowName);
			}
		} else
			inlineDefaultFinishCell(cell, i, rowName);
	}
}

function inlineDefaultFinishCell(cell, i, rowName) {
	var cellContent = "";
	cellContent += inlineEditRowContents[rowName][i];
	cell.innerHTML = cellContent;
}
<commit_msg>Support for cells that aren't editable<commit_after>var inlineEditRowContents = {};

function inlineEdit(rowName, options) {
	var tableRow = document.getElementById(rowName);
	inlineEditRowContents[rowName] = {};
	for (var i=0; i<tableRow.childElementCount; i++) {
		var cell = tableRow.children[i];
		inlineEditRowContents[rowName][i] = cell.innerHTML;
		if (options.hasOwnProperty("updateCell"))
			options.updateCell(cell, i, rowName, options);
		else
			inlineDefaultUpdateCell(cell, i, rowName, options);
	}
}

function inlineDefaultUpdateCell(cell, i, rowName, options) {
	var attributesFilter = ["inlineoptionsvalue", "inlineoptionstitle"];
	var cellContent = "";
	var key;
	if (i === 0) {
		cellContent += `<form id='${rowName}Form'></form>`;
	}
	switch (cell.dataset.inlinetype) {
		case "plain":
			cellContent = inlineEditRowContents[rowName][i];
			break;
		case "doneButton":
			cellContent += `<input type='submit' value='Finish' form='${rowName}Form'/>`;
			break;
		case "button":
			cellContent += inlineEditRowContents[rowName][i];
			break;
		case "text":
			cellContent += `<input type='text' value='${inlineEditRowContents[rowName][i]}' form='${rowName}Form'`;
			for (key in cell.dataset) {
				if (cell.dataset.hasOwnProperty(key) && key.substr(0, 6) == "inline" && attributesFilter.indexOf(key) == -1) {
					cellContent += ` ${key.substr(6)}='${cell.dataset[key]}'`;
				}
			}
			cellContent += "/>";
			break;
		case "select":
			cellContent += "<select";
			for (key in cell.dataset) {
				if (cell.dataset.hasOwnProperty(key) && key.substr(0, 6) == "inline" && attributesFilter.indexOf(key) == -1) {
					cellContent += ` ${key.substr(6)}='${cell.dataset[key]}'`;
				}
			}
			cellContent += ">";
			var optionsTitle = JSON.parse(cell.dataset.inlineoptionstitle);
			var optionsValue = cell.dataset.hasOwnProperty("inlineoptionsvalue") ? JSON.parse(cell.dataset.inlineoptionsvalue) : [];
			for (var j=0; j<optionsTitle.length; j++) {
				cellContent += "<option ";
				cellContent += ((optionsValue.length<=j) ? "" : `value='${optionsValue[j]}'`);
				cellContent += ((inlineEditRowContents[rowName][i] == optionsTitle[j]) ? " selected='selected'" : "");
				cellContent += ">";
				cellContent += optionsTitle[j];
				cellContent += "</option>";
			}
			cellContent += "</select>";
			break;
		case "textarea":
			cellContent += `<textarea form='${rowName}Form'`;
			for (key in cell.dataset) {
				if (cell.dataset.hasOwnProperty(key) && key.substr(0, 6) == "inline" && attributesFilter.indexOf(key) == -1) {
					cellContent += ` ${key.substr(6)}='${cell.dataset[key]}'`;
				}
			}
			cellContent += ">";
			cellContent += inlineEditRowContents[rowName][i];
			cellContent += "</textarea>";
			break;
		default:
			cellContent = inlineEditRowContents[rowName][i];
			break;
	}
	cell.innerHTML = cellContent;
	if (i === 0) {
		// set the onsubmit function of the form of this row
		document.getElementById(rowName+"Form").onsubmit = function () {
			event.preventDefault();
			if (this.reportValidity()) {
				if (options.hasOwnProperty("finish"))
					options.finish(rowName, options);
				else
					inlineDefaultFinish(rowName, options);
			}
		};
	}
}

function inlineDefaultFinish(rowName, options) {
	var tableRow = document.getElementById(rowName);
	var rowData = {};
	for (var i=0; i<tableRow.childElementCount; i++) {
		var cell = tableRow.children[i];
		var getFromChildren = (i === 0) ? 1 : 0;
		switch (cell.dataset.inlinetype) {
			case "plain":
				break;
			case "doneButton":
				break;
			case "button":
				break;
			case "text":
				rowData[cell.dataset.inlinename] = cell.children[getFromChildren].value;
				inlineEditRowContents[rowName][i] = cell.children[getFromChildren].value;
				break;
			case "select":
				rowData[cell.dataset.inlinename] = cell.children[getFromChildren].selectedIndex;
				inlineEditRowContents[rowName][i] = JSON.parse(cell.dataset.inlineoptionstitle)[cell.children[getFromChildren].selectedIndex];
				break;
			case "textarea":
				// TODO textarea value is \n not

				rowData[cell.dataset.inlinename] = cell.children[getFromChildren].value;
				inlineEditRowContents[rowName][i] = cell.children[getFromChildren].value;
				break;
			default:
				break;
		}
	}

	// do whatever ajax magic
	if (options.hasOwnProperty("finishCallback"))
		options.finishCallback(rowData, rowName);

	for (i=0; i<tableRow.childElementCount; i++) {
		var cell = tableRow.children[i];
		if (options.hasOwnProperty("finishCell")) {
			// return true invokes the default finishCell function
			if (options.finishCell(cell, i, rowName, inlineEditRowContents[rowName][i]) === true) {
				inlineDefaultFinishCell(cell, i, rowName);
			}
		} else
			inlineDefaultFinishCell(cell, i, rowName);
	}
}

function inlineDefaultFinishCell(cell, i, rowName) {
	var cellContent = "";
	cellContent += inlineEditRowContents[rowName][i];
	cell.innerHTML = cellContent;
}
<|endoftext|>


<commit_before>var Snackbar = require('snackbarlightjs');

var Templates = require('../templates');

var JSONToHTML = require('../../lib/transforms/json-to-html-data');

var getData = require('../lib/get-data.js');
var hideKeyboard = require('../lib/hide-keyboard');

var getArticles = require('../lib/listeners/get-articles');
var getWikiData = require('../lib/listeners/get-wiki-data');
var searchListener = require('../lib/listeners/search-listener');
var downloadImageListener = require('../lib/listeners/download-image');
var archiveListeners = require('../lib/listeners/archive-listeners');
var initComp = require('../lib/listeners/init-components.js');
// var internalHeader = require('../lib/listeners/internal-header');

module.exports = function (type) {
  return {
    load: function (ctx, next) {
      load(ctx, next, type);
    },
    render: function (ctx, next) {
      render(ctx, next, type);
    },
    listeners: function (ctx, next) {
      listeners(ctx, next, type);
    }
  };
};

function load (ctx, next, type) {
  var pageType = type === 'people' ? type : type + 's';
  var page = type === 'people' ? 'person' : type;

  if (!ctx.isInitialRender) {
    var opts = {
      headers: { Accept: 'application/vnd.api+json' }
    };
    var id = ctx.params.id;
    var url = '/' + pageType + '/' + id + '?ajax=true';

    getData(url, opts, function (err, json) {
      if (err) {
        console.error(err);
        Snackbar.create('Error getting data from the server.\n
Please check your internet connection or try again shortly');
        return;
      }
      var data = JSONToHTML(json);

      ctx.state.data = data;
      ctx.state.data.back = sessionStorage.getItem('backPath');
      // analytics
      window.dataLayer.push(JSON.parse(data.layer));
      window.dataLayer.push({ 'recordTitle': data.title });
      window.dataLayer.push({ 'event': 'recordEvent' });

      ctx.state.data.page = page;
      next();
    });
  } else {
    ctx.state.data = {};
    ctx.state.data.page = page;
    listeners(ctx, next, type);
  }
}

function render (ctx, next, type) {
  var pageType = type === 'people' ? type : type + 's';
  var pageEl = document.getElementById('main-page');

  hideKeyboard();
  document.getElementsByTagName('title')[0].textContent = ctx.state.data.titlePage;
  document.body.className = ctx.state.data.type;
  pageEl.innerHTML = Templates[pageType](ctx.state.data);

  if (window.location.href.indexOf('#') === -1) {
    window.scrollTo(0, 0);
  }

  next();
}

function listeners (ctx, next, type) {
  var funcs = [initComp, searchListener];

  if (type === 'object' || type === 'document') {
    funcs.push(downloadImageListener);
  }

  if (type === 'object') {
    // funcs.push(getArticles); # disabled while caching investigated
  } else if (type === 'document') {
    funcs.push(archiveListeners);
  } else if (type === 'people') {
    // funcs.push(getWikiData, internalHeader);
    funcs.push(getWikiData);
  }

  funcs.forEach(function (el) {
    el(ctx);
  });
}
<commit_msg>disable related articles while caching investigated<commit_after>var Snackbar = require('snackbarlightjs');

var Templates = require('../templates');

var JSONToHTML = require('../../lib/transforms/json-to-html-data');

var getData = require('../lib/get-data.js');
var hideKeyboard = require('../lib/hide-keyboard');

// var getArticles = require('../lib/listeners/get-articles');
var getWikiData = require('../lib/listeners/get-wiki-data');
var searchListener = require('../lib/listeners/search-listener');
var downloadImageListener = require('../lib/listeners/download-image');
var archiveListeners = require('../lib/listeners/archive-listeners');
var initComp = require('../lib/listeners/init-components.js');
// var internalHeader = require('../lib/listeners/internal-header');

module.exports = function (type) {
  return {
    load: function (ctx, next) {
      load(ctx, next, type);
    },
    render: function (ctx, next) {
      render(ctx, next, type);
    },
    listeners: function (ctx, next) {
      listeners(ctx, next, type);
    }
  };
};

function load (ctx, next, type) {
  var pageType = type === 'people' ? type : type + 's';
  var page = type === 'people' ? 'person' : type;

  if (!ctx.isInitialRender) {
    var opts = {
      headers: { Accept: 'application/vnd.api+json' }
    };
    var id = ctx.params.id;
    var url = '/' + pageType + '/' + id + '?ajax=true';

    getData(url, opts, function (err, json) {
      if (err) {
        console.error(err);
        Snackbar.create('Error getting data from the server.\n
Please check your internet connection or try again shortly');
        return;
      }
      var data = JSONToHTML(json);

      ctx.state.data = data;
      ctx.state.data.back = sessionStorage.getItem('backPath');
      // analytics
      window.dataLayer.push(JSON.parse(data.layer));
      window.dataLayer.push({ 'recordTitle': data.title });
      window.dataLayer.push({ 'event': 'recordEvent' });

      ctx.state.data.page = page;
      next();
    });
  } else {
    ctx.state.data = {};
    ctx.state.data.page = page;
    listeners(ctx, next, type);
  }
}

function render (ctx, next, type) {
  var pageType = type === 'people' ? type : type + 's';
  var pageEl = document.getElementById('main-page');

  hideKeyboard();
  document.getElementsByTagName('title')[0].textContent = ctx.state.data.titlePage;
  document.body.className = ctx.state.data.type;
  pageEl.innerHTML = Templates[pageType](ctx.state.data);

  if (window.location.href.indexOf('#') === -1) {
    window.scrollTo(0, 0);
  }

  next();
}

function listeners (ctx, next, type) {
  var funcs = [initComp, searchListener];

  if (type === 'object' || type === 'document') {
    funcs.push(downloadImageListener);
  }

  if (type === 'object') {
    // funcs.push(getArticles); # disabled while caching investigated
  } else if (type === 'document') {
    funcs.push(archiveListeners);
  } else if (type === 'people') {
    // funcs.push(getWikiData, internalHeader);
    funcs.push(getWikiData);
  }

  funcs.forEach(function (el) {
    el(ctx);
  });
}
<|endoftext|>


<commit_before>// This file is part of the Shapy project.
// Licensing information can be found in the LICENSE file.
// (C) 2015 The Shapy Team. All rights reserved.
goog.provide('shapy.editor.Layout');
goog.provide('shapy.editor.Layout.Single');
goog.provide('shapy.editor.Layout.Double');
goog.provide('shapy.editor.Layout.Quad');
goog.provide('shapy.editor.Viewport');

goog.require('goog.math.Rect');
goog.require('goog.math.Size');

/**
 * Class that manages the layout of multiple viewports on the screen.
 *
 * @constructor
 *
 * @param {!Object<string, shapy.editor.Viewport>} viewports Map of viewports.
 */
shapy.editor.Layout = function(viewports) {
  /**
   * Map of all viewports on the screen.
   * @public {!Object<string, shapy.editor.Viewport>}
   * @const
   */
  this.viewports = viewports;

  /**
   * The size of the entire screen.
   * @protected {!goog.math.Size}
   * @const
   */
  this.size = new goog.math.Size(0, 0);

  /**
   * Pointer to the active viewport.
   * @public {!shapy.editor.Viewport}
   */
  this.active = viewports[0];
};

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the screen.
 * @param {number} h Height of the screen.
 */
shapy.editor.Layout.prototype.resize = function(w, h) {
  this.size.width = w;
  this.size.height = h;
};

/**
 * Finds out which viewport is touched by the mouse.
 *
 * @private
 *
 * @param {number} x Mouse X position.
 * @param {number} y Mouse Y position.
 *
 * @return {?{ vp: shapy.editor.Viewport, x: number, y: number }}
 */
shapy.editor.Layout.prototype.getViewport_ = function(x, y) {
  y = this.size.height - y;
  for (var name in this.viewports) {
    if (!this.viewports.hasOwnProperty(name)) {
      continue;
    }
    var vp = this.viewports[name];
    if (x < vp.rect.x || vp.rect.x + vp.rect.w < x) {
      continue;
    }
    if (y < vp.rect.y || vp.rect.y + vp.rect.h < y) {
      continue;
    }
    return {
      vp: vp,
      x: x - vp.rect.x,
      y: y - vp.rect.y
    };
  }
  return null;
};

/**
 * Handles a mouse motion event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseMove = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || !result.vp) {
    return;
  }

  if (this.active && result.vp == this.active) {
    this.active.mouseMove(result.x, result.y);
  } else {
    if (this.active) {
      this.active.mouseLeave();
    }

    this.active = result.vp;
    this.active.mouseEnter(result.x, result.y);
  }
};

/**
 * Handles a mouse press event.
 *
 * @param {MouseEvent} e Original event.
 */
shapy.editor.Layout.prototype.mouseDown = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || !result.vp) {
    return;
  }

  result.vp.mouseDown(result.x, result.y);
};

/**
 * Handles a mouse release event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseUp = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || !result.vp) {
    return;
  }

  result.vp.mouseUp(result.x, result.y);
};

/**
 * Handles a mouse enter event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseEnter = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || result.vp) {
    return;
  }

  this.active = result.vp;
  this.active.mouseEnter(result.x, result.y);
};

/**
 * Handles a mouse leave event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseLeave = function(e) {
  if (!this.active) {
    return;
  }

  this.active.mouseLeave();
  this.active = null;
};

/**
 * Handles a mouse wheel event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseWheel = function(e) {
  if (!this.active) {
    return;
  }

  this.active.mouseWheel(e.wheelDelta);
};

/**
 * Creates a layout with a single viewport.
 *
 * @constructor
 * @extends {shapy.editor.Layout}
 */
shapy.editor.Layout.Single = function() {
  /** @public {!shapy.editor.Viewport} @const */
  this.viewport = new shapy.editor.Viewport('viewport');

  shapy.editor.Layout.call(this, {
      'viewport': this.viewport
  });
};
goog.inherits(shapy.editor.Layout.Single, shapy.editor.Layout);

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the window.
 * @param {number} h Height of the window.
 */
shapy.editor.Layout.Single.prototype.resize = function(w, h) {
  goog.base(this, 'resize', w, h);

  this.viewport.resize(0, 0, w, h);
};

/**
 * Creates a layout by splitting the screen into 2 vertically.
 *
 * @constructor
 * @extends {shapy.editor.Layout}
 */
shapy.editor.Layout.Double = function() {
  /** @public {!shapy.editor.Viewport} @const */
  this.left = new shapy.editor.Viewport('left');
  /** @public {!shapy.editor.Viewport} @const */
  this.right = new shapy.editor.Viewport('right');

  shapy.editor.Layout.call(this, {
      'left': this.left,
      'right': this.right,
  });
};
goog.inherits(shapy.editor.Layout.Double, shapy.editor.Layout);

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the window.
 * @param {number} h Height of the window.
 */
shapy.editor.Layout.Double.prototype.resize = function(w, h) {
  goog.base(this, 'resize', w, h);

  this.left.resize(0, 0, w / 2, h);
  this.right.resize(w / 2, 0, w / 2, h);
};

/**
 * Creates a layout by splitting the screen into 4 viewport.
 *
 * @constructor
 * @extends {shapy.editor.Layout}
 */
shapy.editor.Layout.Quad = function() {
  /** @public {!shapy.editor.Viewport} @const */
  this.topLeft = new shapy.editor.Viewport('top-left');
  /** @public {!shapy.editor.Viewport} @const */
  this.topRight = new shapy.editor.Viewport('top-right');
  /** @public {!shapy.editor.Viewport} @const */
  this.bottomLeft = new shapy.editor.Viewport('bottom-left');
  /** @public {!shapy.editor.Viewport} @const */
  this.bottomRight = new shapy.editor.Viewport('bottom-right');

  shapy.editor.Layout.call(this, {
      'top-left': this.topLeft,
      'top-right': this.topRight,
      'bottom-left': this.bottomLeft,
      'bottom-right': this.bottomRight
  });
};
goog.inherits(shapy.editor.Layout.Quad, shapy.editor.Layout);

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the window.
 * @param {number} h Height of the window.
 */
shapy.editor.Layout.Quad.prototype.resize = function(w, h) {
  goog.base(this, 'resize', w, h);

  this.topLeft.resize(0, 0, w / 2, h / 2);
  this.topRight.resize(w / 2, 0, w / 2, h / 2);
  this.bottomLeft.resize(0, h / 2, w / 2, h / 2);
  this.bottomRight.resize(w / 2, h / 2, w / 2, h / 2);
};

/**
 * A single viewport, our eye into the world.
 *
 * @constructor
 *
 * @param {string} name Name of the viewport.
 */
shapy.editor.Viewport = function(name) {
  /**
   * The name of the viewport.
   * @public {string}
   * @const
   */
  this.name = name;

  /**
   * The camera attached to the viewport.
   * @public {!shapy.editor.Camera}
   * @const
   */
  this.camera = new shapy.editor.Camera.Persp();

  /**
   * The size and position of the viewport.
   * @public {!goog.math.Size}
   * @const
   */
  this.rect = new goog.math.Rect(0, 0, 0, 0);

  /**
   * Type of the viewport.
   * @public {!shapy.editor.Viewport.Type}
   */
  this.type = shapy.editor.Viewport.Type.PERSPECTIVE;
};

/**
 * Resizes the viewport, specifying a new position and size.
 *
 * @param {number} x
 * @param {number} y
 * @param {number} w
 * @param {number} h
 */
shapy.editor.Viewport.prototype.resize = function(x, y, w, h) {
  this.rect.x = x;
  this.rect.y = y;
  this.rect.w = w;
  this.rect.h = h;
  this.camera.resize(w, h);
};

/**
 * Handles a mouse motion event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseMove = function(x, y) {
};

/**
 * Handles a mouse enter event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseEnter = function(x, y) {
  //console.log('enter', x, y);
};

/**
 * Handles a mouse leave event.
 */
shapy.editor.Viewport.prototype.mouseLeave = function() {
  //console.log('leave');
};

/**
 * Handles a mouse press event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseDown = function(x, y) {
  //console.log('down', x, y);
};

/**
 * Handles a mouse release event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseUp = function(x, y) {
  //console.log('up', x, y);
};

/**
 * Handles a mouse wheel event.
 *
 * @param {number} delta Mouse wheel delta value.
 */
shapy.editor.Viewport.prototype.mouseWheel = function(delta) {
  var diff = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, diff);
  var zoomLevel = goog.vec.Vec3.magnitude(diff);

  // Determine if zooming in or out and update accordingly.
  if (delta < 0) {
    zoomLevel /= shapy.editor.Camera.ZOOM_SPEED;
  } else {
    zoomLevel *= shapy.editor.Camera.ZOOM_SPEED;
  }

  // Clip to MIN_ZOOM.
  if (zoomLevel < shapy.editor.Camera.MIN_ZOOM) {
    zoomLevel = shapy.editor.Camera.MIN_ZOOM;
  }

  // Clip to MAX_ZOOM.
  if (zoomLevel > shapy.editor.Camera.MAX_ZOOM) {
    zoomLevel = shapy.editor.Camera.MAX_ZOOM;
  }

  // Update the position of the camera.
  goog.vec.Vec3.normalize(diff, diff);
  goog.vec.Vec3.scale(diff, zoomLevel, diff);
  goog.vec.Vec3.add(this.camera.center, diff, this.camera.eye);
};

/**
 * List of viewport types.
 * @enum {number}
 */
shapy.editor.Viewport.Type = {
  LEFT: 'left',
  RIGHT: 'right',
  TOP: 'top',
  BOTTOM: 'bottom',
  FRONT: 'front',
  BACK: 'back',
  ORTHO: 'ortho',
  PERSPECTIVE: 'perp'
};
<commit_msg>Camera rotation to be finished.<commit_after>// This file is part of the Shapy project.
// Licensing information can be found in the LICENSE file.
// (C) 2015 The Shapy Team. All rights reserved.
goog.provide('shapy.editor.Layout');
goog.provide('shapy.editor.Layout.Single');
goog.provide('shapy.editor.Layout.Double');
goog.provide('shapy.editor.Layout.Quad');
goog.provide('shapy.editor.Viewport');

goog.require('goog.math.Rect');
goog.require('goog.math.Size');
goog.require('goog.vec.Vec2');

/**
 * Class that manages the layout of multiple viewports on the screen.
 *
 * @constructor
 *
 * @param {!Object<string, shapy.editor.Viewport>} viewports Map of viewports.
 */
shapy.editor.Layout = function(viewports) {
  /**
   * Map of all viewports on the screen.
   * @public {!Object<string, shapy.editor.Viewport>}
   * @const
   */
  this.viewports = viewports;

  /**
   * The size of the entire screen.
   * @protected {!goog.math.Size}
   * @const
   */
  this.size = new goog.math.Size(0, 0);

  /**
   * Pointer to the active viewport.
   * @public {!shapy.editor.Viewport}
   */
  this.active = viewports[0];
};

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the screen.
 * @param {number} h Height of the screen.
 */
shapy.editor.Layout.prototype.resize = function(w, h) {
  this.size.width = w;
  this.size.height = h;
};

/**
 * Finds out which viewport is touched by the mouse.
 *
 * @private
 *
 * @param {number} x Mouse X position.
 * @param {number} y Mouse Y position.
 *
 * @return {?{ vp: shapy.editor.Viewport, x: number, y: number }}
 */
shapy.editor.Layout.prototype.getViewport_ = function(x, y) {
  y = this.size.height - y;
  for (var name in this.viewports) {
    if (!this.viewports.hasOwnProperty(name)) {
      continue;
    }
    var vp = this.viewports[name];
    if (x < vp.rect.x || vp.rect.x + vp.rect.w < x) {
      continue;
    }
    if (y < vp.rect.y || vp.rect.y + vp.rect.h < y) {
      continue;
    }
    return {
      vp: vp,
      x: x - vp.rect.x,
      y: y - vp.rect.y
    };
  }
  return null;
};

/**
 * Handles a mouse motion event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseMove = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || !result.vp) {
    return;
  }

  if (this.active && result.vp == this.active) {
    this.active.mouseMove(result.x, result.y);
  } else {
    if (this.active) {
      this.active.mouseLeave();
    }

    this.active = result.vp;
    this.active.mouseEnter(result.x, result.y);
  }
};

/**
 * Handles a mouse press event.
 *
 * @param {MouseEvent} e Original event.
 */
shapy.editor.Layout.prototype.mouseDown = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || !result.vp) {
    return;
  }

  result.vp.mouseDown(result.x, result.y);
};

/**
 * Handles a mouse release event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseUp = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || !result.vp) {
    return;
  }

  result.vp.mouseUp(result.x, result.y);
};

/**
 * Handles a mouse enter event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseEnter = function(e) {
  var result = this.getViewport_(e.offsetX, e.offsetY);
  if (!result || result.vp) {
    return;
  }

  this.active = result.vp;
  this.active.mouseEnter(result.x, result.y);
};

/**
 * Handles a mouse leave event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseLeave = function(e) {
  if (!this.active) {
    return;
  }

  this.active.mouseLeave();
  this.active = null;
};

/**
 * Handles a mouse wheel event.
 *
 * @param {MouseEvent} e
 */
shapy.editor.Layout.prototype.mouseWheel = function(e) {
  if (!this.active) {
    return;
  }

  this.active.mouseWheel(e.wheelDelta);
};

/**
 * Creates a layout with a single viewport.
 *
 * @constructor
 * @extends {shapy.editor.Layout}
 */
shapy.editor.Layout.Single = function() {
  /** @public {!shapy.editor.Viewport} @const */
  this.viewport = new shapy.editor.Viewport('viewport');

  shapy.editor.Layout.call(this, {
      'viewport': this.viewport
  });
};
goog.inherits(shapy.editor.Layout.Single, shapy.editor.Layout);

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the window.
 * @param {number} h Height of the window.
 */
shapy.editor.Layout.Single.prototype.resize = function(w, h) {
  goog.base(this, 'resize', w, h);

  this.viewport.resize(0, 0, w, h);
};

/**
 * Creates a layout by splitting the screen into 2 vertically.
 *
 * @constructor
 * @extends {shapy.editor.Layout}
 */
shapy.editor.Layout.Double = function() {
  /** @public {!shapy.editor.Viewport} @const */
  this.left = new shapy.editor.Viewport('left');
  /** @public {!shapy.editor.Viewport} @const */
  this.right = new shapy.editor.Viewport('right');

  shapy.editor.Layout.call(this, {
      'left': this.left,
      'right': this.right,
  });
};
goog.inherits(shapy.editor.Layout.Double, shapy.editor.Layout);

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the window.
 * @param {number} h Height of the window.
 */
shapy.editor.Layout.Double.prototype.resize = function(w, h) {
  goog.base(this, 'resize', w, h);

  this.left.resize(0, 0, w / 2, h);
  this.right.resize(w / 2, 0, w / 2, h);
};

/**
 * Creates a layout by splitting the screen into 4 viewport.
 *
 * @constructor
 * @extends {shapy.editor.Layout}
 */
shapy.editor.Layout.Quad = function() {
  /** @public {!shapy.editor.Viewport} @const */
  this.topLeft = new shapy.editor.Viewport('top-left');
  /** @public {!shapy.editor.Viewport} @const */
  this.topRight = new shapy.editor.Viewport('top-right');
  /** @public {!shapy.editor.Viewport} @const */
  this.bottomLeft = new shapy.editor.Viewport('bottom-left');
  /** @public {!shapy.editor.Viewport} @const */
  this.bottomRight = new shapy.editor.Viewport('bottom-right');

  shapy.editor.Layout.call(this, {
      'top-left': this.topLeft,
      'top-right': this.topRight,
      'bottom-left': this.bottomLeft,
      'bottom-right': this.bottomRight
  });
};
goog.inherits(shapy.editor.Layout.Quad, shapy.editor.Layout);

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} w Width of the window.
 * @param {number} h Height of the window.
 */
shapy.editor.Layout.Quad.prototype.resize = function(w, h) {
  goog.base(this, 'resize', w, h);

  this.topLeft.resize(0, 0, w / 2, h / 2);
  this.topRight.resize(w / 2, 0, w / 2, h / 2);
  this.bottomLeft.resize(0, h / 2, w / 2, h / 2);
  this.bottomRight.resize(w / 2, h / 2, w / 2, h / 2);
};

/**
 * A single viewport, our eye into the world.
 *
 * @constructor
 *
 * @param {string} name Name of the viewport.
 */
shapy.editor.Viewport = function(name) {
  /**
   * The name of the viewport.
   * @public {string}
   * @const
   */
  this.name = name;

  /**
   * The camera attached to the viewport.
   * @public {!shapy.editor.Camera}
   * @const
   */
  this.camera = new shapy.editor.Camera.Persp();

  /**
   * The size and position of the viewport.
   * @public {!goog.math.Size}
   * @const
   */
  this.rect = new goog.math.Rect(0, 0, 0, 0);

  /**
   * Type of the viewport.
   * @public {!shapy.editor.Viewport.Type}
   */
  this.type = shapy.editor.Viewport.Type.PERSPECTIVE;

  /**
   * Current position of the mouse.
   * @private {!goog.vec.Vec2.Float32}
   */
  this.currMousePos = goog.vec.Vec2.createFloat32();

  /**
   * Last position of the mouse.
   * @private {!goog.vec.Vec2.Float32}
   */
  this.lastMousePos = goog.vec.Vec2.createFloat32();

  /**
   * Flag indicating if the mouse is down.
   * @privaet {boolean}
   */
   this.isDown = false;
};

/**
 * Resizes the viewport, specifying a new position and size.
 *
 * @param {number} x
 * @param {number} y
 * @param {number} w
 * @param {number} h
 */
shapy.editor.Viewport.prototype.resize = function(x, y, w, h) {
  this.rect.x = x;
  this.rect.y = y;
  this.rect.w = w;
  this.rect.h = h;
  this.camera.resize(w, h);
};

/**
 * Handles a mouse motion event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseMove = function(x, y) {
  if (this.isDown) {
    goog.vec.Vec2.setFromValues(this.currMousePos, x, y);
    // TODO: call rotate
  }
};

/**
 * Handles a mouse enter event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseEnter = function(x, y) {
  //console.log('enter', x, y);
};

/**
 * Handles a mouse leave event.
 */
shapy.editor.Viewport.prototype.mouseLeave = function() {
  if (this.isDown) {
    this.isDown = false;
  }
};

/**
 * Handles a mouse press event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseDown = function(x, y) {
  goog.vec.Vec2.setFromValues(this.currMousePos, x, y);
  goog.vec.Vec2.setFromValues(this.lastMousePos, x, y);
  this.isDown = true;
};

/**
 * Handles a mouse release event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseUp = function(x, y) {
  this.isDown = false;
};

/**
 * Handles a mouse wheel event.
 *
 * @param {number} delta Mouse wheel delta value.
 */
shapy.editor.Viewport.prototype.mouseWheel = function(delta) {
  var diff = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, diff);
  var zoomLevel = goog.vec.Vec3.magnitude(diff);

  // Determine if zooming in or out and update accordingly.
  if (delta < 0) {
    zoomLevel /= shapy.editor.Camera.ZOOM_SPEED;
  } else {
    zoomLevel *= shapy.editor.Camera.ZOOM_SPEED;
  }

  // Clip to MIN_ZOOM.
  if (zoomLevel < shapy.editor.Camera.MIN_ZOOM) {
    zoomLevel = shapy.editor.Camera.MIN_ZOOM;
  }

  // Clip to MAX_ZOOM.
  if (zoomLevel > shapy.editor.Camera.MAX_ZOOM) {
    zoomLevel = shapy.editor.Camera.MAX_ZOOM;
  }

  // Update the position of the camera.
  goog.vec.Vec3.normalize(diff, diff);
  goog.vec.Vec3.scale(diff, zoomLevel, diff);
  goog.vec.Vec3.add(this.camera.center, diff, this.camera.eye);
};

/**
 * Computes a normalised vector from the center of the virtual ball to
 * the point on the virtual ball surface that is aligned with the point (x, y)
 * on the screen.
 *
 * @param {!goog.vec.Vec2.Float32} pos Position of the mouse.
 */
shapy.editor.Viewport.prototype.getArcballVector = function(pos) {
  // Convert pos to camera coordinates [-1, 1].
  var p = goog.vec.Vec3.createFloat32FromValues(pos[0] / this.rect.w * 2 - 1.0,
                                                pos[1] / this.rect.h * 2 - 1.0,
                                                0);
  // Compute the square of the l2 norm of p.
  var l2Squared = p[0] * p[0] + p[1] * p[1];

  // Compute the z coordinate.
  if (l2Squared < 1.0) {
    // Use Pythagoras to get z.
    p[2] = Math.sqrt(1 - l2Squared);
  } else {
    // Get the nearest point on the surface of the ball.
    goog.vec.Vec3.normalize(p, p);
  }

  return p;
};

/**
 * Computes the angle and axis of the camera rotation.
 */
shapy.editor.Viewport.prototype.rotate = function() {
  // Compute the points at the ball surface that match the click.
  var va = this.getArcballVector(this.lastMousePos);
  var vb = this.getArcballVector(this.currMousePos);

  // Compute the angle.
  var angle = Math.acos(Math.min(1.0, goog.vec.Vec3.dot(va, vb)));

  // Compute the axis.
  // TODO: finish
};

/**
 * List of viewport types.
 * @enum {number}
 */
shapy.editor.Viewport.Type = {
  LEFT: 'left',
  RIGHT: 'right',
  TOP: 'top',
  BOTTOM: 'bottom',
  FRONT: 'front',
  BACK: 'back',
  ORTHO: 'ortho',
  PERSPECTIVE: 'perp'
};
<|endoftext|>


<commit_before>//------------------------------------------------------------------------------
// Copyright Jonathan Kaufman 2015
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//------------------------------------------------------------------------------

import d3 from 'd3';

/**
 * Properties defined during construction:
 *   svg
 *   html
 *   legend
 *   bubble
 *   diameter
 *   colorRange
 *   colorLegend
 *   selectedColor
 *   legendSpacing
 *   smallDiameter
 *   textColorRange
 *   mediumDiameter
 *   configureLegend
 *   selectedTextColor
 */
export default class ReactBubbleChartD3 {

  constructor(el, props) {
    props = props || {};
    this.legendSpacing = props.legendSpacing || 3;
    this.selectedColor = props.selectedColor;
    this.selectedTextColor = props.selectedTextColor;
    this.smallDiameter = props.smallDiameter || 40;
    this.mediumDiameter = props.mediumDiameter || 115;
    this.createLegend = props.legend;

    // create an <svg> and <html> element - store a reference to it for later
    this.svg = d3.select(el).append('svg');
    this.html = d3.select(el).append('div');
    if (this.createLegend) {
      this.legend = d3.select(el).append('svg')
    }

    // create legend and update
    this.adjustSize(el);
    this.update(el, props);
  }

  adjustSize(el) {
    // helper values for positioning
    this.diameter = Math.min(el.offsetWidth, el.offsetHeight);
    var top  = Math.max((el.offsetHeight - this.diameter)/2, 0);

    // center some stuff vertically
    this.svg.attr('width', this.diameter)
      .attr('height', this.diameter)
      .style('position', 'relative')
      .style('top', top + 'px')   // center vertically
      .attr('class', 'bubble-chart-d3');
    this.html.style('width', this.diameter + 'px')
      .style('height', this.diameter + 'px')
      .style('position', 'absolute')
      .style('top', top + 'px')   // center vertically
      .style('left', 0)           // center horizontally
      .style('right', 0)
      .style('margin-left', 'auto')
      .style('margin-right', 'auto')
      .attr('class', 'bubble-chart-text');

    // create the bubble layout that we will use to position our bubbles\
    this.bubble = d3.layout.pack()
      .sort(null)
      .size([this.diameter, this.diameter])
      .padding(3);
  }

  /** create and configure the legend */
  configureLegend(el, props) {
    var legendRectSize = Math.min((el.offsetHeight - (this.colorLegend.length-1)*this.legendSpacing)/this.colorLegend.length, 18);
    var legendHeight = this.colorLegend.length * (legendRectSize + this.legendSpacing) - this.legendSpacing;
    this.legend.attr('class', 'bubble-legend')
      .style('position', 'absolute')
      .style('height', legendHeight + 'px')
      .style('width', '100px')
      .style('top', (el.offsetHeight - legendHeight)/2 + 'px')
      .style('left', 60 + 'px');

    // for each color in the legend, remove any existing, then
    // create a g and set its transform
    this.legend.selectAll('.legend-key').remove();
    var legendKeys = this.legend.selectAll('.legend-key')
      .data(this.colorLegend)
      .enter()
      .append('g')
      .attr('class', 'legend-key')
      .attr('transform', (d, i) => {
        var height = legendRectSize + this.legendSpacing;
        var vert = i * height;
        return 'translate(' + 0 + ',' + vert + ')';
      });

    // for each <g> create a rect and have its color... be the color
    legendKeys.append('rect')
      .attr('width', legendRectSize)
      .attr('height', legendRectSize)
      .style('fill', c => c.color)
      .style('stroke', c => c.color);

    // add necessary labels to the legend
    legendKeys.append('text')
      .attr('x', legendRectSize + this.legendSpacing)
      .attr('y', legendRectSize - this.legendSpacing)
      .text(c => c.text);
  }

  update(el, props) {
    this.adjustSize(el);
    // initialize color legend values and color range values
    // color range is just an array of the hex values
    // color legend is an array of the color/text objects
    var colorLegend = props.colorLegend || [];
    this.colorRange = colorLegend.map(c =>
      typeof c === 'string' ? c : c.color
    );
    this.colorLegend = colorLegend.slice(0).reverse().map(c =>
      typeof c === 'string' ? {color: c} : c
    );
    this.textColorRange = colorLegend.map(c =>
      typeof c === 'string' ? '#000000' : (c.textColor || '#000000')
    );
    if (this.createLegend) {
      this.configureLegend(el, props);
    }
    var duration = 500;
    var delay = 0;
    var data = props.data;
    if (!data) return;

    // define a color scale for our colorValues
    var color = d3.scale.quantize()
      .domain([
        props.fixedDomain ? props.fixedDomain.min : d3.min(data, d => d.colorValue),
        props.fixedDomain ? props.fixedDomain.max : d3.max(data, d => d.colorValue)
      ])
      .range(this.colorRange);

    // define a color scale for text town
    var textColor = d3.scale.quantize()
      .domain([
        props.fixedDomain ? props.fixedDomain.min : d3.min(data, d => d.colorValue),
        props.fixedDomain ? props.fixedDomain.max : d3.max(data, d => d.colorValue)
      ])
      .range(this.textColorRange)

    // generate data with calculated layout values
    var nodes = this.bubble.nodes({children: data})
      .filter((d) => !d.children); // filter out the outer bubble

    // assign new data to existing DOM for circles and labels
    var circles = this.svg.selectAll('circle')
      .data(nodes, (d) => 'g' + d._id);
    var labels = this.html.selectAll('.bubble-label')
      .data(nodes, (d) => 'g' + d._id);

    // update - this is created before enter.append. it only applies to updating nodes.
    // create the transition on the updating elements before the entering elements
    // because enter.append merges entering elements into the update selection
    // for circles we transition their transform, r, and fill
    circles.transition()
      .duration(duration)
      .delay((d, i) => {delay = i * 7; return delay;})
      .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')')
      .attr('r', (d) => d.r)
      .style('opacity', 1)
      .style('fill', d => d.selected ? this.selectedColor : color(d.colorValue));
    // for the labels we transition their height, width, left, top, and color
    labels.transition()
      .duration(duration)
      .delay((d, i) => {delay = i * 7; return delay;})
      .style('height', d => 2 * d.r + 'px')
      .style('width', d => 2 * d.r + 'px')
      .style('left', d =>  d.x - d.r + 'px')
      .style('top', d =>  d.y - d.r + 'px')
      .style('opacity', 1)
      .style('color', d => d.selected ? this.selectedTextColor : textColor(d.colorValue))
      .attr('class', d => {
        var size;
        if (2*d.r < this.smallDiameter) size = 'small';
        else if (2*d.r < this.mediumDiameter) size = 'medium';
        else size = 'large';
        return 'bubble-label ' + size
      });

    // enter - only applies to incoming elements (once emptying data)
    if (data.length) {
      // initialize new circles
      circles.enter().append('circle')
        .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')')
        .attr('r', (d) => 0)
        .attr('class', 'bubble')
        .style('fill', d => d.selected ? this.selectedColor : color(d.colorValue))
        .transition()
        .duration(duration * 1.2)
        .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')')
        .attr('r', (d) => d.r)
        .style('opacity', 1);
      // intialize new labels
      labels.enter().append('div')
        .attr('class', d => {
          var size;
          if (2*d.r < this.smallDiameter) size = 'small';
          else if (2*d.r < this.mediumDiameter) size = 'medium';
          else size = 'large';
          return 'bubble-label ' + size
        })
        .text(d => d.displayText || d._id)
        .on('click', (d,i) => {d3.event.stopPropagation(); props.onClick(d)})
        .style('position', 'absolute')
        .style('height', d => 2 * d.r + 'px')
        .style('width', d => 2 * d.r + 'px')
        .style('left', d =>  d.x - d.r + 'px')
        .style('top', d =>  d.y - d.r + 'px')
        .style('color', d => d.selected ? this.selectedTextColor : textColor(d.colorValue))
        .style('opacity', 0)
        .transition()
        .duration(duration * 1.2)
        .style('opacity', 1);
    }

    // exit - only applies to... exiting elements
    // for circles have them shrink to 0 as they're flying all over
    circles.exit()
      .transition()
      .duration(duration)
      .attr('transform', (d) => {
        var dy = d.y - this.diameter/2;
        var dx = d.x - this.diameter/2;
        var theta = Math.atan2(dy,dx);
        var destX = this.diameter * (1 + Math.cos(theta) )/ 2;
        var destY = this.diameter * (1 + Math.sin(theta) )/ 2;
        return 'translate(' + destX + ',' + destY + ')'; })
      .attr('r', 0)
      .remove();
    // for text have them fade out as they're flying all over
    labels.exit()
      .transition()
      .duration(duration)
      .style('top', (d) => {
        var dy = d.y - this.diameter/2;
        var dx = d.x - this.diameter/2;
        var theta = Math.atan2(dy,dx);
        var destY = this.diameter * (1 + Math.sin(theta) )/ 2;
        return destY + 'px'; })
      .style('left', (d) => {
        var dy = d.y - this.diameter/2;
        var dx = d.x - this.diameter/2;
        var theta = Math.atan2(dy,dx);
        var destX = this.diameter * (1 + Math.cos(theta) )/ 2;
        return destX + 'px'; })
      .style('opacity', 0)
      .style('width', 0)
      .style('height', 0)
      .remove();
  }

  /** Any necessary cleanup */
  destroy(el) { }
}
<commit_msg>allow legend text to overflow<commit_after>//------------------------------------------------------------------------------
// Copyright Jonathan Kaufman 2015
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//------------------------------------------------------------------------------

import d3 from 'd3';

/**
 * Properties defined during construction:
 *   svg
 *   html
 *   legend
 *   bubble
 *   diameter
 *   colorRange
 *   colorLegend
 *   selectedColor
 *   legendSpacing
 *   smallDiameter
 *   textColorRange
 *   mediumDiameter
 *   configureLegend
 *   selectedTextColor
 */
export default class ReactBubbleChartD3 {

  constructor(el, props) {
    props = props || {};
    this.legendSpacing = typeof props.legendSpacing === 'number' ? props.legendSpacing : 3;
    this.selectedColor = props.selectedColor;
    this.selectedTextColor = props.selectedTextColor;
    this.smallDiameter = props.smallDiameter || 40;
    this.mediumDiameter = props.mediumDiameter || 115;
    this.createLegend = props.legend;

    // create an <svg> and <html> element - store a reference to it for later
    this.svg = d3.select(el).append('svg');
    this.html = d3.select(el).append('div');
    if (this.createLegend) {
      this.legend = d3.select(el).append('svg')
    }

    // create legend and update
    this.adjustSize(el);
    this.update(el, props);
  }

  adjustSize(el) {
    // helper values for positioning
    this.diameter = Math.min(el.offsetWidth, el.offsetHeight);
    var top  = Math.max((el.offsetHeight - this.diameter)/2, 0);

    // center some stuff vertically
    this.svg.attr('width', this.diameter)
      .attr('height', this.diameter)
      .style('position', 'relative')
      .style('top', top + 'px')   // center vertically
      .attr('class', 'bubble-chart-d3');
    this.html.style('width', this.diameter + 'px')
      .style('height', this.diameter + 'px')
      .style('position', 'absolute')
      .style('top', top + 'px')   // center vertically
      .style('left', 0)           // center horizontally
      .style('right', 0)
      .style('margin-left', 'auto')
      .style('margin-right', 'auto')
      .attr('class', 'bubble-chart-text');

    // create the bubble layout that we will use to position our bubbles\
    this.bubble = d3.layout.pack()
      .sort(null)
      .size([this.diameter, this.diameter])
      .padding(3);
  }

  /** create and configure the legend */
  configureLegend(el, props) {
    var legendRectSize = Math.min(((el.offsetHeight-20) - (this.colorLegend.length-1)*this.legendSpacing)/this.colorLegend.length, 18);
    var legendHeight = this.colorLegend.length * (legendRectSize + this.legendSpacing) - this.legendSpacing;
    this.legend.attr('class', 'bubble-legend')
      .style('position', 'absolute')
      .style('height', legendHeight + 'px')
      .style('width', legendRectSize + 'px')
      .style('overflow', 'visible')
      .style('top', (el.offsetHeight - legendHeight)/2 + 'px')
      .style('left', 60 + 'px');

    // for each color in the legend, remove any existing, then
    // create a g and set its transform
    this.legend.selectAll('.legend-key').remove();
    var legendKeys = this.legend.selectAll('.legend-key')
      .data(this.colorLegend)
      .enter()
      .append('g')
      .attr('class', 'legend-key')
      .attr('transform', (d, i) => {
        var height = legendRectSize + this.legendSpacing;
        var vert = i * height;
        return 'translate(' + 0 + ',' + vert + ')';
      });

    // for each <g> create a rect and have its color... be the color
    legendKeys.append('rect')
      .attr('width', legendRectSize)
      .attr('height', legendRectSize)
      .style('fill', c => c.color)
      .style('stroke', c => c.color);

    // add necessary labels to the legend
    legendKeys.append('text')
      .attr('x', legendRectSize + 2)
      .attr('y', legendRectSize - 4)
      .text(c => c.text);
  }

  update(el, props) {
    this.adjustSize(el);
    // initialize color legend values and color range values
    // color range is just an array of the hex values
    // color legend is an array of the color/text objects
    var colorLegend = props.colorLegend || [];
    this.colorRange = colorLegend.map(c =>
      typeof c === 'string' ? c : c.color
    );
    this.colorLegend = colorLegend.slice(0).reverse().map(c =>
      typeof c === 'string' ? {color: c} : c
    );
    this.textColorRange = colorLegend.map(c =>
      typeof c === 'string' ? '#000000' : (c.textColor || '#000000')
    );
    if (this.createLegend) {
      this.configureLegend(el, props);
    }
    var duration = 500;
    var delay = 0;
    var data = props.data;
    if (!data) return;

    // define a color scale for our colorValues
    var color = d3.scale.quantize()
      .domain([
        props.fixedDomain ? props.fixedDomain.min : d3.min(data, d => d.colorValue),
        props.fixedDomain ? props.fixedDomain.max : d3.max(data, d => d.colorValue)
      ])
      .range(this.colorRange);

    // define a color scale for text town
    var textColor = d3.scale.quantize()
      .domain([
        props.fixedDomain ? props.fixedDomain.min : d3.min(data, d => d.colorValue),
        props.fixedDomain ? props.fixedDomain.max : d3.max(data, d => d.colorValue)
      ])
      .range(this.textColorRange)

    // generate data with calculated layout values
    var nodes = this.bubble.nodes({children: data})
      .filter((d) => !d.children); // filter out the outer bubble

    // assign new data to existing DOM for circles and labels
    var circles = this.svg.selectAll('circle')
      .data(nodes, (d) => 'g' + d._id);
    var labels = this.html.selectAll('.bubble-label')
      .data(nodes, (d) => 'g' + d._id);

    // update - this is created before enter.append. it only applies to updating nodes.
    // create the transition on the updating elements before the entering elements
    // because enter.append merges entering elements into the update selection
    // for circles we transition their transform, r, and fill
    circles.transition()
      .duration(duration)
      .delay((d, i) => {delay = i * 7; return delay;})
      .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')')
      .attr('r', (d) => d.r)
      .style('opacity', 1)
      .style('fill', d => d.selected ? this.selectedColor : color(d.colorValue));
    // for the labels we transition their height, width, left, top, and color
    labels.transition()
      .duration(duration)
      .delay((d, i) => {delay = i * 7; return delay;})
      .style('height', d => 2 * d.r + 'px')
      .style('width', d => 2 * d.r + 'px')
      .style('left', d =>  d.x - d.r + 'px')
      .style('top', d =>  d.y - d.r + 'px')
      .style('opacity', 1)
      .style('color', d => d.selected ? this.selectedTextColor : textColor(d.colorValue))
      .attr('class', d => {
        var size;
        if (2*d.r < this.smallDiameter) size = 'small';
        else if (2*d.r < this.mediumDiameter) size = 'medium';
        else size = 'large';
        return 'bubble-label ' + size
      });

    // enter - only applies to incoming elements (once emptying data)
    if (data.length) {
      // initialize new circles
      circles.enter().append('circle')
        .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')')
        .attr('r', (d) => 0)
        .attr('class', 'bubble')
        .style('fill', d => d.selected ? this.selectedColor : color(d.colorValue))
        .transition()
        .duration(duration * 1.2)
        .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')')
        .attr('r', (d) => d.r)
        .style('opacity', 1);
      // intialize new labels
      labels.enter().append('div')
        .attr('class', d => {
          var size;
          if (2*d.r < this.smallDiameter) size = 'small';
          else if (2*d.r < this.mediumDiameter) size = 'medium';
          else size = 'large';
          return 'bubble-label ' + size
        })
        .text(d => d.displayText || d._id)
        .on('click', (d,i) => {d3.event.stopPropagation(); props.onClick(d)})
        .style('position', 'absolute')
        .style('height', d => 2 * d.r + 'px')
        .style('width', d => 2 * d.r + 'px')
        .style('left', d =>  d.x - d.r + 'px')
        .style('top', d =>  d.y - d.r + 'px')
        .style('color', d => d.selected ? this.selectedTextColor : textColor(d.colorValue))
        .style('opacity', 0)
        .transition()
        .duration(duration * 1.2)
        .style('opacity', 1);
    }

    // exit - only applies to... exiting elements
    // for circles have them shrink to 0 as they're flying all over
    circles.exit()
      .transition()
      .duration(duration)
      .attr('transform', (d) => {
        var dy = d.y - this.diameter/2;
        var dx = d.x - this.diameter/2;
        var theta = Math.atan2(dy,dx);
        var destX = this.diameter * (1 + Math.cos(theta) )/ 2;
        var destY = this.diameter * (1 + Math.sin(theta) )/ 2;
        return 'translate(' + destX + ',' + destY + ')'; })
      .attr('r', 0)
      .remove();
    // for text have them fade out as they're flying all over
    labels.exit()
      .transition()
      .duration(duration)
      .style('top', (d) => {
        var dy = d.y - this.diameter/2;
        var dx = d.x - this.diameter/2;
        var theta = Math.atan2(dy,dx);
        var destY = this.diameter * (1 + Math.sin(theta) )/ 2;
        return destY + 'px'; })
      .style('left', (d) => {
        var dy = d.y - this.diameter/2;
        var dx = d.x - this.diameter/2;
        var theta = Math.atan2(dy,dx);
        var destX = this.diameter * (1 + Math.cos(theta) )/ 2;
        return destX + 'px'; })
      .style('opacity', 0)
      .style('width', 0)
      .style('height', 0)
      .remove();
  }

  /** Any necessary cleanup */
  destroy(el) { }
}
<|endoftext|>


<commit_before>#!/usr/bin/env node

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

// @ts-check

const http = require('http');
const url = require('url');
const fs = require('fs');
const path = require('path');
const util = require('util');
const opn = require('opn');
const minimist = require('vscode-minimist');

const APP_ROOT = path.dirname(__dirname);
const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions');
const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html');

const args = minimist(process.argv, {
	string: [
		'no-launch',
		'scheme',
		'host'
	],
	number: [
		'port'
	]
});

const PORT = args.port || process.env.PORT || 8080;
const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http';
const HOST = args.host || 'localhost';
const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`;

const server = http.createServer((req, res) => {
	const parsedUrl = url.parse(req.url, true);
	const pathname = parsedUrl.pathname;

	try {
		if (pathname === '/favicon.ico') {
			// favicon
			return serveFile(req, res, path.join(APP_ROOT, 'resources', 'win32', 'code.ico'));
		}
		if (/^\/static\//.test(pathname)) {
			// static requests
			return handleStatic(req, res, parsedUrl);
		}
		if (/^\/static-extension\//.test(pathname)) {
			// static extension requests
			return handleStaticExtension(req, res, parsedUrl);
		}
		if (pathname === '/') {
			// main web
			return handleRoot(req, res);
		}

		return serveError(req, res, 404, 'Not found.');
	} catch (error) {
		console.error(error.toString());

		return serveError(req, res, 500, 'Internal Server Error.');
	}
});

server.listen(PORT, () => {
	console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`);
});

server.on('error', err => {
	console.error(`Error occurred in server:`);
	console.error(err);
});

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {import('url').UrlWithParsedQuery} parsedUrl
 */
function handleStatic(req, res, parsedUrl) {

	// Strip `/static/` from the path
	const relativeFilePath = path.normalize(decodeURIComponent(parsedUrl.pathname.substr('/static/'.length)));

	return serveFile(req, res, path.join(APP_ROOT, relativeFilePath));
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {import('url').UrlWithParsedQuery} parsedUrl
 */
function handleStaticExtension(req, res, parsedUrl) {

	// Strip `/static-extension/` from the path
	const relativeFilePath = path.normalize(decodeURIComponent(parsedUrl.pathname.substr('/static-extension/'.length)));

	const filePath = path.join(EXTENSIONS_ROOT, relativeFilePath);

	return serveFile(req, res, filePath);
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 */
async function handleRoot(req, res) {
	const extensionFolders = await util.promisify(fs.readdir)(EXTENSIONS_ROOT);
	const mapExtensionFolderToExtensionPackageJSON = new Map();

	await Promise.all(extensionFolders.map(async extensionFolder => {
		try {
			const packageJSON = JSON.parse((await util.promisify(fs.readFile)(path.join(EXTENSIONS_ROOT, extensionFolder, 'package.json'))).toString());
			if (packageJSON.main && packageJSON.name !== 'vscode-api-tests') {
				return; // unsupported
			}

			if (packageJSON.name === 'scss') {
				return; // seems to fail to JSON.parse()?!
			}

			packageJSON.extensionKind = 'web'; // enable for Web

			mapExtensionFolderToExtensionPackageJSON.set(extensionFolder, packageJSON);
		} catch (error) {
			return null;
		}
	}));

	const staticExtensions = [];

	// Built in extensions
	mapExtensionFolderToExtensionPackageJSON.forEach((packageJSON, extensionFolder) => {
		staticExtensions.push({
			packageJSON,
			extensionLocation: { scheme: SCHEME, authority: AUTHORITY, path: `/static-extension/${extensionFolder}` }
		});
	});

	const data = (await util.promisify(fs.readFile)(WEB_MAIN)).toString()
		.replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({
			staticExtensions,
			folderUri: { scheme: 'memfs', path: `/sample-folder` }
		})))
		.replace('{{WEBVIEW_ENDPOINT}}', '')
		.replace('{{REMOTE_USER_DATA_URI}}', '');

	res.writeHead(200, { 'Content-Type': 'text/html' });
	return res.end(data);
}

/**
 * @param {string} value
 */
function escapeAttribute(value) {
	return value.replace(/"/g, '"');
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {string} errorMessage
 */
function serveError(req, res, errorCode, errorMessage) {
	res.writeHead(errorCode, { 'Content-Type': 'text/plain' });
	res.end(errorMessage);
}

const textMimeType = {
	'.html': 'text/html',
	'.js': 'text/javascript',
	'.json': 'application/json',
	'.css': 'text/css',
	'.svg': 'image/svg+xml',
};

const mapExtToMediaMimes = {
	'.bmp': 'image/bmp',
	'.gif': 'image/gif',
	'.ico': 'image/x-icon',
	'.jpe': 'image/jpg',
	'.jpeg': 'image/jpg',
	'.jpg': 'image/jpg',
	'.png': 'image/png',
	'.tga': 'image/x-tga',
	'.tif': 'image/tiff',
	'.tiff': 'image/tiff',
	'.woff': 'application/font-woff'
};

/**
 * @param {string} forPath
 */
function getMediaMime(forPath) {
	const ext = path.extname(forPath);

	return mapExtToMediaMimes[ext.toLowerCase()];
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {string} filePath
 */
async function serveFile(req, res, filePath, responseHeaders = Object.create(null)) {
	try {
		const stat = await util.promisify(fs.stat)(filePath);

		// Check if file modified since
		const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
		if (req.headers['if-none-match'] === etag) {
			res.writeHead(304);
			return res.end();
		}

		// Headers
		responseHeaders['Content-Type'] = textMimeType[path.extname(filePath)] || getMediaMime(filePath) || 'text/plain';
		responseHeaders['Etag'] = etag;

		res.writeHead(200, responseHeaders);

		// Data
		fs.createReadStream(filePath).pipe(res);
	} catch (error) {
		console.error(error.toString());
		res.writeHead(404, { 'Content-Type': 'text/plain' });
		return res.end('Not found');
	}
}

if (args.launch !== false) {
	opn(`${SCHEME}://${HOST}:${PORT}`);
}
<commit_msg> Make local and remote ports configurable (#84958)<commit_after>#!/usr/bin/env node

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

// @ts-check

const http = require('http');
const url = require('url');
const fs = require('fs');
const path = require('path');
const util = require('util');
const opn = require('opn');
const minimist = require('vscode-minimist');

const APP_ROOT = path.dirname(__dirname);
const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions');
const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html');

const args = minimist(process.argv, {
	boolean:[
		'no-launch',
		'help'
	],
	string: [
		'scheme',
		'host',
		'port',
		'local_port'
	],
});

if(args.help){
	console.log(
		'yarn web [options]\n' +
			' --no-launch   Do not start browser\n' +
			' --scheme      Protocol (https or http)\n' +
			' --host        Remote host\n' +
			' --port        Remote/Local port\n' +
			' --local_port  Local port override\n' +
			' --help\n' +
			'[Example]\n' +
			' yarn web --no-launch --scheme https --host example.com --port 8080 --local_port 30000'
	);
	process.exit(0);
}

const PORT = args.port || process.env.PORT || 8080;
const LOCAL_PORT = args.local_port || process.env.LOCAL_PORT || PORT;
const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http';
const HOST = args.host || 'localhost';
const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`;

const server = http.createServer((req, res) => {
	const parsedUrl = url.parse(req.url, true);
	const pathname = parsedUrl.pathname;

	try {
		if (pathname === '/favicon.ico') {
			// favicon
			return serveFile(req, res, path.join(APP_ROOT, 'resources', 'win32', 'code.ico'));
		}
		if (/^\/static\//.test(pathname)) {
			// static requests
			return handleStatic(req, res, parsedUrl);
		}
		if (/^\/static-extension\//.test(pathname)) {
			// static extension requests
			return handleStaticExtension(req, res, parsedUrl);
		}
		if (pathname === '/') {
			// main web
			return handleRoot(req, res);
		}

		return serveError(req, res, 404, 'Not found.');
	} catch (error) {
		console.error(error.toString());

		return serveError(req, res, 500, 'Internal Server Error.');
	}
});

server.listen(LOCAL_PORT, () => {
	if(LOCAL_PORT !== PORT)
		console.log(`Operating location at http://0.0.0.0:${LOCAL_PORT}`);
	console.log(`Web UI available at   ${SCHEME}://${AUTHORITY}`);
});

server.on('error', err => {
	console.error(`Error occurred in server:`);
	console.error(err);
});

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {import('url').UrlWithParsedQuery} parsedUrl
 */
function handleStatic(req, res, parsedUrl) {

	// Strip `/static/` from the path
	const relativeFilePath = path.normalize(decodeURIComponent(parsedUrl.pathname.substr('/static/'.length)));

	return serveFile(req, res, path.join(APP_ROOT, relativeFilePath));
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {import('url').UrlWithParsedQuery} parsedUrl
 */
function handleStaticExtension(req, res, parsedUrl) {

	// Strip `/static-extension/` from the path
	const relativeFilePath = path.normalize(decodeURIComponent(parsedUrl.pathname.substr('/static-extension/'.length)));

	const filePath = path.join(EXTENSIONS_ROOT, relativeFilePath);

	return serveFile(req, res, filePath);
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 */
async function handleRoot(req, res) {
	const extensionFolders = await util.promisify(fs.readdir)(EXTENSIONS_ROOT);
	const mapExtensionFolderToExtensionPackageJSON = new Map();

	await Promise.all(extensionFolders.map(async extensionFolder => {
		try {
			const packageJSON = JSON.parse((await util.promisify(fs.readFile)(path.join(EXTENSIONS_ROOT, extensionFolder, 'package.json'))).toString());
			if (packageJSON.main && packageJSON.name !== 'vscode-api-tests') {
				return; // unsupported
			}

			if (packageJSON.name === 'scss') {
				return; // seems to fail to JSON.parse()?!
			}

			packageJSON.extensionKind = 'web'; // enable for Web

			mapExtensionFolderToExtensionPackageJSON.set(extensionFolder, packageJSON);
		} catch (error) {
			return null;
		}
	}));

	const staticExtensions = [];

	// Built in extensions
	mapExtensionFolderToExtensionPackageJSON.forEach((packageJSON, extensionFolder) => {
		staticExtensions.push({
			packageJSON,
			extensionLocation: { scheme: SCHEME, authority: AUTHORITY, path: `/static-extension/${extensionFolder}` }
		});
	});

	const data = (await util.promisify(fs.readFile)(WEB_MAIN)).toString()
		.replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({
			staticExtensions,
			folderUri: { scheme: 'memfs', path: `/sample-folder` }
		})))
		.replace('{{WEBVIEW_ENDPOINT}}', '')
		.replace('{{REMOTE_USER_DATA_URI}}', '');

	res.writeHead(200, { 'Content-Type': 'text/html' });
	return res.end(data);
}

/**
 * @param {string} value
 */
function escapeAttribute(value) {
	return value.replace(/"/g, '"');
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {string} errorMessage
 */
function serveError(req, res, errorCode, errorMessage) {
	res.writeHead(errorCode, { 'Content-Type': 'text/plain' });
	res.end(errorMessage);
}

const textMimeType = {
	'.html': 'text/html',
	'.js': 'text/javascript',
	'.json': 'application/json',
	'.css': 'text/css',
	'.svg': 'image/svg+xml',
};

const mapExtToMediaMimes = {
	'.bmp': 'image/bmp',
	'.gif': 'image/gif',
	'.ico': 'image/x-icon',
	'.jpe': 'image/jpg',
	'.jpeg': 'image/jpg',
	'.jpg': 'image/jpg',
	'.png': 'image/png',
	'.tga': 'image/x-tga',
	'.tif': 'image/tiff',
	'.tiff': 'image/tiff',
	'.woff': 'application/font-woff'
};

/**
 * @param {string} forPath
 */
function getMediaMime(forPath) {
	const ext = path.extname(forPath);

	return mapExtToMediaMimes[ext.toLowerCase()];
}

/**
 * @param {import('http').IncomingMessage} req
 * @param {import('http').ServerResponse} res
 * @param {string} filePath
 */
async function serveFile(req, res, filePath, responseHeaders = Object.create(null)) {
	try {
		const stat = await util.promisify(fs.stat)(filePath);

		// Check if file modified since
		const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
		if (req.headers['if-none-match'] === etag) {
			res.writeHead(304);
			return res.end();
		}

		// Headers
		responseHeaders['Content-Type'] = textMimeType[path.extname(filePath)] || getMediaMime(filePath) || 'text/plain';
		responseHeaders['Etag'] = etag;

		res.writeHead(200, responseHeaders);

		// Data
		fs.createReadStream(filePath).pipe(res);
	} catch (error) {
		console.error(error.toString());
		res.writeHead(404, { 'Content-Type': 'text/plain' });
		return res.end('Not found');
	}
}

if (args.launch !== false) {
	opn(`${SCHEME}://${HOST}:${PORT}`);
}
<|endoftext|>