UNPKG

ember-frost-bunsen

Version:
527 lines (429 loc) 14 kB
import {utils} from 'bunsen-core' import {set} from 'bunsen-core/immutable-utils' const {getSubModel, parseVariables} = utils import Ember from 'ember' const {Logger, get, inject, run, typeOf} = Ember import computed, {readOnly} from 'ember-computed-decorators' import config from 'ember-get-config' import {PropTypes} from 'ember-prop-types' import _ from 'lodash' import immutable from 'seamless-immutable' import AbstractInput from './abstract-input' import countries from 'ember-frost-bunsen/fixtures/countries' import addressView from 'ember-frost-bunsen/fixtures/geolocation-address-view' import locationView from 'ember-frost-bunsen/fixtures/geolocation-location-view' import subFormModel from 'ember-frost-bunsen/fixtures/geolocation-model' import layout from 'ember-frost-bunsen/templates/components/frost-bunsen-input-geolocation' const {keys} = Object const MAPQUEST_API_KEY = get(config, 'ember-frost-bunsen.MAPQUEST_API_KEY') const LOOKUP_ENDPOINT = 'http://www.mapquestapi.com/geocoding/v1/address' const REVERSE_LOOKUP_ENDPOINT = 'http://www.mapquestapi.com/geocoding/v1/reverse' const subFormValueShape = { address: PropTypes.string, city: PropTypes.string, country: PropTypes.string, latitude: PropTypes.string, longitude: PropTypes.string, postalCode: PropTypes.string, state: PropTypes.string } /** * Get bunsen path from a reference * i.e. convert '${./foo/bar}' to 'foo.bar' * @param {String} ref - reference * @returns {String} bunsen path */ function bunsenPathFromRef (ref) { return ref.replace(/^\$\{\.\/(.+)}/g, '.$1').replace(/\//g, '.') } /** * Convert country code to name * Note: If code is missing in countries list just return code * @param {String} code - country code * @returns {String} country name */ function countryCodeToName (code) { const country = countries.find((country) => country.code === code) if (!country) { return code } return country.name } /** * Convert country name to code * Note: If name is missing in countries list just return name * @param {String} name - country name * @returns {String} country code */ function countryNameToCode (name) { const country = countries.find((country) => country.name === name) if (!country) { return name } return country.code } /** * Deserialize property value to format consumer expects * @param {String} bunsenId - property key * @param {String} value - property value * @param {Object} bunsenModel - bunsen model * @returns {String|Number} deserialized value */ function deserializeProperty (bunsenId, value, bunsenModel) { const subModel = getSubModel(bunsenModel, bunsenId) switch (subModel.type) { case 'integer': return parseInt(value) case 'number': return parseFloat(value) default: return value } } /** * Serialize form value to be in correct format for sub-forms * @param {Object} formValue - unserialized form value */ function serializeFormValue (formValue) { if (formValue.country) { formValue.country = countryCodeToName(formValue.country) } if (typeOf(formValue.latitude) === 'number') { formValue.latitude = `${formValue.latitude}` } if (typeOf(formValue.longitude) === 'number') { formValue.longitude = `${formValue.longitude}` } } export default AbstractInput.extend({ // == Dependencies =========================================================== ajax: inject.service(), // == Component Properties =================================================== classNames: [ 'frost-bunsen-input-geolocation', 'frost-field' ], layout, // == State Properties ======================================================= propTypes: { // private addressView: PropTypes.object, internalFormValue: PropTypes.shape(subFormValueShape), isLoading: PropTypes.bool, isLoadingUserLocation: PropTypes.bool, locationView: PropTypes.object, subFormModel: PropTypes.object }, getDefaultProps () { return { addressView, internalFormValue: {}, isLoading: false, locationView, subFormModel } }, @readOnly @computed('cellConfig', 'value') subFormValue (cellConfig, value) { const internalFormValue = this.get('internalFormValue') const refs = get(cellConfig, 'renderer.refs') || {} const stringifiedFormValue = parseVariables(value, JSON.stringify(refs), '', true) try { const formValue = JSON.parse(stringifiedFormValue) keys(subFormValueShape) .forEach((key) => { if (key in refs) { return } formValue[key] = internalFormValue[key] }) serializeFormValue(formValue) return formValue } catch (e) { return {} } }, // == Functions ============================================================== /** * Get error message for user location lookup error * @param {Error} e - error * @returns {String} error message */ _getErrorMessage (e) { switch (e.code) { case e.PERMISSION_DENIED: return 'Location lookup is currently disabled in your browser.' case e.POSITION_UNAVAILABLE: return 'Location information is unavailable.' case e.TIMEOUT: return 'The request to get your location timed out.' default: return null } }, /** * Get location in format necessary for API request * @param {Object} value - current address information * @returns {String} normalized location string for API */ _getNormalizedLocation (value) { const {address, city, country, postalCode, state} = value return [ address, city && state ? `${city} ${state}` : city || state || '', postalCode, country ] .filter((segment) => typeOf(segment) === 'string' && segment.length !== 0) .join(',') }, /** * Convert API response key to bunsen model property * @param {String} key - API key * @returns {String} bunsen model property */ _normalizeKey (key) { if (key === 'street') { return 'address' } return key }, /** * Handle error from browsers geolocation lookup API * @param {Error} e - error */ _onGetCurrentPositionError (e) { if (this.isDestroyed || this.isDestroying) return let msg = this._getErrorMessage(e) if (msg) { this.setProperties({ getUserLocationErrorMessage: msg, isLoading: false }) } else { this.set('isLoading', false) Logger.error('Failed to get users location', e) } }, /** * Handle success from browsers geolocation lookup API */ _onGetCurrentPositionSuccess ({coords}) { if (this.isDestroyed || this.isDestroying) return this._updateProperties({ latitude: coords.latitude, longitude: coords.longitude }) this._performReverseLookup(coords.latitude, coords.longitude) }, /** * Handle error from lookup API * @param {Error} e - error */ _onLookupError (e) { if (this.isDestroyed || this.isDestroying) return this.set('isLoading', false) Logger.error('Failed to perform lookup', e) }, /** * Handle success from lookup API * @param {Object} resp - lookup response */ _onLookupSuccess (resp) { if (this.isDestroyed || this.isDestroying) return const location = get(resp, 'results.0.locations.0') || {} const props = this._updateAdminAreaProperties(location, false) Object.assign(props, { latitude: get(location, 'latLng.lat'), longitude: get(location, 'latLng.lng') }) ;[ 'postalCode', 'street' ] .forEach((key) => { if (location[key]) { const normalizedKey = this._normalizeKey(key) if (this.get(`internalFormValue.${normalizedKey}`)) return props[normalizedKey] = location[key] } }) this._updateProperties(props) this.set('isLoading', false) }, /** * Handle error from reverse lookup API * @param {Error} e - error */ _onReverseLookupError (e) { if (this.isDestroyed || this.isDestroying) return this.set('isLoading', false) Logger.error('Failed to perform reverse lookup', e) }, /** * Handle success from reverse lookup API * @param {Object} resp - reverse lookup response */ _onReverseLookupSuccess (resp) { if (this.isDestroyed || this.isDestroying) return const location = get(resp, 'results.0.locations.0') || {} const props = this._updateAdminAreaProperties(location, true) ;[ 'postalCode', 'street' ] .forEach((key) => { if (location[key]) { const normalizedKey = this._normalizeKey(key) props[normalizedKey] = location[key] } }) this._updateProperties(props) this.set('isLoading', false) }, /** * Lookup street address information from location * @param {String} latitude - latitude * @param {String} longitude - longitude */ _performReverseLookup (latitude, longitude) { const key = this.get('cellConfig.renderer.apiKey') || MAPQUEST_API_KEY let url = `${REVERSE_LOOKUP_ENDPOINT}?location=${latitude},${longitude}` if (key) { url += `&key=${key}` } this.get('ajax').request(url) .then(this._onReverseLookupSuccess.bind(this)) .catch(this._onReverseLookupError.bind(this)) }, /** * Update UI to reflect lookup is occurring * @param {Boolean} userLocation - are we looking up user's location */ _startLoading (userLocation) { this.setProperties({ isLoading: true, isLoadingUserLocation: userLocation, getUserLocationErrorMessage: null }) }, _updateAdminAreaProperties (location, overwrite) { const props = {} if (!overwrite) return props for (let i = 1; i < 7; i++) { let type = location[`adminArea${i}Type`] if (!type) { continue } type = type.toLowerCase() if (['city', 'country', 'state'].indexOf(type) !== -1) { props[type] = location[`adminArea${i}`] } } return props }, /* eslint-disable complexity */ /** * Update properties * @param {Object} props - properties to update */ _updateProperties (props) { const bunsenModel = this.get('bunsenModel') const onChange = this.get('onChange') const refs = this.get('cellConfig.renderer.refs') || {} let internalFormValue = this.get('internalFormValue') || immutable({}) let value = this.get('value') || immutable({}) Object.keys(props).forEach((key) => { if (refs[key]) { let bunsenId = bunsenPathFromRef(refs[key]) if (bunsenId[0] === '.') bunsenId = bunsenId.substr(1) const propValue = deserializeProperty(bunsenId, props[key], bunsenModel) value = set(value, bunsenId, propValue) } else { internalFormValue = set(internalFormValue, key, props[key]) } }) run.next(() => { if (this.isDestroyed || this.isDestroying) return if (!_.isEqual(value, this.get('value'))) { onChange(this.get('bunsenId'), value) } if (!_.isEqual(internalFormValue, this.get('internalFormValue'))) { this.set('internalFormValue', internalFormValue) } }) }, /* eslint-enable complexity */ // == Actions ================================================================ actions: { handleSubFormChange (formValue) { const bunsenModel = this.get('bunsenModel') const oldFormValue = this.get('internalFormValue') if (formValue.country) { const countryCode = countryNameToCode(formValue.country) formValue = formValue.set('country', countryCode) } let value = this.get('value') || immutable({}) let valueChanged = false keys(subFormValueShape) .forEach((key) => { // We must convert to strings before comparing because we can run // into case where one is a number and one is a string if (`${oldFormValue[key]}` !== `${formValue[key]}`) { const ref = this.get(`cellConfig.renderer.refs.${key}`) if (ref) { let bunsenId = bunsenPathFromRef(ref) if (bunsenId[0] === '.') bunsenId = bunsenId.substr(1) const propValue = deserializeProperty(bunsenId, formValue[key], bunsenModel) value = set(value, bunsenId, propValue) valueChanged = true } } }) if (valueChanged) { run.next(() => { const currentValue = this.get('value') const changes = Object.keys(value).some((key) => `${get(value, key)}` !== `${get(currentValue, key)}`) if (changes) { this.get('onChange')(this.get('bunsenId'), value) } }) } if (!_.isEqual(formValue, oldFormValue)) { this.set('internalFormValue', formValue) } }, /** * Lookup street address from lat/lon */ lookupAddress () { this._startLoading(false) const latitude = this.get('internalFormValue.latitude') const longitude = this.get('internalFormValue.longitude') this._performReverseLookup(latitude, longitude) }, /** * Lookup lat/lon from street address */ lookupLocation () { const key = this.get('cellConfig.renderer.apiKey') || MAPQUEST_API_KEY const formValue = this.get('internalFormValue') const location = this._getNormalizedLocation(formValue) let url = `${LOOKUP_ENDPOINT}?location=${location}` if (key) { url += `&key=${key}` } this.get('ajax').request(url) .then(this._onLookupSuccess.bind(this)) .catch(this._onLookupError.bind(this)) }, useCurrentLocation () { this._startLoading(true) navigator.geolocation.getCurrentPosition( this._onGetCurrentPositionSuccess.bind(this), this._onGetCurrentPositionError.bind(this) ) } } })