<commit_before>/* eslint-disable prefer-arrow-callback */
import { Player, Container, version } from 'clappr'
import ContextMenuPlugin from './context_menu'

const menuItems = [
  { label: 'Copy URL', name: 'copyURL' },
  { label: 'Copy URL on current time', name: 'copyURLCurrentTime' },
  { label: 'Loop: ', name: 'loop', class: 'off' },
  { label: `Clappr Player v${version}`, name: 'playerVersion', noAction: true },
]

describe('Context Menu Plugin', function() {
  beforeEach(function() {
    this.playerElement = document.getElementById('context')
  })

  afterEach(function() {
    this.player && this.player.destroy()
  })

  it('is loaded on core plugins array', function() {
    this.player = new Player({ source: 'test.mp4' })
    this.player.attachTo(this.playerElement)
    this.plugin = new ContextMenuPlugin(this.player.core)
    this.player.core.addPlugin(this.plugin)

    expect(this.player.core.getPlugin(this.plugin.name).name).toEqual('context_menu')
  })

  it('shows at context menu event trigger occurs', function() {
    this.player = new Player({ source: 'test.mp4' })
    this.player.attachTo(this.playerElement)
    this.plugin = new ContextMenuPlugin(this.player.core)
    spyOn(this.plugin, 'show')
    this.player.core.addPlugin(this.plugin)
    this.player.core.activeContainer = new Container({ playerId: 1, playback: this.player.core.activePlayback })

    const evt = new Event('contextmenu', { bubbles: true, cancelable: true })
    this.player.core.activeContainer.trigger('container:contextmenu', evt)

    expect(this.plugin.show).toHaveBeenCalled()
  })

  it('respect player size limits to render', function() {
    this.player = new Player({ source: 'test.mp4' })
    this.player.attachTo(this.playerElement)
    this.plugin = new ContextMenuPlugin(this.player.core)
    this.player.core.addPlugin(this.plugin)
    this.player.core.trigger('core:resize', { height: 360, width: 640 })
    this.plugin.show(100, 100)

    expect(this.plugin.$el[0].style.top).toEqual('100px')
    expect(this.plugin.$el[0].style.left).toEqual('100px')

    this.plugin.show(1000, 2000)

    expect(this.plugin.$el[0].style.top).toEqual('160px')
    expect(this.plugin.$el[0].style.left).toEqual('480px')
  })

  describe('with no configuration', function() {
    it('render default items', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual(menuItems)
    })
  })

  describe('with menuItems configured', function() {
    const contextMenuOptions = { menuItems: [] }

    it('discard empty config', function() {
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual(menuItems)
    })

    it('discard unrecognized config', function() {
      contextMenuOptions.menuItems.push('xpto')
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual(menuItems)
    })

    it('apply custom menuItem config', function() {
      contextMenuOptions.menuItems.push('playerVersion')
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual([{ label: `Clappr Player v${version}`, name: 'playerVersion', noAction: true }])
    })
  })

  describe('with extraOptions configured', function() {
    const extraOption = { name: 'test', label: 'Test Label' }
    const contextMenuOptions = { extraOptions: [extraOption] }

    it('use setted extram options', function() {
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toContain(extraOption)
    })

    it('trigger callback at click on extra options', function() {
      let callbackCalled = false
      extraOption.callback = function() { callbackCalled = true }

      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const evt = new Event('click', { bubbles: true, cancelable: false })
      const extraOptionElement = this.plugin.el.querySelector('[data-test]')
      extraOptionElement.dispatchEvent(evt)

      expect(callbackCalled).toBeTruthy()
    })
  })

  describe('with customStyle configured', function() {
    const customStyle = {
      list: { 'background-color': 'gray' },
      items: { color: 'yellow' },
    }
    const contextMenuOptions = { customStyle }

    it('apply style correctly', function() {
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const listElement = this.plugin.$el.find('.context-menu-list')
      const ListItemElement = this.plugin.$el.find('.context-menu-list-item')

      expect(listElement[0].style.backgroundColor).toEqual('gray')
      expect(ListItemElement[0].style.color).toEqual('yellow')
    })
  })

  describe('player version feature', function() {
    it('show correct Clappr version', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const playerVersion = this.plugin.el.querySelector('[data-playerversion]')

      expect(playerVersion.innerHTML).toEqual(`Clappr Player v${version}`)
    })
  })

  describe('loop feature', function() {
    it('show current loop status', function() {
      this.player = new Player({ source: 'test.mp4', loop: true })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const loop = this.plugin.el.querySelector('[data-loop]')

      expect([...loop.classList]).toContain('on')
    })

    it('update loop playback property value', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const loop = this.plugin.el.querySelector('[data-loop]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      loop.dispatchEvent(evt)

      expect([...loop.classList]).toContain('on')
    })
  })

  describe('copyURL feature', function() {
    it('copy url website at Clappr as embeded', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)
      spyOn(this.plugin, 'copyToClipboard')

      const copyURL = this.plugin.el.querySelector('[data-copyURL]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(window.location.href, this.plugin.$el)
    })
  })

  describe('CopyURLCurrentTime feature', function() {
    beforeEach(function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)
      this.player.core.activeContainer = new Container({ playerId: 1, playback: this.player.core.activePlayback })
      spyOn(this.plugin, 'copyToClipboard')
    })

    it('copy url without query string and put current video time', function() {
      spyOn(this.plugin.container, 'getCurrentTime').and.returnValue(1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}?t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })

    it('copy url with other query string and put current video time', function() {
      window.history.pushState({}, '', `${window.location.href}?other_query_string=0`)
      spyOn(this.plugin.container, 'getCurrentTime').and.returnValue(1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}&t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })

    it('copy url with resume at query string and put current video time', function() {
      const sitePageURL = window.location.href
      window.history.pushState({}, '', `${window.location.href}?t=0`)
      spyOn(this.plugin.container, 'getCurrentTime').and.returnValue(1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${sitePageURL}?t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })
  })
})
<commit_msg>test(src): update test coverage<commit_after>/* eslint-disable prefer-arrow-callback */
import { Player, Container, version } from 'clappr'
import ContextMenuPlugin from './context_menu'

const menuItems = [
  { label: 'Copy URL', name: 'copyURL' },
  { label: 'Copy URL on current time', name: 'copyURLCurrentTime' },
  { label: 'Loop: ', name: 'loop', class: 'off' },
  { label: `Clappr Player v${version}`, name: 'playerVersion', noAction: true },
]

describe('Context Menu Plugin', function() {
  beforeEach(function() {
    this.playerElement = document.getElementById('context')
  })

  afterEach(function() {
    this.player && this.player.destroy()
  })

  it('is loaded on core plugins array', function() {
    this.player = new Player({ source: 'test.mp4' })
    this.player.attachTo(this.playerElement)
    this.plugin = new ContextMenuPlugin(this.player.core)
    this.player.core.addPlugin(this.plugin)

    expect(this.player.core.getPlugin(this.plugin.name).name).toEqual('context_menu')
  })

  it('shows at context menu event trigger occurs', function() {
    this.player = new Player({ source: 'test.mp4' })
    this.player.attachTo(this.playerElement)
    this.plugin = new ContextMenuPlugin(this.player.core)
    spyOn(this.plugin, 'show')
    this.player.core.addPlugin(this.plugin)
    this.player.core.activeContainer = new Container({ playerId: 1, playback: this.player.core.activePlayback })

    const evt = new Event('contextmenu', { bubbles: true, cancelable: true })
    this.player.core.activeContainer.trigger('container:contextmenu', evt)

    expect(this.plugin.show).toHaveBeenCalled()
  })

  it('only accept absolute size format', function() {
    const customSize = { height: 360, width: 640 }
    this.player = new Player({ source: 'test.mp4', height: '100%', width: '100%', plugins: [ContextMenuPlugin] })
    this.player.attachTo(this.playerElement)
    this.plugin = this.player.core.getPlugin('context_menu')

    expect(this.plugin.playerSize).toBeUndefined()
    this.player.resize(customSize)
    expect(this.plugin.playerSize).toEqual(customSize)
  })

  it('respect player size limits to render', function() {
    this.player = new Player({ source: 'test.mp4' })
    this.player.attachTo(this.playerElement)
    this.plugin = new ContextMenuPlugin(this.player.core)
    this.player.core.addPlugin(this.plugin)
    this.player.core.trigger('core:resize', { height: 360, width: 640 })
    this.plugin.show(100, 100)

    expect(this.plugin.$el[0].style.top).toEqual('100px')
    expect(this.plugin.$el[0].style.left).toEqual('100px')

    this.plugin.show(1000, 2000)

    expect(this.plugin.$el[0].style.top).toEqual('160px')
    expect(this.plugin.$el[0].style.left).toEqual('480px')
  })

  describe('with no configuration', function() {
    it('render default items', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual(menuItems)
    })
  })

  describe('with menuItems configured', function() {
    const contextMenuOptions = { menuItems: [] }

    it('discard empty config', function() {
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual(menuItems)
    })

    it('discard unrecognized config', function() {
      contextMenuOptions.menuItems.push('xpto')
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual(menuItems)
    })

    it('apply custom menuItem config', function() {
      contextMenuOptions.menuItems.push('playerVersion')
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toEqual([{ label: `Clappr Player v${version}`, name: 'playerVersion', noAction: true }])
    })
  })

  describe('with extraOptions configured', function() {
    const extraOption = { name: 'test', label: 'Test Label' }
    const contextMenuOptions = { extraOptions: [extraOption] }

    it('use setted extram options', function() {
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      expect(this.plugin.menuOptions).toContain(extraOption)
    })

    it('trigger callback at click on extra options', function() {
      let callbackCalled = false
      extraOption.callback = function() { callbackCalled = true }

      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const evt = new Event('click', { bubbles: true, cancelable: false })
      const extraOptionElement = this.plugin.el.querySelector('[data-test]')
      extraOptionElement.dispatchEvent(evt)

      expect(callbackCalled).toBeTruthy()
    })
  })

  describe('with customStyle configured', function() {
    const customStyle = {
      list: { 'background-color': 'gray' },
      items: { color: 'yellow' },
    }
    const contextMenuOptions = { customStyle }

    it('apply style correctly', function() {
      this.player = new Player({ source: 'test.mp4', contextMenu: contextMenuOptions })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const listElement = this.plugin.$el.find('.context-menu-list')
      const ListItemElement = this.plugin.$el.find('.context-menu-list-item')

      expect(listElement[0].style.backgroundColor).toEqual('gray')
      expect(ListItemElement[0].style.color).toEqual('yellow')
    })
  })

  describe('player version feature', function() {
    it('show correct Clappr version', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const playerVersion = this.plugin.el.querySelector('[data-playerversion]')

      expect(playerVersion.innerHTML).toEqual(`Clappr Player v${version}`)
    })
  })

  describe('loop feature', function() {
    it('show current loop status', function() {
      this.player = new Player({ source: 'test.mp4', loop: true })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const loop = this.plugin.el.querySelector('[data-loop]')

      expect([...loop.classList]).toContain('on')
    })

    it('update loop playback property value', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)

      const loop = this.plugin.el.querySelector('[data-loop]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      loop.dispatchEvent(evt)

      expect([...loop.classList]).toContain('on')
    })
  })

  describe('copyURL feature', function() {
    it('copy url website at Clappr as embeded', function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)
      spyOn(this.plugin, 'copyToClipboard')

      const copyURL = this.plugin.el.querySelector('[data-copyURL]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(window.location.href, this.plugin.$el)
    })
  })

  describe('CopyURLCurrentTime feature', function() {
    beforeEach(function() {
      this.player = new Player({ source: 'test.mp4' })
      this.player.attachTo(this.playerElement)
      this.plugin = new ContextMenuPlugin(this.player.core)
      this.player.core.addPlugin(this.plugin)
      this.player.core.activeContainer = new Container({ playerId: 1, playback: this.player.core.activePlayback })
      spyOn(this.plugin, 'copyToClipboard')
    })

    it('copy url without query string and put current video time', function() {
      spyOn(this.plugin.container, 'getCurrentTime').and.returnValue(1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}?t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })

    it('copy url with other query string and put current video time', function() {
      window.history.pushState({}, '', `${window.location.href}?other_query_string=0`)
      spyOn(this.plugin.container, 'getCurrentTime').and.returnValue(1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}&t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })

    it('copy url with resume at query string and put current video time', function() {
      const sitePageURL = window.location.href
      window.history.pushState({}, '', `${window.location.href}?t=0`)
      spyOn(this.plugin.container, 'getCurrentTime').and.returnValue(1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${sitePageURL}?t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })
  })
})
<|endoftext|>


<commit_before>var url = require('url');

var webdriver	= require('wd'),
	promises	= require('q');

var Runner = new Class( /** @lends Runner# */ {

	Extends: require('events').EventEmitter,

	/** The promise object for results, resolved when all features of this Runner have been evaluated.
	*@type	{Promise}
	*/
	promise: null,

	/** A hash mapping all failed features to their reasons for rejection.
	*If empty, the run was successful.
	*@type	{Object.<Feature, String>}
	*@private
	*/
	failures: Object.create(null),

	/** The list of all features to evaluate with this configuration.
	*@type	{Array.<Feature>}
	*@private
	*/
	features: [],

	/** Index of the currently evaluated feature.
	*@type	{integer}
	*@private
	*/
	currentFeature: 0,

	/** Whether the baseURL page has been loaded or not.
	*@type	{Boolean}
	*@private
	*/
	ready: false,

	/** True if the driver is currently waiting for the baseURL page to load.
	*@type	{Boolean}
	*@private
	*/
	loading: false,

	/** The promise controller (deferred object) for results, resolved when all features of this Runner have been evaluated.
	*@type	{q.deferred}
	*@private
	*/
	deferred: null,

	/**@class	Manages a set of features and the driver in which they are run.
	*
	* A `Runner` is mostly set up through a configuration object.
	* Such an object MUST contain the following items:
	*	- `baseURL`: the URL at which the driver should start;
	* It SHOULD contain:
	*	- `driverCapabilities`: an object that will be passed straight to the WebDriver instance, that describes the browser on which the tests should be run.
	* It MAY contain:
	*	- `name`: the name of the suite. Defaults to the name of the containing folder.
	*
	*@constructs
	*@param	{Object}	config	A configuration object, as defined above.
	*/
	initialize: function init(config) {
		this.parseConfig(config);

		this.initDriver();
	},

	/** Checks the passed configuration hash for any missing mandatory definitions.
	*
	*@param	{Object}	config	The configuration object to check (may not be defined, which will return an error).
	*@return	{Error|null}	An error object describing the encountered problem, or `null` if no error was found.
	*@see	#initialize	For details on the configuration object.
	*/
	parseConfig: function parseConfig(config) {
		if (! config)
			throw new Error('You need to provide a configuration to create a Runner!');

		try {
			config.seleniumServerURL = url.format(config.seleniumServerURL);
		} catch (err) {
			throw new Error('The given Selenium server URL ("' + config.seleniumServerURL + '") is unreadable (' + err.message + ')');
		}

		try {
			config.baseURL = url.format(config.baseURL);	// allow taking objects describing the URL
		} catch (err) {
			throw new Error('The given base URL ("' + config.baseURL + '") is unreadable (' + err.message + ')');
		}

		this.config = config;
	},

	/** Initializes the underlying driver of this Runner.
	*@return	this	For chainability.
	*@private
	*/
	initDriver: function initDriver() {
		this.ready = false;
		this.driver = this.buildDriverFrom(this.config);
		this.loadBaseURL();
		return this;
	},

	/** Navigates to the base page for this runner.
	*@private
	*/
	loadBaseURL: function loadBaseURL() {
		this.loading = true;
		this.driver.get(this.config.baseURL).then(this.onReady.bind(this));
	},

	/** Constructs a new WebDriver instance based on the given configuration.
	*
	*@param	{Object}	config	The configuration object based on which the driver will be built.
	*@return	{WebDriver}	The matching WebDriver instance.
	*@see	#initialize	For details on the configuration object.
	*@private
	*/
	buildDriverFrom: function buildDriverFrom(config) {
		var seleniumServer = url.parse(config.seleniumServerURL);	// TODO: get the URL already parsed from the config instead of serializing it at each step

		var result = webdriver.promiseRemote();

		result.init(Object.merge(config.driverCapabilities, {
			host: seleniumServer.hostname,
			port: seleniumServer.port
		}));

		return result;
	},

	/** Tells whether the underlying driver of this Runner has loaded the base page or not.
	* This changes after the `ready` event has been emitted by this Runner.
	*
	*@return	{Boolean}	`true` if the page has been loaded, `false` otherwise.
	*/
	isReady: function isReady() {
		return this.ready;
	},

	/** Emits the "ready" event and updates this runner's status.
	*@private
	*/
	onReady: function onReady() {
		this.loading = false;
		this.ready = true;
		this.emit('ready', this);
	},

	/** Adds the given Feature to the list of those that this Runner will evaluate.
	*
	*@param	{Feature}	feature	A Feature for this Runner to evaluate.
	*@return	This Runner, for chaining.
	*/
	addFeature: function addFeature(feature) {
		this.features.push(feature);

		return this;
	},

	/** Returns the WebDriver instance this Runner created for the current run.
	*
	*@returns	WebDriver
	*/
	getDriver: function getDriver() {
		return this.driver;
	},

	/** Evaluates all features added to this Runner.
	* Emits "driverInit".
	*
	*@returns	{Promise}	A promise for results, resolved if all features pass (param: this Runner), rejected otherwise (param: hash mapping failed features to their reasons for rejection, or an Error if an error appeared in the runner itself or the evaluation was cancelled).
	*@see	#addFeature
	*/
	test: function test() {
		this.deferred = promises.defer();
		this.promise = this.deferred.promise;

		this.emit('driverInit', this);

		if (this.ready) {
			this.start();
		} else {	// we already run before, or we just initialized
			this.once('ready', this.start.bind(this));

			if (! this.loading) {
				if (this.driver)
					this.loadBaseURL();
				else	// the driver has been explicitly killed before running again
					this.initDriver();
			}
		}

		return this.promise;
	},

	/** Actually starts the evaluation process.
	* Emits "start".
	*
	*@private
	*/
	start: function start() {
		this.failures = Object.create(null);
		this.currentFeature = -1;

		this.emit('start', this);

		this.startNextFeature();
	},

	/** Increments the feature index, starts evaluation of the next feature, and quits the driver if all features were evaluated.
	*
	*@private
	*/
	startNextFeature: function startNextFeature() {
		this.currentFeature++;

		if (this.ready
			&& this.currentFeature < this.features.length)
			this.evaluateFeature(this.features[this.currentFeature]);
		else
			this.finish();
	},

	/** Prepares and triggers the evaluation of the given feature.
	*
	*@private
	*/
	evaluateFeature: function evaluateFeature(feature) {
		this.emit('feature', feature);

		feature.test().then(this.startNextFeature.bind(this),
							this.handleFeatureFailure.bind(this, feature)); // leave last arg to pass failure description
	},

	/** Callback handler upon feature evaluation. Flags failures and calls the `startNextFeature` handler.
	*
	*@private
	*@see	#startNextFeature
	*/
	handleFeatureFailure: function handleFeatureFailure(feature, message) {
		this.failures[feature] = message;
		this.failed = true;

		this.startNextFeature();
	},

	/** Informs of the end result and cleans the instance up after tests runs.
	*
	*@private
	*/
	finish: function finish() {
		var resolve			= this.deferred.resolve.bind(this.deferred, this),
			reject			= this.deferred.reject.bind(this.deferred, this.failures),
			fulfill			= resolve,
			precondition	= (this.config.quit == 'always'
								? this.killDriver
								: this.markUsed
							  ).bind(this),
			failures		= this.failures;	// copy them in case the precondition cleans them up

		if (Object.getLength(failures) == 0) {
			if (this.config.quit == 'on success')
				precondition = this.killDriver.bind(this);
		} else {
			fulfill = reject;
		}

		promises.when(precondition(), fulfill, reject);
	},

	/** Updates inner state so that consequent calls to `test()` know they need to reload the driver.
	*
	*@private
	*/
	markUsed: function markUsed() {
		this.ready = false;
	},

	/** Stops the current evaluation.
	*/
	cancel: function cancel() {
		this.removeListener('ready', this.start);
		return this.finish();
	},

	/** Quits the managed browser.
	*
	*@return	{Promise}	A promise resolved once the browser has been properly quit.
	*/
	killDriver: function killDriver() {
		var driver = this.driver;
		this.driver = null;

		this.markUsed();

		return (driver
				? driver.quit()
				: promises());	// normalize return type to a promise, so that it can safely be called even if the driver had already been quit
	},

	toString: function toString() {
		return this.config.name;
	}
});

module.exports = Runner;	// CommonJS export
<commit_msg>Minor Runner refactor<commit_after>var url = require('url');

var webdriver	= require('wd'),
	promises	= require('q');

var Runner = new Class( /** @lends Runner# */ {

	Extends: require('events').EventEmitter,

	/** The promise object for results, resolved when all features of this Runner have been evaluated.
	*@type	{Promise}
	*/
	promise: null,

	/** A hash mapping all failed features to their reasons for rejection.
	*If empty, the run was successful.
	*@type	{Object.<Feature, String>}
	*@private
	*/
	failures: Object.create(null),

	/** The list of all features to evaluate with this configuration.
	*@type	{Array.<Feature>}
	*@private
	*/
	features: [],

	/** Index of the currently evaluated feature.
	*@type	{integer}
	*@private
	*/
	currentFeature: 0,

	/** Whether the baseURL page has been loaded or not.
	*@type	{Boolean}
	*@private
	*/
	ready: false,

	/** True if the driver is currently waiting for the baseURL page to load.
	*@type	{Boolean}
	*@private
	*/
	loading: false,

	/** The promise controller (deferred object) for results, resolved when all features of this Runner have been evaluated.
	*@type	{q.deferred}
	*@private
	*/
	deferred: null,

	/**@class	Manages a set of features and the driver in which they are run.
	*
	* A `Runner` is mostly set up through a configuration object.
	* Such an object MUST contain the following items:
	*	- `baseURL`: the URL at which the driver should start;
	* It SHOULD contain:
	*	- `driverCapabilities`: an object that will be passed straight to the WebDriver instance, that describes the browser on which the tests should be run.
	* It MAY contain:
	*	- `name`: the name of the suite. Defaults to the name of the containing folder.
	*
	*@constructs
	*@param	{Object}	config	A configuration object, as defined above.
	*/
	initialize: function init(config) {
		this.parseConfig(config);

		this.initDriver();
	},

	/** Checks the passed configuration hash for any missing mandatory definitions.
	*
	*@param	{Object}	config	The configuration object to check (may not be defined, which will return an error).
	*@return	{Error|null}	An error object describing the encountered problem, or `null` if no error was found.
	*@see	#initialize	For details on the configuration object.
	*/
	parseConfig: function parseConfig(config) {
		if (! config)
			throw new Error('You need to provide a configuration to create a Runner!');

		try {
			config.seleniumServerURL = url.format(config.seleniumServerURL);
		} catch (err) {
			throw new Error('The given Selenium server URL ("' + config.seleniumServerURL + '") is unreadable (' + err.message + ')');
		}

		try {
			config.baseURL = url.format(config.baseURL);	// allow taking objects describing the URL
		} catch (err) {
			throw new Error('The given base URL ("' + config.baseURL + '") is unreadable (' + err.message + ')');
		}

		this.config = config;
	},

	/** Initializes the underlying driver of this Runner.
	*@return	this	For chainability.
	*@private
	*/
	initDriver: function initDriver() {
		this.ready = false;
		this.driver = this.buildDriverFrom(this.config);
		this.loadBaseURL();
		return this;
	},

	/** Navigates to the base page for this runner.
	*@private
	*/
	loadBaseURL: function loadBaseURL() {
		this.loading = true;
		this.driver.get(this.config.baseURL).done(this.onReady.bind(this));
	},

	/** Constructs a new WebDriver instance based on the given configuration.
	*
	*@param	{Object}	config	The configuration object based on which the driver will be built.
	*@return	{WebDriver}	The matching WebDriver instance.
	*@see	#initialize	For details on the configuration object.
	*@private
	*/
	buildDriverFrom: function buildDriverFrom(config) {
		var seleniumServer = url.parse(config.seleniumServerURL);	// TODO: get the URL already parsed from the config instead of serializing it at each step

		var result = webdriver.promiseRemote();

		result.init(Object.merge(config.driverCapabilities, {
			host: seleniumServer.hostname,
			port: seleniumServer.port
		}));

		return result;
	},

	/** Tells whether the underlying driver of this Runner has loaded the base page or not.
	* This changes after the `ready` event has been emitted by this Runner.
	*
	*@return	{Boolean}	`true` if the page has been loaded, `false` otherwise.
	*/
	isReady: function isReady() {
		return this.ready;
	},

	/** Emits the "ready" event and updates this runner's status.
	*@private
	*/
	onReady: function onReady() {
		this.loading = false;
		this.ready = true;
		this.emit('ready', this);
	},

	/** Adds the given Feature to the list of those that this Runner will evaluate.
	*
	*@param	{Feature}	feature	A Feature for this Runner to evaluate.
	*@return	This Runner, for chaining.
	*/
	addFeature: function addFeature(feature) {
		this.features.push(feature);

		return this;
	},

	/** Returns the WebDriver instance this Runner created for the current run.
	*
	*@returns	WebDriver
	*/
	getDriver: function getDriver() {
		return this.driver;
	},

	/** Evaluates all features added to this Runner.
	* Emits "driverInit".
	*
	*@returns	{Promise}	A promise for results, resolved if all features pass (param: this Runner), rejected otherwise (param: hash mapping failed features to their reasons for rejection, or an Error if an error appeared in the runner itself or the evaluation was cancelled).
	*@see	#addFeature
	*/
	test: function test() {
		this.deferred = promises.defer();
		this.promise = this.deferred.promise;

		this.emit('driverInit', this);

		if (this.ready) {
			this.start();
		} else {	// we already run before, or we just initialized
			this.once('ready', this.start.bind(this));

			if (! this.loading) {
				if (this.driver)
					this.loadBaseURL();
				else	// the driver has been explicitly killed before running again
					this.initDriver();
			}
		}

		return this.promise;
	},

	/** Actually starts the evaluation process.
	* Emits "start".
	*
	*@private
	*/
	start: function start() {
		this.failures = Object.create(null);
		this.currentFeature = -1;

		this.emit('start', this);

		this.startNextFeature();
	},

	/** Increments the feature index, starts evaluation of the next feature, and quits the driver if all features were evaluated.
	*
	*@private
	*/
	startNextFeature: function startNextFeature() {
		this.currentFeature++;

		if (this.ready
			&& this.currentFeature < this.features.length)
			this.evaluateFeature(this.features[this.currentFeature]);
		else
			this.finish();
	},

	/** Prepares and triggers the evaluation of the given feature.
	*
	*@private
	*/
	evaluateFeature: function evaluateFeature(feature) {
		this.emit('feature', feature);

		feature.test().then(this.startNextFeature.bind(this),
							this.handleFeatureFailure.bind(this, feature)); // leave last arg to pass failure description
	},

	/** Callback handler upon feature evaluation. Flags failures and calls the `startNextFeature` handler.
	*
	*@private
	*@see	#startNextFeature
	*/
	handleFeatureFailure: function handleFeatureFailure(feature, message) {
		this.failures[feature] = message;
		this.failed = true;

		this.startNextFeature();
	},

	/** Informs of the end result and cleans the instance up after tests runs.
	*
	*@private
	*/
	finish: function finish() {
		var resolve			= this.deferred.resolve.bind(this.deferred, this),
			reject			= this.deferred.reject.bind(this.deferred, this.failures),
			fulfill			= resolve,
			killDriver		= this.killDriver.bind(this),
			precondition	= (this.config.quit == 'always'
								? killDriver
								: this.markUsed.bind(this)),
			failures		= this.failures;	// copy them in case the precondition cleans them up

		if (Object.getLength(failures) > 0) {
			fulfill = reject;
		} else {
			if (this.config.quit == 'on success')
				precondition = killDriver;
		}

		promises.when(precondition(), fulfill, reject);
	},

	/** Updates inner state so that consequent calls to `test()` know they need to reload the driver.
	*
	*@private
	*/
	markUsed: function markUsed() {
		this.ready = false;
	},

	/** Stops the current evaluation.
	*/
	cancel: function cancel() {
		this.removeListener('ready', this.start);
		return this.finish();
	},

	/** Quits the managed browser.
	*
	*@return	{Promise}	A promise resolved once the browser has been properly quit.
	*/
	killDriver: function killDriver() {
		var driver = this.driver;
		this.driver = null;

		this.markUsed();

		return (driver
				? driver.quit()
				: promises());	// normalize return type to a promise, so that it can safely be called even if the driver had already been quit
	},

	toString: function toString() {
		return this.config.name;
	}
});

module.exports = Runner;	// CommonJS export
<|endoftext|>


<commit_before>import React, { Component } from 'react';

class Author extends Component {
  render() {
    const { firstname, lastname, company, presenter, sessionCodes, tagName, ...other } = this.props;
    let element = tagName;
    // If no tagName defined we base it off the presenter value.
    if (!tagName) {
      if (presenter) {
        element = 'presenter';
      } else {
        element = 'author';
      }
    }

    const fullName = `${firstname} ${lastname}`;
    const separator = ', ';
    // Define the insides of the component.
    const DetailsEl = (
      <person>
        <fullname>{fullName}</fullname>
        { separator }
        { company ? <company>{ company }</company> : false }
        { sessionCodes ?  : false }
      </person>
    );
    // Use function call instead of jsx to use computed tag name.
    return React.createElement(element, other, DetailsEl);
  }
}

export default Author;
<commit_msg>Fix #28 authors lastname, firstname<commit_after>import React, { Component } from 'react';

class Author extends Component {
  render() {
    const { firstname, lastname, company, presenter, sessionCodes, tagName, ...other } = this.props;
    let element = tagName;
    // If no tagName defined we base it off the presenter value.
    if (!tagName) {
      if (presenter) {
        element = 'presenter';
      } else {
        element = 'author';
      }
    }

    let fullName = `${firstname} ${lastname}`;
    if (sessionCodes) {
      fullName = `${lastname}, ${firstname}`;
    }
    const separator = ', ';
    // Define the insides of the component.
    const DetailsEl = (
      <person>
        <fullname>{fullName}</fullname>
        { separator }
        { company ? <company>{ company }</company> : false }
        { sessionCodes ?  : false }
      </person>
    );
    // Use function call instead of jsx to use computed tag name.
    return React.createElement(element, other, DetailsEl);
  }
}

export default Author;
<|endoftext|>


<commit_before>//     Cannon.js 0.0.1
//     (c) 2014 Abraham Haskins
//     Cannon may be freely distributed under the MIT license.

// Our main Cannon class which takes the URL of the data we are looking to retreive and manipulate.

// For example `new Cannon("http://api.reddit.com/user/unidan/")`
var Cannon = function (resource) {

    // Initial setup
    // --------------

    // Cannon takes a "resource" which is either a URL or an Object.
    if (typeof resource == "string") {
        this.url = resource;
    }else{
        this.url = false;
    }

    if (typeof resource == "object") {
        this.initialData = resource;
    }else{
        this.initialData = false;
    }

    // The object containing reloader information
    this.reloaderStrs = {};

    // All data loaded from URL and reloads, used for `Cannon.recoil`
    this.rawData = [];

    // The default projectile is `"*"` i.e. all children of data
    this.projectileStr = "*";

    // The total HTTP requests made, used for `Cannon.limit`
    this.fetches = 0;

    // The total HTTP requests we can make, used for `Cannon.limit`
    this.limitInt = 1;

    // The delay (in milliseconds) between each HTTP request, used for `Cannon.delay`
    this.delayInt = 0;

    // Our definition of self.
    var self = this;

    // Our projectile data.
    var projectileBlob;

    // Exported Methods
    // --------------

    // The `Cannon.fire` method is used to activate the Cannon (i.e. make HTTP requests, parse data, etc). The projectiles are passed to the callback as an array of data shaped by string passed to `Cannon.projectile`.
    this.fire = function (callback) {
        self._fetch(callback);
    };

    // The `Cannon.rapidfire` is similar to `Cannon.fire` except that instead of having the callback called once with an array containing the data, it is called once for each protectile in the array.
    this.rapidfire = function (callback) {
        self._fetch(function (projectiles) {
            self._each(projectiles, function (projectile) {
                callback(projectile);
            });
        });
    };

    // The `Cannon.reloader` method is used for dealing with paginated content. It takes an object of reloader strings like `{after: "data.after"}` and stores it in the Cannon. After the initial URL provided to the Cannon is loaded, another URL will be loaded which has the added query data `?after={{data.after}}` with the value for `data.after` being pulled from the data for the first page.
    this.reloader = function (reloaderStrs) {
        self.reloaderStrs = reloaderStrs;
        return self;
    };

    // The `Cannon.limit` method is used for limiting the number of HTTP requests when a reloader is used. Theoredically, a reloader could be used to load an infinite amount of pages, which may not be what we want. The `limit` method takes a number and will not let the total page requests from this Cannon exceed that number.
    this.limit = function (limit) {
        self.limitInt = limit;
        return self;
    };

    // The `Cannon.fetcher` adds a callback to be called as pages are being fetched. The `fetcherFunc` will be passed the current number of HTTP requests and the limit (if any).
    this.fetcher = function (fetcherFunc) {
        self.fetcherFunc = fetcherFunc;
        return self;
    };

    // The `Cannon.delay` method is used for adding a delay between HTTP requests when a reloader is used. This method takes a number of millseconds and delays each HTTP request by that amount of time.
    this.delay = function (delayInt) {
        self.delayInt = delayInt;
        return self;
    };

    // The `Cannon.projectile` method is used for defining the shape of the data passed to `Cannon.fire`'s callback. An example `projectileStr` would be `"data.children.*.data.title"`. This tells the Cannon to fire the data in `dataFromURL['data']['children'][*]['data']['title']`. This syntax should be familiar to any developer with the exception of the special `*` field. This character is the single most important concept when building a Cannon. It acts as a wild card and allows us to get a list of titles from all the children, not just a specific one.
    this.projectile = function (projectileStr) {
        self.projectileStr = projectileStr;
        return self;
    };

    // The `Cannon.recoil` method is typically used from within the `Cannon.fire` callback. This method will take a single projectile and return the parent(s) of that projectile. For example, if we have our projectile set as `"data.children.*.data.title"`, then our projectile will be the value of `title`. In order to access the complete child's data, we can `Cannon.recoil` the `title` and we will receive the `data` object which has the `title` field. It is important to note that *this method returns a list of matches, not a single match* due to the fact that there is no guarantee that the projectile provided will be unique.
    this.recoil = function (projectile) {
        return self._getField(self.rawData, self.projectileStr, projectile);
    };

    this.getter = function (getterFunc) {
        self._getterFunc = getterFunc;
    };

    // Internal Methods
    // --------------

    // The `Cannon._fetch` method kicks off loading data for the Cannon.
    this._fetch = function (callback, projectileBlob, queryData) {
        if (self.limitInt && self.fetches++ == self.limitInt) {
            callback(projectileBlob, self);
            return;
        }else if (self.fetcherFunc) {
            self.fetcherFunc(self.fetches, self.limitInt);
        }

        queryData = queryData? queryData: {};

        if (self.url) {
            self._getterFunc(self.url, queryData, callback).then(self._processIncoming);
        }else{
            self._processIncoming(self.initialData, callback);
        }
    };

    // The `Cannon._processIncoming` processes incoming data by dealing with the `projectilePartial` received from a single HTTP request.
    this._processIncoming = function (data, callback) {
        self.rawData.push(data);
        var projectilePartial = self._getField([data], self.projectileStr);

        if (!projectileBlob) {
            projectileBlob = [];
        }

        this._each(projectilePartial, function (chunk) {
            projectileBlob.push(chunk);
        });

        this._reload(data, callback);
    };

    // The `Cannon._reload` is called after processing incoming data. If a reload can be done, then `_fetch` will be called again with the reloaded URL.
    this._reload = function (data, callback) {
        var reloaders = {};
        var hasReloader = false;

        for (var reloaderKey in self.reloaderStrs) {
            var reloader = self.reloaderStrs[reloaderKey];
            reloaders[reloaderKey] = self._getField([data], self.reloaderStrs[reloaderKey])[0];
            hasReloader = reloaders[reloaderKey] || hasReloader;
        }

        if (hasReloader && self.url) {
            setTimeout(function () {
                self._fetch(callback, projectileBlob, reloaders);
            }, self.delayInt);
        }else{
            callback(projectileBlob, self);
            return;
        }
    };

    // The `Cannon._getField` method is the method used to parse and fetch data pased on a `projectileStr` (or `fieldStr`). This method takes an array of pools, a field string, and an optional target.
    this._getField = function (pools, fieldsStr, target) {
        var fields = fieldsStr.split('.');
        var targets = [];
        var newPools = [];

        var addToPool = function (item, pool, child) {
            newPools.push(item);
            if (item == target) {
                targets.push(pool);
            }
        };

        for (var f=0; f<fields.length; f++) {
            var field = fields[f];
            newPools = [];

            for (var p=0; p<pools.length; p++) {
                var pool = pools[p];
                if (field == "*") {
                    self._each(pool, addToPool);
                }
                else if (pool[field]) {
                    newPools.push(pool[field]);
                    if (pool[field] == target) {
                        targets.push(pool);
                    }
                }
            }
            pools = newPools;
        }

        if (target) {
            return targets;
        }else{
            return pools;
        }
    };

    // The `Cannon._each` is a utility method for looping through arrays or objects.
    this._each = function (obj, func) {
        var child;
        if (self._isArray(obj)) {
            for (child=0; child<obj.length; child++) {
                func(obj[child], obj, child);
            }
        }
        else {
            for (child in obj) {
                func(obj[child], obj, child);
            }
        }
    };

    // The default `Cannon._getterFunc` method is used for requesting data via XHR.
    this._getURL =  function (src, data, userdata) {
        var deferred = self._defer();
        var queryPairs = [];
        var queryString = "";

        for (var field in data) {
            queryPairs.push(field + "=" + data[field]);
        }

        if (queryPairs.length) {
            queryString = "?" + queryPairs.join("&");
        }

        function reqListener (res) {
            deferred.resolve(JSON.parse(res.responseText || res.currentTarget.responseText), userdata   );
        }

        var xhr = new XMLHttpRequest();
        xhr.onload = reqListener;
        xhr.open("get", src + queryString, true);
        xhr.send();

        return deferred.promise;
    };

    this._getterFunc = this._getURL;

    // The `Cannon._defer` is a minimalist promise implemtation used in `Cannon._getURL`.
    this._defer = function () {
        var local = {};

        local.promise = {
            then: function (callback) {
                local.callback = callback;
                if (local.resolved) {
                    local.finish();
                }
            }
        };

        local.resolve = function () {
            local.args = arguments;
            if (local.callback) {
                local.finish();
            }
            local.resolved = true;
        };

        local.finish = function () {
            local.callback.apply(self, local.args);
        };

        return local;
    };

    this._isArray = function (obj) {
        return toString.call(obj) == '[object Array]';
    };

    return this;
};<commit_msg>Fixes getter()<commit_after>//     Cannon.js 0.0.1
//     (c) 2014 Abraham Haskins
//     Cannon may be freely distributed under the MIT license.

// Our main Cannon class which takes the URL of the data we are looking to retreive and manipulate.

// For example `new Cannon("http://api.reddit.com/user/unidan/")`
var Cannon = function (resource) {

    // Initial setup
    // --------------

    // Cannon takes a "resource" which is either a URL or an Object.
    if (typeof resource == "string") {
        this.url = resource;
    }else{
        this.url = false;
    }

    if (typeof resource == "object") {
        this.initialData = resource;
    }else{
        this.initialData = false;
    }

    // The object containing reloader information
    this.reloaderStrs = {};

    // All data loaded from URL and reloads, used for `Cannon.recoil`
    this.rawData = [];

    // The default projectile is `"*"` i.e. all children of data
    this.projectileStr = "*";

    // The total HTTP requests made, used for `Cannon.limit`
    this.fetches = 0;

    // The total HTTP requests we can make, used for `Cannon.limit`
    this.limitInt = 1;

    // The delay (in milliseconds) between each HTTP request, used for `Cannon.delay`
    this.delayInt = 0;

    // Our definition of self.
    var self = this;

    // Our projectile data.
    var projectileBlob;

    // Exported Methods
    // --------------

    // The `Cannon.fire` method is used to activate the Cannon (i.e. make HTTP requests, parse data, etc). The projectiles are passed to the callback as an array of data shaped by string passed to `Cannon.projectile`.
    this.fire = function (callback) {
        self._fetch(callback);
    };

    // The `Cannon.rapidfire` is similar to `Cannon.fire` except that instead of having the callback called once with an array containing the data, it is called once for each protectile in the array.
    this.rapidfire = function (callback) {
        self._fetch(function (projectiles) {
            self._each(projectiles, function (projectile) {
                callback(projectile);
            });
        });
    };

    // The `Cannon.reloader` method is used for dealing with paginated content. It takes an object of reloader strings like `{after: "data.after"}` and stores it in the Cannon. After the initial URL provided to the Cannon is loaded, another URL will be loaded which has the added query data `?after={{data.after}}` with the value for `data.after` being pulled from the data for the first page.
    this.reloader = function (reloaderStrs) {
        self.reloaderStrs = reloaderStrs;
        return self;
    };

    // The `Cannon.limit` method is used for limiting the number of HTTP requests when a reloader is used. Theoredically, a reloader could be used to load an infinite amount of pages, which may not be what we want. The `limit` method takes a number and will not let the total page requests from this Cannon exceed that number.
    this.limit = function (limit) {
        self.limitInt = limit;
        return self;
    };

    // The `Cannon.fetcher` adds a callback to be called as pages are being fetched. The `fetcherFunc` will be passed the current number of HTTP requests and the limit (if any).
    this.fetcher = function (fetcherFunc) {
        self.fetcherFunc = fetcherFunc;
        return self;
    };

    // The `Cannon.delay` method is used for adding a delay between HTTP requests when a reloader is used. This method takes a number of millseconds and delays each HTTP request by that amount of time.
    this.delay = function (delayInt) {
        self.delayInt = delayInt;
        return self;
    };

    // The `Cannon.projectile` method is used for defining the shape of the data passed to `Cannon.fire`'s callback. An example `projectileStr` would be `"data.children.*.data.title"`. This tells the Cannon to fire the data in `dataFromURL['data']['children'][*]['data']['title']`. This syntax should be familiar to any developer with the exception of the special `*` field. This character is the single most important concept when building a Cannon. It acts as a wild card and allows us to get a list of titles from all the children, not just a specific one.
    this.projectile = function (projectileStr) {
        self.projectileStr = projectileStr;
        return self;
    };

    // The `Cannon.recoil` method is typically used from within the `Cannon.fire` callback. This method will take a single projectile and return the parent(s) of that projectile. For example, if we have our projectile set as `"data.children.*.data.title"`, then our projectile will be the value of `title`. In order to access the complete child's data, we can `Cannon.recoil` the `title` and we will receive the `data` object which has the `title` field. It is important to note that *this method returns a list of matches, not a single match* due to the fact that there is no guarantee that the projectile provided will be unique.
    this.recoil = function (projectile) {
        return self._getField(self.rawData, self.projectileStr, projectile);
    };

    this.getter = function (getterFunc) {
        self._getterFunc = getterFunc;
        return self;
    };

    // Internal Methods
    // --------------

    // The `Cannon._fetch` method kicks off loading data for the Cannon.
    this._fetch = function (callback, projectileBlob, queryData) {
        if (self.limitInt && self.fetches++ == self.limitInt) {
            callback(projectileBlob, self);
            return;
        }else if (self.fetcherFunc) {
            self.fetcherFunc(self.fetches, self.limitInt);
        }

        queryData = queryData? queryData: {};

        if (self.url) {
            self._getterFunc(self.url, queryData, callback).then(self._processIncoming);
        }else{
            self._processIncoming(self.initialData, callback);
        }
    };

    // The `Cannon._processIncoming` processes incoming data by dealing with the `projectilePartial` received from a single HTTP request.
    this._processIncoming = function (data, callback) {
        self.rawData.push(data);
        var projectilePartial = self._getField([data], self.projectileStr);

        if (!projectileBlob) {
            projectileBlob = [];
        }

        this._each(projectilePartial, function (chunk) {
            projectileBlob.push(chunk);
        });

        this._reload(data, callback);
    };

    // The `Cannon._reload` is called after processing incoming data. If a reload can be done, then `_fetch` will be called again with the reloaded URL.
    this._reload = function (data, callback) {
        var reloaders = {};
        var hasReloader = false;

        for (var reloaderKey in self.reloaderStrs) {
            var reloader = self.reloaderStrs[reloaderKey];
            reloaders[reloaderKey] = self._getField([data], self.reloaderStrs[reloaderKey])[0];
            hasReloader = reloaders[reloaderKey] || hasReloader;
        }

        if (hasReloader && self.url) {
            setTimeout(function () {
                self._fetch(callback, projectileBlob, reloaders);
            }, self.delayInt);
        }else{
            callback(projectileBlob, self);
            return;
        }
    };

    // The `Cannon._getField` method is the method used to parse and fetch data pased on a `projectileStr` (or `fieldStr`). This method takes an array of pools, a field string, and an optional target.
    this._getField = function (pools, fieldsStr, target) {
        var fields = fieldsStr.split('.');
        var targets = [];
        var newPools = [];

        var addToPool = function (item, pool, child) {
            newPools.push(item);
            if (item == target) {
                targets.push(pool);
            }
        };

        for (var f=0; f<fields.length; f++) {
            var field = fields[f];
            newPools = [];

            for (var p=0; p<pools.length; p++) {
                var pool = pools[p];
                if (field == "*") {
                    self._each(pool, addToPool);
                }
                else if (pool[field]) {
                    newPools.push(pool[field]);
                    if (pool[field] == target) {
                        targets.push(pool);
                    }
                }
            }
            pools = newPools;
        }

        if (target) {
            return targets;
        }else{
            return pools;
        }
    };

    // The `Cannon._each` is a utility method for looping through arrays or objects.
    this._each = function (obj, func) {
        var child;
        if (self._isArray(obj)) {
            for (child=0; child<obj.length; child++) {
                func(obj[child], obj, child);
            }
        }
        else {
            for (child in obj) {
                func(obj[child], obj, child);
            }
        }
    };

    // The default `Cannon._getterFunc` method is used for requesting data via XHR.
    this._getURL =  function (src, data, userdata) {
        var deferred = self._defer();
        var queryPairs = [];
        var queryString = "";

        for (var field in data) {
            queryPairs.push(field + "=" + data[field]);
        }

        if (queryPairs.length) {
            queryString = "?" + queryPairs.join("&");
        }

        function reqListener (res) {
            deferred.resolve(JSON.parse(res.responseText || res.currentTarget.responseText), userdata   );
        }

        var xhr = new XMLHttpRequest();
        xhr.onload = reqListener;
        xhr.open("get", src + queryString, true);
        xhr.send();

        return deferred.promise;
    };

    this._getterFunc = this._getURL;

    // The `Cannon._defer` is a minimalist promise implemtation used in `Cannon._getURL`.
    this._defer = function () {
        var local = {};

        local.promise = {
            then: function (callback) {
                local.callback = callback;
                if (local.resolved) {
                    local.finish();
                }
            }
        };

        local.resolve = function () {
            local.args = arguments;
            if (local.callback) {
                local.finish();
            }
            local.resolved = true;
        };

        local.finish = function () {
            local.callback.apply(self, local.args);
        };

        return local;
    };

    this._isArray = function (obj) {
        return toString.call(obj) == '[object Array]';
    };

    return this;
};<|endoftext|>


<commit_before>/**
 * Represents a line on the canvas.
 * @constructor
 * @member {boolean} visible - if the line is visible or not
 * @member {Color} color - color of the line
 * @member {Coordinate2D} origin - coordinates of the upper left corner of the enclosing rectangle
 * @member {Coordinate2D} point1 - one endpoint of the line
 * @member {Coordinate2D} point2 - one endpoint of the line
 * @member {Animations} animations - animations for the line
 */
var Line = function (point1, point2, color, visible) {
    var coor = new Coordinate2D(Math.min(point1.x, point2.x),
                                Math.min(point1.y, point2.y));
    Element.call(this, coor, color, visible);

    this.point1 = point1;
    this.point2 = point2;
};

// inheritance
extend(Element, Line);

/**
 * Graphically drawing the line on the canvas
 * @param {HTML5 Canvas object} canvas
 */
Line.prototype.draw = function (canvas) {
    var context = Element.prototype.draw.call(this, canvas);
    if (context !== undefined) {
        context.beginPath();
        context.moveTo(this.point1.x, this.point1.y);
        context.lineTo(this.point2.x, this.point2.y);
        context.closePath();
        context.stroke();
    }
};<commit_msg>Modifies Line to be compatible with MoveAnimation<commit_after>/**
 * Represents a line on the canvas.
 * @constructor
 * @member {boolean} visible - if the line is visible or not
 * @member {Color} color - color of the line
 * @member {Coordinate2D} origin - coordinates of the upper left corner of the enclosing rectangle
 * @member {Coordinate2D} point1 - one endpoint of the line
 * @member {Coordinate2D} point2 - one endpoint of the line
 * @member {Animations} animations - animations for the line
 */
var Line = function (point1, point2, color, visible) {
    var coor = new Coordinate2D(Math.min(point1.x, point2.x),
                                Math.min(point1.y, point2.y));
    Element.call(this, coor, color, visible);

    this.delta1x = Math.abs(point1.x - coor.x);
    this.delta1y = Math.abs(point1.y - coor.y);
    this.delta2x = Math.abs(point2.x - coor.x);
    this.delta2y = Math.abs(point2.y - coor.y);
};

// inheritance
extend(Element, Line);

/**
 * Graphically drawing the line on the canvas
 * @param {HTML5 Canvas object} canvas
 */
Line.prototype.draw = function (canvas) {
    var context = Element.prototype.draw.call(this, canvas);
    if (context !== undefined) {
        context.beginPath();
        context.moveTo(this.coordinates.x + this.delta1x, this.coordinates.y + this.delta1y);
        context.lineTo(this.coordinates.x + this.delta2x, this.coordinates.y + this.delta2y);
        context.closePath();
        context.stroke();
    }
};<|endoftext|>


<commit_before>import React, {PropTypes} from 'react';
import EventListener from 'react-event-listener';
import Api from './Api';

export default class Canvas extends React.Component {

  static propTypes = {
    api: PropTypes.instanceOf(Api),
    zoom: PropTypes.number,
    fixed: PropTypes.bool,
    onFirstRender: PropTypes.func,
    snapDistance: PropTypes.number
  };

  static defaultProps = {
    fixed: false,
    zoom: 1,
    snapDistance: 5
  };

  static childContextTypes = {
    canvas: PropTypes.object,
    api: PropTypes.object
  };

  constructor(props, context) {
    super(props, context);

    this.api = props.api;
    this.api.on('selectionChange', this.handleSelectionChange);
    this.api.on('dataChange', this.handleDataChange);

    this.snaplines = {};
    this.nodes = {};

    this.state = {
      readyForRender: false,
      width: 0,
      height: 0,
      selection: this.api.selection,
      slide: this.api.slide,
      slideLeft: 0,
      slideTop: 0
    };

  }

  getChildContext() {
    return {
      canvas: this,
      api: this.props.api
    };
  }

  componentWillUnmount() {
    this.api.unbind('selectionChange', this.handleSelectionChange);
    this.api.unbind('dataChange', this.handleDataChange);
  }

  componentDidMount() {
    this.checkIfReadyForRender();
  }

  checkIfReadyForRender() {
    if (!this.state.readyForRender) {
      // check if DOM is ready to render
      const readyForRender = this.loadingNode && this.loadingNode.offsetHeight > 0;
      if (readyForRender) {
        this.setState({
          readyForRender: true
        });
      } else {
        setTimeout(() => {
          this.checkIfReadyForRender();
        }, 50);
      }
    }
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.api !== this.props.api && nextProps.api) {
      this.props.api.unbind('selectionChange', this.handleSelectionChange);
      this.props.api.unbind('dataChange', this.handleDataChange);

      nextProps.api.bind('selectionChange', this.handleSelectionChange);
      nextProps.api.bind('dataChange', this.handleDataChange);

      this.handleDataChange();
      this.handleSelectionChange();
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (!prevState.readyForRender && this.state.readyForRender) {
      this.positionSlide();
      this.props.onFirstRender && this.props.onFirstRender(this);
    } else if (this.state.readyForRender && (prevProps.zoom !== this.props.zoom || this.state.slide !== prevState.slide)) {
      this.positionSlide();
    }

    if (prevState.slideLeft !== this.state.slideLeft
      || prevState.slideTop !== this.state.slideTop
      || prevState.width !== this.state.width
      || prevState.height !== this.state.height) {
      this.props.api.triggerSelectionChange();
      this.centerContent();
    }
  }

  centerContent() {
    if (this.props.fixed) {
      const bbox = this.svgRoot.parentNode.getBoundingClientRect();
      this.wrapperNode.scrollLeft = (this.state.width - bbox.width) * 0.5;
      this.wrapperNode.scrollTop = (this.state.height - bbox.height) * 0.5;
    }
  }

  positionSlide() {
    if (this.svgRoot) {
      const bbox = this.svgRoot.parentNode.getBoundingClientRect();
      const slide = this.state.slide;
      const zoom = this.props.zoom;
      const slideWidth = slide.width * zoom;
      const slideHeight = slide.height * zoom;
      const width = Math.max(slideWidth, bbox.width - 20);
      const height = Math.max(slideHeight, bbox.height - 20);

      const slideLeft = ((width || bbox.width) - slideWidth) * 0.5;
      const slideTop = ((height || bbox.height) - slideHeight) * 0.5;
      if (this.state.width !== width || this.state.height !== height || this.state.slideLeft !== slideLeft || this.state.slideTop !== slideTop) {
        this.setState({
          slideLeft,
          slideTop,
          width,
          height
        });
      }
    }
  }

  findSnaplines(node, snaplines) {
    if (!this.possibleSnaplines) {
      return {x: 0, y: 0};
    }
    const matchingSnaplines = [];
    const matchedSnapline = {};
    const diffs = {x: 0, y: 0};
    // (this.state.snaplines || []).forEach(s => s.ref.highlight(false));
    const maxDiff = {x: 3 * this.props.zoom, y: 3 * this.props.zoom};
    (snaplines || []).forEach((tmpSnapline) => {
      tmpSnapline = matchedSnapline[tmpSnapline.mode] || tmpSnapline;
      this.possibleSnaplines.forEach((snapline) => {
        const diff = tmpSnapline.pos - snapline.pos;
        if (tmpSnapline.mode === snapline.mode && Math.abs(diff) <= maxDiff[tmpSnapline.mode]) {
          maxDiff[tmpSnapline.mode] = 0;
          matchingSnaplines.push(snapline);
          matchedSnapline[tmpSnapline.mode] = matchedSnapline[tmpSnapline.mode] || snapline;
          diffs[tmpSnapline.mode] = diffs[tmpSnapline.mode] || diff;
        }
      });
    });

    this.possibleSnaplines.map(snapline => snapline.ref).filter((elem, pos, self) => self.indexOf(elem) === pos).forEach((element) => {
      element.highlight(matchingSnaplines.find(s => s.ref === element));
    });

    this.setState({
      snaplines: matchingSnaplines
    });
    return diffs;
  }

  registerNode(node) {
    if (node.props.id) {
      this.nodes[node.props.id] = node;
    }
  }

  unregisterNode(node) {
    if (node.props.id) {
      delete this.nodes[node.props.id];
    }
  }

  clearSnaplines() {
    this.state.snaplines && this.state.snaplines.forEach(s => s.ref.highlight(false));
    this.setState({
      snaplines: null
    });
  }

  prepareSnaplines(node) {
    let possibleSnaplines = [];
    Object.keys(this.nodes).forEach((nodeId) => {
      const oNode = this.nodes[nodeId];
      if (nodeId !== node.props.id && !node.contains(oNode)) {
        possibleSnaplines = possibleSnaplines.concat(oNode.getCurrentSnaplines());
      }
    });

    this.possibleSnaplines = possibleSnaplines;
  }

  focus() {
    this.wrapperNode && this.wrapperNode.focus();
  }

  handleResize = () => {
    this._resizeTimeout && clearTimeout(this._resizeTimeout);

    this._resizeTimeout = setTimeout(() => {
      this.positionSlide();
    }, 100);
  };

  handleKeyDown = (e) => {
    const api = this.props.api;
    if (!/input|textarea/gi.test(e.target.tagName) && !e.target.getAttribute('contenteditable')) {
      if (e.which === 8 || e.which === 46) {
        api.removeSelectedElements();
        api.triggerDataChange();
        api.triggerSelectionChange();
      } else if (e.which === 90 && e.metaKey) {
        e.preventDefault();
        if (!e.shiftKey) {
          api.undo();
        } else {
          api.redo();
        }
      }
    }
  };

  handleDataChange = () => {
    this.setState({
      slide: this.api.slide,
      selection: this.api.selection
    });
  };

  handleSelectionChange = () => {
    this.setState({
      selection: this.api.selection
    });
  };

  handleRef = (ref) => {
    this.firstNode = ref;
  };

  handleMouseDown = () => {
    this.mouseDown = true;
    this.triggerSelection(null);
  };

  handleMouseUp = () => {
    this.mouseDown = false;
  };

  triggerSelection() {
    // todo: implement multiple selections
    this.props.api.selectElement(null);
  }

  handleSelectionRefs = (ref) => {
    this.selections = ref;
  };

  handleRootRef = (ref) => {
    this.svgRoot = ref;
  };

  handleSlideRef = (ref) => {
    this.slideNode = ref;
  };

  handleLoadingRef = (ref) => {
    this.loadingNode = ref;
  };

  handleWrapperRef = (ref) => {
    this.wrapperNode = ref;
  };

  render() {
    if (!this.state.readyForRender) {
      // TODO: implement loader
      return
Loading ...
;
    }

    const wrapperStyle = {width: '100%', height: '100%', overflow: this.props.fixed ? 'hidden' : 'auto', position: 'absolute'};

    return (

      <EventListener target="window" onResize={this.handleResize}/>
      <EventListener target="window" onKeyDown={this.handleKeyDown}/>
      <svg
        xmlns="http://www.w3.org/2000/svg" version="1.1"
        width={this.state.width || '100%'} height={this.state.height || '100%'}
        ref={this.handleRootRef}
        shapeRendering="geometricPrecision"
        onTouchStart={this.handleMouseDown}
        onMouseDown={this.handleMouseDown}
        onMouseUp={this.handleMouseUp}
        onTouchEnd={this.handleMouseUp}
      >

        <g ref={this.handleRef}>
          <g ref={this.handleSlideRef} transform={`translate(${this.state.slideLeft}, ${this.state.slideTop}) scale(${this.props.zoom})`}>
            <rect
              width={this.state.slide.width}
              height={this.state.slide.height}
              fill={this.state.slide.background}
            />
            <g className="slide-elements">
              {this.state.slide.elements.map((el) => {
                const factory = this.props.api.resolveFactory(el.type);
                const {id, ...other} = el;
                return React.createElement(factory, {key: id, id, ...other});
              })}
            </g>
            <g className="snaplines">
              {(this.state.snaplines || []).map((snapline, i) => {
                if (snapline.mode === 'x') {
                  return <line key={i} vectorEffect="non-scaling-stroke" strokeDasharray="5 5" stroke="blue" strokeWidth="0.8" y1={0} y2={this.state.slide.height} x1={snapline.pos} x2={snapline.pos}/>
                } else {
                  return <line key={i} vectorEffect="non-scaling-stroke" strokeDasharray="5 5" stroke="blue" strokeWidth="0.8" x1={0} x2={this.state.slide.width} y1={snapline.pos} y2={snapline.pos}/>
                }
              })}
            </g>
          </g>
          <g ref={this.handleSelectionRefs} className="slide-selections">
            {Object.keys(this.state.selection).filter(id => !!this.nodes[id]).map(id => this.nodes[id].renderSelection())}
          </g>
        </g>
      </svg>

);
  }

}
<commit_msg>always center slide after resize<commit_after>import React, {PropTypes} from 'react';
import EventListener from 'react-event-listener';
import Api from './Api';

export default class Canvas extends React.Component {

  static propTypes = {
    api: PropTypes.instanceOf(Api),
    zoom: PropTypes.number,
    fixed: PropTypes.bool,
    onFirstRender: PropTypes.func,
    snapDistance: PropTypes.number
  };

  static defaultProps = {
    fixed: false,
    zoom: 1,
    snapDistance: 5
  };

  static childContextTypes = {
    canvas: PropTypes.object,
    api: PropTypes.object
  };

  constructor(props, context) {
    super(props, context);

    this.api = props.api;
    this.api.on('selectionChange', this.handleSelectionChange);
    this.api.on('dataChange', this.handleDataChange);

    this.snaplines = {};
    this.nodes = {};

    this.state = {
      readyForRender: false,
      width: 0,
      height: 0,
      selection: this.api.selection,
      slide: this.api.slide,
      slideLeft: 0,
      slideTop: 0
    };

  }

  getChildContext() {
    return {
      canvas: this,
      api: this.props.api
    };
  }

  componentWillUnmount() {
    this.api.unbind('selectionChange', this.handleSelectionChange);
    this.api.unbind('dataChange', this.handleDataChange);
  }

  componentDidMount() {
    this.checkIfReadyForRender();
  }

  checkIfReadyForRender() {
    if (!this.state.readyForRender) {
      // check if DOM is ready to render
      const readyForRender = this.loadingNode && this.loadingNode.offsetHeight > 0;
      if (readyForRender) {
        this.setState({
          readyForRender: true
        });
      } else {
        setTimeout(() => {
          this.checkIfReadyForRender();
        }, 50);
      }
    }
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.api !== this.props.api && nextProps.api) {
      this.props.api.unbind('selectionChange', this.handleSelectionChange);
      this.props.api.unbind('dataChange', this.handleDataChange);

      nextProps.api.bind('selectionChange', this.handleSelectionChange);
      nextProps.api.bind('dataChange', this.handleDataChange);

      this.handleDataChange();
      this.handleSelectionChange();
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (!prevState.readyForRender && this.state.readyForRender) {
      this.positionSlide();
      this.props.onFirstRender && this.props.onFirstRender(this);
    } else if (this.state.readyForRender && (prevProps.zoom !== this.props.zoom || this.state.slide !== prevState.slide)) {
      this.positionSlide();
    }

    if (prevState.slideLeft !== this.state.slideLeft
      || prevState.slideTop !== this.state.slideTop
      || prevState.width !== this.state.width
      || prevState.height !== this.state.height) {
      this.props.api.triggerSelectionChange();
      this.centerContent();
    }
  }

  centerContent() {
    const bbox = this.svgRoot.parentNode.getBoundingClientRect();
    this.wrapperNode.scrollLeft = (this.state.width - bbox.width) * 0.5;
    this.wrapperNode.scrollTop = (this.state.height - bbox.height) * 0.5;
  }

  positionSlide() {
    if (this.svgRoot) {
      const bbox = this.svgRoot.parentNode.getBoundingClientRect();
      const slide = this.state.slide;
      const zoom = this.props.zoom;
      const slideWidth = slide.width * zoom;
      const slideHeight = slide.height * zoom;
      const width = Math.max(slideWidth, bbox.width - 20);
      const height = Math.max(slideHeight, bbox.height - 20);

      const slideLeft = ((width || bbox.width) - slideWidth) * 0.5;
      const slideTop = ((height || bbox.height) - slideHeight) * 0.5;
      if (this.state.width !== width || this.state.height !== height || this.state.slideLeft !== slideLeft || this.state.slideTop !== slideTop) {
        this.setState({
          slideLeft,
          slideTop,
          width,
          height
        });
      }
    }
  }

  findSnaplines(node, snaplines) {
    if (!this.possibleSnaplines) {
      return {x: 0, y: 0};
    }
    const matchingSnaplines = [];
    const matchedSnapline = {};
    const diffs = {x: 0, y: 0};
    // (this.state.snaplines || []).forEach(s => s.ref.highlight(false));
    const maxDiff = {x: 3 * this.props.zoom, y: 3 * this.props.zoom};
    (snaplines || []).forEach((tmpSnapline) => {
      tmpSnapline = matchedSnapline[tmpSnapline.mode] || tmpSnapline;
      this.possibleSnaplines.forEach((snapline) => {
        const diff = tmpSnapline.pos - snapline.pos;
        if (tmpSnapline.mode === snapline.mode && Math.abs(diff) <= maxDiff[tmpSnapline.mode]) {
          maxDiff[tmpSnapline.mode] = 0;
          matchingSnaplines.push(snapline);
          matchedSnapline[tmpSnapline.mode] = matchedSnapline[tmpSnapline.mode] || snapline;
          diffs[tmpSnapline.mode] = diffs[tmpSnapline.mode] || diff;
        }
      });
    });

    this.possibleSnaplines.map(snapline => snapline.ref).filter((elem, pos, self) => self.indexOf(elem) === pos).forEach((element) => {
      element.highlight(matchingSnaplines.find(s => s.ref === element));
    });

    this.setState({
      snaplines: matchingSnaplines
    });
    return diffs;
  }

  registerNode(node) {
    if (node.props.id) {
      this.nodes[node.props.id] = node;
    }
  }

  unregisterNode(node) {
    if (node.props.id) {
      delete this.nodes[node.props.id];
    }
  }

  clearSnaplines() {
    this.state.snaplines && this.state.snaplines.forEach(s => s.ref.highlight(false));
    this.setState({
      snaplines: null
    });
  }

  prepareSnaplines(node) {
    let possibleSnaplines = [];
    Object.keys(this.nodes).forEach((nodeId) => {
      const oNode = this.nodes[nodeId];
      if (nodeId !== node.props.id && !node.contains(oNode)) {
        possibleSnaplines = possibleSnaplines.concat(oNode.getCurrentSnaplines());
      }
    });

    this.possibleSnaplines = possibleSnaplines;
  }

  focus() {
    this.wrapperNode && this.wrapperNode.focus();
  }

  handleResize = () => {
    this._resizeTimeout && clearTimeout(this._resizeTimeout);

    this._resizeTimeout = setTimeout(() => {
      this.positionSlide();
    }, 100);
  };

  handleKeyDown = (e) => {
    const api = this.props.api;
    if (!/input|textarea/gi.test(e.target.tagName) && !e.target.getAttribute('contenteditable')) {
      if (e.which === 8 || e.which === 46) {
        api.removeSelectedElements();
        api.triggerDataChange();
        api.triggerSelectionChange();
      } else if (e.which === 90 && e.metaKey) {
        e.preventDefault();
        if (!e.shiftKey) {
          api.undo();
        } else {
          api.redo();
        }
      }
    }
  };

  handleDataChange = () => {
    this.setState({
      slide: this.api.slide,
      selection: this.api.selection
    });
  };

  handleSelectionChange = () => {
    this.setState({
      selection: this.api.selection
    });
  };

  handleRef = (ref) => {
    this.firstNode = ref;
  };

  handleMouseDown = () => {
    this.mouseDown = true;
    this.triggerSelection(null);
  };

  handleMouseUp = () => {
    this.mouseDown = false;
  };

  triggerSelection() {
    // todo: implement multiple selections
    this.props.api.selectElement(null);
  }

  handleSelectionRefs = (ref) => {
    this.selections = ref;
  };

  handleRootRef = (ref) => {
    this.svgRoot = ref;
  };

  handleSlideRef = (ref) => {
    this.slideNode = ref;
  };

  handleLoadingRef = (ref) => {
    this.loadingNode = ref;
  };

  handleWrapperRef = (ref) => {
    this.wrapperNode = ref;
  };

  render() {
    if (!this.state.readyForRender) {
      // TODO: implement loader
      return
Loading ...
;
    }

    const wrapperStyle = {width: '100%', height: '100%', overflow: this.props.fixed ? 'hidden' : 'auto', position: 'absolute'};

    return (

      <EventListener target="window" onResize={this.handleResize}/>
      <EventListener target="window" onKeyDown={this.handleKeyDown}/>
      <svg
        xmlns="http://www.w3.org/2000/svg" version="1.1"
        width={this.state.width || '100%'} height={this.state.height || '100%'}
        ref={this.handleRootRef}
        shapeRendering="geometricPrecision"
        onTouchStart={this.handleMouseDown}
        onMouseDown={this.handleMouseDown}
        onMouseUp={this.handleMouseUp}
        onTouchEnd={this.handleMouseUp}
      >

        <g ref={this.handleRef}>
          <g ref={this.handleSlideRef} transform={`translate(${this.state.slideLeft}, ${this.state.slideTop}) scale(${this.props.zoom})`}>
            <rect
              width={this.state.slide.width}
              height={this.state.slide.height}
              fill={this.state.slide.background}
            />
            <g className="slide-elements">
              {this.state.slide.elements.map((el) => {
                const factory = this.props.api.resolveFactory(el.type);
                const {id, ...other} = el;
                return React.createElement(factory, {key: id, id, ...other});
              })}
            </g>
            <g className="snaplines">
              {(this.state.snaplines || []).map((snapline, i) => {
                if (snapline.mode === 'x') {
                  return <line key={i} vectorEffect="non-scaling-stroke" strokeDasharray="5 5" stroke="blue" strokeWidth="0.8" y1={0} y2={this.state.slide.height} x1={snapline.pos} x2={snapline.pos}/>
                } else {
                  return <line key={i} vectorEffect="non-scaling-stroke" strokeDasharray="5 5" stroke="blue" strokeWidth="0.8" x1={0} x2={this.state.slide.width} y1={snapline.pos} y2={snapline.pos}/>
                }
              })}
            </g>
          </g>
          <g ref={this.handleSelectionRefs} className="slide-selections">
            {Object.keys(this.state.selection).filter(id => !!this.nodes[id]).map(id => this.nodes[id].renderSelection())}
          </g>
        </g>
      </svg>

);
  }

}
<|endoftext|>


<commit_before>var React = require('react');

var styles = {
	string: {
		color: '#0e4889',
		cursor: 'default'
	},
	bool: {
		color: '#06624b',
		cursor: 'default',
		fontStyle: 'italic'
	},
	number: {
		color: '#ca000a',
		cursor: 'default'
	},
	date: {
		color: '#009f7b',
		cursor: 'default'
	},
	empty: {
		color: '#999999',
		cursor: 'default'
	},
	array: {
		color: '#666666',
		cursor: 'default'
	},
	object: {
		color: '#0b89b6',
		cursor: 'default'
	}
};

function transform(obj, fromRecur) {

	var tag = (fromRecur) ? 'span' : 'div';
	var nextLevel = (fromRecur || 0) + 1;
	var children = [];

	// strings
	if (typeof obj === 'string') {
		return React.createElement(tag, { style: styles.string }, obj);
	}
	// booleans, null and undefined
	else if (typeof obj === 'boolean' || obj === null || obj === undefined) {
		return React.createElement(tag, { style: styles.bool }, String(obj));
	}
	// numbers
	else if (typeof obj === 'number') {
		return React.createElement(tag, { style: styles.number }, String(obj));
	}
	// dates
	else if (Object.prototype.toString.call(obj) === '[object Date]') {
		return React.createElement(tag, { style: styles.date }, String(obj));
	}
	// arrays
	else if (Array.isArray(obj)) {

		if (!obj.length) {
			return React.createElement(tag, { style: styles.empty }, 'Array: []');
		}

		children.push(React.createElement(tag, { key: '__array:open__', style: styles.array }, 'Array: ['));

		// rtn += '</' + tag + '>
';
		//
		for (var i = 0; i < obj.length; i++) {
			children.push(

					{transform(obj[i], nextLevel)}
					{(i < obj.length - 1) ? ',' : null}

			);
		}

		children.push(React.createElement(tag, { key: '__array:close__', style: styles.array }, ']'));

	}
	// objects
	else if (obj && typeof obj === 'object') {

		var len = Object.keys(obj).length;

		if (fromRecur && !len) {
			return React.createElement(tag, { style: styles.empty }, 'Object: {}');
		}

		if (fromRecur) {
			children.push(React.createElement(tag, { key: '__object:open__', style: styles.object }, 'Object: {'));
		}

		for (var key in obj) {
			if (typeof obj[key] !== 'function') {
				children.push(

						{key}:
						{transform(obj[key], nextLevel)}

				);
			}
		}

		if (fromRecur) {
			children.push(React.createElement(tag, { key: '__object:close__', style: styles.object }, '}'));
		}

	}

	return
{children}
;

}

var DOMify = React.createClass({
	render () {
		return transform(this.props.value);
	}
});

module.exports = DOMify;
<commit_msg>Better comma support for Arrays with nested Objects<commit_after>var React = require('react');

var styles = {
	string: {
		color: '#0e4889',
		cursor: 'default'
	},
	bool: {
		color: '#06624b',
		cursor: 'default',
		fontStyle: 'italic'
	},
	number: {
		color: '#ca000a',
		cursor: 'default'
	},
	date: {
		color: '#009f7b',
		cursor: 'default'
	},
	empty: {
		color: '#999999',
		cursor: 'default'
	},
	array: {
		color: '#666666',
		cursor: 'default'
	},
	object: {
		color: '#0b89b6',
		cursor: 'default'
	},
	comma: {
		color: '#999999',
		cursor: 'default'
	}
};

function transform(obj, fromRecur, comma) {

	var tag = (fromRecur) ? 'span' : 'div';
	var nextLevel = (fromRecur || 0) + 1;
	var children = [];
	comma = comma ? , : null;

	// strings
	if (typeof obj === 'string') {
		return React.createElement(tag, { style: styles.string }, obj, comma);
	}
	// booleans, null and undefined
	else if (typeof obj === 'boolean' || obj === null || obj === undefined) {
		return React.createElement(tag, { style: styles.bool }, String(obj), comma);
	}
	// numbers
	else if (typeof obj === 'number') {
		return React.createElement(tag, { style: styles.number }, String(obj), comma);
	}
	// dates
	else if (Object.prototype.toString.call(obj) === '[object Date]') {
		return React.createElement(tag, { style: styles.date }, String(obj), comma);
	}
	// arrays
	else if (Array.isArray(obj)) {

		if (!obj.length) {
			return React.createElement(tag, { style: styles.empty }, 'Array: []');
		}

		children.push(React.createElement(tag, { key: '__array:open__', style: styles.array }, 'Array: ['));

		for (var i = 0; i < obj.length; i++) {
			children.push(

					{transform(obj[i], nextLevel, i < obj.length - 1)}

			);
		}

		children.push(React.createElement(tag, { key: '__array:close__', style: styles.array }, ']'));

	}
	// objects
	else if (obj && typeof obj === 'object') {

		var len = Object.keys(obj).length;

		if (fromRecur && !len) {
			return React.createElement(tag, { style: styles.empty }, 'Object: {}');
		}

		if (fromRecur) {
			children.push(React.createElement(tag, { key: '__object:open__', style: styles.object }, 'Object: {'));
		}

		for (var key in obj) {
			if (typeof obj[key] !== 'function') {
				children.push(

						{key}:
						{transform(obj[key], nextLevel)}

				);
			}
		}

		if (fromRecur) {
			children.push(React.createElement(tag, { key: '__object:close__', style: styles.object }, '}'));
		}

	}

	return
{children}{comma}
;

}

var DOMify = React.createClass({
	render () {
		return transform(this.props.value);
	}
});

module.exports = DOMify;
<|endoftext|>


<commit_before>import React, { Component } from 'react';

export default class Footer extends Component {
  trackDonate() {
    mixpanel.track('Clicked Donate');
  }

  render() {
    let styleObj = {
      textAlign: 'center',
    };

    let mixPanelImg = {
      margin: 'auto',
      paddingRight: '30px'
    };

    return (

            Feedback

          Brought to you by Brian, Rico and Susan

    )
  }
};
<commit_msg>Footer updated<commit_after>import React, { Component } from 'react';

export default class Footer extends Component {
  trackDonate() {
    mixpanel.track('Clicked Donate');
  }

  render() {
    let styleObj = {
      textAlign: 'center',
      margin: 'auto'
    };

    let mixPanelImg = {
      margin: 'right',
      paddingRight: '30px'
    };

    return (

            Feedback

          Brought to you by Brian, Rico and Susan

    )
  }
};
<|endoftext|>