<commit_before>/*
* Copyright (c) 2017 Ken Hibino.
* Licensed under the MIT License (MIT).
* See https://kenny-hibino.github.io/react-places-autocomplete
*/

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import debounce from 'lodash.debounce'
import defaultStyles from './defaultStyles'

class PlacesAutocomplete extends Component {
  constructor(props) {
    super(props)

    this.state = { autocompleteItems: [] }

    this.autocompleteCallback = this.autocompleteCallback.bind(this)
    this.handleInputKeyDown = this.handleInputKeyDown.bind(this)
    this.handleInputChange = this.handleInputChange.bind(this)
    this.debouncedFetchPredictions = debounce(
      this.fetchPredictions,
      this.props.debounce
    )
    this.clearSuggestions = this.clearSuggestions.bind(this)
  }

  componentDidMount() {
    if (!window.google) {
      throw new Error(
        'Google Maps JavaScript API library must be loaded. See: https://github.com/kenny-hibino/react-places-autocomplete#load-google-library'
      )
    }

    if (!window.google.maps.places) {
      throw new Error(
        'Google Maps Places library must be loaded. Please add `libraries=places` to the src URL. See: https://github.com/kenny-hibino/react-places-autocomplete#load-google-library'
      )
    }

    this.autocompleteService = new google.maps.places.AutocompleteService()
    this.autocompleteOK = google.maps.places.PlacesServiceStatus.OK
  }

  autocompleteCallback(predictions, status) {
    if (status !== this.autocompleteOK) {
      this.props.onError(status, this.clearSuggestions)
      return
    }

    // transform snake_case to camelCase
    const formattedSuggestion = structured_formatting => ({
      mainText: structured_formatting.main_text,
      secondaryText: structured_formatting.secondary_text,
    })

    const { highlightFirstSuggestion } = this.props

    this.setState({
      autocompleteItems: predictions.map((p, idx) => ({
        suggestion: p.description,
        placeId: p.place_id,
        active: highlightFirstSuggestion && idx === 0 ? true : false,
        index: idx,
        formattedSuggestion: formattedSuggestion(p.structured_formatting),
      })),
    })
  }

  fetchPredictions() {
    const { value } = this.props.inputProps
    if (value.length) {
      this.autocompleteService.getPlacePredictions(
        {
          ...this.props.options,
          input: value,
        },
        this.autocompleteCallback
      )
    }
  }

  clearSuggestions() {
    this.setState({ autocompleteItems: [] })
  }

  selectAddress(address, placeId) {
    this.clearSuggestions()
    this.handleSelect(address, placeId)
  }

  handleSelect(address, placeId) {
    this.props.onSelect
      ? this.props.onSelect(address, placeId)
      : this.props.inputProps.onChange(address)
  }

  getActiveItem() {
    return this.state.autocompleteItems.find(item => item.active)
  }

  selectActiveItemAtIndex(index) {
    const activeName = this.state.autocompleteItems.find(
      item => item.index === index
    ).suggestion
    this.setActiveItemAtIndex(index)
    this.props.inputProps.onChange(activeName)
  }

  handleEnterKey() {
    const activeItem = this.getActiveItem()
    if (activeItem === undefined) {
      this.handleEnterKeyWithoutActiveItem()
    } else {
      this.selectAddress(activeItem.suggestion, activeItem.placeId)
    }
  }

  handleEnterKeyWithoutActiveItem() {
    if (this.props.onEnterKeyDown) {
      this.props.onEnterKeyDown(this.props.inputProps.value)
      this.clearSuggestions()
    } else {
      return //noop
    }
  }

  handleDownKey() {
    if (this.state.autocompleteItems.length === 0) {
      return
    }

    const activeItem = this.getActiveItem()
    if (activeItem === undefined) {
      this.selectActiveItemAtIndex(0)
    } else {
      const nextIndex =
        (activeItem.index + 1) % this.state.autocompleteItems.length
      this.selectActiveItemAtIndex(nextIndex)
    }
  }

  handleUpKey() {
    if (this.state.autocompleteItems.length === 0) {
      return
    }

    const activeItem = this.getActiveItem()
    if (activeItem === undefined) {
      this.selectActiveItemAtIndex(this.state.autocompleteItems.length - 1)
    } else {
      let prevIndex
      if (activeItem.index === 0) {
        prevIndex = this.state.autocompleteItems.length - 1
      } else {
        prevIndex = (activeItem.index - 1) % this.state.autocompleteItems.length
      }
      this.selectActiveItemAtIndex(prevIndex)
    }
  }

  handleInputKeyDown(event) {
    switch (event.key) {
      case 'Enter':
        event.preventDefault()
        this.handleEnterKey()
        break
      case 'ArrowDown':
        event.preventDefault() // prevent the cursor from moving
        this.handleDownKey()
        break
      case 'ArrowUp':
        event.preventDefault() // prevent the cursor from moving
        this.handleUpKey()
        break
      case 'Escape':
        this.clearSuggestions()
        break
    }

    if (this.props.inputProps.onKeyDown) {
      this.props.inputProps.onKeyDown(event)
    }
  }

  setActiveItemAtIndex(index) {
    this.setState({
      autocompleteItems: this.state.autocompleteItems.map((item, idx) => {
        if (idx === index) {
          return { ...item, active: true }
        } else {
          return { ...item, active: false }
        }
      }),
    })
  }

  handleInputChange(event) {
    const { value } = event.target
    this.props.inputProps.onChange(value)
    if (!value) {
      this.clearSuggestions()
      return
    }
    if (this.props.shouldFetchSuggestions({ value })) {
      this.debouncedFetchPredictions()
    }
  }

  handleInputOnBlur(event) {
    this.clearSuggestions()

    if (this.props.inputProps.onBlur) {
      this.props.inputProps.onBlur(event)
    }
  }

  inlineStyleFor(...props) {
    const { classNames, styles } = this.props
    // No inline style if className is passed via props for the element.
    if (props.some(prop => classNames.hasOwnProperty(prop))) {
      return {}
    }

    return props.reduce((acc, prop) => {
      return {
        ...acc,
        ...defaultStyles[prop],
        ...styles[prop],
      }
    }, {})
  }

  classNameFor(...props) {
    const { classNames } = this.props

    return props.reduce((acc, prop) => {
      const name = classNames[prop] || ''
      return name ? `${acc} ${name}` : acc
    }, '')
  }

  getInputProps() {
    const defaultInputProps = {
      type: 'text',
      autoComplete: 'off',
    }

    return {
      ...defaultInputProps,
      ...this.props.inputProps,
      onChange: event => {
        this.handleInputChange(event)
      },
      onKeyDown: event => {
        this.handleInputKeyDown(event)
      },
      onBlur: event => {
        this.handleInputOnBlur(event)
      },
      style: this.inlineStyleFor('input'),
      className: this.classNameFor('input'),
    }
  }

  render() {
    const { autocompleteItems } = this.state
    const inputProps = this.getInputProps()

    return (
      <div
        id="PlacesAutocomplete__root"
        style={this.inlineStyleFor('root')}
        className={this.classNameFor('root')}
      >
        <input {...inputProps} />
        {autocompleteItems.length > 0 && (
          <div
            id="PlacesAutocomplete__autocomplete-container"
            style={this.inlineStyleFor('autocompleteContainer')}
            className={this.classNameFor('autocompleteContainer')}
          >
            {autocompleteItems.map((p, idx) => (
              <div
                key={p.placeId}
                onMouseOver={() => this.setActiveItemAtIndex(p.index)}
                onMouseDown={() => this.selectAddress(p.suggestion, p.placeId)}
                onTouchStart={() => this.setActiveItemAtIndex(p.index)}
                onTouchEnd={() => this.selectAddress(p.suggestion, p.placeId)}
                style={
                  p.active
                    ? this.inlineStyleFor(
                        'autocompleteItem',
                        'autocompleteItemActive'
                      )
                    : this.inlineStyleFor('autocompleteItem')
                }
                className={
                  p.active
                    ? this.classNameFor(
                        'autocompleteItem',
                        'autocompleteItemActive'
                      )
                    : this.classNameFor('autocompleteItem')
                }
              >
                {this.props.renderSuggestion({
                  suggestion: p.suggestion,
                  formattedSuggestion: p.formattedSuggestion,
                })}
              </div>
            ))}
            {this.props.renderFooter && this.props.renderFooter()}
          </div>
        )}
      </div>
    )
  }
}

PlacesAutocomplete.propTypes = {
  inputProps: (props, propName) => {
    const inputProps = props[propName]

    if (!inputProps.hasOwnProperty('value')) {
      throw new Error("'inputProps' must have 'value'.")
    }

    if (!inputProps.hasOwnProperty('onChange')) {
      throw new Error("'inputProps' must have 'onChange'.")
    }
  },
  onError: PropTypes.func,
  onSelect: PropTypes.func,
  renderSuggestion: PropTypes.func,
  classNames: PropTypes.shape({
    root: PropTypes.string,
    input: PropTypes.string,
    autocompleteContainer: PropTypes.string,
    autocompleteItem: PropTypes.string,
    autocompleteItemActive: PropTypes.string,
  }),
  styles: PropTypes.shape({
    root: PropTypes.object,
    input: PropTypes.object,
    autocompleteContainer: PropTypes.object,
    autocompleteItem: PropTypes.object,
    autocompleteItemActive: PropTypes.object,
  }),
  options: PropTypes.shape({
    bounds: PropTypes.object,
    componentRestrictions: PropTypes.object,
    location: PropTypes.object,
    offset: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    radius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    types: PropTypes.array,
  }),
  debounce: PropTypes.number,
  highlightFirstSuggestion: PropTypes.bool,
  renderFooter: PropTypes.func,
  shouldFetchSuggestions: PropTypes.func.isRequired,
}

PlacesAutocomplete.defaultProps = {
  onError: (status, _clearSuggestions) =>
    console.error(
      '[react-places-autocomplete]: error happened when fetching data from Google Maps API.\nPlease check the docs here (https://developers.google.com/maps/documentation/javascript/places#place_details_responses)\nStatus: ',
      status
    ),
  classNames: {},
  renderSuggestion: ({ suggestion }) => <div>{suggestion}</div>,
  styles: {},
  options: {},
  debounce: 200,
  highlightFirstSuggestion: false,
  shouldFetchSuggestions: () => true,
}

export default PlacesAutocomplete
<commit_msg>fix($browser): Added e.preventDefault() to AutoCompleteItem touch event<commit_after>/*
* Copyright (c) 2017 Ken Hibino.
* Licensed under the MIT License (MIT).
* See https://kenny-hibino.github.io/react-places-autocomplete
*/

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import debounce from 'lodash.debounce'
import defaultStyles from './defaultStyles'

class PlacesAutocomplete extends Component {
  constructor(props) {
    super(props)

    this.state = { autocompleteItems: [] }

    this.autocompleteCallback = this.autocompleteCallback.bind(this)
    this.handleInputKeyDown = this.handleInputKeyDown.bind(this)
    this.handleInputChange = this.handleInputChange.bind(this)
    this.debouncedFetchPredictions = debounce(
      this.fetchPredictions,
      this.props.debounce
    )
    this.clearSuggestions = this.clearSuggestions.bind(this)
  }

  componentDidMount() {
    if (!window.google) {
      throw new Error(
        'Google Maps JavaScript API library must be loaded. See: https://github.com/kenny-hibino/react-places-autocomplete#load-google-library'
      )
    }

    if (!window.google.maps.places) {
      throw new Error(
        'Google Maps Places library must be loaded. Please add `libraries=places` to the src URL. See: https://github.com/kenny-hibino/react-places-autocomplete#load-google-library'
      )
    }

    this.autocompleteService = new google.maps.places.AutocompleteService()
    this.autocompleteOK = google.maps.places.PlacesServiceStatus.OK
  }

  autocompleteCallback(predictions, status) {
    if (status !== this.autocompleteOK) {
      this.props.onError(status, this.clearSuggestions)
      return
    }

    // transform snake_case to camelCase
    const formattedSuggestion = structured_formatting => ({
      mainText: structured_formatting.main_text,
      secondaryText: structured_formatting.secondary_text,
    })

    const { highlightFirstSuggestion } = this.props

    this.setState({
      autocompleteItems: predictions.map((p, idx) => ({
        suggestion: p.description,
        placeId: p.place_id,
        active: highlightFirstSuggestion && idx === 0 ? true : false,
        index: idx,
        formattedSuggestion: formattedSuggestion(p.structured_formatting),
      })),
    })
  }

  fetchPredictions() {
    const { value } = this.props.inputProps
    if (value.length) {
      this.autocompleteService.getPlacePredictions(
        {
          ...this.props.options,
          input: value,
        },
        this.autocompleteCallback
      )
    }
  }

  clearSuggestions() {
    this.setState({ autocompleteItems: [] })
  }

  selectAddress(address, placeId, e) {
    if (e !== undefined) {
      e.preventDefault()
    }
    this.clearSuggestions()
    this.handleSelect(address, placeId)
  }

  handleSelect(address, placeId) {
    this.props.onSelect
      ? this.props.onSelect(address, placeId)
      : this.props.inputProps.onChange(address)
  }

  getActiveItem() {
    return this.state.autocompleteItems.find(item => item.active)
  }

  selectActiveItemAtIndex(index) {
    const activeName = this.state.autocompleteItems.find(
      item => item.index === index
    ).suggestion
    this.setActiveItemAtIndex(index)
    this.props.inputProps.onChange(activeName)
  }

  handleEnterKey() {
    const activeItem = this.getActiveItem()
    if (activeItem === undefined) {
      this.handleEnterKeyWithoutActiveItem()
    } else {
      this.selectAddress(activeItem.suggestion, activeItem.placeId)
    }
  }

  handleEnterKeyWithoutActiveItem() {
    if (this.props.onEnterKeyDown) {
      this.props.onEnterKeyDown(this.props.inputProps.value)
      this.clearSuggestions()
    } else {
      return //noop
    }
  }

  handleDownKey() {
    if (this.state.autocompleteItems.length === 0) {
      return
    }

    const activeItem = this.getActiveItem()
    if (activeItem === undefined) {
      this.selectActiveItemAtIndex(0)
    } else {
      const nextIndex =
        (activeItem.index + 1) % this.state.autocompleteItems.length
      this.selectActiveItemAtIndex(nextIndex)
    }
  }

  handleUpKey() {
    if (this.state.autocompleteItems.length === 0) {
      return
    }

    const activeItem = this.getActiveItem()
    if (activeItem === undefined) {
      this.selectActiveItemAtIndex(this.state.autocompleteItems.length - 1)
    } else {
      let prevIndex
      if (activeItem.index === 0) {
        prevIndex = this.state.autocompleteItems.length - 1
      } else {
        prevIndex = (activeItem.index - 1) % this.state.autocompleteItems.length
      }
      this.selectActiveItemAtIndex(prevIndex)
    }
  }

  handleInputKeyDown(event) {
    switch (event.key) {
      case 'Enter':
        event.preventDefault()
        this.handleEnterKey()
        break
      case 'ArrowDown':
        event.preventDefault() // prevent the cursor from moving
        this.handleDownKey()
        break
      case 'ArrowUp':
        event.preventDefault() // prevent the cursor from moving
        this.handleUpKey()
        break
      case 'Escape':
        this.clearSuggestions()
        break
    }

    if (this.props.inputProps.onKeyDown) {
      this.props.inputProps.onKeyDown(event)
    }
  }

  setActiveItemAtIndex(index) {
    this.setState({
      autocompleteItems: this.state.autocompleteItems.map((item, idx) => {
        if (idx === index) {
          return { ...item, active: true }
        } else {
          return { ...item, active: false }
        }
      }),
    })
  }

  handleInputChange(event) {
    const { value } = event.target
    this.props.inputProps.onChange(value)
    if (!value) {
      this.clearSuggestions()
      return
    }
    if (this.props.shouldFetchSuggestions({ value })) {
      this.debouncedFetchPredictions()
    }
  }

  handleInputOnBlur(event) {
    this.clearSuggestions()

    if (this.props.inputProps.onBlur) {
      this.props.inputProps.onBlur(event)
    }
  }

  inlineStyleFor(...props) {
    const { classNames, styles } = this.props
    // No inline style if className is passed via props for the element.
    if (props.some(prop => classNames.hasOwnProperty(prop))) {
      return {}
    }

    return props.reduce((acc, prop) => {
      return {
        ...acc,
        ...defaultStyles[prop],
        ...styles[prop],
      }
    }, {})
  }

  classNameFor(...props) {
    const { classNames } = this.props

    return props.reduce((acc, prop) => {
      const name = classNames[prop] || ''
      return name ? `${acc} ${name}` : acc
    }, '')
  }

  getInputProps() {
    const defaultInputProps = {
      type: 'text',
      autoComplete: 'off',
    }

    return {
      ...defaultInputProps,
      ...this.props.inputProps,
      onChange: event => {
        this.handleInputChange(event)
      },
      onKeyDown: event => {
        this.handleInputKeyDown(event)
      },
      onBlur: event => {
        this.handleInputOnBlur(event)
      },
      style: this.inlineStyleFor('input'),
      className: this.classNameFor('input'),
    }
  }

  render() {
    const { autocompleteItems } = this.state
    const inputProps = this.getInputProps()

    return (
      <div
        id="PlacesAutocomplete__root"
        style={this.inlineStyleFor('root')}
        className={this.classNameFor('root')}
      >
        <input {...inputProps} />
        {autocompleteItems.length > 0 && (
          <div
            id="PlacesAutocomplete__autocomplete-container"
            style={this.inlineStyleFor('autocompleteContainer')}
            className={this.classNameFor('autocompleteContainer')}
          >
            {autocompleteItems.map((p, idx) => (
              <div
                key={p.placeId}
                onMouseOver={() => this.setActiveItemAtIndex(p.index)}
                onMouseDown={e =>
                  this.selectAddress(p.suggestion, p.placeId, e)
                }
                onTouchStart={() => this.setActiveItemAtIndex(p.index)}
                onTouchEnd={e => this.selectAddress(p.suggestion, p.placeId, e)}
                style={
                  p.active
                    ? this.inlineStyleFor(
                        'autocompleteItem',
                        'autocompleteItemActive'
                      )
                    : this.inlineStyleFor('autocompleteItem')
                }
                className={
                  p.active
                    ? this.classNameFor(
                        'autocompleteItem',
                        'autocompleteItemActive'
                      )
                    : this.classNameFor('autocompleteItem')
                }
              >
                {this.props.renderSuggestion({
                  suggestion: p.suggestion,
                  formattedSuggestion: p.formattedSuggestion,
                })}
              </div>
            ))}
            {this.props.renderFooter && this.props.renderFooter()}
          </div>
        )}
      </div>
    )
  }
}

PlacesAutocomplete.propTypes = {
  inputProps: (props, propName) => {
    const inputProps = props[propName]

    if (!inputProps.hasOwnProperty('value')) {
      throw new Error("'inputProps' must have 'value'.")
    }

    if (!inputProps.hasOwnProperty('onChange')) {
      throw new Error("'inputProps' must have 'onChange'.")
    }
  },
  onError: PropTypes.func,
  onSelect: PropTypes.func,
  renderSuggestion: PropTypes.func,
  classNames: PropTypes.shape({
    root: PropTypes.string,
    input: PropTypes.string,
    autocompleteContainer: PropTypes.string,
    autocompleteItem: PropTypes.string,
    autocompleteItemActive: PropTypes.string,
  }),
  styles: PropTypes.shape({
    root: PropTypes.object,
    input: PropTypes.object,
    autocompleteContainer: PropTypes.object,
    autocompleteItem: PropTypes.object,
    autocompleteItemActive: PropTypes.object,
  }),
  options: PropTypes.shape({
    bounds: PropTypes.object,
    componentRestrictions: PropTypes.object,
    location: PropTypes.object,
    offset: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    radius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    types: PropTypes.array,
  }),
  debounce: PropTypes.number,
  highlightFirstSuggestion: PropTypes.bool,
  renderFooter: PropTypes.func,
  shouldFetchSuggestions: PropTypes.func.isRequired,
}

PlacesAutocomplete.defaultProps = {
  onError: (status, _clearSuggestions) =>
    console.error(
      '[react-places-autocomplete]: error happened when fetching data from Google Maps API.\nPlease check the docs here (https://developers.google.com/maps/documentation/javascript/places#place_details_responses)\nStatus: ',
      status
    ),
  classNames: {},
  renderSuggestion: ({ suggestion }) => <div>{suggestion}</div>,
  styles: {},
  options: {},
  debounce: 200,
  highlightFirstSuggestion: false,
  shouldFetchSuggestions: () => true,
}

export default PlacesAutocomplete
<|endoftext|>


<commit_before>define(['jquery', 'lacuna', 'template'], function($, Lacuna, Template) {

    Template.load(['mapStars']);

    function MapStars() {
        // Use scope to reduce confusion about this.
        var scope = this;

        // A 'tile' is a block of the starmap 100 units wide by 30 units high
        // Each of these tiles can be populated by a single call to get_star_map
        // Nine of these tiles arranged in a 3x3 grid form the starmap
        // the 'view-port' is a window onto this grid
        // As the map is dragged, some tiles will go out of scope and tiles on
        // the other side will be created (by calls to get_star_map).
        // Since the view-port is generally smaller than a tile (especially at high zoom)
        // then tiles will generally be created outside of the viewport and it will
        // give the appearance of continuous smooth scrolling.

        var defaults = {
            parentContainer     : '#starmap',   // The parent div to contain the starmap
            zoomLevel           : 1,            // Default zoom level
            viewX               : 0,            // The start X unit in the starmap
            viewY               : 0,            // The start Y unit in the starmap
            boundLeft           : -1500,        // Bounds of the starmap
            boundRight          : 1499,         // +1500 has no bodies, so we can ignore it
            boundTop            : 1499,         // Likewise on the Y axis
            boundBottom         : -1500,        // Lower bound
        };
        var options;
        // tiles are an array of 9 tiles arranged by index
        //
        //  0  1  2
        //  3  4  5
        //  6  7  8
        //
        scope.tiles = new Array();
        scope.centreTile = {     // the starmap location for the centre tile
            top     : 0,
            left    : 0
        };

        // convert a zoom level into pixels per starmap 'unit'
        var zoomToPixels = {
            6   : 150,
            5   : 100,
            4   : 75,
            3   : 50,
            2   : 35,
            1   : 20
        };

        // We should only need to 'renderStars' when we first display
        // the starmap, when we zoom in/out, or make a big change to
        // our x,y position such that all current tiles go out of range.
        scope.renderStars = function(o) {

            // TODO change this to cater for options already defined
            // so we can call it multiple times, but retain old options
            if (typeof o == 'object') {
                options = $.extend(defaults, o);
            }
            else {
                options = defaults;
            }
            // First determine where the centre tile is positioned in the starmap units
            scope.centreTile.left   = Math.floor((options.viewX - options.boundLeft) / 100) * 100 + options.boundLeft;
            scope.centreTile.top    = Math.floor((options.viewY - options.boundBottom) / 30) * 30 + options.boundBottom + 29;
            Lacuna.debug("Centre tile at "+scope.centreTile.left+"|"+scope.centreTile.top);

            // First just position the nine tiles, they will be empty until
            // they are rendered by calls to 'get_star_map'.

            // The starsParent is the draggable object, it's children (the tiles) can be dragged
            // with it. Let's make it as big as the expanse (in pixels)
            var expanseWidthPx = scope.unitWidthPx() * (options.boundRight - options.boundLeft);
            var expanseHeightPx = scope.unitHeightPx() * (options.boundTop - options.boundBottom);
            var $starsParent = $("#starsParent");
            $starsParent.draggable({
                stop : function(event, ui) {
                    scope.recalculateCentre()
                }
            });
            $starsParent.html('').width(expanseWidthPx).height(expanseHeightPx);

            for (var x=0; x<9; x++) {
                var bounds = scope.getTileBounds(x);
                // calculate the pixel offset to position the tile on the background
                var absLeft = (bounds.left - options.boundLeft) * scope.unitWidthPx();
                var absTop  = (options.boundTop - bounds.top) * scope.unitHeightPx();

                scope.tiles[x] = Template.read.mapStar_tile({
                    absLeft     : absLeft,
                    absTop      : absTop,
                    x           : bounds.left,
                    y           : bounds.top,
                    tileId      : x,
                    widthPx     : 100 * scope.unitWidthPx(),
                    heightPx    : 30 * scope.unitHeightPx(),
                });
                $starsParent.append(scope.tiles[x]);
            }

            // Render all 9 tiles
            // TODO: We should render all tiles that are visible first
            // but for now render the centre tile first
            scope.renderTile(4);
            for (var x=0; x < 9; x++) {
                if (x != 4) {
                    scope.renderTile(x);
                }
            }
            // Get the size of the viewport so we can position the target in the centre of the screen
            var $starsViewport = $("#starsViewport");
            var viewWidth = $starsViewport.width();
            var viewHeight = $starsViewport.height();
            var left = viewWidth / 2 - (options.viewX - options.boundLeft) * scope.unitWidthPx();
            var top = viewHeight / 2 - (options.boundTop - options.viewY) * scope.unitHeightPx();
            $starsParent.css('left', left);
            $starsParent.css('top', top);
            $starsParent = null; // avoid memory leak
        };

        // After a drag-drop, we need to recalculate the centre tile
        scope.recalculateCentre = function() {
            //alert('get here');
            var $starsParent    = $("#starsParent");
            var $starsViewport  = $("#starsViewport");

            var parentLeft      = parseInt($starsParent.css('left'));
            var parentTop       = parseInt($starsParent.css('top'));
            var viewWidth       = parseInt($starsViewport.width());
            var viewHeight      = parseInt($starsViewport.height());
            var unitX           = Math.round((viewWidth / 2 - parentLeft) / scope.unitWidthPx() + options.boundLeft);
            var unitY           = Math.round(options.boundTop - (viewHeight / 2 - parentTop) / scope.unitHeightPx());
//            alert("parentLeft="+parentLeft+", parentTop="+parentTop+", viewWidth="+viewWidth+", viewHeight="+viewHeight+", unitX="+unitX+", unitY="+unitY);

            // Given, the star unit X,Y we need to see if it falls within any of the 9 currently rendered tiles
            var deltas = scope.getTileDelta(unitX,unitY);
            if (deltas.xDelta == 0 && deltas.yDelta == 0) {
                // No change, still in the middle
            }
//            else if (Math.abs(deltas.xDelta) > 1 || Math.abs(deltas.yDelta) > 1) {
                // moved totally outside the current 9 tiles, recalculate everything
//            }
            else {
                // Moved somewhere within the outer tiles, do some juggling
                scope.renderStars({
                    viewX : unitX,
                    viewY : unitY
                });
            }

        };

        // The expanse is tiled in fixed size tiles 100 units wide by 30 units high.
        // We can break the starmap into 30 tiles wide (ignoring x=1500 since it
        // does not contain any bodies) and 100 tiles high (again ignoring y=1500)
        // Taking advantage of this makes the code a bit easier.
        //
        // We can position the tile in an area equivalent to the size of the expanse
        // (in pixels, depending on zoom level) and then position the stars and planets
        // absolutely within the tile.
        //
        scope.unitWidthPx = function() {
            return zoomToPixels[options.zoomLevel];
        };
        scope.unitHeightPx = function() {
            return zoomToPixels[options.zoomLevel];
        };

        // Given a star unit X,Y location, work out the delta change in tiles from
        // the centre tile. e.g. yDelta = -1 means the new position is one tile to the left of the current centre tile
        scope.getTileDelta = function(unitX, unitY) {
            var xDelta  = (Math.floor((unitX - options.boundLeft)/100) * 100 + options.boundLeft - scope.centreTile.left) / 100;
            var yDelta  = (Math.floor((unitY - options.boundBottom)/30) * 30 + options.boundBottom + 29 - scope.centreTile.top) / 30;
            return {xDelta : xDelta, yDelta : yDelta};
        };

        // tileId is the tile who's position we want to find
        // by referring to the central tile
        //
        scope.getTileBounds = function(tileId) {
            // xDelta and yDelta are the tile offsets to the centre tile
            var yDelta  = Math.floor((8 - tileId)/3) - 1;
            var xDelta  = tileId % 3 - 1;
            // Convert to position for the specified tileId (in starmap units)
            var left    = scope.centreTile.left + xDelta * 100;
            var top     = scope.centreTile.top - yDelta * 30;
            var right   = left + 99;
            var bottom  = top - 29;

            return {
                left    : left,
                right   : right,
                top     : top,
                bottom  : bottom
            };
        };

        // Render a single tile given it's ID (0-9)
        scope.renderTile = function(tileId) {
            var bounds = scope.getTileBounds(tileId);

            if (    bounds.left   >= options.boundLeft
                &&  bounds.right  <= options.boundRight
                &&  bounds.bottom >= options.boundBottom
                &&  bounds.top    <= options.boundTop) {
                // Then we are within the bounds of the starmap
                Lacuna.send({
                    module: '/map',
                    method: 'get_star_map',
                    params: [{
                        session_id  : Lacuna.getSession(),
                        left        : bounds.left,
                        top         : bounds.top,
                        right       : bounds.right,
                        bottom      : bounds.bottom
                    }],
                    success : function(o) {
                        var stars = o.result.stars;

                        $("#starmap_tile"+tileId).html('');

                        // Map each star onto the tile
                        for (var i = 0; i < stars.length; i++) {
                            var star = stars[i];
                            var star_div = Template.read.mapStar_star({
                                assetsUrl   : window.assetsUrl,
                                id          : star.id,
                                x           : star.x,
                                y           : star.y,
                                name        : star.name,
                                tile_width  : scope.unitWidthPx() * 3,
                                tile_height : scope.unitHeightPx() * 3,
                                tile_left   : (star.x - bounds.left) * scope.unitWidthPx(),
                                tile_top    : (bounds.top - star.y) * scope.unitHeightPx(),
                                star_color  : star.color,
                                star_width  : scope.unitWidthPx() * 3,
                                star_height : scope.unitHeightPx() * 3,
                                margin_top  : 5,
                                star_seized : 0
                            });
                            $("#starmap_tile"+tileId).append(star_div);
                        }

                        // Now populate the tile with the ships
                    }
                });
            }
            else {
                // Then we are outside the bounds, just render a starfield
                // (we may not need to render anything if we have a tiled background image)
                Lacuna.alert('Outside bounds');
            }
        };
    };

    return new MapStars();
});

<commit_msg>First attempt at moving tile data<commit_after>define(['jquery', 'lacuna', 'template'], function($, Lacuna, Template) {

    Template.load(['mapStars']);

    function MapStars() {
        // Use scope to reduce confusion about this.
        var scope = this;

        // A 'tile' is a block of the starmap 100 units wide by 30 units high
        // Each of these tiles can be populated by a single call to get_star_map
        // Nine of these tiles arranged in a 3x3 grid form the starmap
        // the 'view-port' is a window onto this grid
        // As the map is dragged, some tiles will go out of scope and tiles on
        // the other side will be created (by calls to get_star_map).
        // Since the view-port is generally smaller than a tile (especially at high zoom)
        // then tiles will generally be created outside of the viewport and it will
        // give the appearance of continuous smooth scrolling.

        var defaults = {
            parentContainer     : '#starmap',   // The parent div to contain the starmap
            zoomLevel           : 1,            // Default zoom level
            viewX               : 0,            // The start X unit in the starmap
            viewY               : 0,            // The start Y unit in the starmap
            boundLeft           : -1500,        // Bounds of the starmap
            boundRight          : 1499,         // +1500 has no bodies, so we can ignore it
            boundTop            : 1499,         // Likewise on the Y axis
            boundBottom         : -1500,        // Lower bound
        };
        var options;
        // tiles are an array of 9 tiles arranged by index
        //
        //  0  1  2
        //  3  4  5
        //  6  7  8
        //
        scope.tiles = new Array();
        scope.centreTile = {     // the starmap location for the centre tile
            top     : 0,
            left    : 0
        };

        // convert a zoom level into pixels per starmap 'unit'
        var zoomToPixels = {
            6   : 150,
            5   : 100,
            4   : 75,
            3   : 50,
            2   : 35,
            1   : 20
        };

        // We should only need to 'renderStars' when we first display
        // the starmap, when we zoom in/out, or make a big change to
        // our x,y position such that all current tiles go out of range.
        scope.renderStars = function(o) {

            // TODO change this to cater for options already defined
            // so we can call it multiple times, but retain old options
            if (typeof o == 'object') {
                options = $.extend(defaults, o);
            }
            else {
                options = defaults;
            }
            // First determine where the centre tile is positioned in the starmap units
            scope.centreTile.left   = Math.floor((options.viewX - options.boundLeft) / 100) * 100 + options.boundLeft;
            scope.centreTile.top    = Math.floor((options.viewY - options.boundBottom) / 30) * 30 + options.boundBottom + 29;
            Lacuna.debug("Centre tile at "+scope.centreTile.left+"|"+scope.centreTile.top);

            // First just position the nine tiles, they will be empty until
            // they are rendered by calls to 'get_star_map'.

            // The starsParent is the draggable object, it's children (the tiles) can be dragged
            // with it. Let's make it as big as the expanse (in pixels)
            var expanseWidthPx = scope.unitWidthPx() * (options.boundRight - options.boundLeft);
            var expanseHeightPx = scope.unitHeightPx() * (options.boundTop - options.boundBottom);
            var $starsParent = $("#starsParent");
            $starsParent.draggable({
                stop : function(event, ui) {
                    scope.recalculateCentre()
                }
            });
            $starsParent.html('').width(expanseWidthPx).height(expanseHeightPx);

            for (var x=0; x<9; x++) {
                var bounds = scope.getTileBounds(x);
                // calculate the pixel offset to position the tile on the background
                var absLeft = (bounds.left - options.boundLeft) * scope.unitWidthPx();
                var absTop  = (options.boundTop - bounds.top) * scope.unitHeightPx();

                scope.tiles[x] = Template.read.mapStar_tile({
                    absLeft     : absLeft,
                    absTop      : absTop,
                    x           : bounds.left,
                    y           : bounds.top,
                    tileId      : x,
                    widthPx     : 100 * scope.unitWidthPx(),
                    heightPx    : 30 * scope.unitHeightPx(),
                });
                $starsParent.append(scope.tiles[x]);
            }

            // Render all 9 tiles
            // TODO: We should render all tiles that are visible first
            // but for now render the centre tile first
            scope.renderTile(4);
            for (var x=0; x < 9; x++) {
                if (x != 4) {
                    scope.renderTile(x);
                }
            }
            // Get the size of the viewport so we can position the target in the centre of the screen
            var $starsViewport = $("#starsViewport");
            var viewWidth = $starsViewport.width();
            var viewHeight = $starsViewport.height();
            var left = viewWidth / 2 - (options.viewX - options.boundLeft) * scope.unitWidthPx();
            var top = viewHeight / 2 - (options.boundTop - options.viewY) * scope.unitHeightPx();
            $starsParent.css('left', left);
            $starsParent.css('top', top);
            $starsParent = null; // avoid memory leak
        };

        // After a drag-drop, we need to recalculate the centre tile
        scope.recalculateCentre = function() {
            //alert('get here');
            var $starsParent    = $("#starsParent");
            var $starsViewport  = $("#starsViewport");

            var parentLeft      = parseInt($starsParent.css('left'));
            var parentTop       = parseInt($starsParent.css('top'));
            var viewWidth       = parseInt($starsViewport.width());
            var viewHeight      = parseInt($starsViewport.height());
            var unitX           = Math.round((viewWidth / 2 - parentLeft) / scope.unitWidthPx() + options.boundLeft);
            var unitY           = Math.round(options.boundTop - (viewHeight / 2 - parentTop) / scope.unitHeightPx());
//            alert("parentLeft="+parentLeft+", parentTop="+parentTop+", viewWidth="+viewWidth+", viewHeight="+viewHeight+", unitX="+unitX+", unitY="+unitY);

            // Given, the star unit X,Y we need to see if it falls within any of the 9 currently rendered tiles
            var moveToTile = scope.getTileToMoveTo(unitX,unitY);
            if (moveToTile == -1) {
                // moved totally outside the current 9 tiles, recalculate everything
                scope.renderStars({
                    viewX : unitX,
                    viewY : unitY
                });
            }
            else {
                // Moved somewhere within the outer tiles, do some juggling
                // We can keep some of the existing tiles (but move them)
                //  moveToTile  Move tiles      Replace tiles
                //  0           4,3,1,0
                //  1           5,4,3,2,1,0     0,1,2
                //  2           5,4,2,1
                //  3           7,6,4,3,1,0
                //  4           no change, see above
                //  5           1,2,4,5,7,8
                //  6           3,4,6,7
                //  7           3,4,5,6,7,8
                //  8           4,5,7,8
                if (moveToTile == 4) {
                }
                else if (moveToTile == 7) {
                    scope.moveTile(5,8);
                    scope.moveTile(4,7);
                    scope.moveTile(3,6);
                    scope.moveTile(2,5);
                    scope.moveTile(1,4);
                    scope.moveTile(0,3);
                    scope.centreTile.top = scope.centreTile.top + 100;
                    scope.renderTile(0);
                    scope.renderTile(1);
                    scope.renderTile(2);
                }
                else {
                    scope.renderStars({
                        viewX : unitX,
                        viewY : unitY
                    });
                }
            }
        };
        scope.moveTile = function(from, to) {
            scope.tiles[to] = scope.tiles[from];
            $("#starmap_tile"+to).html($("#starmap_tile"+from).html());
        };

        // The expanse is tiled in fixed size tiles 100 units wide by 30 units high.
        // We can break the starmap into 30 tiles wide (ignoring x=1500 since it
        // does not contain any bodies) and 100 tiles high (again ignoring y=1500)
        // Taking advantage of this makes the code a bit easier.
        //
        // We can position the tile in an area equivalent to the size of the expanse
        // (in pixels, depending on zoom level) and then position the stars and planets
        // absolutely within the tile.
        //
        scope.unitWidthPx = function() {
            return zoomToPixels[options.zoomLevel];
        };
        scope.unitHeightPx = function() {
            return zoomToPixels[options.zoomLevel];
        };

        // Given a star unit X,Y location, work out which of the existing tiles have we moved to
        // (or return -1 if we are out of range of all existing tiles)
        scope.getTileToMoveTo = function(unitX, unitY) {
            var xDelta  = (Math.floor((unitX - options.boundLeft)/100) * 100 + options.boundLeft - scope.centreTile.left) / 100;
            var yDelta  = (Math.floor((unitY - options.boundBottom)/30) * 30 + options.boundBottom + 29 - scope.centreTile.top) / 30;
            if (Math.abs(xDelta) < 2 && Math.abs(yDelta) < 2) {
                return 4 + yDelta * 3 + xDelta;
            }
            return -1;      // Out of range of all tiles
        };

        // tileId is the tile who's position we want to find
        // by referring to the central tile
        //
        scope.getTileBounds = function(tileId) {
            // xDelta and yDelta are the tile offsets to the centre tile
            var yDelta  = Math.floor((8 - tileId)/3) - 1;
            var xDelta  = tileId % 3 - 1;
            // Convert to position for the specified tileId (in starmap units)
            var left    = scope.centreTile.left + xDelta * 100;
            var top     = scope.centreTile.top - yDelta * 30;
            var right   = left + 99;
            var bottom  = top - 29;

            return {
                left    : left,
                right   : right,
                top     : top,
                bottom  : bottom
            };
        };

        // Render a single tile given it's ID (0-9)
        scope.renderTile = function(tileId) {
            var bounds = scope.getTileBounds(tileId);

            if (    bounds.left   >= options.boundLeft
                &&  bounds.right  <= options.boundRight
                &&  bounds.bottom >= options.boundBottom
                &&  bounds.top    <= options.boundTop) {
                // Then we are within the bounds of the starmap
                Lacuna.send({
                    module: '/map',
                    method: 'get_star_map',
                    params: [{
                        session_id  : Lacuna.getSession(),
                        left        : bounds.left,
                        top         : bounds.top,
                        right       : bounds.right,
                        bottom      : bounds.bottom
                    }],
                    success : function(o) {
                        var stars = o.result.stars;

                        $("#starmap_tile"+tileId).html('');

                        // Map each star onto the tile
                        for (var i = 0; i < stars.length; i++) {
                            var star = stars[i];
                            var star_div = Template.read.mapStar_star({
                                assetsUrl   : window.assetsUrl,
                                id          : star.id,
                                x           : star.x,
                                y           : star.y,
                                name        : star.name,
                                tile_width  : scope.unitWidthPx() * 3,
                                tile_height : scope.unitHeightPx() * 3,
                                tile_left   : (star.x - bounds.left) * scope.unitWidthPx(),
                                tile_top    : (bounds.top - star.y) * scope.unitHeightPx(),
                                star_color  : star.color,
                                star_width  : scope.unitWidthPx() * 3,
                                star_height : scope.unitHeightPx() * 3,
                                margin_top  : 5,
                                star_seized : 0
                            });
                            $("#starmap_tile"+tileId).append(star_div);
                        }

                        // Now populate the tile with the ships
                    }
                });
            }
            else {
                // Then we are outside the bounds, just render a starfield
                // (we may not need to render anything if we have a tiled background image)
                Lacuna.alert('Outside bounds');
            }
        };
    };

    return new MapStars();
});

<|endoftext|>


<commit_before>import { observable, autorun, computed, action } from 'mobx'
import { app } from '@mindhive/di'
import { WindowSize } from '../responsiveUi/windowMetrics'

const dockedWindowSize = WindowSize.MEDIUM

export class DrawerDomain {

  @observable wantDocked = true
  @observable wantOpen = null

  @computed get canDock() {
    return app().windowMetricsDomain.size.ordinal >= dockedWindowSize.ordinal
  }

  @computed get docked() {
    return this.canDock && this.wantDocked
  }

  @computed get open() {
    return this.docked ? null : this.wantOpen
  }

  @action toggle = () => {
    if (this.canDock) {
      this.wantDocked = ! this.wantDocked
    } else {
      this.wantOpen = ! this.wantOpen
    }
  }

  onTouchTap = () => {
    if (! this.docked) {
      this.setWantOpen(false)
    }
  }

  @action setWantOpen = (open) => {
    this.wantOpen = open
  }
}
<commit_msg>Unused import<commit_after>import { observable, computed, action } from 'mobx'
import { app } from '@mindhive/di'
import { WindowSize } from '../responsiveUi/windowMetrics'

const dockedWindowSize = WindowSize.MEDIUM

export class DrawerDomain {

  @observable wantDocked = true
  @observable wantOpen = null

  @computed get canDock() {
    return app().windowMetricsDomain.size.ordinal >= dockedWindowSize.ordinal
  }

  @computed get docked() {
    return this.canDock && this.wantDocked
  }

  @computed get open() {
    return this.docked ? null : this.wantOpen
  }

  @action toggle = () => {
    if (this.canDock) {
      this.wantDocked = ! this.wantDocked
    } else {
      this.wantOpen = ! this.wantOpen
    }
  }

  onTouchTap = () => {
    if (! this.docked) {
      this.setWantOpen(false)
    }
  }

  @action setWantOpen = (open) => {
    this.wantOpen = open
  }
}
<|endoftext|>


<commit_before>/*jshint node:true */
"use strict";

var root = require("./root"),
	nativeResource = require("./native"),
	httpStatus = require("./httpStatus");

var mongooseResource,
	hasMongoose = false;

try {
	require("mongoose");
	hasMongoose = true;
} catch(e) {}

if (hasMongoose) {
	mongooseResource = require("./mongoose");
}

function yarm(options) {
	options = options || {};
	options.defaultLimit = options.defaultLimit || 10;
	options.errorStack = options.errorStack || false;

	function handleError(req, res, err) {
		if (err) {
			err.code = err.code || 500;
			res.send(err.code, options.errorStack ? err.stack : err.message);
			return true;
		}
	}

	function makeCallback(req, res) {
		function cb(err, body, mime) {
			if (!handleError(req, res, err)) {
				sendResponse(req, res, body, mime);
			}
		}

		cb.file = function(err, path, mime) {
			if (!handleError(req, res, err)) {
				sendFile(req, res, path, mime);
			}
		};

		cb.status = function(code, body) {
			handleError(req, res, httpStatus(code, body));
		};

		cb.list = function(counter, lister) {
			sendListResponse(req, res, counter, lister);
		};

		httpStatus.names.forEach(function(name) {
			cb[name] = function() {
				console.log("\ncalled cb."+name);
				handleError(req, res, httpStatus[name]());
			};
		});

		return cb;
	}

	function sendFile(req, res, path, mime) {
		if (mime) {
			res.type(mime);
		}

		res.sendfile(path);
	}

	function sendResponse(req, res, body, mime) {
		if (body === null || body === undefined) {
			handleError(req, res, httpStatus.noContent());
			return;
		}

		if (mime) {
			res.type(mime);
		}

		if (typeof body === "number") {
			// Cast to string to avoid mistaking body for HTTP status
			body = "" + body;
		}

		// TODO look for a cleaner way to identify Readables
		if (body && typeof body._read === "function") {
			body.pipe(res);
		} else if (body) {
			res.send(body);
		}
	}

	function sendListResponse(req, res, counter, lister) {
		var skip = parseInt(req.param("skip"), 10),
			limit = parseInt(req.param("limit"), 10);

		if (isNaN(skip)) {
			skip = 0;
		}

		if (isNaN(limit)) {
			limit = options.defaultLimit;
		}

		counter(req, function(err, count) {
			if (handleError(req, res, err)) {
				return;
			}

			lister(req, skip, limit, function(err, items) {
				if (handleError(req, res, err)) {
					return;
				}

				res.send({
					_count: count,
					_items: items
				});
			});
		});
	}

	function restResult(req, res, currentSpec) {
		var method = req.method.toUpperCase();

		switch(method) {
			case "GET":
			case "HEAD":
				if (currentSpec.get) {
					return currentSpec.get(req, makeCallback(req, res));
				} else if (currentSpec.count && currentSpec.list) {
					return sendListResponse(req, res, currentSpec.count, currentSpec.list);
				}

				break;

			case "PUT":
			case "PATCH":
				if (currentSpec.put) {
					return currentSpec.put(req, method === "PATCH", makeCallback(req, res));
				}

				break;

			case "DELETE":
				if (currentSpec.del) {
					return currentSpec.del(req, makeCallback(req, res));
				}

				break;

			case "POST":
				if (currentSpec.post) {
					return currentSpec.post(req, makeCallback(req, res));
				}

				break;

		}

		(makeCallback(req, res)).methodNotAllowed();
	}

	return function(req, res) {
		var data = root.match(req);

		function nextHook(err) {
			if (err) {
				handleError(req, res, err);
				return;
			}

			var hook = data.hooks.shift();

			if (hook) {
				hook.call(null, req, nextHook);
			} else {
				restResult(req, res, data.spec);
			}
		}

		httpStatus.names.forEach(function(name) {
			nextHook[name] = function() {
				nextHook(httpStatus[name]());
			};
		});

		if (data.spec) {
			data.hooks = data.hooks || [];
			nextHook();
		} else {
			handleError(req, res, httpStatus.notFound());
		}
	};
}

/* Resource definers */
yarm.resource = function(name) {
	return root.sub(name);
};
yarm.remove = function(name) {
	root.remove(name);
};

yarm.native = nativeResource;

if (hasMongoose) {
	yarm.mongoose = mongooseResource;
}

module.exports = yarm;<commit_msg>Remove console.log call<commit_after>/*jshint node:true */
"use strict";

var root = require("./root"),
	nativeResource = require("./native"),
	httpStatus = require("./httpStatus");

var mongooseResource,
	hasMongoose = false;

try {
	require("mongoose");
	hasMongoose = true;
} catch(e) {}

if (hasMongoose) {
	mongooseResource = require("./mongoose");
}

function yarm(options) {
	options = options || {};
	options.defaultLimit = options.defaultLimit || 10;
	options.errorStack = options.errorStack || false;

	function handleError(req, res, err) {
		if (err) {
			err.code = err.code || 500;
			res.send(err.code, options.errorStack ? err.stack : err.message);
			return true;
		}
	}

	function makeCallback(req, res) {
		function cb(err, body, mime) {
			if (!handleError(req, res, err)) {
				sendResponse(req, res, body, mime);
			}
		}

		cb.file = function(err, path, mime) {
			if (!handleError(req, res, err)) {
				sendFile(req, res, path, mime);
			}
		};

		cb.status = function(code, body) {
			handleError(req, res, httpStatus(code, body));
		};

		cb.list = function(counter, lister) {
			sendListResponse(req, res, counter, lister);
		};

		httpStatus.names.forEach(function(name) {
			cb[name] = function() {
				handleError(req, res, httpStatus[name]());
			};
		});

		return cb;
	}

	function sendFile(req, res, path, mime) {
		if (mime) {
			res.type(mime);
		}

		res.sendfile(path);
	}

	function sendResponse(req, res, body, mime) {
		if (body === null || body === undefined) {
			handleError(req, res, httpStatus.noContent());
			return;
		}

		if (mime) {
			res.type(mime);
		}

		if (typeof body === "number") {
			// Cast to string to avoid mistaking body for HTTP status
			body = "" + body;
		}

		// TODO look for a cleaner way to identify Readables
		if (body && typeof body._read === "function") {
			body.pipe(res);
		} else if (body) {
			res.send(body);
		}
	}

	function sendListResponse(req, res, counter, lister) {
		var skip = parseInt(req.param("skip"), 10),
			limit = parseInt(req.param("limit"), 10);

		if (isNaN(skip)) {
			skip = 0;
		}

		if (isNaN(limit)) {
			limit = options.defaultLimit;
		}

		counter(req, function(err, count) {
			if (handleError(req, res, err)) {
				return;
			}

			lister(req, skip, limit, function(err, items) {
				if (handleError(req, res, err)) {
					return;
				}

				res.send({
					_count: count,
					_items: items
				});
			});
		});
	}

	function restResult(req, res, currentSpec) {
		var method = req.method.toUpperCase();

		switch(method) {
			case "GET":
			case "HEAD":
				if (currentSpec.get) {
					return currentSpec.get(req, makeCallback(req, res));
				} else if (currentSpec.count && currentSpec.list) {
					return sendListResponse(req, res, currentSpec.count, currentSpec.list);
				}

				break;

			case "PUT":
			case "PATCH":
				if (currentSpec.put) {
					return currentSpec.put(req, method === "PATCH", makeCallback(req, res));
				}

				break;

			case "DELETE":
				if (currentSpec.del) {
					return currentSpec.del(req, makeCallback(req, res));
				}

				break;

			case "POST":
				if (currentSpec.post) {
					return currentSpec.post(req, makeCallback(req, res));
				}

				break;

		}

		(makeCallback(req, res)).methodNotAllowed();
	}

	return function(req, res) {
		var data = root.match(req);

		function nextHook(err) {
			if (err) {
				handleError(req, res, err);
				return;
			}

			var hook = data.hooks.shift();

			if (hook) {
				hook.call(null, req, nextHook);
			} else {
				restResult(req, res, data.spec);
			}
		}

		httpStatus.names.forEach(function(name) {
			nextHook[name] = function() {
				nextHook(httpStatus[name]());
			};
		});

		if (data.spec) {
			data.hooks = data.hooks || [];
			nextHook();
		} else {
			handleError(req, res, httpStatus.notFound());
		}
	};
}

/* Resource definers */
yarm.resource = function(name) {
	return root.sub(name);
};
yarm.remove = function(name) {
	root.remove(name);
};

yarm.native = nativeResource;

if (hasMongoose) {
	yarm.mongoose = mongooseResource;
}

module.exports = yarm;<|endoftext|>


<commit_before>(function (factory) {
    if (typeof define === "function" && define.amd) {
        define('bforms-panel', ['jquery', 'bootstrap', 'amplify', 'bforms-ajax', 'bforms-form'], factory);
    } else {
        factory(window.jQuery);
    }
})(function ($) {

    var bsPanel = function () {

    };

    bsPanel.prototype.options = {
        collapse: true,
        loaded: false,
        editable: true,

        toggleSelector: '.bs-togglePanel',

        editSelector: '.bs-editPanel',
        cancelEditSelector: '.bs-cancelEdit',
        saveFormSelector: '.bs-savePanel',

        containerSelector: '.bs-containerPanel',
        contentSelector: '.bs-contentPanel',

        retrySelector: '.bs-retryBtn',

        headerToggle: false,
        headerSelector: '.bs-panelHeader',

        cacheReadonlyContent: true,
        additionalData: {
        },
        formOptions: {

        },

        retryMessage : 'Reload'
    };

    //#region init
    bsPanel.prototype._init = function () {
        this.$element = this.element;

        this.$element.addClass('bs-hasPanel');

        this.$toggleEleemnt = this.$element.find(this.options.toggleSelector);
        this._loadOptions();

        this._initDefaultProperties();
        this._initSelectors();
        this._delegateEvents();

        if (this.options.loaded === true) {
            this._initControls();
            this._loadState();
            this._initCurrentContent();
        } else {
            var method = this._readonly ? '_loadReadonlyContent' : "_loadEditableContent";

            this[method]().then($.proxy(function () {
                this._initControls();
                this._loadState();
                this._initCurrentContent();
            }, this));
        }

        if (!this.$toggleEleemnt.data("expandable")) {
            this._loadState(true);
        }
    };

    bsPanel.prototype._loadOptions = function () {
        var settings = this.$element.data('settings');

        $.extend(true, this.options, settings);
    };

    bsPanel.prototype._initDefaultProperties = function () {
        this._name = this.options.name || this.$element.prop('id');

        if (typeof this._name === "undefined" || this._name == '') {
            throw "boxForm requires an unique name";
        }

        this._componentId = this.$element.data('component');

        if (typeof this._componentId === "undefined") {
            console.warn("No component id specified for " + this._name);
        }

        this._objId = this.$element.data('objid');

        this._key = window.location.pathname + '|BoxForm|' + this._name;

        if ((typeof this.options.initialReadonly === "undefined" || this.options.initialReadonly == true) && this.$element.data('readonly') !== false) {
            this.showReadonly();
        } else {
            this.showEditable();
        }

        this.options.readonlyUrl = this.options.readonlyUrl || this.$element.data('readonlyurl');
        this.options.editableUrl = this.options.editableUrl || this.$element.data('editableurl');
        this.options.saveUrl = this.options.saveUrl || this.$element.data('saveurl');

        this._allowExpand = this.options.loaded;
    };

    bsPanel.prototype._initSelectors = function () {
        this.$container = this.$element.find(this.options.containerSelector);
        this.$content = this.$element.find(this.options.contentSelector);
    };

    bsPanel.prototype._delegateEvents = function () {

        this.$element.on('click', this.options.toggleSelector, $.proxy(this._onToggleClick, this));

        if (this.options.editable !== false) {
            if (this.options.headerToggle) {
                this.$element.on('click', this.options.headerSelector, $.proxy(this._onHeaderClick, this));
            } else {
                this.$element.on('click', this.options.editSelector, $.proxy(this._onEditClick, this));
            }
        }

        this.$element.on('click', this.options.cancelEditSelector, $.proxy(this._onCancelEditClick, this));

        this.$element.on('click', this.options.retrySelector, $.proxy(this._onRetryClick, this));
    };

    bsPanel.prototype._initCurrentContent = function () {
        if (this._readonly) {

        } else {
            this._initEditable();
        }
    };
    //#endregion

    //#region events
    bsPanel.prototype._onToggleClick = function (e) {
        e.preventDefault();
        e.stopPropagation();
        var currentTarget = $(e.currentTarget);

        if (!currentTarget.data("expandable")) {
            return;
        }
        if (this._allowExpand) {
            if (this._state) {
                this.close();
            } else {
                this.open();
            }
        }
    };

    bsPanel.prototype._onHeaderClick = function (e) {
        if (this._readonly == true) {
            this._loadEditableContent().then($.proxy(function () {
                if (!this._state && this._allowExpand) {
                    this.open();
                }
            }, this));

            e.preventDefault();
            e.stopPropagation();
        }
    };

    bsPanel.prototype._onEditClick = function (e) {
        e.preventDefault();
        e.stopPropagation();

        this._loadEditableContent().then($.proxy(function () {
            if (!this._state && this._allowExpand) {
                this.open();
            }
        }, this));
    };

    bsPanel.prototype._onCancelEditClick = function (e) {
        e.preventDefault();
        e.stopPropagation();

        if (this.options.cacheReadonlyContent && this._cachedReadonlyContent) {
            this.$content.html(this._cachedReadonlyContent);
            this.showReadonly();
            this._toggleEditBtn(true);

            if (!this._state) {
                this.open();
            }
        } else {
            this._loadReadonlyContent().then($.proxy(function () {
                this._toggleEditBtn(true);

                if (!this._state) {
                    this.open();
                }
            }, this));
        }
    };

    bsPanel.prototype._onRetryClick = function (e) {
        var $target = $(e.currentTarget),
            method = $target.data('method');

        if (typeof this[method] === "function") {
            this[method]();
        }
    };
    //#endregion

    //#region private methods
    bsPanel.prototype._saveState = function () {
        amplify.store(this._key, this._state);
    };

    bsPanel.prototype._loadState = function (forceOpen) {
        if (forceOpen) {
            this.open();
            return;
        }

        var lastState = amplify.store(this._key);
        if (lastState != null) {

            if (lastState == true) {
                this.open();
            } else {
                this.close();
            }
        }

    };

    bsPanel.prototype._initControls = function () {

        if (this.options.editable) {
            this._toggleEditBtn(this._readonly ? true : false);
        }

    };

    bsPanel.prototype._toggleLoading = function (show) {
        if (show) {
            this.$element.find('.bs-panelLoader').show();
        } else {
            this.$element.find('.bs-panelLoader').hide();
        }
    };

    bsPanel.prototype._toggleCaret = function (show) {
        if (show) {
            this.$element.find('.bs-panelCaret').show();
        } else {
            this.$element.find('.bs-panelCaret').hide();
        }
    };

    bsPanel.prototype._toggleEditBtn = function (show) {
        if (show) {
            this.$element.find(this.options.cancelEditSelector).hide().end()
                .find(this.options.editSelector).show();
        } else {
            this.$element.find(this.options.editSelector).hide().end()
                .find(this.options.cancelEditSelector).show();
        }
    };

    bsPanel.prototype._getXhrData = function () {

        return $.extend(true, {}, {
            componentId: this._componentId,
            objId: this._objId
        }, this.options.additionalData);

    };

    bsPanel.prototype._showErrorMessage = function (message, $errorContainer, replace, method) {

        var $error = $('
' +
                           '<button class="close" data-dismiss="alert" type="button">×</button>' +
                               message +
                            '
' +
                                '' + this.options.retryMessage +'  ' +
                            '
' +
                       '
');
        if (replace) {
            console.log($errorContainer)

            $errorContainer.html($error);
        } else {
            $errorContainer.append($error);
        }
    };

    bsPanel.prototype._initEditable = function () {

        var $form = this.$content.find('form').show(),
            $saveBtn = $form.find(this.options.saveFormSelector);

        this._allowExpand = true;

        if ($form.length == 0) {
            console.warn('No editable form found');
        }

        if ($saveBtn.length == 0) {
            console.warn("No save button found");
        }

        var formOptions = $.extend(true, {}, {
            prefix: this.options.prefix,
            actions: [{
                name: 'save',
                selector: this.options.saveFormSelector,
                validate: true,
                actionUrl: this.options.saveUrl,
                parse: true,
                getExtraData: $.proxy(function (data) {
                    data.componentId = this._componentId;
                    data.objId = this._objId;

                    $.extend(true, data, this.options.formAdditionalData);

                    this._trigger('beforeSaveAjax', 0, data);

                }, this),
                handler: $.proxy(function (sent, data) {

                    this._trigger('beforeEditSuccessHandler', 0, data);

                    this.$content.html(data.Html);

                    this._toggleLoading();
                    this._toggleEditBtn(true);

                    if (this.options.cacheReadonlyContent) {
                        this._cachedReadonlyContent = this.$content.html();
                    }

                    this.showReadonly();

                    this._trigger('editSuccessHandler', 0, data);
                }, this)
            }]
        }, this.options.formOptions);

        this.$content.find('form').bsForm(formOptions);

        this._trigger('afterFormInit', 0);
    };
    //#endregion

    //#region ajax
    bsPanel.prototype._loadReadonlyContent = function () {

        var data = this._getXhrData();

        this._trigger('beforeReadonlyLoad', data);

        this.$content.find('form').addClass('loading');

        return $.bforms.ajax({
            name: 'BsPanel|LoadReadonly|' + this._name,
            url: this.options.readonlyUrl,

            data: data,

            context: this,
            success: this._onReadonlyLoadSuccess,
            error: this._onReadonlyLoadError
        });
    };

    bsPanel.prototype._onReadonlyLoadSuccess = function (response) {
        this.$content.html(response.Html);

        this._toggleLoading();
        this._toggleCaret(true);

        this._allowExpand = true;

        if (this.options.cacheReadonlyContent) {
            this._cachedReadonlyContent = this.$content.html();
        }

        this._trigger('onReadonlyLoadSuccess', 0, response);
        this.showReadonly();
    };

    bsPanel.prototype._onReadonlyLoadError = function (data) {

        this.$content.find('form').removeClass('loading');

        if (data.Message) {

            var $errorContainer = this.$element.find('.bs-panel-error');

            if ($errorContainer.length == 0) {
                $errorContainer = $('

');
                this.$container.before($errorContainer);
            }

            this._toggleLoading();

            this._showErrorMessage(data.Message, $errorContainer, true, '_loadReadonlyContent');
        }
    };

    bsPanel.prototype._loadEditableContent = function () {

        var data = this._getXhrData();

        this._trigger('beforeEditableLoad', data);

        this.$content.find('form').addClass('loading');

        return $.bforms.ajax({
            name: 'BsPanel|LoadEditable|' + this._name,
            url: this.options.editableUrl,
            context: this,

            data: data,

            success: this._onEditableLoadSuccess,
            error: this._onEditableLoadError
        });
    };

    bsPanel.prototype._onEditableLoadSuccess = function (response) {

        if (this.options.cacheReadonlyContent) {
            this._cachedReadonlyContent = this.$content.clone().find('form').removeClass('loading').end()
                                                                .html();
        }

        this.$content.html(response.Html);

        this._initEditable();

        this._toggleEditBtn();

        this._toggleLoading();
        this._toggleCaret(true);

        this._trigger('onEditableLoadSuccess', 0, response);

        this.showEditable();
    };

    bsPanel.prototype._onEditableLoadError = function (data) {

        this.$content.find('form').removeClass('loading');

        if (data.Message) {

            var $errorContainer = this.$element.find('.bs-panel-error');

            if ($errorContainer.length == 0) {
                $errorContainer = $('

');
                this.$container.before($errorContainer);
            }

            this._toggleLoading();

            this._showErrorMessage(data.Message, $errorContainer, true, '_loadEditableContent');
        }
    };
    //#endregion

    //#region public methods
    bsPanel.prototype.open = function () {
        var openData = {
            allowOpen: true,
            $content: this.$content
        };

        this._trigger('beforeOpen', openData);

        if (openData.allowOpen === true) {
            this.$container.stop(true, true).slideDown(300);
            this.$element.find(this.options.toggleSelector).addClass('dropup');
        }

        this._state = true;
        this._saveState();

        this._trigger('afterOpen');
    };

    bsPanel.prototype.close = function () {
        var closeData = {
            allowClose: true,
            $content: this.$content
        };

        this._trigger('beforeClose', closeData);

        if (closeData.allowClose === true) {
            this.$container.stop(true, true).slideUp(300);
            this.$element.find(this.options.toggleSelector).removeClass('dropup');
        }

        this._state = false;

        this._saveState();

        this._trigger('afterClose');
    };

    bsPanel.prototype.showReadonly = function () {
        this._readonly = true;
        this.$element.data('readonly', true);
        this.$element.removeClass('bs-panelEditMode');

        this._trigger('onReadonlyShow', 0);
    };

    bsPanel.prototype.showEditable = function () {
        this._readonly = false;
        this.$element.data('readonly', false);
        this.$element.addClass('bs-panelEditMode');

        this._trigger('onEditableShow', 0);
    };

    bsPanel.prototype.save = function() {
        var $form = this.$content.find('form');

        if (typeof $form.bsForm === "function") {
            $form.bsForm('triggerAction', 'save');
        }
    };

    bsPanel.prototype.refresh = function () {

        var method = this._readonly ? '_loadReadonlyContent' : "_loadEditableContent";

        this[method]().then($.proxy(function () {
            this._initControls();
            this._loadState();

        }, this));

    };
    //#endregion

    $.widget('bforms.bsPanel', bsPanel.prototype);

    return bsPanel;
});<commit_msg>cache content only if current state is readonly<commit_after>(function (factory) {
    if (typeof define === "function" && define.amd) {
        define('bforms-panel', ['jquery', 'bootstrap', 'amplify', 'bforms-ajax', 'bforms-form'], factory);
    } else {
        factory(window.jQuery);
    }
})(function ($) {

    var bsPanel = function () {

    };

    bsPanel.prototype.options = {
        collapse: true,
        loaded: false,
        editable: true,

        toggleSelector: '.bs-togglePanel',

        editSelector: '.bs-editPanel',
        cancelEditSelector: '.bs-cancelEdit',
        saveFormSelector: '.bs-savePanel',

        containerSelector: '.bs-containerPanel',
        contentSelector: '.bs-contentPanel',

        retrySelector: '.bs-retryBtn',

        headerToggle: false,
        headerSelector: '.bs-panelHeader',

        cacheReadonlyContent: true,
        additionalData: {
        },
        formOptions: {

        },

        retryMessage : 'Reload'
    };

    //#region init
    bsPanel.prototype._init = function () {
        this.$element = this.element;

        this.$element.addClass('bs-hasPanel');

        this.$toggleEleemnt = this.$element.find(this.options.toggleSelector);
        this._loadOptions();

        this._initDefaultProperties();
        this._initSelectors();
        this._delegateEvents();

        if (this.options.loaded === true) {
            this._initControls();
            this._loadState();
            this._initCurrentContent();
        } else {
            var method = this._readonly ? '_loadReadonlyContent' : "_loadEditableContent";

            this[method]().then($.proxy(function () {
                this._initControls();
                this._loadState();
                this._initCurrentContent();
            }, this));
        }

        if (!this.$toggleEleemnt.data("expandable")) {
            this._loadState(true);
        }
    };

    bsPanel.prototype._loadOptions = function () {
        var settings = this.$element.data('settings');

        $.extend(true, this.options, settings);
    };

    bsPanel.prototype._initDefaultProperties = function () {
        this._name = this.options.name || this.$element.prop('id');

        if (typeof this._name === "undefined" || this._name == '') {
            throw "boxForm requires an unique name";
        }

        this._componentId = this.$element.data('component');

        if (typeof this._componentId === "undefined") {
            console.warn("No component id specified for " + this._name);
        }

        this._objId = this.$element.data('objid');

        this._key = window.location.pathname + '|BoxForm|' + this._name;

        if ((typeof this.options.initialReadonly === "undefined" || this.options.initialReadonly == true) && this.$element.data('readonly') !== false) {
            this.showReadonly();
        } else {
            this.showEditable();
        }

        this.options.readonlyUrl = this.options.readonlyUrl || this.$element.data('readonlyurl');
        this.options.editableUrl = this.options.editableUrl || this.$element.data('editableurl');
        this.options.saveUrl = this.options.saveUrl || this.$element.data('saveurl');

        this._allowExpand = this.options.loaded;
    };

    bsPanel.prototype._initSelectors = function () {
        this.$container = this.$element.find(this.options.containerSelector);
        this.$content = this.$element.find(this.options.contentSelector);
    };

    bsPanel.prototype._delegateEvents = function () {

        this.$element.on('click', this.options.toggleSelector, $.proxy(this._onToggleClick, this));

        if (this.options.editable !== false) {
            if (this.options.headerToggle) {
                this.$element.on('click', this.options.headerSelector, $.proxy(this._onHeaderClick, this));
            } else {
                this.$element.on('click', this.options.editSelector, $.proxy(this._onEditClick, this));
            }
        }

        this.$element.on('click', this.options.cancelEditSelector, $.proxy(this._onCancelEditClick, this));

        this.$element.on('click', this.options.retrySelector, $.proxy(this._onRetryClick, this));
    };

    bsPanel.prototype._initCurrentContent = function () {
        if (this._readonly) {

        } else {
            this._initEditable();
        }
    };
    //#endregion

    //#region events
    bsPanel.prototype._onToggleClick = function (e) {
        e.preventDefault();
        e.stopPropagation();
        var currentTarget = $(e.currentTarget);

        if (!currentTarget.data("expandable")) {
            return;
        }
        if (this._allowExpand) {
            if (this._state) {
                this.close();
            } else {
                this.open();
            }
        }
    };

    bsPanel.prototype._onHeaderClick = function (e) {
        if (this._readonly == true) {
            this._loadEditableContent().then($.proxy(function () {
                if (!this._state && this._allowExpand) {
                    this.open();
                }
            }, this));

            e.preventDefault();
            e.stopPropagation();
        }
    };

    bsPanel.prototype._onEditClick = function (e) {
        e.preventDefault();
        e.stopPropagation();

        this._loadEditableContent().then($.proxy(function () {
            if (!this._state && this._allowExpand) {
                this.open();
            }
        }, this));
    };

    bsPanel.prototype._onCancelEditClick = function (e) {
        e.preventDefault();
        e.stopPropagation();

        if (this.options.cacheReadonlyContent && this._cachedReadonlyContent) {
            this.$content.html(this._cachedReadonlyContent);
            this.showReadonly();
            this._toggleEditBtn(true);

            if (!this._state) {
                this.open();
            }
        } else {
            this._loadReadonlyContent().then($.proxy(function () {
                this._toggleEditBtn(true);

                if (!this._state) {
                    this.open();
                }
            }, this));
        }
    };

    bsPanel.prototype._onRetryClick = function (e) {
        var $target = $(e.currentTarget),
            method = $target.data('method');

        if (typeof this[method] === "function") {
            this[method]();
        }
    };
    //#endregion

    //#region private methods
    bsPanel.prototype._saveState = function () {
        amplify.store(this._key, this._state);
    };

    bsPanel.prototype._loadState = function (forceOpen) {
        if (forceOpen) {
            this.open();
            return;
        }

        var lastState = amplify.store(this._key);
        if (lastState != null) {

            if (lastState == true) {
                this.open();
            } else {
                this.close();
            }
        }

    };

    bsPanel.prototype._initControls = function () {

        if (this.options.editable) {
            this._toggleEditBtn(this._readonly ? true : false);
        }

    };

    bsPanel.prototype._toggleLoading = function (show) {
        if (show) {
            this.$element.find('.bs-panelLoader').show();
        } else {
            this.$element.find('.bs-panelLoader').hide();
        }
    };

    bsPanel.prototype._toggleCaret = function (show) {
        if (show) {
            this.$element.find('.bs-panelCaret').show();
        } else {
            this.$element.find('.bs-panelCaret').hide();
        }
    };

    bsPanel.prototype._toggleEditBtn = function (show) {
        if (show) {
            this.$element.find(this.options.cancelEditSelector).hide().end()
                .find(this.options.editSelector).show();
        } else {
            this.$element.find(this.options.editSelector).hide().end()
                .find(this.options.cancelEditSelector).show();
        }
    };

    bsPanel.prototype._getXhrData = function () {

        return $.extend(true, {}, {
            componentId: this._componentId,
            objId: this._objId
        }, this.options.additionalData);

    };

    bsPanel.prototype._showErrorMessage = function (message, $errorContainer, replace, method) {

        var $error = $('
' +
                           '<button class="close" data-dismiss="alert" type="button">×</button>' +
                               message +
                            '
' +
                                '' + this.options.retryMessage +'  ' +
                            '
' +
                       '
');
        if (replace) {

            $errorContainer.html($error);
        } else {
            $errorContainer.append($error);
        }
    };

    bsPanel.prototype._initEditable = function () {

        var $form = this.$content.find('form').show(),
            $saveBtn = $form.find(this.options.saveFormSelector);

        this._allowExpand = true;

        if ($form.length == 0) {
            console.warn('No editable form found');
        }

        if ($saveBtn.length == 0) {
            console.warn("No save button found");
        }

        var formOptions = $.extend(true, {}, {
            prefix: this.options.prefix,
            actions: [{
                name: 'save',
                selector: this.options.saveFormSelector,
                validate: true,
                actionUrl: this.options.saveUrl,
                parse: true,
                getExtraData: $.proxy(function (data) {
                    data.componentId = this._componentId;
                    data.objId = this._objId;

                    $.extend(true, data, this.options.formAdditionalData);

                    this._trigger('beforeSaveAjax', 0, data);

                }, this),
                handler: $.proxy(function (sent, data) {

                    this._trigger('beforeEditSuccessHandler', 0, data);

                    this.$content.html(data.Html);

                    this._toggleLoading();
                    this._toggleEditBtn(true);

                    if (this.options.cacheReadonlyContent) {
                        this._cachedReadonlyContent = this.$content.html();
                    }

                    this.showReadonly();

                    this._trigger('editSuccessHandler', 0, data);
                }, this)
            }]
        }, this.options.formOptions);

        this.$content.find('form').bsForm(formOptions);

        this._trigger('afterFormInit', 0);
    };
    //#endregion

    //#region ajax
    bsPanel.prototype._loadReadonlyContent = function () {

        var data = this._getXhrData();

        this._trigger('beforeReadonlyLoad', data);

        this.$content.find('form').addClass('loading');

        return $.bforms.ajax({
            name: 'BsPanel|LoadReadonly|' + this._name,
            url: this.options.readonlyUrl,

            data: data,

            context: this,
            success: this._onReadonlyLoadSuccess,
            error: this._onReadonlyLoadError
        });
    };

    bsPanel.prototype._onReadonlyLoadSuccess = function (response) {
        this.$content.html(response.Html);

        this._toggleLoading();
        this._toggleCaret(true);

        this._allowExpand = true;

        if (this.options.cacheReadonlyContent) {
            this._cachedReadonlyContent = this.$content.html();
        }

        this._trigger('onReadonlyLoadSuccess', 0, response);
        this.showReadonly();
    };

    bsPanel.prototype._onReadonlyLoadError = function (data) {

        this.$content.find('form').removeClass('loading');

        if (data.Message) {

            var $errorContainer = this.$element.find('.bs-panel-error');

            if ($errorContainer.length == 0) {
                $errorContainer = $('

');
                this.$container.before($errorContainer);
            }

            this._toggleLoading();

            this._showErrorMessage(data.Message, $errorContainer, true, '_loadReadonlyContent');
        }
    };

    bsPanel.prototype._loadEditableContent = function () {

        var data = this._getXhrData();

        this._trigger('beforeEditableLoad', data);

        this.$content.find('form').addClass('loading');

        return $.bforms.ajax({
            name: 'BsPanel|LoadEditable|' + this._name,
            url: this.options.editableUrl,
            context: this,

            data: data,

            success: this._onEditableLoadSuccess,
            error: this._onEditableLoadError
        });
    };

    bsPanel.prototype._onEditableLoadSuccess = function (response) {

        if (this.options.cacheReadonlyContent && this._readonly == true ) {
            this._cachedReadonlyContent = this.$content.clone().find('form').removeClass('loading').end()
                                                                .html();
        }

        this.$content.html(response.Html);

        this._initEditable();

        this._toggleEditBtn();

        this._toggleLoading();
        this._toggleCaret(true);

        this._trigger('onEditableLoadSuccess', 0, response);

        this.showEditable();
    };

    bsPanel.prototype._onEditableLoadError = function (data) {

        this.$content.find('form').removeClass('loading');

        if (data.Message) {

            var $errorContainer = this.$element.find('.bs-panel-error');

            if ($errorContainer.length == 0) {
                $errorContainer = $('

');
                this.$container.before($errorContainer);
            }

            this._toggleLoading();

            this._showErrorMessage(data.Message, $errorContainer, true, '_loadEditableContent');
        }
    };
    //#endregion

    //#region public methods
    bsPanel.prototype.open = function () {
        var openData = {
            allowOpen: true,
            $content: this.$content
        };

        this._trigger('beforeOpen', openData);

        if (openData.allowOpen === true) {
            this.$container.stop(true, true).slideDown(300);
            this.$element.find(this.options.toggleSelector).addClass('dropup');
        }

        this._state = true;
        this._saveState();

        this._trigger('afterOpen');
    };

    bsPanel.prototype.close = function () {
        var closeData = {
            allowClose: true,
            $content: this.$content
        };

        this._trigger('beforeClose', closeData);

        if (closeData.allowClose === true) {
            this.$container.stop(true, true).slideUp(300);
            this.$element.find(this.options.toggleSelector).removeClass('dropup');
        }

        this._state = false;

        this._saveState();

        this._trigger('afterClose');
    };

    bsPanel.prototype.showReadonly = function () {
        this._readonly = true;
        this.$element.data('readonly', true);
        this.$element.removeClass('bs-panelEditMode');

        this._trigger('onReadonlyShow', 0);
    };

    bsPanel.prototype.showEditable = function () {
        this._readonly = false;
        this.$element.data('readonly', false);
        this.$element.addClass('bs-panelEditMode');

        this._trigger('onEditableShow', 0);
    };

    bsPanel.prototype.save = function() {
        var $form = this.$content.find('form');

        if (typeof $form.bsForm === "function") {
            $form.bsForm('triggerAction', 'save');
        }
    };

    bsPanel.prototype.refresh = function () {

        var method = this._readonly ? '_loadReadonlyContent' : "_loadEditableContent";

        this[method]().then($.proxy(function () {
            this._initControls();
            this._loadState();

        }, this));

    };
    //#endregion

    $.widget('bforms.bsPanel', bsPanel.prototype);

    return bsPanel;
});<|endoftext|>


<commit_before>'use strict';

import { L10nError } from './errors';

var KNOWN_MACROS = ['plural'];
var MAX_PLACEABLE_LENGTH = 2500;
var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g;

function createEntry(node, lang) {
  var keys = Object.keys(node);

  // the most common scenario: a simple string with no arguments
  if (typeof node.$v === 'string' && keys.length === 2) {
    return node.$v;
  }

  var attrs;

  for (var i = 0, key; (key = keys[i]); i++) {
    // skip $i (id), $v (value), $x (index)
    if (key[0] === '$') {
      continue;
    }

    if (!attrs) {
      attrs = Object.create(null);
    }
    attrs[key] = createAttribute(node[key], lang, node.$i + '.' + key);
  }

  return {
    id: node.$i,
    value: node.$v !== undefined ? node.$v : null,
    index: node.$x || null,
    attrs: attrs || null,
    lang: lang,
    // the dirty guard prevents cyclic or recursive references
    dirty: false
  };
}

function createAttribute(node, lang, id) {
  if (typeof node === 'string') {
    return node;
  }

  return {
    id: id,
    value: node.$v || (node !== undefined ? node : null),
    index: node.$x || null,
    lang: lang,
    dirty: false
  };
}

function format(ctx, args, entity) {
  var locals = {
    overlay: false
  };

  if (typeof entity === 'string') {
    return [locals, entity];
  }

  if (entity.dirty) {
    throw new L10nError('Cyclic reference detected: ' + entity.id);
  }

  entity.dirty = true;

  var rv;

  // if format fails, we want the exception to bubble up and stop the whole
  // resolving process;  however, we still need to clean up the dirty flag
  try {
    rv = resolveValue(
      locals, ctx, entity.lang, args, entity.value, entity.index);
  } finally {
    entity.dirty = false;
  }
  return rv;
}

function resolveIdentifier(ctx, lang, args, id) {
  if (KNOWN_MACROS.indexOf(id) > -1) {
    return [{}, ctx._getMacro(lang, id)];
  }

  if (args && args.hasOwnProperty(id)) {
    if (typeof args[id] === 'string' || (typeof args[id] === 'number' &&
        !isNaN(args[id]))) {
      return [{}, args[id]];
    } else {
      throw new L10nError('Arg must be a string or a number: ' + id);
    }
  }

  // XXX: special case for Node.js where still:
  // '__proto__' in Object.create(null) => true
  if (id === '__proto__') {
    throw new L10nError('Illegal id: ' + id);
  }

  var entity = ctx._getEntity(lang, id);

  if (entity) {
    return format(ctx, args, entity);
  }

  throw new L10nError('Unknown reference: ' + id);
}

function subPlaceable(ctx, lang, args, id) {
  var res;

  try {
    res = resolveIdentifier(ctx, lang, args, id);
  } catch (err) {
    return [{ error: err }, '{{ ' + id + ' }}'];
  }

  var value = res[1];

  if (typeof value === 'number') {
    return res;
  }

  if (typeof value === 'string') {
    // prevent Billion Laughs attacks
    if (value.length >= MAX_PLACEABLE_LENGTH) {
      throw new L10nError('Too many characters in placeable (' +
                          value.length + ', max allowed is ' +
                          MAX_PLACEABLE_LENGTH + ')');
    }
    return res;
  }

  return [{}, '{{ ' + id + ' }}'];
}

function interpolate(locals, ctx, lang, args, arr) {
  return arr.reduce(function(prev, cur) {
    if (typeof cur === 'string') {
      return [prev[0], prev[1] + cur];
    } else if (cur.t === 'idOrVar'){
      var placeable = subPlaceable(ctx, lang, args, cur.v);
      if (placeable[0].overlay) {
        prev[0].overlay = true;
      }
      return [prev[0], prev[1] + placeable[1]];
    }
  }, [locals, '']);
}

function resolveSelector(ctx, lang, args, expr, index) {
    var selectorName = index[0].v;
    var selector = resolveIdentifier(ctx, lang, args, selectorName)[1];

    if (typeof selector !== 'function') {
      // selector is a simple reference to an entity or args
      return selector;
    }

    var argValue = index[1] ?
      resolveIdentifier(ctx, lang, args, index[1])[1] : undefined;

    if (selector === ctx._getMacro(lang, 'plural')) {
      // special cases for zero, one, two if they are defined on the hash
      if (argValue === 0 && 'zero' in expr) {
        return 'zero';
      }
      if (argValue === 1 && 'one' in expr) {
        return 'one';
      }
      if (argValue === 2 && 'two' in expr) {
        return 'two';
      }
    }

    return selector(argValue);
}

function resolveValue(locals, ctx, lang, args, expr, index) {
  if (!expr) {
    return [locals, expr];
  }

  if (expr.$o) {
    expr = expr.$o;
    locals.overlay = true;
  }

  if (typeof expr === 'string' ||
      typeof expr === 'boolean' ||
      typeof expr === 'number') {
    return [locals, expr];
  }

  if (Array.isArray(expr)) {
    return interpolate(locals, ctx, lang, args, expr);
  }

  // otherwise, it's a dict
  if (index) {
    // try to use the index in order to select the right dict member
    var selector = resolveSelector(ctx, lang, args, expr, index);
    if (expr.hasOwnProperty(selector)) {
      return resolveValue(locals, ctx, lang, args, expr[selector]);
    }
  }

  // if there was no index or no selector was found, try 'other'
  if ('other' in expr) {
    return resolveValue(locals, ctx, lang, args, expr.other);
  }

  // XXX Specify entity id
  throw new L10nError('Unresolvable value');
}

export default { createEntry, format, rePlaceables };
<commit_msg>remove rePleacables<commit_after>'use strict';

import { L10nError } from './errors';

var KNOWN_MACROS = ['plural'];
var MAX_PLACEABLE_LENGTH = 2500;

function createEntry(node, lang) {
  var keys = Object.keys(node);

  // the most common scenario: a simple string with no arguments
  if (typeof node.$v === 'string' && keys.length === 2) {
    return node.$v;
  }

  var attrs;

  for (var i = 0, key; (key = keys[i]); i++) {
    // skip $i (id), $v (value), $x (index)
    if (key[0] === '$') {
      continue;
    }

    if (!attrs) {
      attrs = Object.create(null);
    }
    attrs[key] = createAttribute(node[key], lang, node.$i + '.' + key);
  }

  return {
    id: node.$i,
    value: node.$v !== undefined ? node.$v : null,
    index: node.$x || null,
    attrs: attrs || null,
    lang: lang,
    // the dirty guard prevents cyclic or recursive references
    dirty: false
  };
}

function createAttribute(node, lang, id) {
  if (typeof node === 'string') {
    return node;
  }

  return {
    id: id,
    value: node.$v || (node !== undefined ? node : null),
    index: node.$x || null,
    lang: lang,
    dirty: false
  };
}

function format(ctx, args, entity) {
  var locals = {
    overlay: false
  };

  if (typeof entity === 'string') {
    return [locals, entity];
  }

  if (entity.dirty) {
    throw new L10nError('Cyclic reference detected: ' + entity.id);
  }

  entity.dirty = true;

  var rv;

  // if format fails, we want the exception to bubble up and stop the whole
  // resolving process;  however, we still need to clean up the dirty flag
  try {
    rv = resolveValue(
      locals, ctx, entity.lang, args, entity.value, entity.index);
  } finally {
    entity.dirty = false;
  }
  return rv;
}

function resolveIdentifier(ctx, lang, args, id) {
  if (KNOWN_MACROS.indexOf(id) > -1) {
    return [{}, ctx._getMacro(lang, id)];
  }

  if (args && args.hasOwnProperty(id)) {
    if (typeof args[id] === 'string' || (typeof args[id] === 'number' &&
        !isNaN(args[id]))) {
      return [{}, args[id]];
    } else {
      throw new L10nError('Arg must be a string or a number: ' + id);
    }
  }

  // XXX: special case for Node.js where still:
  // '__proto__' in Object.create(null) => true
  if (id === '__proto__') {
    throw new L10nError('Illegal id: ' + id);
  }

  var entity = ctx._getEntity(lang, id);

  if (entity) {
    return format(ctx, args, entity);
  }

  throw new L10nError('Unknown reference: ' + id);
}

function subPlaceable(ctx, lang, args, id) {
  var res;

  try {
    res = resolveIdentifier(ctx, lang, args, id);
  } catch (err) {
    return [{ error: err }, '{{ ' + id + ' }}'];
  }

  var value = res[1];

  if (typeof value === 'number') {
    return res;
  }

  if (typeof value === 'string') {
    // prevent Billion Laughs attacks
    if (value.length >= MAX_PLACEABLE_LENGTH) {
      throw new L10nError('Too many characters in placeable (' +
                          value.length + ', max allowed is ' +
                          MAX_PLACEABLE_LENGTH + ')');
    }
    return res;
  }

  return [{}, '{{ ' + id + ' }}'];
}

function interpolate(locals, ctx, lang, args, arr) {
  return arr.reduce(function(prev, cur) {
    if (typeof cur === 'string') {
      return [prev[0], prev[1] + cur];
    } else if (cur.t === 'idOrVar'){
      var placeable = subPlaceable(ctx, lang, args, cur.v);
      if (placeable[0].overlay) {
        prev[0].overlay = true;
      }
      return [prev[0], prev[1] + placeable[1]];
    }
  }, [locals, '']);
}

function resolveSelector(ctx, lang, args, expr, index) {
    var selectorName = index[0].v;
    var selector = resolveIdentifier(ctx, lang, args, selectorName)[1];

    if (typeof selector !== 'function') {
      // selector is a simple reference to an entity or args
      return selector;
    }

    var argValue = index[1] ?
      resolveIdentifier(ctx, lang, args, index[1])[1] : undefined;

    if (selector === ctx._getMacro(lang, 'plural')) {
      // special cases for zero, one, two if they are defined on the hash
      if (argValue === 0 && 'zero' in expr) {
        return 'zero';
      }
      if (argValue === 1 && 'one' in expr) {
        return 'one';
      }
      if (argValue === 2 && 'two' in expr) {
        return 'two';
      }
    }

    return selector(argValue);
}

function resolveValue(locals, ctx, lang, args, expr, index) {
  if (!expr) {
    return [locals, expr];
  }

  if (expr.$o) {
    expr = expr.$o;
    locals.overlay = true;
  }

  if (typeof expr === 'string' ||
      typeof expr === 'boolean' ||
      typeof expr === 'number') {
    return [locals, expr];
  }

  if (Array.isArray(expr)) {
    return interpolate(locals, ctx, lang, args, expr);
  }

  // otherwise, it's a dict
  if (index) {
    // try to use the index in order to select the right dict member
    var selector = resolveSelector(ctx, lang, args, expr, index);
    if (expr.hasOwnProperty(selector)) {
      return resolveValue(locals, ctx, lang, args, expr[selector]);
    }
  }

  // if there was no index or no selector was found, try 'other'
  if ('other' in expr) {
    return resolveValue(locals, ctx, lang, args, expr.other);
  }

  // XXX Specify entity id
  throw new L10nError('Unresolvable value');
}

export default { createEntry, format };
<|endoftext|>