<commit_before>import React, {PropTypes} from 'react'
import {Button, Alert, HelpBlock} from 'react-bootstrap'
import DatePicker from 'react-bootstrap-date-picker'
import autobind from 'autobind-decorator'

import ValidatableFormWrapper from 'components/ValidatableFormWrapper'
import Form from 'components/Form'
import Fieldset from 'components/Fieldset'
import Messages from 'components/Messages'
import TextEditor from 'components/TextEditor'
import History from 'components/History'
import ButtonToggleGroup from 'components/ButtonToggleGroup'

import API from 'api'
import dict from 'dictionary'
import {Person} from 'models'

import CALENDAR_ICON from 'resources/calendar.png'
import { Col, ControlLabel, FormGroup } from 'react-bootstrap'
import '../../components/NameInput.css'

const WILDCARD = '*'

export default class PersonForm extends ValidatableFormWrapper {
	static propTypes = {
		person: PropTypes.object.isRequired,
		edit: PropTypes.bool,
		legendText: PropTypes.string,
		saveText: PropTypes.string,
	}

	static contextTypes = {
		app: PropTypes.object.isRequired,
		currentUser: PropTypes.object.isRequired,
	}

	constructor(props) {
		super(props)
		this.state = {
			person: null,
			error: null,
			originalStatus: props.person.status,
		}
	}

	countries = person => {
		switch(person.role) {
			case Person.ROLE.ADVISOR:
				return this.lookupCountries('countries')
			case Person.ROLE.PRINCIPAL:
				return this.lookupCountries('principal_countries')
			default:
				return []
		}
	}

	lookupCountries = key => {
		return dict.lookup(key) || []
	}

	renderCountrySelectOptions = (countries) => {
		return countries.map(country =>
			<option key={country} value={country}>{country}</option>
		)
	}

	render() {
		if (this.state.person === null) return null
		const { person } = this.state
		const { edit } = this.props
		const isAdvisor = person.isAdvisor()
		const legendText = this.props.legendText || (edit ? `Edit Person ${person.name}` : 'Create a new Person')

		const {ValidatableForm, RequiredField} = this

		let willAutoKickPosition = person.status === 'INACTIVE' && person.position && !!person.position.id
		let warnDomainUsername = person.status === 'INACTIVE' && person.domainUsername
		let ranks = dict.lookup('ranks') || []

		const countries = this.countries(person)
		const nationalityDefaultValue = countries.length === 1 ? countries[0] : ''

		let currentUser = this.context.currentUser
		let isAdmin = currentUser && currentUser.isAdmin()
		let disableStatusChange = this.state.originalStatus === 'INACTIVE' || Person.isEqual(currentUser, person)

		return <ValidatableForm formFor={person} onChange={this.onChange} onSubmit={this.onSubmit} horizontal
			submitText={this.props.saveText || 'Save person'}>

			<Messages error={this.state.error} />

			<Fieldset title={legendText}>
				<FormGroup>
					<Col sm={2} componentClass={ControlLabel}>Name</Col>
					<Col sm={8}>
						<Col sm={5}>
							<RequiredField
								id="lastName"
								type="text"
								display="inline"
								placeholder="LAST NAME"
								onChange={this.handleOnChangeLastName}
								onKeyDown={this.handleOnKeyDown}
								/>
						</Col>
						<Col sm={1} className="name-input">,</Col>
						<Col sm={6}>
							<RequiredField
								id="firstName"
								type="text"
								display="inline"
								placeholder="First name(s)"
								onChange={this.handleOnChangeFirstName}
								/>
						</Col>
						<RequiredField className="hidden" id="name" value={this.fullName(this.state.person)} />
					</Col>
				</FormGroup>

				{edit ?
					<Form.Field type="static" id="role" value={person.humanNameOfRole()} />
					:
					<Form.Field id="role">
						<ButtonToggleGroup>
							<Button id="roleAdvisorButton" disabled={!isAdmin} value={Person.ROLE.ADVISOR}>{dict.lookup('ADVISOR_PERSON_TITLE')}</Button>
							<Button id="rolePrincipalButton" value={Person.ROLE.PRINCIPAL}>{dict.lookup('PRINCIPAL_PERSON_TITLE')}</Button>
						</ButtonToggleGroup>
					</Form.Field>
				}

				{disableStatusChange ?
					<Form.Field type="static" id="status" value={person.humanNameOfStatus()} />
					:
					person.isNewUser() ?
						<Form.Field type="static" id="status" value="New user" />
						:
						<Form.Field id="status" >
							<ButtonToggleGroup>
								<Button id="statusActiveButton" value="ACTIVE">Active</Button>
								<Button id="statusInactiveButton" value="INACTIVE">Inactive</Button>
							</ButtonToggleGroup>

							{willAutoKickPosition && <HelpBlock>
								Setting this person to inactive will automatically remove them from the {person.position.name} position.
							</HelpBlock> }

							{warnDomainUsername && <HelpBlock>
								Setting this person to inactive means the next person to logon with the user name {person.domainUsername} will have to create a new profile. Do you want the next person to login with this user name to create a new profile?
							</HelpBlock> }
						</Form.Field>
				}

				{!edit && isAdvisor &&
					<Alert bsStyle="warning">
						Creating a {dict.lookup('ADVISOR_PERSON_TITLE')} in ANET could result in duplicate accounts if this person logs in later. If you notice duplicate accounts, please contact an ANET administrator.
					</Alert>
				}
			</Fieldset>

			<Fieldset title="Additional information">
				<RequiredField id="emailAddress" label="Email" required={isAdvisor}
					humanName="Valid email address"
					type="email"
					validate={ this.handleEmailValidation } />
				<Form.Field id="phoneNumber" label="Phone" />
				<RequiredField id="rank"  componentClass="select"
					required={isAdvisor}>

					<option />
					{ranks.map(rank =>
						<option key={rank} value={rank}>{rank}</option>
					)}
				</RequiredField>

				<RequiredField id="gender" componentClass="select"
					required={isAdvisor}>
					<option />
					<option value="MALE" >Male</option>
					<option value="FEMALE" >Female</option>
				</RequiredField>

				<RequiredField id="country" label="Nationality" componentClass="select"
					value={nationalityDefaultValue}
					required={isAdvisor}>
					<option />
					{this.renderCountrySelectOptions(countries)}
				</RequiredField>

				<Form.Field id="endOfTourDate" label="End of tour" addon={CALENDAR_ICON}>
					<DatePicker placeholder="End of Tour Date" dateFormat="DD/MM/YYYY" showClearButton={false} />
				</Form.Field>

				<Form.Field id="biography" componentClass={TextEditor} className="biography" />
			</Fieldset>
		</ValidatableForm>
	}

	componentWillReceiveProps(nextProps) {
		const { person } = nextProps
		const emptyName = { lastName: '', firstName: ''}

		const parsedName = person.name ? this.parseFullName(person.name) : emptyName

		this.savePersonWithFullName(person, parsedName)
	}

	savePersonWithFullName(person, editName) {
		if (editName.lastName) { person.lastName = editName.lastName }
		if (editName.firstName) { person.firstName = editName.firstName }

		person.name = this.fullName(person)

		this.setState({ person })
	}

	handleOnKeyDown = (event) => {
		if (event.key === ',') {
			let nameInput = document.getElementById("firstName")
			nameInput.focus()
		}
	}

	handleOnChangeLastName = (event) => {
		const value = event.target.value
		const { person } = this.state

		this.savePersonWithFullName(person, { lastName: value.toUpperCase() })
	}

	handleOnChangeFirstName = (event) => {
		const { person } = this.state
		const target = event.target
		const value = target.value

		if (value[0] === ',') target.value = ''
		this.savePersonWithFullName(person, { firstName: value })
	}

	fullName = (person) => {
		if (person.lastName && person.firstName) {
			return(`${person.lastName.trim()}, ${person.firstName.trim()}`)
		}
	}

	parseFullName = (name) => {
		const delimiter = name.indexOf(',')
		let lastName = name
		let firstName = ''

		if(delimiter > -1) {
			lastName = name.substring(0, delimiter)
			firstName = name.substring(delimiter + 1, name.length)
		}

		return(
			{
				lastName: lastName.trim().toUpperCase(),
				firstName: firstName.trim()
			}
		)
	}

	@autobind
	onChange() {
		this.forceUpdate()
	}

	@autobind
	handleEmailValidation(value) {
		let domainNames = dict.lookup('domainNames')
		if (!this.props.person.isAdvisor() || domainNames.length === 0) {
			return { isValid: null, message: 'No custom validator is set' }
		}

		let wildcardDomains = this.getWildcardDomains(domainNames, WILDCARD)
		let isValid = this.validateEmail(value, domainNames, wildcardDomains)

		return { isValid: isValid, message: this.emailErrorMessage(domainNames) }
	}

	validateEmail(emailValue, domainNames, wildcardDomains) {
		let email = emailValue.split('@')
		let from =  email[0].trim()
		let domain = email[1]
		return (
			this.validateWithWhitelist(from, domain, domainNames) ||
			this.validateWithWildcard(domain, wildcardDomains)
		)
	}

	validateWithWhitelist(from, domain, whitelist) {
		return from.length > 0 && whitelist.includes(domain)
	}

	validateWithWildcard(domain, wildcardDomains) {
		let isValid = false
		if (domain) {
			isValid = wildcardDomains.some(wildcard => {
				return domain[0] !== '.' && domain.endsWith(wildcard.substr(1))
			})
		}
		return isValid
	}

	getWildcardDomains(domainList, token) {
		let wildcardDomains = domainList.filter(domain => {
			return domain[0] === token
		})
		return wildcardDomains
	}

	emailErrorMessage(validDomainNames) {
		let items = validDomainNames.map(name => [

{name}

		])
		return (

Email address is invalid, only the following email domain names are allowed

{items}

		)
	}

	@autobind
	onSubmit(event) {
		const { edit } = this.props
		let { person } = this.state
		let isFirstTimeUser = false
		if (person.isNewUser()) {
			isFirstTimeUser = true
			person.status = 'ACTIVE'
		}

		// Clean up person object for JSON response
		person = Object.without(person, 'firstName', 'lastName')

		let url = `/api/people/${edit ? 'update' : 'new'}`
		API.send(url, person, {disableSubmits: true})
			.then(response => {
				if (response.code) {
					throw response.code
				}

				if (isFirstTimeUser) {
					localStorage.clear()
					localStorage.newUser = 'true'
					this.context.app.loadData()
					History.push('/', {skipPageLeaveWarning: true})
				} else {
					if (response.id) {
						person.id = response.id
					}

					History.replace(Person.pathForEdit(person), false)
					History.push(Person.pathFor(person), {success: 'Person saved successfully', skipPageLeaveWarning: true})
				}
			}).catch(error => {
				this.setState({error: error})
				window.scrollTo(0, 0)
			})
	}
}
<commit_msg>NCI-Agency/anet#201-fix Remove required input for firstname<commit_after>import React, {PropTypes} from 'react'
import {Button, Alert, HelpBlock} from 'react-bootstrap'
import DatePicker from 'react-bootstrap-date-picker'
import autobind from 'autobind-decorator'

import ValidatableFormWrapper from 'components/ValidatableFormWrapper'
import Form from 'components/Form'
import Fieldset from 'components/Fieldset'
import Messages from 'components/Messages'
import TextEditor from 'components/TextEditor'
import History from 'components/History'
import ButtonToggleGroup from 'components/ButtonToggleGroup'

import API from 'api'
import dict from 'dictionary'
import {Person} from 'models'

import CALENDAR_ICON from 'resources/calendar.png'
import { Col, ControlLabel, FormGroup } from 'react-bootstrap'
import '../../components/NameInput.css'

const WILDCARD = '*'

export default class PersonForm extends ValidatableFormWrapper {
	static propTypes = {
		person: PropTypes.object.isRequired,
		edit: PropTypes.bool,
		legendText: PropTypes.string,
		saveText: PropTypes.string,
	}

	static contextTypes = {
		app: PropTypes.object.isRequired,
		currentUser: PropTypes.object.isRequired,
	}

	constructor(props) {
		super(props)
		this.state = {
			person: null,
			error: null,
			originalStatus: props.person.status,
		}
	}

	countries = person => {
		switch(person.role) {
			case Person.ROLE.ADVISOR:
				return this.lookupCountries('countries')
			case Person.ROLE.PRINCIPAL:
				return this.lookupCountries('principal_countries')
			default:
				return []
		}
	}

	lookupCountries = key => {
		return dict.lookup(key) || []
	}

	renderCountrySelectOptions = (countries) => {
		return countries.map(country =>
			<option key={country} value={country}>{country}</option>
		)
	}

	render() {
		if (this.state.person === null) return null
		const { person } = this.state
		const { edit } = this.props
		const isAdvisor = person.isAdvisor()
		const legendText = this.props.legendText || (edit ? `Edit Person ${person.name}` : 'Create a new Person')

		const {ValidatableForm, RequiredField} = this

		let willAutoKickPosition = person.status === 'INACTIVE' && person.position && !!person.position.id
		let warnDomainUsername = person.status === 'INACTIVE' && person.domainUsername
		let ranks = dict.lookup('ranks') || []

		const countries = this.countries(person)
		const nationalityDefaultValue = countries.length === 1 ? countries[0] : ''

		let currentUser = this.context.currentUser
		let isAdmin = currentUser && currentUser.isAdmin()
		let disableStatusChange = this.state.originalStatus === 'INACTIVE' || Person.isEqual(currentUser, person)

		return <ValidatableForm formFor={person} onChange={this.onChange} onSubmit={this.onSubmit} horizontal
			submitText={this.props.saveText || 'Save person'}>

			<Messages error={this.state.error} />

			<Fieldset title={legendText}>
				<FormGroup>
					<Col sm={2} componentClass={ControlLabel}>Name</Col>
					<Col sm={8}>
						<Col sm={5}>
							<RequiredField
								id="lastName"
								type="text"
								display="inline"
								placeholder="LAST NAME"
								onChange={this.handleOnChangeLastName}
								onKeyDown={this.handleOnKeyDown}
								/>
						</Col>
						<Col sm={1} className="name-input">,</Col>
						<Col sm={6}>
							<Form.Field
								id="firstName"
								type="text"
								display="inline"
								placeholder="First name(s)"
								onChange={this.handleOnChangeFirstName}
								/>
						</Col>
						<RequiredField className="hidden" id="name" value={this.fullName(this.state.person)} />
					</Col>
				</FormGroup>

				{edit ?
					<Form.Field type="static" id="role" value={person.humanNameOfRole()} />
					:
					<Form.Field id="role">
						<ButtonToggleGroup>
							<Button id="roleAdvisorButton" disabled={!isAdmin} value={Person.ROLE.ADVISOR}>{dict.lookup('ADVISOR_PERSON_TITLE')}</Button>
							<Button id="rolePrincipalButton" value={Person.ROLE.PRINCIPAL}>{dict.lookup('PRINCIPAL_PERSON_TITLE')}</Button>
						</ButtonToggleGroup>
					</Form.Field>
				}

				{disableStatusChange ?
					<Form.Field type="static" id="status" value={person.humanNameOfStatus()} />
					:
					person.isNewUser() ?
						<Form.Field type="static" id="status" value="New user" />
						:
						<Form.Field id="status" >
							<ButtonToggleGroup>
								<Button id="statusActiveButton" value="ACTIVE">Active</Button>
								<Button id="statusInactiveButton" value="INACTIVE">Inactive</Button>
							</ButtonToggleGroup>

							{willAutoKickPosition && <HelpBlock>
								Setting this person to inactive will automatically remove them from the {person.position.name} position.
							</HelpBlock> }

							{warnDomainUsername && <HelpBlock>
								Setting this person to inactive means the next person to logon with the user name {person.domainUsername} will have to create a new profile. Do you want the next person to login with this user name to create a new profile?
							</HelpBlock> }
						</Form.Field>
				}

				{!edit && isAdvisor &&
					<Alert bsStyle="warning">
						Creating a {dict.lookup('ADVISOR_PERSON_TITLE')} in ANET could result in duplicate accounts if this person logs in later. If you notice duplicate accounts, please contact an ANET administrator.
					</Alert>
				}
			</Fieldset>

			<Fieldset title="Additional information">
				<RequiredField id="emailAddress" label="Email" required={isAdvisor}
					humanName="Valid email address"
					type="email"
					validate={ this.handleEmailValidation } />
				<Form.Field id="phoneNumber" label="Phone" />
				<RequiredField id="rank"  componentClass="select"
					required={isAdvisor}>

					<option />
					{ranks.map(rank =>
						<option key={rank} value={rank}>{rank}</option>
					)}
				</RequiredField>

				<RequiredField id="gender" componentClass="select"
					required={isAdvisor}>
					<option />
					<option value="MALE" >Male</option>
					<option value="FEMALE" >Female</option>
				</RequiredField>

				<RequiredField id="country" label="Nationality" componentClass="select"
					value={nationalityDefaultValue}
					required={isAdvisor}>
					<option />
					{this.renderCountrySelectOptions(countries)}
				</RequiredField>

				<Form.Field id="endOfTourDate" label="End of tour" addon={CALENDAR_ICON}>
					<DatePicker placeholder="End of Tour Date" dateFormat="DD/MM/YYYY" showClearButton={false} />
				</Form.Field>

				<Form.Field id="biography" componentClass={TextEditor} className="biography" />
			</Fieldset>
		</ValidatableForm>
	}

	componentWillReceiveProps(nextProps) {
		const { person } = nextProps
		const emptyName = { lastName: '', firstName: ''}

		const parsedName = person.name ? this.parseFullName(person.name) : emptyName

		this.savePersonWithFullName(person, parsedName)
	}

	savePersonWithFullName(person, editName) {
		if (editName.lastName) { person.lastName = editName.lastName }
		if (editName.firstName) { person.firstName = editName.firstName }

		person.name = this.fullName(person)
		this.setState({ person })
	}

	handleOnKeyDown = (event) => {
		if (event.key === ',') {
			let nameInput = document.getElementById("firstName")
			nameInput.focus()
		}
	}

	handleOnChangeLastName = (event) => {
		const value = event.target.value
		const { person } = this.state

		this.savePersonWithFullName(person, { lastName: value.toUpperCase() })
	}

	handleOnChangeFirstName = (event) => {
		const { person } = this.state
		const target = event.target
		const value = target.value

		if (value[0] === ',') target.value = ''
		this.savePersonWithFullName(person, { firstName: value })
	}

	fullName = (person) => {
		if (person.lastName && person.firstName) {
			return(`${person.lastName.trim()}, ${person.firstName.trim()}`)
		}
		else if (person.lastName) {
			return person.lastName.trim()
		}
		else {
			return ''
		}
	}

	parseFullName = (name) => {
		const delimiter = name.indexOf(',')
		let lastName = name
		let firstName = ''

		if(delimiter > -1) {
			lastName = name.substring(0, delimiter)
			firstName = name.substring(delimiter + 1, name.length)
		}

		return(
			{
				lastName: lastName.trim().toUpperCase(),
				firstName: firstName.trim()
			}
		)
	}

	@autobind
	onChange() {
		this.forceUpdate()
	}

	@autobind
	handleEmailValidation(value) {
		let domainNames = dict.lookup('domainNames')
		if (!this.props.person.isAdvisor() || domainNames.length === 0) {
			return { isValid: null, message: 'No custom validator is set' }
		}

		let wildcardDomains = this.getWildcardDomains(domainNames, WILDCARD)
		let isValid = this.validateEmail(value, domainNames, wildcardDomains)

		return { isValid: isValid, message: this.emailErrorMessage(domainNames) }
	}

	validateEmail(emailValue, domainNames, wildcardDomains) {
		let email = emailValue.split('@')
		let from =  email[0].trim()
		let domain = email[1]
		return (
			this.validateWithWhitelist(from, domain, domainNames) ||
			this.validateWithWildcard(domain, wildcardDomains)
		)
	}

	validateWithWhitelist(from, domain, whitelist) {
		return from.length > 0 && whitelist.includes(domain)
	}

	validateWithWildcard(domain, wildcardDomains) {
		let isValid = false
		if (domain) {
			isValid = wildcardDomains.some(wildcard => {
				return domain[0] !== '.' && domain.endsWith(wildcard.substr(1))
			})
		}
		return isValid
	}

	getWildcardDomains(domainList, token) {
		let wildcardDomains = domainList.filter(domain => {
			return domain[0] === token
		})
		return wildcardDomains
	}

	emailErrorMessage(validDomainNames) {
		let items = validDomainNames.map(name => [

{name}

		])
		return (

Email address is invalid, only the following email domain names are allowed

{items}

		)
	}

	@autobind
	onSubmit(event) {
		const { edit } = this.props
		let { person } = this.state
		let isFirstTimeUser = false
		if (person.isNewUser()) {
			isFirstTimeUser = true
			person.status = 'ACTIVE'
		}

		// Clean up person object for JSON response
		person = Object.without(person, 'firstName', 'lastName')

		let url = `/api/people/${edit ? 'update' : 'new'}`
		API.send(url, person, {disableSubmits: true})
			.then(response => {
				if (response.code) {
					throw response.code
				}

				if (isFirstTimeUser) {
					localStorage.clear()
					localStorage.newUser = 'true'
					this.context.app.loadData()
					History.push('/', {skipPageLeaveWarning: true})
				} else {
					if (response.id) {
						person.id = response.id
					}

					History.replace(Person.pathForEdit(person), false)
					History.push(Person.pathFor(person), {success: 'Person saved successfully', skipPageLeaveWarning: true})
				}
			}).catch(error => {
				this.setState({error: error})
				window.scrollTo(0, 0)
			})
	}
}
<|endoftext|>


<commit_before>/* global THREE */
var bind = require('../utils/bind');
var registerComponent = require('../core/component').registerComponent;
var controllerUtils = require('../utils/tracked-controls');
var utils = require('../utils/');

var debug = utils.debug('components:windows-motion-controls:debug');
var warn = utils.debug('components:windows-motion-controls:warn');

var DEFAULT_HANDEDNESS = require('../constants').DEFAULT_HANDEDNESS;

var MODEL_BASE_URL = 'https://cdn.aframe.io/controllers/microsoft/';
var MODEL_FILENAMES = { left: 'left.glb', right: 'right.glb', default: 'universal.glb' };

var GAMEPAD_ID_PREFIX = 'Spatial Controller (Spatial Interaction Source) ';
var GAMEPAD_ID_PATTERN = /([0-9a-zA-Z]+-[0-9a-zA-Z]+)$/;

/**
 * Windows Motion Controller Controls Component
 * Interfaces with Windows Motion Controller controllers and maps Gamepad events to
 * common controller buttons: trackpad, trigger, grip, menu and system
 * It loads a controller model and transforms the pressed buttons
 */
module.exports.Component = registerComponent('windows-motion-controls', {
  schema: {
    hand: {default: DEFAULT_HANDEDNESS},
    // It is possible to have multiple pairs of controllers attached (a pair has both left and right).
    // Set this to 1 to use a controller from the second pair, 2 from the third pair, etc.
    pair: {default: 0},
    // If true, loads the controller glTF asset.
    model: {default: true},
    // If true, will hide the model from the scene if no matching gamepad (based on ID & hand) is connected.
    hideDisconnected: {default: true}
  },

  mapping: {
    // A-Frame specific semantic axis names
    axes: {'thumbstick': [0, 1], 'trackpad': [2, 3]},
    // A-Frame specific semantic button names
    buttons: ['thumbstick', 'trigger', 'grip', 'menu', 'trackpad'],
    // A mapping of the semantic name to node name in the glTF model file,
    // that should be transformed by axis value.
    // This array mirrors the browser Gamepad.axes array, such that
    // the mesh corresponding to axis 0 is in this array index 0.
    axisMeshNames: [
      'THUMBSTICK_X',
      'THUMBSTICK_Y',
      'TOUCHPAD_TOUCH_X',
      'TOUCHPAD_TOUCH_Y'
    ],
    // A mapping of the semantic name to button node name in the glTF model file,
    // that should be transformed by button value.
    buttonMeshNames: {
      'trigger': 'SELECT',
      'menu': 'MENU',
      'grip': 'GRASP',
      'thumbstick': 'THUMBSTICK_PRESS',
      'trackpad': 'TOUCHPAD_PRESS'
    },
    pointingPoseMeshName: 'Pointing_Pose',
    holdingPoseMeshName: 'Holding_Pose'
  },

  bindMethods: function () {
    this.onModelError = bind(this.onModelError, this);
    this.onModelLoaded = bind(this.onModelLoaded, this);
    this.onControllersUpdate = bind(this.onControllersUpdate, this);
    this.checkIfControllerPresent = bind(this.checkIfControllerPresent, this);
    this.onAxisMoved = bind(this.onAxisMoved, this);
  },

  init: function () {
    var self = this;
    this.onButtonChanged = bind(this.onButtonChanged, this);
    this.onButtonDown = function (evt) { self.onButtonEvent(evt, 'down'); };
    this.onButtonUp = function (evt) { self.onButtonEvent(evt, 'up'); };
    this.onButtonTouchStart = function (evt) { self.onButtonEvent(evt, 'touchstart'); };
    this.onButtonTouchEnd = function (evt) { self.onButtonEvent(evt, 'touchend'); };
    this.controllerPresent = false;
    this.lastControllerCheck = 0;
    this.previousButtonValues = {};
    this.bindMethods();

    // Cache for submeshes that we have looked up by name.
    this.loadedMeshInfo = {
      buttonMeshes: null,
      axisMeshes: null
    };

    // Pointing poses
    this.rayOrigin = {
      origin: new THREE.Vector3(),
      direction: new THREE.Vector3(0, 0, -1)
    };

    // Stored on object to allow for mocking in tests
    this.emitIfAxesChanged = controllerUtils.emitIfAxesChanged;
    this.checkControllerPresentAndSetup = controllerUtils.checkControllerPresentAndSetup;
  },

  addEventListeners: function () {
    var el = this.el;
    el.addEventListener('buttonchanged', this.onButtonChanged);
    el.addEventListener('buttondown', this.onButtonDown);
    el.addEventListener('buttonup', this.onButtonUp);
    el.addEventListener('touchstart', this.onButtonTouchStart);
    el.addEventListener('touchend', this.onButtonTouchEnd);
    el.addEventListener('axismove', this.onAxisMoved);
    el.addEventListener('model-error', this.onModelError);
    el.addEventListener('model-loaded', this.onModelLoaded);
    this.controllerEventsActive = true;
  },

  removeEventListeners: function () {
    var el = this.el;
    el.removeEventListener('buttonchanged', this.onButtonChanged);
    el.removeEventListener('buttondown', this.onButtonDown);
    el.removeEventListener('buttonup', this.onButtonUp);
    el.removeEventListener('touchstart', this.onButtonTouchStart);
    el.removeEventListener('touchend', this.onButtonTouchEnd);
    el.removeEventListener('axismove', this.onAxisMoved);
    el.removeEventListener('model-error', this.onModelError);
    el.removeEventListener('model-loaded', this.onModelLoaded);
    this.controllerEventsActive = false;
  },

  checkIfControllerPresent: function () {
    this.checkControllerPresentAndSetup(this, GAMEPAD_ID_PREFIX, {
      hand: this.data.hand,
      index: this.data.pair
    });

    if (this.data.hideDisconnected) {
      this.el.setAttribute('visible', this.controllerPresent);
    }
  },

  play: function () {
    this.checkIfControllerPresent();
    this.addControllersUpdateListener();

    window.addEventListener('gamepadconnected', this.checkIfControllerPresent, false);
    window.addEventListener('gamepaddisconnected', this.checkIfControllerPresent, false);
  },

  pause: function () {
    this.removeEventListeners();
    this.removeControllersUpdateListener();

    window.removeEventListener('gamepadconnected', this.checkIfControllerPresent, false);
    window.removeEventListener('gamepaddisconnected', this.checkIfControllerPresent, false);
  },

  updateControllerModel: function () {
    // If we do not want to load a model, or, have already loaded the model, emit the controllermodelready event.
    if (!this.data.model || this.el.getAttribute('gltf-model')) {
      this.modelReady();
      return;
    }

    var sourceUrl = this.createControllerModelUrl();
    this.loadModel(sourceUrl);
  },

  /**
   * Helper function that constructs a URL from the controller ID suffix, for future proofed
   * art assets.
   */
  createControllerModelUrl: function (forceDefault) {
    // Determine the device specific folder based on the ID suffix
    var trackedControlsComponent = this.el.components['tracked-controls'];
    var controller = trackedControlsComponent ? trackedControlsComponent.controller : null;
    var device = 'default';
    var hand = this.data.hand;
    var filename;

    if (controller) {
      // Read hand directly from the controller, rather than this.data, as in the case that the controller
      // is unhanded this.data will still have 'left' or 'right' (depending on what the user inserted in to the scene).
      // In this case, we want to load the universal model, so need to get the '' from the controller.
      hand = controller.hand;

      if (!forceDefault) {
        var match = controller.id.match(GAMEPAD_ID_PATTERN);
        device = ((match && match[0]) || device);
      }
    }

    // Hand
    filename = MODEL_FILENAMES[hand] || MODEL_FILENAMES.default;

    // Final url
    return MODEL_BASE_URL + device + '/' + filename;
  },

  injectTrackedControls: function () {
    var data = this.data;
    this.el.setAttribute('tracked-controls', {
      idPrefix: GAMEPAD_ID_PREFIX,
      controller: data.pair,
      hand: data.hand
    });

    this.updateControllerModel();
  },

  addControllersUpdateListener: function () {
    this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false);
  },

  removeControllersUpdateListener: function () {
    this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false);
  },

  onControllersUpdate: function () {
    this.checkIfControllerPresent();
  },

  onModelError: function (evt) {
    var defaultUrl = this.createControllerModelUrl(true);
    if (evt.detail.src !== defaultUrl) {
      warn('Failed to load controller model for device, attempting to load default.');
      this.loadModel(defaultUrl);
    } else {
      warn('Failed to load default controller model.');
    }
  },

  loadModel: function (url) {
    debug('Loading asset from: ' + url);

    // The model is loaded by the gltf-model compoent when this attribute is initially set,
    // removed and re-loaded if the given url changes.
    this.el.setAttribute('gltf-model', 'url(' + url + ')');
  },

  onModelLoaded: function (evt) {
    var controllerObject3D = evt.detail.model;
    var loadedMeshInfo = this.loadedMeshInfo;
    var i;
    var meshName;
    var mesh;
    var meshInfo;
    var quaternion = new THREE.Quaternion();

    debug('Processing model');

    // Find the appropriate nodes
    var rootNode = controllerObject3D.getObjectByName('RootNode');
    rootNode.updateMatrixWorld();

    // Reset the caches
    loadedMeshInfo.buttonMeshes = {};
    loadedMeshInfo.axisMeshes = {};

    // Cache our meshes so we aren't traversing the hierarchy per frame
    if (rootNode) {
      // Button Meshes
      for (i = 0; i < this.mapping.buttons.length; i++) {
        meshName = this.mapping.buttonMeshNames[this.mapping.buttons[i]];
        if (!meshName) {
          debug('Skipping unknown button at index: ' + i + ' with mapped name: ' + this.mapping.buttons[i]);
          continue;
        }

        mesh = rootNode.getObjectByName(meshName);
        if (!mesh) {
          warn('Missing button mesh with name: ' + meshName);
          continue;
        }

        meshInfo = {
          index: i,
          value: getImmediateChildByName(mesh, 'VALUE'),
          pressed: getImmediateChildByName(mesh, 'PRESSED'),
          unpressed: getImmediateChildByName(mesh, 'UNPRESSED')
        };
        if (meshInfo.value && meshInfo.pressed && meshInfo.unpressed) {
          loadedMeshInfo.buttonMeshes[this.mapping.buttons[i]] = meshInfo;
        } else {
          // If we didn't find the mesh, it simply means this button won't have transforms applied as mapped button value changes.
          warn('Missing button submesh under mesh with name: ' + meshName +
            '(VALUE: ' + !!meshInfo.value +
            ', PRESSED: ' + !!meshInfo.pressed +
            ', UNPRESSED:' + !!meshInfo.unpressed +
            ')');
        }
      }

      // Axis Meshes
      for (i = 0; i < this.mapping.axisMeshNames.length; i++) {
        meshName = this.mapping.axisMeshNames[i];
        if (!meshName) {
          debug('Skipping unknown axis at index: ' + i);
          continue;
        }

        mesh = rootNode.getObjectByName(meshName);
        if (!mesh) {
          warn('Missing axis mesh with name: ' + meshName);
          continue;
        }

        meshInfo = {
          index: i,
          value: getImmediateChildByName(mesh, 'VALUE'),
          min: getImmediateChildByName(mesh, 'MIN'),
          max: getImmediateChildByName(mesh, 'MAX')
        };
        if (meshInfo.value && meshInfo.min && meshInfo.max) {
          loadedMeshInfo.axisMeshes[i] = meshInfo;
        } else {
          // If we didn't find the mesh, it simply means this axis won't have transforms applied as mapped axis values change.
          warn('Missing axis submesh under mesh with name: ' + meshName +
            '(VALUE: ' + !!meshInfo.value +
            ', MIN: ' + !!meshInfo.min +
            ', MAX:' + !!meshInfo.max +
            ')');
        }
      }

      // Calculate the pointer pose (used for rays), by applying the inverse holding pose, then the pointing pose.
      this.rayOrigin.origin.set(0, 0, 0);
      this.rayOrigin.direction.set(0, 0, -1);

      // Holding pose
      mesh = rootNode.getObjectByName(this.mapping.holdingPoseMeshName);
      if (mesh) {
        mesh.localToWorld(this.rayOrigin.origin);
        this.rayOrigin.direction.applyQuaternion(quaternion.setFromEuler(mesh.rotation).inverse());
      } else {
        debug('Mesh does not contain holding origin data.');
        document.getElementById('debug').innerHTML += '<br />Mesh does not contain holding origin data.';
      }

      // Pointing pose
      mesh = rootNode.getObjectByName(this.mapping.pointingPoseMeshName);
      if (mesh) {
        var offset = new THREE.Vector3();
        mesh.localToWorld(offset);
        this.rayOrigin.origin.add(offset);

        this.rayOrigin.direction.applyQuaternion(quaternion.setFromEuler(mesh.rotation));
      } else {
        debug('Mesh does not contain pointing origin data, defaulting to none.');
      }

      // Emit event stating that our pointing ray is now accurate.
      this.modelReady();
    } else {
      warn('No node with name "RootNode" in controller glTF.');
    }

    debug('Model load complete.');

    // Look through only immediate children. This will return null if no mesh exists with the given name.
    function getImmediateChildByName (object3d, value) {
      for (var i = 0, l = object3d.children.length; i < l; i++) {
        var obj = object3d.children[i];
        if (obj && obj['name'] === value) {
          return obj;
        }
      }
      return undefined;
    }
  },

  lerpAxisTransform: (function () {
    var quaternion = new THREE.Quaternion();
    return function (axis, axisValue) {
      var axisMeshInfo = this.loadedMeshInfo.axisMeshes[axis];
      if (!axisMeshInfo) return;

      var min = axisMeshInfo.min;
      var max = axisMeshInfo.max;
      var target = axisMeshInfo.value;

      // Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
      var lerpValue = axisValue * 0.5 + 0.5;
      target.setRotationFromQuaternion(quaternion.copy(min.quaternion).slerp(max.quaternion, lerpValue));
      target.position.lerpVectors(min.position, max.position, lerpValue);
    };
  })(),

  lerpButtonTransform: (function () {
    var quaternion = new THREE.Quaternion();
    return function (buttonName, buttonValue) {
      var buttonMeshInfo = this.loadedMeshInfo.buttonMeshes[buttonName];
      if (!buttonMeshInfo) return;

      var min = buttonMeshInfo.unpressed;
      var max = buttonMeshInfo.pressed;
      var target = buttonMeshInfo.value;

      target.setRotationFromQuaternion(quaternion.copy(min.quaternion).slerp(max.quaternion, buttonValue));
      target.position.lerpVectors(min.position, max.position, buttonValue);
    };
  })(),

  modelReady: function () {
    this.el.emit('controllermodelready', {
      name: 'windows-motion-controls',
      model: this.data.model,
      rayOrigin: this.rayOrigin
    });
  },

  onButtonChanged: function (evt) {
    var buttonName = this.mapping.buttons[evt.detail.id];

    if (buttonName) {
      // Update the button mesh transform
      if (this.loadedMeshInfo && this.loadedMeshInfo.buttonMeshes) {
        this.lerpButtonTransform(buttonName, evt.detail.state.value);
      }

      // Only emit events for buttons that we know how to map from index to name
      this.el.emit(buttonName + 'changed', evt.detail.state);
    }
  },

  onButtonEvent: function (evt, evtName) {
    var buttonName = this.mapping.buttons[evt.detail.id];
    debug('onButtonEvent(' + evt.detail.id + ', ' + evtName + ')');

    if (buttonName) {
      // Only emit events for buttons that we know how to map from index to name
      this.el.emit(buttonName + evtName);
    }
  },

  onAxisMoved: function (evt) {
    var numAxes = this.mapping.axisMeshNames.length;

    // Only attempt to update meshes if we have valid data.
    if (this.loadedMeshInfo && this.loadedMeshInfo.axisMeshes) {
      for (var axis = 0; axis < numAxes; axis++) {
        // Update the button mesh transform
        this.lerpAxisTransform(axis, evt.detail.axis[axis] || 0.0);
      }
    }

    this.emitIfAxesChanged(this, this.mapping.axes, evt);
  }
});
<commit_msg>Windows motion controller glb format update (#3043)<commit_after>/* global THREE */
var bind = require('../utils/bind');
var registerComponent = require('../core/component').registerComponent;
var controllerUtils = require('../utils/tracked-controls');
var utils = require('../utils/');

var debug = utils.debug('components:windows-motion-controls:debug');
var warn = utils.debug('components:windows-motion-controls:warn');

var DEFAULT_HANDEDNESS = require('../constants').DEFAULT_HANDEDNESS;

var MODEL_BASE_URL = 'https://cdn.aframe.io/controllers/microsoft/';
var MODEL_FILENAMES = { left: 'left.glb', right: 'right.glb', default: 'universal.glb' };

var GAMEPAD_ID_PREFIX = 'Spatial Controller (Spatial Interaction Source) ';
var GAMEPAD_ID_PATTERN = /([0-9a-zA-Z]+-[0-9a-zA-Z]+)$/;

/**
 * Windows Motion Controller Controls Component
 * Interfaces with Windows Motion Controller controllers and maps Gamepad events to
 * common controller buttons: trackpad, trigger, grip, menu and system
 * It loads a controller model and transforms the pressed buttons
 */
module.exports.Component = registerComponent('windows-motion-controls', {
  schema: {
    hand: {default: DEFAULT_HANDEDNESS},
    // It is possible to have multiple pairs of controllers attached (a pair has both left and right).
    // Set this to 1 to use a controller from the second pair, 2 from the third pair, etc.
    pair: {default: 0},
    // If true, loads the controller glTF asset.
    model: {default: true},
    // If true, will hide the model from the scene if no matching gamepad (based on ID & hand) is connected.
    hideDisconnected: {default: true}
  },

  mapping: {
    // A-Frame specific semantic axis names
    axes: {'thumbstick': [0, 1], 'trackpad': [2, 3]},
    // A-Frame specific semantic button names
    buttons: ['thumbstick', 'trigger', 'grip', 'menu', 'trackpad'],
    // A mapping of the semantic name to node name in the glTF model file,
    // that should be transformed by axis value.
    // This array mirrors the browser Gamepad.axes array, such that
    // the mesh corresponding to axis 0 is in this array index 0.
    axisMeshNames: [
      'THUMBSTICK_X',
      'THUMBSTICK_Y',
      'TOUCHPAD_TOUCH_X',
      'TOUCHPAD_TOUCH_Y'
    ],
    // A mapping of the semantic name to button node name in the glTF model file,
    // that should be transformed by button value.
    buttonMeshNames: {
      'trigger': 'SELECT',
      'menu': 'MENU',
      'grip': 'GRASP',
      'thumbstick': 'THUMBSTICK_PRESS',
      'trackpad': 'TOUCHPAD_PRESS'
    },
    pointingPoseMeshName: 'POINTING_POSE'
  },

  bindMethods: function () {
    this.onModelError = bind(this.onModelError, this);
    this.onModelLoaded = bind(this.onModelLoaded, this);
    this.onControllersUpdate = bind(this.onControllersUpdate, this);
    this.checkIfControllerPresent = bind(this.checkIfControllerPresent, this);
    this.onAxisMoved = bind(this.onAxisMoved, this);
  },

  init: function () {
    var self = this;
    this.onButtonChanged = bind(this.onButtonChanged, this);
    this.onButtonDown = function (evt) { self.onButtonEvent(evt, 'down'); };
    this.onButtonUp = function (evt) { self.onButtonEvent(evt, 'up'); };
    this.onButtonTouchStart = function (evt) { self.onButtonEvent(evt, 'touchstart'); };
    this.onButtonTouchEnd = function (evt) { self.onButtonEvent(evt, 'touchend'); };
    this.controllerPresent = false;
    this.lastControllerCheck = 0;
    this.previousButtonValues = {};
    this.bindMethods();

    // Cache for submeshes that we have looked up by name.
    this.loadedMeshInfo = {
      buttonMeshes: null,
      axisMeshes: null
    };

    // Pointing poses
    this.rayOrigin = {
      origin: new THREE.Vector3(),
      direction: new THREE.Vector3(0, 0, -1),
      createdFromMesh: false
    };

    // Stored on object to allow for mocking in tests
    this.emitIfAxesChanged = controllerUtils.emitIfAxesChanged;
    this.checkControllerPresentAndSetup = controllerUtils.checkControllerPresentAndSetup;
  },

  addEventListeners: function () {
    var el = this.el;
    el.addEventListener('buttonchanged', this.onButtonChanged);
    el.addEventListener('buttondown', this.onButtonDown);
    el.addEventListener('buttonup', this.onButtonUp);
    el.addEventListener('touchstart', this.onButtonTouchStart);
    el.addEventListener('touchend', this.onButtonTouchEnd);
    el.addEventListener('axismove', this.onAxisMoved);
    el.addEventListener('model-error', this.onModelError);
    el.addEventListener('model-loaded', this.onModelLoaded);
    this.controllerEventsActive = true;
  },

  removeEventListeners: function () {
    var el = this.el;
    el.removeEventListener('buttonchanged', this.onButtonChanged);
    el.removeEventListener('buttondown', this.onButtonDown);
    el.removeEventListener('buttonup', this.onButtonUp);
    el.removeEventListener('touchstart', this.onButtonTouchStart);
    el.removeEventListener('touchend', this.onButtonTouchEnd);
    el.removeEventListener('axismove', this.onAxisMoved);
    el.removeEventListener('model-error', this.onModelError);
    el.removeEventListener('model-loaded', this.onModelLoaded);
    this.controllerEventsActive = false;
  },

  checkIfControllerPresent: function () {
    this.checkControllerPresentAndSetup(this, GAMEPAD_ID_PREFIX, {
      hand: this.data.hand,
      index: this.data.pair
    });

    if (this.data.hideDisconnected) {
      this.el.setAttribute('visible', this.controllerPresent);
    }
  },

  play: function () {
    this.checkIfControllerPresent();
    this.addControllersUpdateListener();

    window.addEventListener('gamepadconnected', this.checkIfControllerPresent, false);
    window.addEventListener('gamepaddisconnected', this.checkIfControllerPresent, false);
  },

  pause: function () {
    this.removeEventListeners();
    this.removeControllersUpdateListener();

    window.removeEventListener('gamepadconnected', this.checkIfControllerPresent, false);
    window.removeEventListener('gamepaddisconnected', this.checkIfControllerPresent, false);
  },

  updateControllerModel: function () {
    // If we do not want to load a model, or, have already loaded the model, emit the controllermodelready event.
    if (!this.data.model || this.rayOrigin.createdFromMesh) {
      this.modelReady();
      return;
    }

    var sourceUrl = this.createControllerModelUrl();
    this.loadModel(sourceUrl);
  },

  /**
   * Helper function that constructs a URL from the controller ID suffix, for future proofed
   * art assets.
   */
  createControllerModelUrl: function (forceDefault) {
    // Determine the device specific folder based on the ID suffix
    var trackedControlsComponent = this.el.components['tracked-controls'];
    var controller = trackedControlsComponent ? trackedControlsComponent.controller : null;
    var device = 'default';
    var hand = this.data.hand;
    var filename;

    if (controller) {
      // Read hand directly from the controller, rather than this.data, as in the case that the controller
      // is unhanded this.data will still have 'left' or 'right' (depending on what the user inserted in to the scene).
      // In this case, we want to load the universal model, so need to get the '' from the controller.
      hand = controller.hand;

      if (!forceDefault) {
        var match = controller.id.match(GAMEPAD_ID_PATTERN);
        device = ((match && match[0]) || device);
      }
    }

    // Hand
    filename = MODEL_FILENAMES[hand] || MODEL_FILENAMES.default;

    // Final url
    return MODEL_BASE_URL + device + '/' + filename;
  },

  injectTrackedControls: function () {
    var data = this.data;
    this.el.setAttribute('tracked-controls', {
      idPrefix: GAMEPAD_ID_PREFIX,
      controller: data.pair,
      hand: data.hand
    });

    this.updateControllerModel();
  },

  addControllersUpdateListener: function () {
    this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false);
  },

  removeControllersUpdateListener: function () {
    this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false);
  },

  onControllersUpdate: function () {
    this.checkIfControllerPresent();
  },

  onModelError: function (evt) {
    var defaultUrl = this.createControllerModelUrl(true);
    if (evt.detail.src !== defaultUrl) {
      warn('Failed to load controller model for device, attempting to load default.');
      this.loadModel(defaultUrl);
    } else {
      warn('Failed to load default controller model.');
    }
  },

  loadModel: function (url) {
    debug('Loading asset from: ' + url);

    // The model is loaded by the gltf-model compoent when this attribute is initially set,
    // removed and re-loaded if the given url changes.
    this.el.setAttribute('gltf-model', 'url(' + url + ')');
  },

  onModelLoaded: function (evt) {
    var rootNode = evt.detail.model;
    var loadedMeshInfo = this.loadedMeshInfo;
    var i;
    var meshName;
    var mesh;
    var meshInfo;

    debug('Processing model');

    // Reset the caches
    loadedMeshInfo.buttonMeshes = {};
    loadedMeshInfo.axisMeshes = {};

    // Cache our meshes so we aren't traversing the hierarchy per frame
    if (rootNode) {
      // Button Meshes
      for (i = 0; i < this.mapping.buttons.length; i++) {
        meshName = this.mapping.buttonMeshNames[this.mapping.buttons[i]];
        if (!meshName) {
          debug('Skipping unknown button at index: ' + i + ' with mapped name: ' + this.mapping.buttons[i]);
          continue;
        }

        mesh = rootNode.getObjectByName(meshName);
        if (!mesh) {
          warn('Missing button mesh with name: ' + meshName);
          continue;
        }

        meshInfo = {
          index: i,
          value: getImmediateChildByName(mesh, 'VALUE'),
          pressed: getImmediateChildByName(mesh, 'PRESSED'),
          unpressed: getImmediateChildByName(mesh, 'UNPRESSED')
        };
        if (meshInfo.value && meshInfo.pressed && meshInfo.unpressed) {
          loadedMeshInfo.buttonMeshes[this.mapping.buttons[i]] = meshInfo;
        } else {
          // If we didn't find the mesh, it simply means this button won't have transforms applied as mapped button value changes.
          warn('Missing button submesh under mesh with name: ' + meshName +
            '(VALUE: ' + !!meshInfo.value +
            ', PRESSED: ' + !!meshInfo.pressed +
            ', UNPRESSED:' + !!meshInfo.unpressed +
            ')');
        }
      }

      // Axis Meshes
      for (i = 0; i < this.mapping.axisMeshNames.length; i++) {
        meshName = this.mapping.axisMeshNames[i];
        if (!meshName) {
          debug('Skipping unknown axis at index: ' + i);
          continue;
        }

        mesh = rootNode.getObjectByName(meshName);
        if (!mesh) {
          warn('Missing axis mesh with name: ' + meshName);
          continue;
        }

        meshInfo = {
          index: i,
          value: getImmediateChildByName(mesh, 'VALUE'),
          min: getImmediateChildByName(mesh, 'MIN'),
          max: getImmediateChildByName(mesh, 'MAX')
        };
        if (meshInfo.value && meshInfo.min && meshInfo.max) {
          loadedMeshInfo.axisMeshes[i] = meshInfo;
        } else {
          // If we didn't find the mesh, it simply means this axis won't have transforms applied as mapped axis values change.
          warn('Missing axis submesh under mesh with name: ' + meshName +
            '(VALUE: ' + !!meshInfo.value +
            ', MIN: ' + !!meshInfo.min +
            ', MAX:' + !!meshInfo.max +
            ')');
        }
      }

      this.calculateRayOriginFromMesh(rootNode);
    }

    debug('Model load complete.');

    // Look through only immediate children. This will return null if no mesh exists with the given name.
    function getImmediateChildByName (object3d, value) {
      for (var i = 0, l = object3d.children.length; i < l; i++) {
        var obj = object3d.children[i];
        if (obj && obj['name'] === value) {
          return obj;
        }
      }
      return undefined;
    }
  },

  calculateRayOriginFromMesh: (function () {
    var quaternion = new THREE.Quaternion();
    return function (rootNode) {
      var mesh;

      // Calculate the pointer pose (used for rays), by applying the world transform of th POINTER_POSE node
      // in the glTF (assumes that root node is at world origin)
      this.rayOrigin.origin.set(0, 0, 0);
      this.rayOrigin.direction.set(0, 0, -1);
      this.rayOrigin.createdFromMesh = true;

      // Try to read Pointing pose from the source model
      mesh = rootNode.getObjectByName(this.mapping.pointingPoseMeshName);
      if (mesh) {
        var parent = rootNode.parent;

        // We need to read pose transforms accumulated from the root of the glTF, not the scene.
        if (parent) {
          rootNode.parent = null;
          rootNode.updateMatrixWorld(true);
          rootNode.parent = parent;
        }

        mesh.getWorldPosition(this.rayOrigin.origin);
        mesh.getWorldQuaternion(quaternion);
        this.rayOrigin.direction.applyQuaternion(quaternion);

        // Recalculate the world matrices now that the rootNode is re-attached to the parent.
        if (parent) {
          rootNode.updateMatrixWorld(true);
        }
      } else {
        debug('Mesh does not contain pointing origin data, defaulting to none.');
      }

      // Emit event stating that our pointing ray is now accurate.
      this.modelReady();
    };
  })(),

  lerpAxisTransform: (function () {
    var quaternion = new THREE.Quaternion();
    return function (axis, axisValue) {
      var axisMeshInfo = this.loadedMeshInfo.axisMeshes[axis];
      if (!axisMeshInfo) return;

      var min = axisMeshInfo.min;
      var max = axisMeshInfo.max;
      var target = axisMeshInfo.value;

      // Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
      var lerpValue = axisValue * 0.5 + 0.5;
      target.setRotationFromQuaternion(quaternion.copy(min.quaternion).slerp(max.quaternion, lerpValue));
      target.position.lerpVectors(min.position, max.position, lerpValue);
    };
  })(),

  lerpButtonTransform: (function () {
    var quaternion = new THREE.Quaternion();
    return function (buttonName, buttonValue) {
      var buttonMeshInfo = this.loadedMeshInfo.buttonMeshes[buttonName];
      if (!buttonMeshInfo) return;

      var min = buttonMeshInfo.unpressed;
      var max = buttonMeshInfo.pressed;
      var target = buttonMeshInfo.value;

      target.setRotationFromQuaternion(quaternion.copy(min.quaternion).slerp(max.quaternion, buttonValue));
      target.position.lerpVectors(min.position, max.position, buttonValue);
    };
  })(),

  modelReady: function () {
    this.el.emit('controllermodelready', {
      name: 'windows-motion-controls',
      model: this.data.model,
      rayOrigin: this.rayOrigin
    });
  },

  onButtonChanged: function (evt) {
    var buttonName = this.mapping.buttons[evt.detail.id];

    if (buttonName) {
      // Update the button mesh transform
      if (this.loadedMeshInfo && this.loadedMeshInfo.buttonMeshes) {
        this.lerpButtonTransform(buttonName, evt.detail.state.value);
      }

      // Only emit events for buttons that we know how to map from index to name
      this.el.emit(buttonName + 'changed', evt.detail.state);
    }
  },

  onButtonEvent: function (evt, evtName) {
    var buttonName = this.mapping.buttons[evt.detail.id];
    debug('onButtonEvent(' + evt.detail.id + ', ' + evtName + ')');

    if (buttonName) {
      // Only emit events for buttons that we know how to map from index to name
      this.el.emit(buttonName + evtName);
    }
  },

  onAxisMoved: function (evt) {
    var numAxes = this.mapping.axisMeshNames.length;

    // Only attempt to update meshes if we have valid data.
    if (this.loadedMeshInfo && this.loadedMeshInfo.axisMeshes) {
      for (var axis = 0; axis < numAxes; axis++) {
        // Update the button mesh transform
        this.lerpAxisTransform(axis, evt.detail.axis[axis] || 0.0);
      }
    }

    this.emitIfAxesChanged(this, this.mapping.axes, evt);
  }
});
<|endoftext|>


<commit_before>/**********************************/
/* Signalfire Javascript Library  */
/* Version: 0.0.3                 */
/*                                */
/* Copyright 2013 Travis Wimer    */
/* http://traviswimer.com         */
/*                                */
/* Released under the MIT license */
/**********************************/

(function(){

var signalfire = function(){

/////////////////
// Peer Object //
/*****************************************************************************/
	var createPeer = function(socket, options, failCallback){

	////////////////////////////////
	// Peer Object Initialization //
	//////////////////////////////////////////////////
	//                                              //

		// The Peer object
		var peerObject = {
			socket: socket
		};

		// listen for server requesting offer
		peerObject.socket.on('serverRequestingOffer', sendOfferToServer);

		// detect receiving an offer from server
		peerObject.socket.on('serverSendingOffer', sendAnswerToServer);

		// listen for server sending an answer
		peerObject.socket.on('serverSendingAnswer', receiveAnswerFromServer);

		// listen for ice candidates
		peerObject.socket.on('serverSendingIce', receiveIceCandidate);

	//                                              //
	//////////////////////////////////////////////////

	/////////////////////////////////////
	// Peer Object's Private Variables //
	//////////////////////////////////////////////////
	//                                              //

		// holds the RTCPeerConnections of peers
		var connectingPeers = {};

		// holds ice candidates for uninitialized peer connections
		var storedIceCandidates = {};

		// holds function to call when server requests a connection offer
		// or provides a connection offer
		var makeRTCPeer = options.connector;

		// holds function to call when signaling process is complete
		var signalingComplete = options.onSignalingComplete || function(){};

		// holds function to call when signaling fails
		var signalingFail = options.onSignalingFail || function(){};

	//                                              //
	//////////////////////////////////////////////////

	///////////////////////////////////
	// Peer Object's Private Methods //
	//////////////////////////////////////////////////
	//                                              //

		// Receive offer request and return an offer
		function sendOfferToServer(data){

			// fail if required data is not received
			if(!data || !data.peerId){
				signalingFail('Invalid data recieved from server');
				return;
			}

			// create RTCPeerConnection
			makeRTCPeer(function(rtcPeerConnection){
				// initialize ice candidate listeners
				iceSetup(rtcPeerConnection, data);

				// Add connection to list of peers
				connectingPeers[data.peerId] = rtcPeerConnection;

				// create offer and send to server
				rtcPeerConnection.createOffer(
					function(offerResponse){
						data.offer = offerResponse;
						rtcPeerConnection.setLocalDescription(offerResponse, function(){
							socket.emit('clientSendingOffer', data);
						});
					},
					null,
					{
						optional: [],
						mandatory: {
							OfferToReceiveAudio: true,
							OfferToReceiveVideo: true
						}
					}
				);
			},
			false);
		}

		// Receive peer offer from server. Return an answer.
		function sendAnswerToServer(data){

			// fail if required data is not received
			if(!data || !data.peerId || !data.offer){
				signalingFail('Invalid data recieved from server');
				return;
			}

			// create RTCPeerConnection
			makeRTCPeer(function(rtcPeerConnection){
				// initialize ice candidate listeners
				iceSetup(rtcPeerConnection, data);

				// Add connection to list of peers
				connectingPeers[data.peerId] = rtcPeerConnection;

				// Create description from offer received
				var offer = new RTCSessionDescription(data.offer);

				// Set Description, create answer, send answer to server
				rtcPeerConnection.setRemoteDescription(offer, function(){
					rtcPeerConnection.createAnswer(

						function(answer){
							rtcPeerConnection.setLocalDescription(answer);
							var answerData = {
								peerId:data.peerId,
								answer:answer
							};
							socket.emit('clientSendingAnswer', answerData);

							// Firefox sends ICE with offers/answers, so
							// connection should be complete on this end
							if(navigator.mozGetUserMedia){
								signalingComplete(connectingPeers[data.peerId]);

								// developer is expected to store rtc connection within
								// their app, so it can be removed from the list
								delete connectingPeers[data.peerId];
							}
						},
						null,
						{
							optional: [],
							mandatory: {
								OfferToReceiveAudio: true,
								OfferToReceiveVideo: true
							}
						}
					);
				});
			},
			true);

		}

		// Receive peer answer from server
		function receiveAnswerFromServer(data){

			// fail if required data is not received
			if(!data || !data.peerId || !data.answer){
				signalingFail('Invalid data recieved from server');
				return;
			}

			var peerConn = connectingPeers[data.peerId];

			var answer = new RTCSessionDescription(data.answer);
			peerConn.setRemoteDescription(answer, function(){

				// Firefox sends ICE with offers/answers, so
				// connection should be complete
				if(navigator.mozGetUserMedia){
					signalingComplete(connectingPeers[data.peerId]);

					// developer is expected to store rtc connection within
					// their app, so it can be removed from the list
					delete connectingPeers[data.peerId];
				}
			});

		}

		// receive ice candidates from server
		function receiveIceCandidate(data){

			// If peer connection has not started, save ice candidates for later
			// otherwise add them to the connection
			if(typeof connectingPeers[data.peerId] == 'undefined'){
				if(!storedIceCandidates[data.peerId]){
					storedIceCandidates[data.peerId] = [];
				}
				if(data && data.candidate !== null){
					storedIceCandidates[data.peerId].push(data);
				}
			}else{
				if(data && data.candidate !== null){
					connectingPeers[data.peerId].addIceCandidate(new RTCIceCandidate(data.candidate));
				}
			}
		}

		// setup ice candidate handling
		function iceSetup(rtcPeerConnection, data){

			// check if connection has been created
			rtcPeerConnection.oniceconnectionstatechange = rtcPeerConnection.onicechange = function(evt){
				if(rtcPeerConnection.iceConnectionState === 'connected'){
					signalingComplete(rtcPeerConnection);

					// developer is expected to store rtc connection within
					// their app, so it can be removed from the list
					delete connectingPeers[data.peerId];
				}
			};

			// listen for ice candidates and send to server
			rtcPeerConnection.onicecandidate = function(iceData){
				var sendingIceInfo = {
					peerId: data.peerId,
					candidate: iceData.candidate
				};
				socket.emit('clientSendingIce', sendingIceInfo);
			};

			// check for ice candidates already recieved from peer and add them
			// (unnecessary in firefox)
			//if(!navigator.mozGetUserMedia){
				var hasVisited = false;
				if(storedIceCandidates[data.peerId]){
					while(hasVisited == false && storedIceCandidates[data.peerId].length > 0){
						hasVisited = true;
						receiveIceCandidate(storedIceCandidates[data.peerId].pop());
					}

					// array will no longer be used, so remove reference
					delete storedIceCandidates[data.peerId];
				}
			//}

		}

	//                                              //
	//////////////////////////////////////////////////

		// Return the Peer object
		return peerObject;
	};

/*____________________________________________________________________________*/

////////////////////
// Public Methods //
/*****************************************************************************/

	var connect = function(options, successCallback, failCallback){
		/*
			--OPTIONS--
			"server" - The url + port of the server to connect to

			"connector" - A function that is called when the server requests
						an RTC offer. Must return an RTCPeerConnction object.

			"onSignalingComplete" - Callback function for when the signaling
						process has successfully created a peer connection.

			"onSignalingFail" - Callback function for when there is an error
						in the signaling process.
		*/

		// Check if options are valid
		failCallback = failCallback || function(e){throw e;};
		if(typeof options != 'object' || typeof options.connector != 'function'){
			failCallback('Invalid option provided');
			return;
		}

		// Create new socket connection
		var socket = io.connect(options.server, {'force new connection': true});

		// Create new peer once connected
		socket.on('connect', function () {
			var peer;
			try{
				peer = createPeer(socket, options, failCallback);
			}catch(e){
				failCallback(e);
			}finally{
				successCallback(peer);
			}
		});

		// detect connection error
		socket.on('connect_error', function(e){
			failCallback(e);
		});

		return socket;
	};

	return {
		connect:connect
	};

/*____________________________________________________________________________*/

};

// set the signalfire global variable
window.signalfire = signalfire();

}());<commit_msg>Adds support for firefox<commit_after>/**********************************/
/* Signalfire Javascript Library  */
/* Version: 0.0.4                 */
/*                                */
/* Copyright 2013 Travis Wimer    */
/* http://traviswimer.com         */
/*                                */
/* Released under the MIT license */
/**********************************/

(function(){

var signalfire = function(){

/////////////////
// Peer Object //
/*****************************************************************************/
	var createPeer = function(socket, options, failCallback){

	////////////////////////////////
	// Peer Object Initialization //
	//////////////////////////////////////////////////
	//                                              //

		// The Peer object
		var peerObject = {
			socket: socket
		};

		// listen for server requesting offer
		peerObject.socket.on('serverRequestingOffer', sendOfferToServer);

		// detect receiving an offer from server
		peerObject.socket.on('serverSendingOffer', sendAnswerToServer);

		// listen for server sending an answer
		peerObject.socket.on('serverSendingAnswer', receiveAnswerFromServer);

		// listen for ice candidates
		peerObject.socket.on('serverSendingIce', receiveIceCandidate);

	//                                              //
	//////////////////////////////////////////////////

	/////////////////////////////////////
	// Peer Object's Private Variables //
	//////////////////////////////////////////////////
	//                                              //

		// holds the RTCPeerConnections of peers
		var connectingPeers = {};

		// holds ice candidates for uninitialized peer connections
		var storedIceCandidates = {};

		// holds function to call when server requests a connection offer
		// or provides a connection offer
		var makeRTCPeer = options.connector;

		// holds function to call when signaling process is complete
		var signalingComplete = options.onSignalingComplete || function(){};

		// holds function to call when signaling fails
		var signalingFail = options.onSignalingFail || function(){};

	//                                              //
	//////////////////////////////////////////////////

	///////////////////////////////////
	// Peer Object's Private Methods //
	//////////////////////////////////////////////////
	//                                              //

		// Receive offer request and return an offer
		function sendOfferToServer(data){

			// fail if required data is not received
			if(!data || !data.peerId){
				signalingFail('Invalid data recieved from server');
				return;
			}

			// create RTCPeerConnection
			makeRTCPeer(function(rtcPeerConnection){
				// initialize ice candidate listeners
				iceSetup(rtcPeerConnection, data);

				// Add connection to list of peers
				connectingPeers[data.peerId] = rtcPeerConnection;

				// create offer and send to server
				rtcPeerConnection.createOffer(
					function(offerResponse){
						data.offer = offerResponse;
						rtcPeerConnection.setLocalDescription(offerResponse, function(){
							socket.emit('clientSendingOffer', data);
						});
					},
					null,
					{
						optional: [],
						mandatory: {
							OfferToReceiveAudio: true,
							OfferToReceiveVideo: true
						}
					}
				);
			},
			false);
		}

		// Receive peer offer from server. Return an answer.
		function sendAnswerToServer(data){

			// fail if required data is not received
			if(!data || !data.peerId || !data.offer){
				signalingFail('Invalid data recieved from server');
				return;
			}

			// create RTCPeerConnection
			makeRTCPeer(function(rtcPeerConnection){
				// initialize ice candidate listeners
				iceSetup(rtcPeerConnection, data);

				// Add connection to list of peers
				connectingPeers[data.peerId] = rtcPeerConnection;

				// Create description from offer received
				var offer = new RTCSessionDescription(data.offer);

				// Set Description, create answer, send answer to server
				rtcPeerConnection.setRemoteDescription(offer, function(){
					rtcPeerConnection.createAnswer(

						function(answer){
							rtcPeerConnection.setLocalDescription(answer);
							var answerData = {
								peerId:data.peerId,
								answer:answer
							};
							socket.emit('clientSendingAnswer', answerData);
						},
						null,
						{
							optional: [],
							mandatory: {
								OfferToReceiveAudio: true,
								OfferToReceiveVideo: true
							}
						}
					);
				});
			},
			true);

		}

		// Receive peer answer from server
		function receiveAnswerFromServer(data){

			// fail if required data is not received
			if(!data || !data.peerId || !data.answer){
				signalingFail('Invalid data recieved from server');
				return;
			}

			var peerConn = connectingPeers[data.peerId];

			var answer = new RTCSessionDescription(data.answer);
			peerConn.setRemoteDescription(answer, function(){

			});

		}

		// receive ice candidates from server
		function receiveIceCandidate(data){

			// If peer connection has not started, save ice candidates for later
			// otherwise add them to the connection
			if(typeof connectingPeers[data.peerId] == 'undefined'){
				if(!storedIceCandidates[data.peerId]){
					storedIceCandidates[data.peerId] = [];
				}
				if(data && data.candidate !== null){
					storedIceCandidates[data.peerId].push(data);
				}
			}else{
				if(data && data.candidate !== null){
					connectingPeers[data.peerId].addIceCandidate(new RTCIceCandidate(data.candidate));
				}
			}
		}

		// setup ice candidate handling
		function iceSetup(rtcPeerConnection, data){
			//////////////////////////////////////////////
			// Determine if connection has been created //
			//////////////////////////////////////////////

			// These are the callbacks implemented by Chrome
			rtcPeerConnection.oniceconnectionstatechange = rtcPeerConnection.onicechange = function(evt){
				if(rtcPeerConnection.iceConnectionState === 'connected'){
					signalingComplete(rtcPeerConnection);

					// developer is expected to store rtc connection within
					// their app, so it can be removed from the list
					delete connectingPeers[data.peerId];
				}
			};

			// These are the callbacks implemented by Firefox
			rtcPeerConnection.ongatheringchange = function(){
				if(rtcPeerConnection.readyState === 'active'){
					signalingComplete(rtcPeerConnection);

					// developer is expected to store rtc connection within
					// their app, so it can be removed from the list
					delete connectingPeers[data.peerId];
				}
			};

			//////////////////////////////////////////////

			// listen for ice candidates and send to server
			rtcPeerConnection.onicecandidate = function(iceData){
				var sendingIceInfo = {
					peerId: data.peerId,
					candidate: iceData.candidate
				};
				socket.emit('clientSendingIce', sendingIceInfo);
			};

			// check for ice candidates already recieved from peer and add them
			var hasVisited = false;
			if(storedIceCandidates[data.peerId]){
				while(hasVisited === false && storedIceCandidates[data.peerId].length > 0){
					hasVisited = true;
					receiveIceCandidate(storedIceCandidates[data.peerId].pop());
				}

				// array will no longer be used, so remove reference
				delete storedIceCandidates[data.peerId];
			}

		}

	//                                              //
	//////////////////////////////////////////////////

		// Return the Peer object
		return peerObject;
	};

/*____________________________________________________________________________*/

////////////////////
// Public Methods //
/*****************************************************************************/

	var connect = function(options, successCallback, failCallback){
		/*
			--OPTIONS--
			"server" - The url + port of the server to connect to

			"connector" - A function that is called when the server requests
						an RTC offer. Must return an RTCPeerConnction object.

			"onSignalingComplete" - Callback function for when the signaling
						process has successfully created a peer connection.

			"onSignalingFail" - Callback function for when there is an error
						in the signaling process.
		*/

		// Check if options are valid
		failCallback = failCallback || function(e){throw e;};
		if(typeof options != 'object' || typeof options.connector != 'function'){
			failCallback('Invalid option provided');
			return;
		}

		// Create new socket connection
		var socket = io.connect(options.server, {'force new connection': true});

		// Create new peer once connected
		socket.on('connect', function () {
			var peer;
			try{
				peer = createPeer(socket, options, failCallback);
			}catch(e){
				failCallback(e);
			}finally{
				successCallback(peer);
			}
		});

		// detect connection error
		socket.on('connect_error', function(e){
			failCallback(e);
		});

		return socket;
	};

	return {
		connect:connect
	};

/*____________________________________________________________________________*/

};

// set the signalfire global variable
window.signalfire = signalfire();

}());<|endoftext|>


<commit_before>'use strict';

var copy              = require('es5-ext/object/copy')
  , assign            = require('es5-ext/object/assign')
  , filter            = require('es5-ext/object/filter')
  , forEach           = require('es5-ext/object/for-each')
  , isSet             = require('es6-set/is-set')
  , d                 = require('d')
  , makeElement       = require('dom-ext/document/#/make-element')
  , castAttribute     = require('dom-ext/element/#/cast-attribute')
  , PropObserv        = require('dbjs/_setup/1.property/observable')
  , DescPropObserv    = require('dbjs/_setup/3.descriptor-property/observable')
  , htmlAttributes    = require('./_html-attributes')

  , toDOM = function () { return this.dom; }, componentRender;

componentRender = function (input, options) {
	var el = makeElement.bind(input.document);
	return el('label',
		(options.label && [el('span', { class: 'label' }, options.label, ':'), ' ']) || null, input,
		// required mark
		el('span', { class: 'required-status' }, '*'),
		// validation status mark
		el('span', { class: 'validation-status' }, '✓'),
		// error message
		el('span', { class: 'error-message error-message-' +
			input._name.replace(/[:#\/]/g, '-') }),
		// hint
		options.hint && el('span', { 'class': 'hint' }, options.hint));
};

Object.defineProperties(PropObserv.prototype, {
	DOMInput: d(null),
	toDOMInput: d(function (document/*, options*/) {
		var input, initOptions = Object(arguments[1]), options = copy(initOptions)
		  , type, value, onChange, desc;

		desc = this.descriptor;

		// Setup input options
		if (options.multiple == null) options.multiple = desc.multiple;
		if (options.name == null) options.name = this.dbId;
		options.dbOptions = desc;
		options.observable = this;

		// Initialize input
		type = desc.type;
		if (options.DOMInput) input = new options.DOMInput(document, type, options);
		else if (desc.DOMInput) input = new desc.DOMInput(document, type, options);
		else input = type.toDOMInput(document, options);
		input.observable = this;

		// Set input value
		value = this.value;
		if (isSet(value) && !options.multiple) value = value.values().next().value || null;
		if ((value === undefined) && (options.value != null)) value = options.value;
		input.value = value;

		// Attach listeners
		onChange = function () {
			var value = this.value;
			if (isSet(value)) {
				if (!options.multiple) value = value.values().next().value || null;
			}
			input.value = value;
		}.bind(this);
		if (isSet(value)) value.on('change', onChange);
		else this.on('change', onChange);

		input.once('destroy', function () {
			delete input.observable;
			if (isSet(value)) value.off('change', onChange);
			this.off('change', onChange);
		}.bind(this));

		type.emit('dominput', input);
		desc.emit('dominput', input);
		return input;
	}),
	toDOMInputComponent: d(function (document/*, options*/) {
		var options = copy(Object(arguments[1])), input, inputOptions, dom, cb, desc = this.descriptor;

		inputOptions = filter(options, function (value, name) {
			if (name === 'render') return false;
			return !htmlAttributes[name];
		});
		if (options.input) {
			assign(inputOptions, options.input);
			delete inputOptions.input;
		}
		input = this.toDOMInput(document, inputOptions);

		if (options.label === false) options.label = null;
		else if (options.label == null) options.label = desc.label;
		if (options.hint == null) options.hint = desc.inputHint;

		dom = (options.render || componentRender)(input, options);
		forEach(options, function (value, name) {
			if (htmlAttributes[name]) castAttribute.call(dom, name, value);
		}, this);

		dom.classList.add('dbjs-input-component');

		// Required
		dom.classList[input.required ? 'add' : 'remove']('required');
		dom.classList[input.required ? 'remove' : 'add']('optional');

		// Changed
		input.on('change:changed', cb = function (value) {
			dom.classList[value ? 'add' : 'remove']('changed');
			dom.classList[value ? 'remove' : 'add']('not-changed');
		});
		cb(input.changed);

		// Valid
		input.on('change:valid', cb = function (value) {
			dom.classList[value ? 'add' : 'remove']('valid');
			dom.classList[value ? 'remove' : 'add']('invalid');
		});
		cb(input.valid);

		// DBJS valid/invalid & empty/filled
		this.on('change', cb = function () {
			var desc = this.descriptor, value;
			value = desc.required && (this.value == null);
			dom.classList[value ? 'add' : 'remove']('dbjs-invalid');
			dom.classList[value ? 'remove' : 'add']('dbjs-valid');
			dom.classList[this.value == null ? 'add' : 'remove']('dbjs-empty');
			dom.classList[this.value == null ? 'remove' : 'add']('dbjs-filled');
		});
		cb.call(this);
		input.on('destroy', function (cb) { this.off('change', cb); }.bind(this, cb));

		return { dom: dom, input: input, toDOM: toDOM };
	})
});

module.exports = Object.defineProperties(DescPropObserv.prototype, {
	DOMInput: d(null),
	toDOMInput: d(function (document/*, options*/) {
		var input, initOptions = Object(arguments[1]), options = copy(initOptions)
		  , type, onChange, descDesc = this.descriptor;

		// Setup input options
		if (options.name == null) options.name = this.dbId;

		// Initialize input
		type = descDesc.type;
		if (options.DOMInput) input = new options.DOMInput(document, type, options);
		else if (this.DOMInput) input = new this.DOMInput(document, type, options);
		else input = type.toDOMInput(document, options);
		input.observable = this;

		// Set input value
		input.value = this.value;

		// Attach listeners
		onChange = function () { input.value = this.value; }.bind(this);
		this.on('change', onChange);

		input.once('destroy', function () {
			delete input.observable;
			this.off('change', onChange);
		}.bind(this));

		type.emit('dominput', input);
		descDesc.emit('dominput', input);
		return input;
	})
});
<commit_msg>Improve generic component markup<commit_after>'use strict';

var copy              = require('es5-ext/object/copy')
  , assign            = require('es5-ext/object/assign')
  , filter            = require('es5-ext/object/filter')
  , forEach           = require('es5-ext/object/for-each')
  , isSet             = require('es6-set/is-set')
  , d                 = require('d')
  , makeElement       = require('dom-ext/document/#/make-element')
  , castAttribute     = require('dom-ext/element/#/cast-attribute')
  , PropObserv        = require('dbjs/_setup/1.property/observable')
  , DescPropObserv    = require('dbjs/_setup/3.descriptor-property/observable')
  , htmlAttributes    = require('./_html-attributes')

  , toDOM = function () { return this.dom; }, componentRender;

componentRender = function (input, options) {
	var el = makeElement.bind(input.document);
	return el('label', { class: 'main-label' },
		(options.label && [el('span', { class: 'label' }, options.label, ':'), ' ']) || null,
		el('span', { class: 'control' }, input,
			// required mark
			el('span', { class: 'required-status' }, '*'),
			// validation status mark
			el('span', { class: 'validation-status' }, '✓'),
			// error message
			el('span', { class: 'error-message error-message-' +
				input._name.replace(/[:#\/]/g, '-') }),
			// hint
			options.hint && el('span', { 'class': 'hint' }, options.hint)));
};

Object.defineProperties(PropObserv.prototype, {
	DOMInput: d(null),
	toDOMInput: d(function (document/*, options*/) {
		var input, initOptions = Object(arguments[1]), options = copy(initOptions)
		  , type, value, onChange, desc;

		desc = this.descriptor;

		// Setup input options
		if (options.multiple == null) options.multiple = desc.multiple;
		if (options.name == null) options.name = this.dbId;
		options.dbOptions = desc;
		options.observable = this;

		// Initialize input
		type = desc.type;
		if (options.DOMInput) input = new options.DOMInput(document, type, options);
		else if (desc.DOMInput) input = new desc.DOMInput(document, type, options);
		else input = type.toDOMInput(document, options);
		input.observable = this;

		// Set input value
		value = this.value;
		if (isSet(value) && !options.multiple) value = value.values().next().value || null;
		if ((value === undefined) && (options.value != null)) value = options.value;
		input.value = value;

		// Attach listeners
		onChange = function () {
			var value = this.value;
			if (isSet(value)) {
				if (!options.multiple) value = value.values().next().value || null;
			}
			input.value = value;
		}.bind(this);
		if (isSet(value)) value.on('change', onChange);
		else this.on('change', onChange);

		input.once('destroy', function () {
			delete input.observable;
			if (isSet(value)) value.off('change', onChange);
			this.off('change', onChange);
		}.bind(this));

		type.emit('dominput', input);
		desc.emit('dominput', input);
		return input;
	}),
	toDOMInputComponent: d(function (document/*, options*/) {
		var options = copy(Object(arguments[1])), input, inputOptions, dom, cb, desc = this.descriptor;

		inputOptions = filter(options, function (value, name) {
			if (name === 'render') return false;
			return !htmlAttributes[name];
		});
		if (options.input) {
			assign(inputOptions, options.input);
			delete inputOptions.input;
		}
		input = this.toDOMInput(document, inputOptions);

		if (options.label === false) options.label = null;
		else if (options.label == null) options.label = desc.label;
		if (options.hint == null) options.hint = desc.inputHint;

		dom = (options.render || componentRender)(input, options);
		forEach(options, function (value, name) {
			if (htmlAttributes[name]) castAttribute.call(dom, name, value);
		}, this);

		dom.classList.add('dbjs-input-component');

		// Required
		dom.classList[input.required ? 'add' : 'remove']('required');
		dom.classList[input.required ? 'remove' : 'add']('optional');

		// Changed
		input.on('change:changed', cb = function (value) {
			dom.classList[value ? 'add' : 'remove']('changed');
			dom.classList[value ? 'remove' : 'add']('not-changed');
		});
		cb(input.changed);

		// Valid
		input.on('change:valid', cb = function (value) {
			dom.classList[value ? 'add' : 'remove']('valid');
			dom.classList[value ? 'remove' : 'add']('invalid');
		});
		cb(input.valid);

		// DBJS valid/invalid & empty/filled
		this.on('change', cb = function () {
			var desc = this.descriptor, value;
			value = desc.required && (this.value == null);
			dom.classList[value ? 'add' : 'remove']('dbjs-invalid');
			dom.classList[value ? 'remove' : 'add']('dbjs-valid');
			dom.classList[this.value == null ? 'add' : 'remove']('dbjs-empty');
			dom.classList[this.value == null ? 'remove' : 'add']('dbjs-filled');
		});
		cb.call(this);
		input.on('destroy', function (cb) { this.off('change', cb); }.bind(this, cb));

		return { dom: dom, input: input, toDOM: toDOM };
	})
});

module.exports = Object.defineProperties(DescPropObserv.prototype, {
	DOMInput: d(null),
	toDOMInput: d(function (document/*, options*/) {
		var input, initOptions = Object(arguments[1]), options = copy(initOptions)
		  , type, onChange, descDesc = this.descriptor;

		// Setup input options
		if (options.name == null) options.name = this.dbId;

		// Initialize input
		type = descDesc.type;
		if (options.DOMInput) input = new options.DOMInput(document, type, options);
		else if (this.DOMInput) input = new this.DOMInput(document, type, options);
		else input = type.toDOMInput(document, options);
		input.observable = this;

		// Set input value
		input.value = this.value;

		// Attach listeners
		onChange = function () { input.value = this.value; }.bind(this);
		this.on('change', onChange);

		input.once('destroy', function () {
			delete input.observable;
			this.off('change', onChange);
		}.bind(this));

		type.emit('dominput', input);
		descDesc.emit('dominput', input);
		return input;
	})
});
<|endoftext|>


<commit_before>Router.route('/resources', function() {
  this.render('resources');
});

groupOrders = new ReactiveArray();

Template.resources.helpers({
  settings: function() {
    var order = this;
    return {
      collection: Resources.find(),
      fields: [
        {
          key: 'order',
          label: 'Order',
          fn: function(value, object) {
            if (_.contains(groupOrders.array(), object._id)) {
              return true;
            } else {
              return false;
            }
          },
          tmpl: Template.orderResource
        },
        {
          key: 'reference',
          label: 'Reference',
          sort: true
        },
        'subject',
        'title',
        'quantity',
        {
          key: 'order',
          label: 'Orders',
          fn: function(value, object) {
            return Orders.find({
              state: 'created',
              orderedResources: {
                $elemMatch: {
                  state: "ordered",
                  resourceId: object._id
                }
              }
            }).count();
          }
        },
        {
          key: 'order',
          label: 'Group Orders',
          fn: function() {
            return 5;
          }
        },
        'group'
      ]
    };
  },
  isConfirm: function() {
    groupOrders.depend();
    if (groupOrders.length > 0) {
      return true;
    } else {
      return false;
    }
  }
});

Template.resources.events({
  'click .reactive-table tr': function(event) {
    if (event.target.className === 'checkbox') {
      var resource = this;
      if (_.contains(groupOrders.array(), resource._id)) {
        var index = groupOrders.indexOf(resource._id);
        groupOrders.splice(index, 1);
      } else {
        groupOrders.push(resource._id);
      }

      if (groupOrders.length > 0) {
        $(".reactive-table-input").val(resource.group).keyup();
      } else {
        $(".reactive-table-input").val('').keyup();
      }
    }
  }
})

Template.orderResource.helpers({
  isSelected: function() {
    var resource = this;
    groupOrders.depend();
    if (_.contains(groupOrders.array(), resource._id)) {
      return true;
    } else {
      return false;
    }
  }
})
<commit_msg>CodeStyle<commit_after>Router.route('/resources', function() {
  this.render('resources');
});

groupOrders = new ReactiveArray();

Template.resources.helpers({
  settings: function() {
    var order = this;
    return {
      collection: Resources.find(),
      fields: [
        {
          key: 'order',
          label: 'Order',
          fn: function(value, object) {
            if (_.contains(groupOrders.array(), object._id)) {
              return true;
            } else {
              return false;
            }
          },
          tmpl: Template.orderResource
        },
        {
          key: 'reference',
          label: 'Reference',
          sort: true
        },
        'subject',
        'title',
        'quantity',
        {
          key: 'order',
          label: 'Orders',
          fn: function(value, object) {
            return Orders.find({
              state: 'created',
              orderedResources: {
                $elemMatch: {
                  state: "ordered",
                  resourceId: object._id
                }
              }
            }).count();
          }
        },
        {
          key: 'order',
          label: 'Group Orders',
          fn: function() {
            return 5;
          }
        },
        'group'
      ]
    };
  },
  isConfirm: function() {
    groupOrders.depend();
    if (groupOrders.length > 0) {
      return true;
    } else {
      return false;
    }
  }
});

Template.resources.events({
  'click .reactive-table tr': function(event) {
    if (event.target.className === 'checkbox') {
      var resource = this;
      if (_.contains(groupOrders.array(), resource._id)) {
        var index = groupOrders.indexOf(resource._id);
        groupOrders.splice(index, 1);
      } else {
        groupOrders.push(resource._id);
      }

      if (groupOrders.length > 0) {
        $(".reactive-table-input").val(resource.group).keyup();
      } else {
        $(".reactive-table-input").val('').keyup();
      }
    }
  }
});

Template.orderResource.helpers({
  isSelected: function() {
    var resource = this;
    groupOrders.depend();
    if (_.contains(groupOrders.array(), resource._id)) {
      return true;
    } else {
      return false;
    }
  }
});
<|endoftext|>