<commit_before>import React from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import t from 'tcomb';
import { props } from 'tcomb-react';
import omit from 'lodash/omit';
import DatePicker from './DatePicker';
import { Value } from './utils/model';
import { format, valueLink, skinnable } from './utils';
import cx from 'classnames';
import Input from './Input';

const INVALID = 'Invalid date';
const ENTER_KEYCODE = 13;

export const Props = {
  value: t.maybe(Value),
  valueLink: t.maybe(t.interface({
    value: t.maybe(Value),
    requestChange: t.Function
  })),
  onChange: t.maybe(t.Function),
  onShow: t.maybe(t.Function),
  onHide: t.maybe(t.Function),
  onClear: t.maybe(t.Function),
  small: t.maybe(t.Boolean),
  defaultValue: t.maybe(Value),
  minDate: t.maybe(Value),
  maxDate: t.maybe(Value),
  locale: t.maybe(t.String),
  startMode: t.maybe(t.enums.of(['day', 'month', 'year'])),
  startDate: t.maybe(Value),
  fixedMode: t.maybe(t.Boolean),
  displayFormat: t.maybe(t.String),
  returnFormat: t.maybe(t.String),
  format: t.maybe(t.String),
  validationFormat: t.maybe(t.String),
  showOnInputClick: t.maybe(t.Boolean),
  closeOnClickOutside: t.maybe(t.Boolean),
  showInputButton: t.maybe(t.Boolean),
  autoClose: t.maybe(t.Boolean),
  floating: t.maybe(t.Boolean),
  disabled: t.maybe(t.Boolean),
  position: t.maybe(t.enums.of(['top', 'bottom'])),
  iconClassName: t.maybe(t.String),
  iconClearClassName: t.maybe(t.String),
  className: t.maybe(t.String), // used to omit from inputProps
  style: t.maybe(t.Object), // used to omit from inputProps
  placeholder: t.maybe(t.String)
};

/** A decent and pretty date picker to be used with React
 * @param value - current date
 * @param valueLink - valueLink object to replace "value" and "onChange"
 * @param onChange - called when value changes
 * @param onShow - called when datepicker is opened
 * @param onHide - called when datepicker is closed
 * @param onClear - called when value is cleared
 * @param small - whether it's small or not
 * @param defaultValue - default date
 * @param minDate - minimum date selectable by the user
 * @param maxDate - maximum date selectable by the user
 * @param locale - locale used for translations
 * @param startMode - the start view of the datepicker
 * @param startDate - specify an initial "visible" date with no need to select a defaultValue
 * @param fixedMode - whether the user can use multiple views or not
 * @param displayFormat - MomentJS format used to display current date
 * @param returnFormat - MomentJS format used to format date before returing through "onChange"
 * @param format - MomentJS format used to format date before returing through "onChange"
 * @param validationFormat - MomentJS format used to format date before returing through "onChange"
 * @param showOnInputClick - whether the datepicker should open when user click on the input
 * @param closeOnClickOutside - whether the datepicker should close when user clicks outside of it
 * @param showInputButton - whether the input-button should be rendered
 * @param autoClose - pass true if you want the datepicker to close automatically after the user selects a value
 * @param floating - whether the datepicker should float over the page content (absolute position)
 * @param position - whether the datepicker should be rendered above or below the input field
 * @param disabled - whether the datepicker should be disabled or not
 * @param iconClassName - classname used for the icon
 * @param iconClearClassName - classname used for the clear icon
 * @param className - className used for the wrapper div
 * @param style - style used for the wrapper div
 * @param placeholder
 */

@format
@valueLink
@skinnable()
@props(Props, { strict: false })
export default class DatePickerInput extends React.Component {

  static defaultProps = {
    onShow: () => {},
    onHide: () => {},
    startMode: 'day',
    autoClose: true,
    closeOnClickOutside: true,
    floating: true,
    small: false,
    showInputButton: true,
    position: 'bottom',
    iconClassName: 'icon-rc-datepicker icon-rc-datepicker_calendar',
    iconClearClassName: 'icon-rc-datepicker icon-rc-datepicker_clear',
    className: '',
    style: {}
  }

  datePickerInputRef = null;

  constructor(props) {
    super(props);
    if (props.locale) {
      moment.locale(props.locale);
    }
    const _date = this.getValueLink().value || props.defaultValue;
    const date = typeof _date === 'string' ? this.parsePropDateString(_date) : moment(_date);
    this.state = {
      date: _date ? date : undefined,
      hasValue: !!_date,
      dateString: _date ? this.formatDisplayedDate(date) : '',
      showing: false
    };
  }

  componentDidMount() {
    if (this.props.closeOnClickOutside) {
      this.addOnClickListener();
    }
  }

  addOnClickListener = () => {
    if (window.attachEvent) {
      // Internet Explorer
      window.attachEvent('onclick', this.hideOnClickOutside);
    } else if (window.addEventListener) {
      window.addEventListener('click', this.hideOnClickOutside, false);
    }
  }

  removeOnClickListener = () => {
    if (window.detachEvent) {
      // Internet Explorer
      window.detachEvent('onclick', this.hideOnClickOutside);
    } else if (window.removeEventListener) {
      window.removeEventListener('click', this.hideOnClickOutside, false);
    }
  }

  getDatePickerInput = () => {
    return ReactDOM.findDOMNode(this.datePickerInputRef);
  }

  isEventInsideDatePickerInput = (el) => {
    if (el === this.getDatePickerInput()) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideDatePickerInput(el.parentNode);
    } else {
      return false;
    }
  }

  hideOnClickOutside = (e) => {
    if (!this.isEventInsideDatePickerInput(e.target) && this.state.showing) {
      this.hide();
    }
  }

  hide = () => {
    this.setState({ showing: false }, this.props.onHide);
  }

  show = () => {
    if (!this.state.showing) {
      this.setState({ showing: true }, this.props.onShow);
    }
  }

  toggleDatePicker = () => {
    const callback = this.state.showing ? this.props.onHide : this.props.onShow;
    this.setState({ showing: !this.state.showing }, callback);
  }

  hideOnEnterKey = (e) => {
    if (e.keyCode === ENTER_KEYCODE) {
      this.hide();
    }
  }

  onClear = () => {
    const _date = this.props.defaultValue;
    const date = typeof _date === 'string' ? this.parsePropDateString(_date) : moment(_date);
    this.setState(
      {
        date: _date ? date : undefined,
        dateString: _date ? this.formatDisplayedDate(date) : '',
        showing: false
      },
      this.props.onClear
    );
  }

  _onChangeDate = (jsDate) => {
    const newDate = moment(jsDate);
    const newDateString = this.formatDisplayedDate(newDate);
    if (this.props.autoClose) {
      this.hide();
    }
    this.getValueLink().requestChange(jsDate, this.formatReturnedDate(newDate));
    if (newDateString !== this.state.dateString) {
      this.setState({
        hasValue: true,
        date: newDate,
        dateString: newDateString
      });
    }
  }

  onChangeInput = ({ target: { value: dateString } }) => {
    if (dateString || this.state.date) {
      const parsedDate = this.parseInputDateString(dateString);
      const date = parsedDate.isValid() ? parsedDate : this.state.date;

      const jsDate = parsedDate.isValid() ? parsedDate.toDate() : INVALID;
      const returnedDateString = jsDate ? this.formatReturnedDate(parsedDate) : INVALID;

      this.setState({
        dateString,
        date,
        hasValue: parsedDate.isValid()
      }, () => this.getValueLink().requestChange(jsDate, returnedDateString));
    } else if (!dateString) {
      this.setState({ dateString });
    }
  }

  getLocals(props) {
    const {
      showInputButton,
      iconClassName,
      showOnInputClick,
      onClear,
      small,
      iconClearClassName,
      defaultValue,
      minDate, maxDate,
      locale,
      startMode,
      startDate,
      fixedMode,
      floating,
      closeOnClickOutside,
      className,
      disabled,
      position,
      style
    } = props;
    const { showing: active, hasValue, dateString: value, date } = this.state;

    const inputProps = omit(props, Object.keys(Props));
    const onInputClick = showOnInputClick ? this.show : undefined;
    const onButtonClick = showInputButton ? this.toggleDatePicker : undefined;
    const onInputClear = onClear ? this.onClear : undefined;

    return {
      style,
      className: cx('react-datepicker-component', { 'is-disabled': disabled }, className),
      inputProps: {
        value,
        small, active, hasValue,
        iconClassName, iconClearClassName,
        onInputClick, onButtonClick, onInputClear,
        onInputChange: this.onChangeInput,
        onInputKeyUp: this.hideOnEnterKey,
        ...inputProps
      },
      datePickerProps: active && {
        defaultValue,
        minDate,
        maxDate,
        locale,
        startMode,
        startDate,
        fixedMode,
        floating,
        position,
        closeOnClickOutside,
        value: date ? date.toDate() : undefined,
        onChange: this._onChangeDate
      }
    };
  }

  template({ className, style, inputProps, datePickerProps }) {
    return (
      <div {...{ style, className }} ref={input => { this.datePickerInputRef = input; }}>
        <Input {...inputProps} />
        {datePickerProps && <DatePicker {...datePickerProps} />}
      </div>
    );
  }

  componentWillReceiveProps(nextProps) {
    const { value } = this.getValueLink(nextProps);

    // Update `date` and `dateString` if `props.value` has changed
    if (value !== INVALID && value !== this.getValueLink().value) {
      if (value) {
        const date = typeof value === 'string' ?
          this.parsePropDateString(value, nextProps) : moment(value);
        this.setState({
          date,
          dateString: date.isValid() ?
            this.formatDisplayedDate(date, nextProps) : this.state.dateString
        });
      } else {
        this.setState({
          date: undefined,
          dateString: ''
        });
      }
    }

    // Close datepicker if `disabled` has switched to `true`
    if (nextProps.disabled && !this.props.disabled) {
      this.hide();
    }
  }

  componentWillUnmount() {
    if (this.props.closeOnClickOutside) {
      this.removeOnClickListener();
    }
  }
}
<commit_msg>Fix #162 Placeholder not showing<commit_after>import React from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import t from 'tcomb';
import { props } from 'tcomb-react';
import omit from 'lodash/omit';
import DatePicker from './DatePicker';
import { Value } from './utils/model';
import { format, valueLink, skinnable } from './utils';
import cx from 'classnames';
import Input from './Input';

const INVALID = 'Invalid date';
const ENTER_KEYCODE = 13;

export const Props = {
  value: t.maybe(Value),
  valueLink: t.maybe(t.interface({
    value: t.maybe(Value),
    requestChange: t.Function
  })),
  onChange: t.maybe(t.Function),
  onShow: t.maybe(t.Function),
  onHide: t.maybe(t.Function),
  onClear: t.maybe(t.Function),
  small: t.maybe(t.Boolean),
  defaultValue: t.maybe(Value),
  minDate: t.maybe(Value),
  maxDate: t.maybe(Value),
  locale: t.maybe(t.String),
  startMode: t.maybe(t.enums.of(['day', 'month', 'year'])),
  startDate: t.maybe(Value),
  fixedMode: t.maybe(t.Boolean),
  displayFormat: t.maybe(t.String),
  returnFormat: t.maybe(t.String),
  format: t.maybe(t.String),
  validationFormat: t.maybe(t.String),
  showOnInputClick: t.maybe(t.Boolean),
  closeOnClickOutside: t.maybe(t.Boolean),
  showInputButton: t.maybe(t.Boolean),
  autoClose: t.maybe(t.Boolean),
  floating: t.maybe(t.Boolean),
  disabled: t.maybe(t.Boolean),
  position: t.maybe(t.enums.of(['top', 'bottom'])),
  iconClassName: t.maybe(t.String),
  iconClearClassName: t.maybe(t.String),
  className: t.maybe(t.String), // used to omit from inputProps
  style: t.maybe(t.Object), // used to omit from inputProps
  placeholder: t.maybe(t.String)
};

/** A decent and pretty date picker to be used with React
 * @param value - current date
 * @param valueLink - valueLink object to replace "value" and "onChange"
 * @param onChange - called when value changes
 * @param onShow - called when datepicker is opened
 * @param onHide - called when datepicker is closed
 * @param onClear - called when value is cleared
 * @param small - whether it's small or not
 * @param defaultValue - default date
 * @param minDate - minimum date selectable by the user
 * @param maxDate - maximum date selectable by the user
 * @param locale - locale used for translations
 * @param startMode - the start view of the datepicker
 * @param startDate - specify an initial "visible" date with no need to select a defaultValue
 * @param fixedMode - whether the user can use multiple views or not
 * @param displayFormat - MomentJS format used to display current date
 * @param returnFormat - MomentJS format used to format date before returing through "onChange"
 * @param format - MomentJS format used to format date before returing through "onChange"
 * @param validationFormat - MomentJS format used to format date before returing through "onChange"
 * @param showOnInputClick - whether the datepicker should open when user click on the input
 * @param closeOnClickOutside - whether the datepicker should close when user clicks outside of it
 * @param showInputButton - whether the input-button should be rendered
 * @param autoClose - pass true if you want the datepicker to close automatically after the user selects a value
 * @param floating - whether the datepicker should float over the page content (absolute position)
 * @param position - whether the datepicker should be rendered above or below the input field
 * @param disabled - whether the datepicker should be disabled or not
 * @param iconClassName - classname used for the icon
 * @param iconClearClassName - classname used for the clear icon
 * @param className - className used for the wrapper div
 * @param style - style used for the wrapper div
 * @param placeholder
 */

@format
@valueLink
@skinnable()
@props(Props, { strict: false })
export default class DatePickerInput extends React.Component {

  static defaultProps = {
    onShow: () => {},
    onHide: () => {},
    startMode: 'day',
    autoClose: true,
    closeOnClickOutside: true,
    floating: true,
    small: false,
    showInputButton: true,
    position: 'bottom',
    iconClassName: 'icon-rc-datepicker icon-rc-datepicker_calendar',
    iconClearClassName: 'icon-rc-datepicker icon-rc-datepicker_clear',
    className: '',
    style: {}
  }

  datePickerInputRef = null;

  constructor(props) {
    super(props);
    if (props.locale) {
      moment.locale(props.locale);
    }
    const _date = this.getValueLink().value || props.defaultValue;
    const date = typeof _date === 'string' ? this.parsePropDateString(_date) : moment(_date);
    this.state = {
      date: _date ? date : undefined,
      hasValue: !!_date,
      dateString: _date ? this.formatDisplayedDate(date) : '',
      showing: false
    };
  }

  componentDidMount() {
    if (this.props.closeOnClickOutside) {
      this.addOnClickListener();
    }
  }

  addOnClickListener = () => {
    if (window.attachEvent) {
      // Internet Explorer
      window.attachEvent('onclick', this.hideOnClickOutside);
    } else if (window.addEventListener) {
      window.addEventListener('click', this.hideOnClickOutside, false);
    }
  }

  removeOnClickListener = () => {
    if (window.detachEvent) {
      // Internet Explorer
      window.detachEvent('onclick', this.hideOnClickOutside);
    } else if (window.removeEventListener) {
      window.removeEventListener('click', this.hideOnClickOutside, false);
    }
  }

  getDatePickerInput = () => {
    return ReactDOM.findDOMNode(this.datePickerInputRef);
  }

  isEventInsideDatePickerInput = (el) => {
    if (el === this.getDatePickerInput()) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideDatePickerInput(el.parentNode);
    } else {
      return false;
    }
  }

  hideOnClickOutside = (e) => {
    if (!this.isEventInsideDatePickerInput(e.target) && this.state.showing) {
      this.hide();
    }
  }

  hide = () => {
    this.setState({ showing: false }, this.props.onHide);
  }

  show = () => {
    if (!this.state.showing) {
      this.setState({ showing: true }, this.props.onShow);
    }
  }

  toggleDatePicker = () => {
    const callback = this.state.showing ? this.props.onHide : this.props.onShow;
    this.setState({ showing: !this.state.showing }, callback);
  }

  hideOnEnterKey = (e) => {
    if (e.keyCode === ENTER_KEYCODE) {
      this.hide();
    }
  }

  onClear = () => {
    const _date = this.props.defaultValue;
    const date = typeof _date === 'string' ? this.parsePropDateString(_date) : moment(_date);
    this.setState(
      {
        date: _date ? date : undefined,
        dateString: _date ? this.formatDisplayedDate(date) : '',
        showing: false
      },
      this.props.onClear
    );
  }

  _onChangeDate = (jsDate) => {
    const newDate = moment(jsDate);
    const newDateString = this.formatDisplayedDate(newDate);
    if (this.props.autoClose) {
      this.hide();
    }
    this.getValueLink().requestChange(jsDate, this.formatReturnedDate(newDate));
    if (newDateString !== this.state.dateString) {
      this.setState({
        hasValue: true,
        date: newDate,
        dateString: newDateString
      });
    }
  }

  onChangeInput = ({ target: { value: dateString } }) => {
    if (dateString || this.state.date) {
      const parsedDate = this.parseInputDateString(dateString);
      const date = parsedDate.isValid() ? parsedDate : this.state.date;

      const jsDate = parsedDate.isValid() ? parsedDate.toDate() : INVALID;
      const returnedDateString = jsDate ? this.formatReturnedDate(parsedDate) : INVALID;

      this.setState({
        dateString,
        date,
        hasValue: parsedDate.isValid()
      }, () => this.getValueLink().requestChange(jsDate, returnedDateString));
    } else if (!dateString) {
      this.setState({ dateString });
    }
  }

  getLocals(props) {
    const {
      showInputButton,
      iconClassName,
      showOnInputClick,
      onClear,
      small,
      iconClearClassName,
      defaultValue,
      minDate, maxDate,
      locale,
      startMode,
      startDate,
      fixedMode,
      floating,
      closeOnClickOutside,
      className,
      disabled,
      position,
      placeholder,
      style
    } = props;
    const { showing: active, hasValue, dateString: value, date } = this.state;

    const inputProps = omit(props, Object.keys(Props));
    const onInputClick = showOnInputClick ? this.show : undefined;
    const onButtonClick = showInputButton ? this.toggleDatePicker : undefined;
    const onInputClear = onClear ? this.onClear : undefined;

    return {
      style,
      className: cx('react-datepicker-component', { 'is-disabled': disabled }, className),
      inputProps: {
        value,
        small, active, hasValue,
        iconClassName, iconClearClassName,
        onInputClick, onButtonClick, onInputClear,
        onInputChange: this.onChangeInput,
        onInputKeyUp: this.hideOnEnterKey,
        placeholder,
        ...inputProps
      },
      datePickerProps: active && {
        defaultValue,
        minDate,
        maxDate,
        locale,
        startMode,
        startDate,
        fixedMode,
        floating,
        position,
        closeOnClickOutside,
        value: date ? date.toDate() : undefined,
        onChange: this._onChangeDate
      }
    };
  }

  template({ className, style, inputProps, datePickerProps }) {
    return (
      <div {...{ style, className }} ref={input => { this.datePickerInputRef = input; }}>
        <Input {...inputProps} />
        {datePickerProps && <DatePicker {...datePickerProps} />}
      </div>
    );
  }

  componentWillReceiveProps(nextProps) {
    const { value } = this.getValueLink(nextProps);

    // Update `date` and `dateString` if `props.value` has changed
    if (value !== INVALID && value !== this.getValueLink().value) {
      if (value) {
        const date = typeof value === 'string' ?
          this.parsePropDateString(value, nextProps) : moment(value);
        this.setState({
          date,
          dateString: date.isValid() ?
            this.formatDisplayedDate(date, nextProps) : this.state.dateString
        });
      } else {
        this.setState({
          date: undefined,
          dateString: ''
        });
      }
    }

    // Close datepicker if `disabled` has switched to `true`
    if (nextProps.disabled && !this.props.disabled) {
      this.hide();
    }
  }

  componentWillUnmount() {
    if (this.props.closeOnClickOutside) {
      this.removeOnClickListener();
    }
  }
}
<|endoftext|>


<commit_before>var height = 960
var width = 960
//var margin = {top: 10, right: 10, bottom: 10, left: 10},
//    width = 980 - margin.left - margin.right,
//    height = 980 - margin.top - margin.bottom;

var xScale = d3.scaleLinear()
  .domain([-180, 180])
  .range([0, width]);

var yScale = d3.scaleLinear()
  .domain([-90, 90])
  .range([height, 0]);

var cScale = d3.scaleLinear()
  .domain([1e-6, 4e-6])
  .range(['cyan', 'darkred'])

//var svg = d3.select("body").select("div").append("svg")
//    .attr("width", width)
//    .attr("height", height);

var svg = d3.select("#map")

var projection = d3.geoMercator()
  .scale((width - 3) / (2 * Math.PI))
  .translate([width / 2, height / 2]);

var path = d3.geoPath()
  .projection(projection);

var graticule = d3.geoGraticule();

svg.append("defs").append("path")
  .datum({
    type: "Sphere"
  })
  .attr("id", "sphere")
  .attr("d", path);

svg.append("use")
  .attr("class", "stroke")
  .attr("xlink:href", "#sphere");

svg.append("use")
  .attr("class", "fill")
  .attr("xlink:href", "#sphere");

svg.append("path")
  .datum(graticule)
  .attr("class", "graticule")
  .attr("d", path);

d3.select("#time-min-value").text(document.getElementById("time-min").value)
d3.select("#time-max-value").text(document.getElementById("time-max").value)
d3.select("#darkrate-min-value").text(document.getElementById("dark-min").value)
d3.select("#darkrate-max-value").text(document.getElementById("dark-max").value)

d3.select("#opacity-value").text(document.getElementById("opacity").value)
d3.select("#pointsize-value").text(document.getElementById("pointsize").value)
d3.select("#sol-opacity-value").text(document.getElementById("solOpacity").value)
d3.select("#sol-pointsize-value").text(document.getElementById("solPointsize").value)

function update(data) {
  // Scale the range of the data
  //cScale.domain(d3.extent(data, function(d) {
  //  return d.dark;
  //}));

  var canvas = svg.selectAll("circle")
    .data(data);

  canvas.attr("class", "update")

  darkrate = canvas.enter().append("g").append("circle")
    .attr('fill', function(d) {
      return cScale(d.dark);
    })
    .attr('fill-opacity', +document.getElementById("opacity").value)
    .attr("r", +document.getElementById("pointsize").value)
    .attr("cx", function(d) {
      return xScale(d.longitude);
    })
    .attr("cy", function(d) {
      return yScale(d.latitude);
    })
    .on("mouseover", mouseover)
    .on("mouseoff", mouseoff)
    .merge(canvas);

  solar = canvas.enter().append("g").append("circle")
    .attr('fill', "orange")
    .attr('fill-opacity', +document.getElementById("solOpacity").value)
    .attr("stroke", "black")
    .attr("r", +document.getElementById("solPointsize").value)
    .attr("cx", function(d) {
      return xScale(d.sun_lon);
    })
    .attr("cy", function(d) {
      return yScale(d.sun_lat);
    })
    .merge(canvas);

    // Select only specific detector data.
    d3.select("#detector").on("input", function(d, i) {

      console.log("here");
      newDetector = document.getElementById("detector").value;
      pointsize = +document.getElementById("pointsize").value;

      darkrate.transition()
        .duration(2000)
        .delay(500)
        .attr('r', function(d, i) {
          if (newDetector == "BOTH") {
            return pointsize;
          } else if (d.detector == newDetector) {
            return pointsize;
          }
          return 0
        });

      solar.transition()
        .duration(2000)
        .delay(500)
        .attr('r', function(d, i) {
          if (newDetector == "BOTH") {
            return pointsize;
          } else if (d.detector == newDetector) {
            return pointsize;
          }
          return 0
        });
    })

    // click to sub-solar coordinates
    d3.select("#relative").on("click", function() {
      darkrate.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", function(d) {
          return xScale(rescale(d))
        })
        .attr("cy", function(d) {
          return yScale(d.latitude - d.sun_lat)
        });

      solar.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", xScale(0))
        .attr("cy", yScale(0))
    })

    // click to default coordinates
    d3.select("#native").on("click", function() {
      darkrate.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", function(d) {
          return xScale(d.longitude);
        })
        .attr("cy", function(d) {
          return yScale(d.latitude);
        })

      solar.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", function(d) {
          return xScale(d.sun_lon);
        })
        .attr("cy", function(d) {
          return yScale(d.sun_lat);
        })

    })

  d3.select("#time-min").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#time-min-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

  });

  d3.select("#time-max").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#time-max-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });
  });

  d3.select("#dark-min").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#darkrate-min-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });
  });

  d3.select("#dark-max").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#darkrate-max-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });
  });

  // Opacity selection
  d3.select("#opacity").on("input", function(d, i) {

    d3.select("#opacity-value").text(this.value);

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .style("opacity", +this.value)
  });

  // Opacity selection
  d3.select("#solOpacity").on("input", function(d, i) {

    d3.select("#sol-opacity-value").text(this.value);

    solar.transition()
      .duration(2000)
      .delay(500)
      .style("opacity", +this.value)
  });

  // Change size of the points
  d3.select("#pointsize").on("input", function(d, i) {

    d3.select("#pointsize-value").text(this.value);

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr("r", +this.value)
  });

  // Change size of the points
  d3.select("#solPointsize").on("input", function(d, i) {

    d3.select("#sol-pointsize-value").text(this.value);

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr("r", +this.value)
  });

  canvas.exit().remove();
}

// Load the world.
d3.json("https://unpkg.com/world-atlas@1/world/50m.json", function(error, world) {
  if (error) throw error;

  svg.insert("path", ".graticule")
    .datum(topojson.feature(world, world.objects.land))
    .attr("class", "land")
    .attr("d", path);

  svg.insert("path", ".graticule")
    .datum(topojson.mesh(world, world.objects.countries, function(a, b) {
      return a !== b;
    }))
    .attr("class", "boundary")
    .attr("d", path);
});

// Load the orbital info stuff
d3.json("./js/orbital_info.json", function(error, data) {
  if (error) throw error;

  // Filter/modify data if necessary
  data.forEach(function(d) {
    //Re-center around the map
    d.longitude = d.longitude > 180 ? +d.longitude - 360 : +d.longitude;
    d.latitude = +d.latitude;

    d.sun_lon = d.sun_lon > 180 ? +d.sun_lon - 360 : +d.sun_lon
    d.sun_lat = +d.sun_lat;

    d.fsol = +d.fsol;

  });

  console.log("running update")
  update(data);

});

function rescale(d) {
  outLat = d.longitude - d.sun_lon > 180 ? (d.longitude - d.sun_lon) - 360 : d.longitude - d.sun_lon;
  outLat = outLat < -180 ? outLat + 360 : outLat
  outLat = outLat > 180 ? outLat - 360 : outLat
  return outLat
}

// Interactivity
function mouseover(d, i) {
  box = document.getElementById("stats");
  message = "Lat: " + d.latitude.toFixed(3) + "\n" + "Lon: " + d.longitude.toFixed(3) + "\n" + "Dark: " + d.dark.toExponential(2) + "Fsol: " + d.fsol.toFixed(3);
  box.value = message;

};

function mouseoff(d, i) {
  d3.select(this).attr({
    fill: "black",
  });

  d3.select("#stamp" + i).remove();
}
<commit_msg>newline for message<commit_after>var height = 960
var width = 960
//var margin = {top: 10, right: 10, bottom: 10, left: 10},
//    width = 980 - margin.left - margin.right,
//    height = 980 - margin.top - margin.bottom;

var xScale = d3.scaleLinear()
  .domain([-180, 180])
  .range([0, width]);

var yScale = d3.scaleLinear()
  .domain([-90, 90])
  .range([height, 0]);

var cScale = d3.scaleLinear()
  .domain([1e-6, 4e-6])
  .range(['cyan', 'darkred'])

//var svg = d3.select("body").select("div").append("svg")
//    .attr("width", width)
//    .attr("height", height);

var svg = d3.select("#map")

var projection = d3.geoMercator()
  .scale((width - 3) / (2 * Math.PI))
  .translate([width / 2, height / 2]);

var path = d3.geoPath()
  .projection(projection);

var graticule = d3.geoGraticule();

svg.append("defs").append("path")
  .datum({
    type: "Sphere"
  })
  .attr("id", "sphere")
  .attr("d", path);

svg.append("use")
  .attr("class", "stroke")
  .attr("xlink:href", "#sphere");

svg.append("use")
  .attr("class", "fill")
  .attr("xlink:href", "#sphere");

svg.append("path")
  .datum(graticule)
  .attr("class", "graticule")
  .attr("d", path);

d3.select("#time-min-value").text(document.getElementById("time-min").value)
d3.select("#time-max-value").text(document.getElementById("time-max").value)
d3.select("#darkrate-min-value").text(document.getElementById("dark-min").value)
d3.select("#darkrate-max-value").text(document.getElementById("dark-max").value)

d3.select("#opacity-value").text(document.getElementById("opacity").value)
d3.select("#pointsize-value").text(document.getElementById("pointsize").value)
d3.select("#sol-opacity-value").text(document.getElementById("solOpacity").value)
d3.select("#sol-pointsize-value").text(document.getElementById("solPointsize").value)

function update(data) {
  // Scale the range of the data
  //cScale.domain(d3.extent(data, function(d) {
  //  return d.dark;
  //}));

  var canvas = svg.selectAll("circle")
    .data(data);

  canvas.attr("class", "update")

  darkrate = canvas.enter().append("g").append("circle")
    .attr('fill', function(d) {
      return cScale(d.dark);
    })
    .attr('fill-opacity', +document.getElementById("opacity").value)
    .attr("r", +document.getElementById("pointsize").value)
    .attr("cx", function(d) {
      return xScale(d.longitude);
    })
    .attr("cy", function(d) {
      return yScale(d.latitude);
    })
    .on("mouseover", mouseover)
    .on("mouseoff", mouseoff)
    .merge(canvas);

  solar = canvas.enter().append("g").append("circle")
    .attr('fill', "orange")
    .attr('fill-opacity', +document.getElementById("solOpacity").value)
    .attr("stroke", "black")
    .attr("r", +document.getElementById("solPointsize").value)
    .attr("cx", function(d) {
      return xScale(d.sun_lon);
    })
    .attr("cy", function(d) {
      return yScale(d.sun_lat);
    })
    .merge(canvas);

    // Select only specific detector data.
    d3.select("#detector").on("input", function(d, i) {

      console.log("here");
      newDetector = document.getElementById("detector").value;
      pointsize = +document.getElementById("pointsize").value;

      darkrate.transition()
        .duration(2000)
        .delay(500)
        .attr('r', function(d, i) {
          if (newDetector == "BOTH") {
            return pointsize;
          } else if (d.detector == newDetector) {
            return pointsize;
          }
          return 0
        });

      solar.transition()
        .duration(2000)
        .delay(500)
        .attr('r', function(d, i) {
          if (newDetector == "BOTH") {
            return pointsize;
          } else if (d.detector == newDetector) {
            return pointsize;
          }
          return 0
        });
    })

    // click to sub-solar coordinates
    d3.select("#relative").on("click", function() {
      darkrate.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", function(d) {
          return xScale(rescale(d))
        })
        .attr("cy", function(d) {
          return yScale(d.latitude - d.sun_lat)
        });

      solar.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", xScale(0))
        .attr("cy", yScale(0))
    })

    // click to default coordinates
    d3.select("#native").on("click", function() {
      darkrate.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", function(d) {
          return xScale(d.longitude);
        })
        .attr("cy", function(d) {
          return yScale(d.latitude);
        })

      solar.transition()
        .duration(2000)
        .delay(500)
        .attr("cx", function(d) {
          return xScale(d.sun_lon);
        })
        .attr("cy", function(d) {
          return yScale(d.sun_lat);
        })

    })

  d3.select("#time-min").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#time-min-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

  });

  d3.select("#time-max").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#time-max-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });
  });

  d3.select("#dark-min").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#darkrate-min-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });
  });

  d3.select("#dark-max").on("input", function(d, i) {
    newVal = +this.value;
    d3.select("#darkrate-max-value").text(newVal);

    minTime = document.getElementById("time-min").value;
    maxTime = document.getElementById("time-max").value;
    minDark = +document.getElementById("dark-min").value;
    maxDark = +document.getElementById("dark-max").value;
    newDetector = document.getElementById("detector").value;
    pointsize = +document.getElementById("pointsize").value;

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr('r', function(d, i) {
        if ((minTime >= +d.date) || (+d.date >= maxTime) || (minDark >= +d.dark) || (+d.dark >= maxDark)) {
          return 0;
        } else if (newDetector == "BOTH") {
          return pointsize;
        } else if (newDetector != d.detector) {
          return 0;
        }
        return pointsize;
      });
  });

  // Opacity selection
  d3.select("#opacity").on("input", function(d, i) {

    d3.select("#opacity-value").text(this.value);

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .style("opacity", +this.value)
  });

  // Opacity selection
  d3.select("#solOpacity").on("input", function(d, i) {

    d3.select("#sol-opacity-value").text(this.value);

    solar.transition()
      .duration(2000)
      .delay(500)
      .style("opacity", +this.value)
  });

  // Change size of the points
  d3.select("#pointsize").on("input", function(d, i) {

    d3.select("#pointsize-value").text(this.value);

    darkrate.transition()
      .duration(2000)
      .delay(500)
      .attr("r", +this.value)
  });

  // Change size of the points
  d3.select("#solPointsize").on("input", function(d, i) {

    d3.select("#sol-pointsize-value").text(this.value);

    solar.transition()
      .duration(2000)
      .delay(500)
      .attr("r", +this.value)
  });

  canvas.exit().remove();
}

// Load the world.
d3.json("https://unpkg.com/world-atlas@1/world/50m.json", function(error, world) {
  if (error) throw error;

  svg.insert("path", ".graticule")
    .datum(topojson.feature(world, world.objects.land))
    .attr("class", "land")
    .attr("d", path);

  svg.insert("path", ".graticule")
    .datum(topojson.mesh(world, world.objects.countries, function(a, b) {
      return a !== b;
    }))
    .attr("class", "boundary")
    .attr("d", path);
});

// Load the orbital info stuff
d3.json("./js/orbital_info.json", function(error, data) {
  if (error) throw error;

  // Filter/modify data if necessary
  data.forEach(function(d) {
    //Re-center around the map
    d.longitude = d.longitude > 180 ? +d.longitude - 360 : +d.longitude;
    d.latitude = +d.latitude;

    d.sun_lon = d.sun_lon > 180 ? +d.sun_lon - 360 : +d.sun_lon
    d.sun_lat = +d.sun_lat;

    d.fsol = +d.fsol;

  });

  console.log("running update")
  update(data);

});

function rescale(d) {
  outLat = d.longitude - d.sun_lon > 180 ? (d.longitude - d.sun_lon) - 360 : d.longitude - d.sun_lon;
  outLat = outLat < -180 ? outLat + 360 : outLat
  outLat = outLat > 180 ? outLat - 360 : outLat
  return outLat
}

// Interactivity
function mouseover(d, i) {
  box = document.getElementById("stats");
  message = "Lat: " + d.latitude.toFixed(3) + "\n" + "Lon: " + d.longitude.toFixed(3) + "\n" + "Dark: " + d.dark.toExponential(2) + "\nFsol: " + d.fsol.toFixed(3);
  box.value = message;

};

function mouseoff(d, i) {
  d3.select(this).attr({
    fill: "black",
  });

  d3.select("#stamp" + i).remove();
}
<|endoftext|>


<commit_before>// Copyright 2021, University of Colorado Boulder

/**
 * CarouselComboBox behaves like a combo box, but its listbox is a carousel. This allows you to scroll through a
 * long list of items, a feature that ComboBoxListBox does not support. ComboBoxItem, ComboBoxButton, and
 * Carousel are reused.
 *
 * NOTE! This was created as a quick way to address situations where ComboBox's listbox gets too long, for example
 * https://github.com/phetsims/sun/issues/673. This tends to happen in internal 'demo' applications
 * (sun, scenery-phet,... ) that have long lists of demos. And as a design best-practice, we tend to avoid
 * longs lists of things in sims. So that's why CarouselComboBox currently lives in sun/demo/.
 * It was written to be fairly general, so should be relatively easy to relocate if it's needed for wider use.
 *
 * Possible future work:
 * - Modify ComboBox so that it can use different types of popups (ComboBoxListBox, Carousel,...), or
 * - Make CarouselComboBox pop up the Carousel in front of everything else
 * - a11y support in CarouselComboBox
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import Multilink from '../../../axon/js/Multilink.js';
import Property from '../../../axon/js/Property.js';
import Dimension2 from '../../../dot/js/Dimension2.js';
import dotRandom from '../../../dot/js/dotRandom.js';
import merge from '../../../phet-core/js/merge.js';
import AssertUtils from '../../../phetcommon/js/AssertUtils.js';
import PressListener from '../../../scenery/js/listeners/PressListener.js';
import AlignBox from '../../../scenery/js/nodes/AlignBox.js';
import AlignGroup from '../../../scenery/js/nodes/AlignGroup.js';
import HBox from '../../../scenery/js/nodes/HBox.js';
import Node from '../../../scenery/js/nodes/Node.js';
import Rectangle from '../../../scenery/js/nodes/Rectangle.js';
import VBox from '../../../scenery/js/nodes/VBox.js';
import Color from '../../../scenery/js/util/Color.js';
import Tandem from '../../../tandem/js/Tandem.js';
import Carousel from '../Carousel.js';
import ComboBoxButton from '../ComboBoxButton.js';
import ComboBoxItem from '../ComboBoxItem.js';
import PageControl from '../PageControl.js';
import sun from '../sun.js';

class CarouselComboBox extends Node {

  /**
   * @param {Property.<*>} property - the Property that is set when an item is selected
   * @param {ComboBoxItem[]} comboBoxItems - the items that appear in the carousel
   * @param {Object} [options]
   */
  constructor( property, comboBoxItems, options ) {

    assert && assert( property instanceof Property, 'invalid property' );
    assert && AssertUtils.assertArrayOf( comboBoxItems, ComboBoxItem );

    options = merge( {

      // Options specific to CarouselComboBox
      align: 'left', // {string} alignment of items in the carousel, 'left'|'right'|'center'
      overColor: Color.grayColor( 245 ), // {ColorDef} background color of the item that the pointer is over
      selectedColor: 'yellow', // {ColorDef} background color of the selected item
      itemXMargin: 6, // {number} x margin for backgrounds on the items in the carousel
      itemYMargin: 2, // {number} y margin for backgrounds on the items in the carousel

      // Options passed to Carousel
      carouselOptions: {
        arrowSize: new Dimension2( 20, 4 ),

        // Like ComboBox, 'vertical' is the only orientation supported (verified below).
        orientation: 'vertical',

        // Spacing and margins will be built into the backgrounds that are add to the item Nodes,
        // so set them to zero for the carousel.
        spacing: 0,
        margin: 0,

        // {number} If there are fewer items than this in the carousel, the actual number of items will be used.
        itemsPerPage: 15
      },

      pageControlOptions: {
        interactive: true
      },

      // Options passed to ComboBoxButton
      buttonOptions: {
        arrowDirection: 'down', // 'up' is not currently supported (verified below)
        baseColor: 'rgb( 218, 236, 255 )',
        xMargin: 6, // You'll typically want this to be the same as itemXMargin, but we're not going to force you :)
        yMargin: 4
      },

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    // Validate options
    assert && assert( options.carouselOptions.orientation === 'vertical', 'orientation must be vertical' );
    assert && assert( options.buttonOptions.arrowDirection === 'down', 'arrowDirection must be down' );

    // Check for options that are not settable by the client
    assert && assert( !options.children, 'CarouselComboBox sets children' );
    assert && assert( !options.buttonOptions.content, 'CarouselComboBox sets buttonOptions.content' );
    assert && assert( !options.buttonOptions.listener, 'CarouselComboBox sets buttonOptions.listener' );

    // Don't create pages that are longer than the number of items
    options.carouselOptions.itemsPerPage = Math.min( options.carouselOptions.itemsPerPage, comboBoxItems.length );

    // Orientations must be the same.
    assert && assert( !options.pageControlOptions.orientation, 'use carouselOptions.orientation' );
    options.pageControlOptions.orientation = options.carouselOptions.orientation;

    // Create tandems for subcomponents, if they were not provided
    options.carouselOptions.tandem = options.carouselOptions.tandem || options.tandem.createTandem( 'carousel' );
    options.buttonOptions.tandem = options.buttonOptions.tandem || options.tandem.createTandem( 'button' );

    // Make all Nodes for the comboBoxItems have the same width and height, left justified.
    const alignGroup = new AlignGroup();
    const itemNodes = _.map( comboBoxItems, item => {
      const node = new AlignBox( item.node, {
        group: alignGroup,
        xAlign: options.align
      } );
      node.itemValue = item.value; // attach the item's value to the Node, we'll need it later
      return node;
    } );

    // Size of the background that we'll put behind all item Nodes. Since we made all of the Nodes have the same
    // dimensions using AlignBox above, we can simply inspect the first Node.
    const backgroundSize = new Dimension2(
      itemNodes[ 0 ].width + 2 * options.itemXMargin,
      itemNodes[ 0 ].height + 2 * options.itemYMargin
    );

    // Create items for the carousel, whose API for 'items' is different than ComboBox. The Node for each item is a
    // label on a background. The background changes color when the item is selected or the pointer is over the item.
    const carouselItemNodes = [];
    const multilinks = [];
    for ( let i = 0; i < itemNodes.length; i++ ) {

      const itemNode = itemNodes[ i ];

      const backgroundNode = new Rectangle( 0, 0, backgroundSize.width, backgroundSize.height, {
        // Actual alignment of an item on the background is determined by AlignBox xAlign, above.
        center: itemNode.center
      } );

      const carouselItemNode = new Node( {
        children: [ backgroundNode, itemNode ]
      } );

      // Press on an item to select its associated value.
      const pressListener = new PressListener( {
        press: () => {
          property.value = itemNode.itemValue;
        }
      } );
      carouselItemNode.addInputListener( pressListener );

      // Selecting an item sets its background to the selected color.
      // Pointer over an item sets its background to the highlighted color.
      // Must be disposed because we do not own property.
      const multilink = new Multilink(
        [ property, pressListener.isOverProperty ],
        ( propertyValue, isOver ) => {
          if ( propertyValue === itemNode.itemValue ) {
            backgroundNode.fill = options.selectedColor;
          }
          else if ( isOver ) {
            backgroundNode.fill = options.overColor;
          }
          else {
            backgroundNode.fill = 'transparent';
          }
        } );
      multilinks.push( multilink );

      carouselItemNodes.push( carouselItemNode );
    }
    assert && assert( carouselItemNodes.length === comboBoxItems.length, 'expected a carouselItem for each comboBoxItem' );

    // Create the carousel.
    const carousel = new Carousel( carouselItemNodes, options.carouselOptions );

    // page control
    const pageControl = new PageControl( carousel.numberOfPages, carousel.pageNumberProperty, options.pageControlOptions );

    // Page control to the left of carousel
    const carouselAndPageControl = new HBox( {
      spacing: 4,
      children: [ carousel, pageControl ]
    } );

    // Pressing this button pops the carousel up and down
    const button = new ComboBoxButton( property, comboBoxItems, merge( {}, {
      listener: () => {
        carouselAndPageControl.visible = !carouselAndPageControl.visible;
      }
    }, options.buttonOptions ) );

    // Put the button above the carousel, left aligned.
    const vBox = new VBox( {
      spacing: 0,
      align: 'left',
      children: [ button, carouselAndPageControl ]
    } );

    // Wrap everything with Node, to hide VBox's API.
    options.children = [ vBox ];

    super( options );

    // If the Property changes, hide the carousel. unlink is needed on disposed.
    const propertyListener = () => { carouselAndPageControl.visible = false; };
    property.link( propertyListener );

    // Clicking outside this UI component will hide the carousel and page control.
    // NOTE: adapted from ComboBox.
    const clickToDismissListener = {
      down: event => {
        assert && assert( carousel.visible, 'this listener should be registered only when carousel is visible' );

        // If fuzzing is enabled, exercise this listener some percentage of the time, so that this listener is tested.
        // The rest of the time, ignore this listener, so that the carousel remains popped up, and we test making
        // choices from the carousel. See https://github.com/phetsims/sun/issues/677
        if ( !phet.chipper.isFuzzEnabled() || dotRandom.nextDouble() < 0.25 ) {
          const trail = event.trail;
          if ( !trail.containsNode( button ) && !trail.containsNode( carousel ) && !trail.containsNode( pageControl ) ) {
            carouselAndPageControl.visible = false;
          }
        }
      }
    };

    // Add clickToDismissListener only when the carousel & page control are visible. unlink is not needed.
    // NOTE: adapted from ComboBox.
    let display = null; // {Display}
    carouselAndPageControl.visibleProperty.link( visible => {
      if ( visible ) {
        assert && assert( !display, 'unexpected display' );
        display = this.getUniqueTrail().rootNode().getRootedDisplays()[ 0 ];
        display.addInputListener( clickToDismissListener );
      }
      else {
        if ( display && display.hasInputListener( clickToDismissListener ) ) {
          display.removeInputListener( clickToDismissListener );
          display = null;
        }
      }
    } );

    // @private
    this.disposeCarouselComboBox = () => {

      // Deregister observers
      property.unlink( propertyListener );
      multilinks.forEach( multilink => multilink.dispose() );

      // Dispose of subcomponents
      button.dispose();
      pageControl.dispose();
      carousel.dispose();
    };
  }

  /**
   * @public
   * @override
   */
  dispose() {
    this.disposeCarouselComboBox();
    super.dispose();
  }
}

sun.register( 'CarouselComboBox', CarouselComboBox );
export default CarouselComboBox;<commit_msg>factor out inner class CarouselItemNode, https://github.com/phetsims/sun/issues/673<commit_after>// Copyright 2021, University of Colorado Boulder

/**
 * CarouselComboBox behaves like a combo box, but its listbox is a carousel. This allows you to scroll through a
 * long list of items, a feature that ComboBoxListBox does not support. ComboBoxItem, ComboBoxButton, and
 * Carousel are reused.
 *
 * NOTE! This was created as a quick way to address situations where ComboBox's listbox gets too long, for example
 * https://github.com/phetsims/sun/issues/673. This tends to happen in internal 'demo' applications
 * (sun, scenery-phet,... ) that have long lists of demos. And as a design best-practice, we tend to avoid
 * longs lists of things in sims. So that's why CarouselComboBox currently lives in sun/demo/.
 * It was written to be fairly general, so should be relatively easy to relocate if it's needed for wider use.
 *
 * Possible future work:
 * - Modify ComboBox so that it can use different types of popups (ComboBoxListBox, Carousel,...), or
 * - Make CarouselComboBox pop up the Carousel in front of everything else
 * - a11y support in CarouselComboBox
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import Multilink from '../../../axon/js/Multilink.js';
import Property from '../../../axon/js/Property.js';
import Dimension2 from '../../../dot/js/Dimension2.js';
import dotRandom from '../../../dot/js/dotRandom.js';
import merge from '../../../phet-core/js/merge.js';
import AssertUtils from '../../../phetcommon/js/AssertUtils.js';
import PressListener from '../../../scenery/js/listeners/PressListener.js';
import AlignBox from '../../../scenery/js/nodes/AlignBox.js';
import AlignGroup from '../../../scenery/js/nodes/AlignGroup.js';
import HBox from '../../../scenery/js/nodes/HBox.js';
import Node from '../../../scenery/js/nodes/Node.js';
import Rectangle from '../../../scenery/js/nodes/Rectangle.js';
import VBox from '../../../scenery/js/nodes/VBox.js';
import Color from '../../../scenery/js/util/Color.js';
import Tandem from '../../../tandem/js/Tandem.js';
import Carousel from '../Carousel.js';
import ComboBoxButton from '../ComboBoxButton.js';
import ComboBoxItem from '../ComboBoxItem.js';
import PageControl from '../PageControl.js';
import sun from '../sun.js';

class CarouselComboBox extends Node {

  /**
   * @param {Property.<*>} property - the Property that is set when an item is selected
   * @param {ComboBoxItem[]} comboBoxItems - the items that appear in the carousel
   * @param {Object} [options]
   */
  constructor( property, comboBoxItems, options ) {

    assert && assert( property instanceof Property, 'invalid property' );
    assert && AssertUtils.assertArrayOf( comboBoxItems, ComboBoxItem );

    options = merge( {

      // Options passed to CarouselItemNode
      itemNodeOptions: {
        align: 'left', // {string} alignment of item nodes on backgrounds, 'left'|'center'|'right'
        overColor: Color.grayColor( 245 ), // {ColorDef} background color of the item that the pointer is over
        selectedColor: 'yellow', // {ColorDef} background color of the selected item
        itemXMargin: 6, // {number} x margin for backgrounds on the items in the carousel
        itemYMargin: 2 // {number} y margin for backgrounds on the items in the carousel
      },

      // Options passed to Carousel
      carouselOptions: {
        arrowSize: new Dimension2( 20, 4 ),

        // Like ComboBox, 'vertical' is the only orientation supported (verified below).
        orientation: 'vertical',

        // Spacing and margins will be built into the backgrounds that are add to the item Nodes,
        // so set them to zero for the carousel.
        spacing: 0,
        margin: 0,

        // {number} If there are fewer items than this in the carousel, the actual number of items will be used.
        itemsPerPage: 15
      },

      pageControlOptions: {
        interactive: true
      },

      // Options passed to ComboBoxButton
      buttonOptions: {
        arrowDirection: 'down', // 'up' is not currently supported (verified below)
        baseColor: 'rgb( 218, 236, 255 )',
        xMargin: 6, // You'll typically want this to be the same as itemXMargin, but we're not going to force you :)
        yMargin: 4
      },

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    // Validate options
    assert && assert( options.carouselOptions.orientation === 'vertical', 'orientation must be vertical' );
    assert && assert( options.buttonOptions.arrowDirection === 'down', 'arrowDirection must be down' );

    // Check for options that are not settable by the client
    assert && assert( !options.children, 'CarouselComboBox sets children' );
    assert && assert( !options.buttonOptions.content, 'CarouselComboBox sets buttonOptions.content' );
    assert && assert( !options.buttonOptions.listener, 'CarouselComboBox sets buttonOptions.listener' );

    // Don't create pages that are longer than the number of items
    options.carouselOptions.itemsPerPage = Math.min( options.carouselOptions.itemsPerPage, comboBoxItems.length );

    // Orientations must be the same.
    assert && assert( !options.pageControlOptions.orientation, 'use carouselOptions.orientation' );
    options.pageControlOptions.orientation = options.carouselOptions.orientation;

    // Create tandems for subcomponents, if they were not provided
    options.carouselOptions.tandem = options.carouselOptions.tandem || options.tandem.createTandem( 'carousel' );
    options.buttonOptions.tandem = options.buttonOptions.tandem || options.tandem.createTandem( 'button' );

    // Make items in the carousel have the same width and height.
    const alignGroup = new AlignGroup();

    // Create items for the carousel, whose API for 'items' is different than ComboBox.
    const carouselItemNodes = _.map( comboBoxItems,
      comboBoxItem => new CarouselItemNode( property, comboBoxItem, alignGroup, options.itemNodeOptions )
    );
    assert && assert( carouselItemNodes.length === comboBoxItems.length, 'expected a carouselItem for each comboBoxItem' );

    // Create the carousel.
    const carousel = new Carousel( carouselItemNodes, options.carouselOptions );

    // page control
    const pageControl = new PageControl( carousel.numberOfPages, carousel.pageNumberProperty, options.pageControlOptions );

    // Page control to the left of carousel
    const carouselAndPageControl = new HBox( {
      spacing: 4,
      children: [ carousel, pageControl ]
    } );

    // Pressing this button pops the carousel up and down
    const button = new ComboBoxButton( property, comboBoxItems, merge( {}, {
      listener: () => {
        carouselAndPageControl.visible = !carouselAndPageControl.visible;
      }
    }, options.buttonOptions ) );

    // Put the button above the carousel, left aligned.
    const vBox = new VBox( {
      spacing: 0,
      align: 'left',
      children: [ button, carouselAndPageControl ]
    } );

    // Wrap everything with Node, to hide VBox's API.
    options.children = [ vBox ];

    super( options );

    // If the Property changes, hide the carousel. unlink is needed on disposed.
    const propertyListener = () => { carouselAndPageControl.visible = false; };
    property.link( propertyListener );

    // Clicking outside this UI component will hide the carousel and page control.
    // NOTE: adapted from ComboBox.
    const clickToDismissListener = {
      down: event => {
        assert && assert( carousel.visible, 'this listener should be registered only when carousel is visible' );

        // If fuzzing is enabled, exercise this listener some percentage of the time, so that this listener is tested.
        // The rest of the time, ignore this listener, so that the carousel remains popped up, and we test making
        // choices from the carousel. See https://github.com/phetsims/sun/issues/677
        if ( !phet.chipper.isFuzzEnabled() || dotRandom.nextDouble() < 0.25 ) {
          const trail = event.trail;
          if ( !trail.containsNode( button ) && !trail.containsNode( carousel ) && !trail.containsNode( pageControl ) ) {
            carouselAndPageControl.visible = false;
          }
        }
      }
    };

    // Add clickToDismissListener only when the carousel & page control are visible. unlink is not needed.
    // NOTE: adapted from ComboBox.
    let display = null; // {Display}
    carouselAndPageControl.visibleProperty.link( visible => {
      if ( visible ) {
        assert && assert( !display, 'unexpected display' );
        display = this.getUniqueTrail().rootNode().getRootedDisplays()[ 0 ];
        display.addInputListener( clickToDismissListener );
      }
      else {
        if ( display && display.hasInputListener( clickToDismissListener ) ) {
          display.removeInputListener( clickToDismissListener );
          display = null;
        }
      }
    } );

    // @private
    this.disposeCarouselComboBox = () => {

      // Deregister observers
      property.unlink( propertyListener );

      // Dispose of subcomponents
      button.dispose();
      pageControl.dispose();
      carousel.dispose();
      carouselItemNodes.forEach( node => node.dispose() );
    };
  }

  /**
   * @public
   * @override
   */
  dispose() {
    this.disposeCarouselComboBox();
    super.dispose();
  }
}

/**
 * CarouselItemNode is an item this UI component's carousel. Carousel and ComboBox have different APIs for 'items'.
 * This class adapts a ComboBoxItem by making the Node have uniform dimensions, and putting a background behind the
 * Node. The background changes color when the item is selected or the pointer is over the item.
 */
class CarouselItemNode extends Node {

  /**
   * @param {Property.<*>} property
   * @param {ComboBoxItem} comboBoxItem
   * @param {AlignGroup} alignGroup - used to make all item Nodes have uniform dimensions
   * @param {Object} [options]
   */
  constructor( property, comboBoxItem, alignGroup, options ) {

    options = merge( {
      align: 'left', // {string} alignment of node on background, 'left'|'center'|'right'
      xMargin: 6, // {number} x margin for backgrounds on the items in the carousel
      yMargin: 2, // {number} y margin for backgrounds on the items in the carousel
      overColor: Color.grayColor( 245 ), // {ColorDef} background color of the item that the pointer is over
      selectedColor: 'yellow' // {ColorDef} background color of the selected item
    }, options );

    const uniformNode = new AlignBox( comboBoxItem.node, {
      xAlign: options.align,
      group: alignGroup
    } );

    const backgroundNode = new Rectangle( 0, 0, 1, 1 );

    // Size backgroundNode to fit uniformNode. Note that uniformNode's bounds may change when additional Nodes are
    // added to alignGroup.
    uniformNode.boundsProperty.link( bounds => {
      backgroundNode.setRectBounds( bounds.dilatedXY( options.xMargin, options.yMargin ) );
    } );

    assert && assert( !options.children, 'CarouselItemNode sets children' );
    options.children = [ backgroundNode, uniformNode ];

    super( options );

    // Press on an item to select its associated value.
    const pressListener = new PressListener( {
      press: () => {
        property.value = comboBoxItem.value;
      }
    } );
    this.addInputListener( pressListener );

    // Selecting an item sets its background to the selected color.
    // Pointer over an item sets its background to the highlighted color.
    // Must be disposed because we do not own property.
    const multilink = new Multilink(
      [ property, pressListener.isOverProperty ],
      ( propertyValue, isOver ) => {
        if ( propertyValue === comboBoxItem.value ) {
          backgroundNode.fill = options.selectedColor;
        }
        else if ( isOver ) {
          backgroundNode.fill = options.overColor;
        }
        else {
          backgroundNode.fill = 'transparent';
        }
      } );

    // @private
    this.disposeCarouselItemNode = () => {
      multilink.dispose();
    };
  }

  /**
   * @public
   * @override
   */
  dispose() {
    this.disposeCarouselItemNode();
    super.dispose();
  }
}

sun.register( 'CarouselComboBox', CarouselComboBox );
export default CarouselComboBox;<|endoftext|>


<commit_before>var IPYTHON = {};

//============================================================================
// Notebook
//============================================================================

var Notebook = function (selector) {
    this.element = $(selector);
    this.element.data("notebook", this);
    this.next_prompt_number = 1;
    this.bind_events();
}

Notebook.prototype.bind_events = function () {
    var that = this;
    that.element.keydown(function (event) {
        // console.log(event);
        if (event.which == 38 && event.shiftKey) {
            that.select_prev();
        } else if (event.which == 40 && event.shiftKey) {
            that.select_next();
        }
    });
};

// Cell indexing, retrieval, etc.

Notebook.prototype.cell_elements = function () {
    return this.element.children("div.cell");
}

Notebook.prototype.ncells = function (cell) {
    return this.cell_elements().length;
}

// TODO: we are often calling cells as cells()[i], which we should optimize
// to cells(i) or a new method.
Notebook.prototype.cells = function () {
    return this.cell_elements().toArray().map(function (e) {
        return $(e).data("cell");
    });
}

Notebook.prototype.find_cell_index = function (cell) {
    var result = null;
    this.cell_elements().filter(function (index) {
        if ($(this).data("cell") === cell) {
            result = index;
        };
    });
    return result;
};

Notebook.prototype.index_or_selected = function (index) {
    return index || this.selected_index() || 0;
}

Notebook.prototype.select = function (index) {
    if (index !== undefined && index >= 0 && index < this.ncells()) {
        if (this.selected_index() !== null) {
            this.selected_cell().unselect();
        };
        this.cells()[index].select();
    };
    return this;
};

Notebook.prototype.select_next = function () {
    var index = this.selected_index();
    if (index !== null && index >= 0 && (index+1) < this.ncells()) {
        this.select(index+1);
    };
    return this;
};

Notebook.prototype.select_prev = function () {
    var index = this.selected_index();
    if (index !== null && index >= 0 && (index-1) < this.ncells()) {
        this.select(index-1);
    };
    return this;
};

Notebook.prototype.selected_index = function () {
    var result = null;
    this.cell_elements().filter(function (index) {
        if ($(this).data("cell").selected === true) {
            result = index;
        };
    });
    return result;
};

Notebook.prototype.selected_cell = function () {
    return this.cell_elements().eq(this.selected_index()).data("cell");
}

// Cell insertion, deletion and moving.

Notebook.prototype.delete_cell = function (index) {
    var i = index || this.selected_index();
    if (i !== null && i >= 0 && i < this.ncells()) {
        this.cell_elements().eq(i).remove();
        if (i === (this.ncells())) {
            this.select(i-1);
        } else {
            this.select(i);
        };
    };
    return this;
};

Notebook.prototype.append_cell = function (cell) {
    this.element.append(cell.element);
    return this;
};

Notebook.prototype.insert_cell_after = function (cell, index) {
    var ncells = this.ncells();
    if (ncells === 0) {
        this.append_cell(cell);
        return this;
    };
    if (index >= 0 && index < ncells) {
        this.cell_elements().eq(index).after(cell.element);
    };
    return this
};

Notebook.prototype.insert_cell_before = function (cell, index) {
    var ncells = this.ncells();
    if (ncells === 0) {
        this.append_cell(cell);
        return this;
    };
    if (index > 0 && index < ncells) {
        this.cell_elements().eq(index).before(cell.element);
    };
    return this;
};

Notebook.prototype.move_cell_up = function (index) {
    var i = index || this.selected_index();
    if (i !== null && i < this.ncells() && i > 0) {
        var pivot = this.cell_elements().eq(i-1);
        var tomove = this.cell_elements().eq(i);
        if (pivot !== null && tomove !== null) {
            tomove.detach();
            pivot.before(tomove);
        };
    };
    return this;
}

Notebook.prototype.move_cell_down = function (index) {
    var i = index || this.selected_index();
    if (i !== null && i < (this.ncells()-1) && i >= 0) {
        var pivot = this.cell_elements().eq(i+1)
        var tomove = this.cell_elements().eq(i)
        if (pivot !== null && tomove !== null) {
            tomove.detach();
            pivot.after(tomove);
        };
    };
    return this;
}

Notebook.prototype.sort_cells = function () {
    var ncells = this.ncells();
    var swapped;
    do {
        swapped = false
        for (var i=1; i<ncells; i++) {
            current = this.cell_elements().eq(i).data("cell");
            previous = this.cell_elements().eq(i-1).data("cell");
            if (previous.input_prompt_number > current.input_prompt_number) {
                this.move_cell_up(i);
                swapped = true;
            };
        };
    } while (swapped);
    return this;
};

Notebook.prototype.insert_code_cell_before = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new CodeCell(this);
    cell.set_input_prompt(this.next_prompt_number);
    this.next_prompt_number = this.next_prompt_number + 1;
    this.insert_cell_before(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.insert_code_cell_after = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new CodeCell(this);
    cell.set_input_prompt(this.next_prompt_number);
    this.next_prompt_number = this.next_prompt_number + 1;
    this.insert_cell_after(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.insert_text_cell_before = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new TextCell(this);
    cell.config_mathjax();
    this.insert_cell_before(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.insert_text_cell_after = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new TextCell(this);
    cell.config_mathjax();
    this.insert_cell_after(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.text_to_code = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var source_element = this.cell_elements().eq(i);
    var source_cell = source_element.data("cell");
    if (source_cell instanceof TextCell) {
        this.insert_code_cell_after(i);
        var target_cell = this.cells()[i+1];
        var text = source_element.find("textarea.text_cell_input").val();
        target_cell.element.find("textarea.input_area").val(text);
        source_element.remove();
    };
};

Notebook.prototype.code_to_text = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var source_element = this.cell_elements().eq(i);
    var source_cell = source_element.data("cell");
    if (source_cell instanceof CodeCell) {
        this.insert_text_cell_after(i);
        var target_cell = this.cells()[i+1];
        var text = source_element.find("textarea.input_area").val();
        target_cell.element.find("textarea.text_cell_input").val(text);
        target_cell.element.find("textarea.text_cell_input").html(text);
        target_cell.element.find("div.text_cell_render").html(text);

        source_element.remove();
    };
};

// Cell collapsing

Notebook.prototype.collapse = function (index) {
    var i = this.index_or_selected(index);
    this.cells()[i].collapse();
}

Notebook.prototype.expand = function (index) {
    var i = this.index_or_selected(index);
    this.cells()[i].expand();
}

//============================================================================
// Cell
//============================================================================

var Cell = function (notebook) {
    this.notebook = notebook;
    this.selected = false;
    this.element;
    this.create_element();
    if (this.element !== undefined) {
        this.element.data("cell", this);
        this.bind_events();
    }
};

Cell.prototype.select = function () {
    this.element.addClass('ui-widget-content ui-corner-all');
    this.selected = true;
    this.element.find('textarea').trigger('focusin');
};

Cell.prototype.unselect = function () {
    this.element.removeClass('ui-widget-content ui-corner-all');
    this.selected = false;
};

Cell.prototype.bind_events = function () {
    var that = this;
    var nb = that.notebook
    that.element.click(function (event) {
        if (that.selected === false) {
            nb.select(nb.find_cell_index(that));
        };
    });
    that.element.focusin(function (event) {
        if (that.selected === false) {
            nb.select(nb.find_cell_index(that));
        };
    });
};

// Subclasses must implement create_element.
Cell.prototype.create_element = function () {};

//============================================================================
// CodeCell
//============================================================================

var CodeCell = function (notebook) {
    Cell.apply(this, arguments);
    this.input_prompt_number = ' ';
    this.output_prompt_number = ' ';
};

CodeCell.prototype = new Cell();

CodeCell.prototype.create_element = function () {
    var cell =  $('

').addClass('cell code_cell')
    var input = $('

').addClass('input').append(
                    $('
').addClass('prompt input_prompt')
                ).append(
                    $('<textarea/>').addClass('input_area').
                    attr('rows',1).
                    attr('cols',80).
                    attr('wrap','hard').
                    autoGrow()
                );
    var output = $('

').addClass('output').append(
                    $('
').addClass('prompt output_prompt')
                ).append(
                    $('
').addClass('output_area')
                );
    output.hide();
    cell.append(input).append(output);
    this.element = cell;
};

CodeCell.prototype.collapse = function () {
    this.element.find('div.output').hide();
};

CodeCell.prototype.expand = function () {
    this.element.find('div.output').show();
};

CodeCell.prototype.set_prompt = function (number) {
    this.set_input_prompt(number);
    this.set_output_prompt(number);
};

CodeCell.prototype.set_input_prompt = function (number) {
    var n = number || ' ';
    this.input_prompt_number = n
    this.element.find('div.input_prompt').html('In [' + n + ']:');
};

CodeCell.prototype.set_output_prompt = function (number) {
    var n = number || ' ';
    this.output_prompt_number = n
    this.element.find('div.output_prompt').html('Out[' + n + ']:');
};

//============================================================================
// TextCell
//============================================================================

var TextCell = function (notebook) {
    Cell.apply(this, arguments);
};

TextCell.prototype = new Cell();

TextCell.prototype.create_element = function () {
    var cell = $('
</div').addClass('cell text_cell').
               append(
                   $('<textarea>Type HTML/LaTex content here</textarea>').
                   addClass('text_cell_input').
                   attr('rows',1).
                   attr('cols',80).
                   autoGrow()
               ).append(
                   $('

').addClass('text_cell_render')
               )
    this.element = cell;
};

TextCell.prototype.config_mathjax = function () {
    var text_cell = this.element;
    var input = text_cell.find("textarea.text_cell_input");
    var output = text_cell.find("div.text_cell_render");

    text_cell.click(function () {
        output.hide();
        input.show().trigger('focus');
    }).focusout(function () {
        var text = input.val();
        output.html(text)
        input.html(text);
        MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
        input.hide();
        output.show();
    });

    text_cell.trigger("focusout");
};

$(document).ready(function () {

    MathJax.Hub.Config({
        tex2jax: {
            inlineMath: [ ['$','$'], ["\\(","\\)"] ],
            displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
        }
    });

    $("ul#main_menu").wijmenu({animation:{animated: "slide", duration: 100, easing: null}});
    IPYTHON.notebook = new Notebook('div.notebook');
    IPYTHON.notebook.insert_code_cell_after();

    $("#move_cell").buttonset();
    $("#move_up").button("option", "icons", {primary:"ui-icon-arrowthick-1-n"});
    $("#move_up").button("option", "text", false);
    $("#move_up").click(function () {IPYTHON.notebook.move_cell_up();});
    $("#move_down").button("option", "icons", {primary:"ui-icon-arrowthick-1-s"});
    $("#move_down").button("option", "text", false);
    $("#move_down").click(function () {IPYTHON.notebook.move_cell_down();});

    $("#insert_delete").buttonset();
    $("#insert_cell_before").click(function () {IPYTHON.notebook.insert_code_cell_before();});
    $("#insert_cell_after").click(function () {IPYTHON.notebook.insert_code_cell_after();});
    $("#delete_cell").button("option", "icons", {primary:"ui-icon-closethick"});
    $("#delete_cell").button("option", "text", false);
    $("#delete_cell").click(function () {IPYTHON.notebook.delete_cell();});

    $("#cell_type").buttonset();
    $("#to_code").click(function () {IPYTHON.notebook.text_to_code();});
    $("#to_text").click(function () {IPYTHON.notebook.code_to_text();});

    $("#sort").buttonset();
    $("#sort_cells").click(function () {IPYTHON.notebook.sort_cells();});

});<commit_msg>Adding shift-enter support.<commit_after>var IPYTHON = {};

//============================================================================
// Notebook
//============================================================================

var Notebook = function (selector) {
    this.element = $(selector);
    this.element.data("notebook", this);
    this.next_prompt_number = 1;
    this.bind_events();
}

Notebook.prototype.bind_events = function () {
    var that = this;
    that.element.keydown(function (event) {
        console.log(event);
        if (event.which == 38 && event.shiftKey) {
            that.select_prev();
        } else if (event.which == 40 && event.shiftKey) {
            that.select_next();
        } else if (event.which == 13 && event.shiftKey) {
            // The focus is not quite working here.
            that.insert_code_cell_after();
        }
    });
};

// Cell indexing, retrieval, etc.

Notebook.prototype.cell_elements = function () {
    return this.element.children("div.cell");
}

Notebook.prototype.ncells = function (cell) {
    return this.cell_elements().length;
}

// TODO: we are often calling cells as cells()[i], which we should optimize
// to cells(i) or a new method.
Notebook.prototype.cells = function () {
    return this.cell_elements().toArray().map(function (e) {
        return $(e).data("cell");
    });
}

Notebook.prototype.find_cell_index = function (cell) {
    var result = null;
    this.cell_elements().filter(function (index) {
        if ($(this).data("cell") === cell) {
            result = index;
        };
    });
    return result;
};

Notebook.prototype.index_or_selected = function (index) {
    return index || this.selected_index() || 0;
}

Notebook.prototype.select = function (index) {
    if (index !== undefined && index >= 0 && index < this.ncells()) {
        if (this.selected_index() !== null) {
            this.selected_cell().unselect();
        };
        this.cells()[index].select();
    };
    return this;
};

Notebook.prototype.select_next = function () {
    var index = this.selected_index();
    if (index !== null && index >= 0 && (index+1) < this.ncells()) {
        this.select(index+1);
    };
    return this;
};

Notebook.prototype.select_prev = function () {
    var index = this.selected_index();
    if (index !== null && index >= 0 && (index-1) < this.ncells()) {
        this.select(index-1);
    };
    return this;
};

Notebook.prototype.selected_index = function () {
    var result = null;
    this.cell_elements().filter(function (index) {
        if ($(this).data("cell").selected === true) {
            result = index;
        };
    });
    return result;
};

Notebook.prototype.selected_cell = function () {
    return this.cell_elements().eq(this.selected_index()).data("cell");
}

// Cell insertion, deletion and moving.

Notebook.prototype.delete_cell = function (index) {
    var i = index || this.selected_index();
    if (i !== null && i >= 0 && i < this.ncells()) {
        this.cell_elements().eq(i).remove();
        if (i === (this.ncells())) {
            this.select(i-1);
        } else {
            this.select(i);
        };
    };
    return this;
};

Notebook.prototype.append_cell = function (cell) {
    this.element.append(cell.element);
    return this;
};

Notebook.prototype.insert_cell_after = function (cell, index) {
    var ncells = this.ncells();
    if (ncells === 0) {
        this.append_cell(cell);
        return this;
    };
    if (index >= 0 && index < ncells) {
        this.cell_elements().eq(index).after(cell.element);
    };
    return this
};

Notebook.prototype.insert_cell_before = function (cell, index) {
    var ncells = this.ncells();
    if (ncells === 0) {
        this.append_cell(cell);
        return this;
    };
    if (index > 0 && index < ncells) {
        this.cell_elements().eq(index).before(cell.element);
    };
    return this;
};

Notebook.prototype.move_cell_up = function (index) {
    var i = index || this.selected_index();
    if (i !== null && i < this.ncells() && i > 0) {
        var pivot = this.cell_elements().eq(i-1);
        var tomove = this.cell_elements().eq(i);
        if (pivot !== null && tomove !== null) {
            tomove.detach();
            pivot.before(tomove);
        };
    };
    return this;
}

Notebook.prototype.move_cell_down = function (index) {
    var i = index || this.selected_index();
    if (i !== null && i < (this.ncells()-1) && i >= 0) {
        var pivot = this.cell_elements().eq(i+1)
        var tomove = this.cell_elements().eq(i)
        if (pivot !== null && tomove !== null) {
            tomove.detach();
            pivot.after(tomove);
        };
    };
    return this;
}

Notebook.prototype.sort_cells = function () {
    var ncells = this.ncells();
    var swapped;
    do {
        swapped = false
        for (var i=1; i<ncells; i++) {
            current = this.cell_elements().eq(i).data("cell");
            previous = this.cell_elements().eq(i-1).data("cell");
            if (previous.input_prompt_number > current.input_prompt_number) {
                this.move_cell_up(i);
                swapped = true;
            };
        };
    } while (swapped);
    return this;
};

Notebook.prototype.insert_code_cell_before = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new CodeCell(this);
    cell.set_input_prompt(this.next_prompt_number);
    this.next_prompt_number = this.next_prompt_number + 1;
    this.insert_cell_before(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.insert_code_cell_after = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new CodeCell(this);
    cell.set_input_prompt(this.next_prompt_number);
    this.next_prompt_number = this.next_prompt_number + 1;
    this.insert_cell_after(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.insert_text_cell_before = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new TextCell(this);
    cell.config_mathjax();
    this.insert_cell_before(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.insert_text_cell_after = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var cell = new TextCell(this);
    cell.config_mathjax();
    this.insert_cell_after(cell, i);
    this.select(this.find_cell_index(cell));
    return this;
}

Notebook.prototype.text_to_code = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var source_element = this.cell_elements().eq(i);
    var source_cell = source_element.data("cell");
    if (source_cell instanceof TextCell) {
        this.insert_code_cell_after(i);
        var target_cell = this.cells()[i+1];
        var text = source_element.find("textarea.text_cell_input").val();
        target_cell.element.find("textarea.input_area").val(text);
        source_element.remove();
    };
};

Notebook.prototype.code_to_text = function (index) {
    // TODO: Bounds check for i
    var i = this.index_or_selected(index);
    var source_element = this.cell_elements().eq(i);
    var source_cell = source_element.data("cell");
    if (source_cell instanceof CodeCell) {
        this.insert_text_cell_after(i);
        var target_cell = this.cells()[i+1];
        var text = source_element.find("textarea.input_area").val();
        target_cell.element.find("textarea.text_cell_input").val(text);
        target_cell.element.find("textarea.text_cell_input").html(text);
        target_cell.element.find("div.text_cell_render").html(text);

        source_element.remove();
    };
};

// Cell collapsing

Notebook.prototype.collapse = function (index) {
    var i = this.index_or_selected(index);
    this.cells()[i].collapse();
}

Notebook.prototype.expand = function (index) {
    var i = this.index_or_selected(index);
    this.cells()[i].expand();
}

//============================================================================
// Cell
//============================================================================

var Cell = function (notebook) {
    this.notebook = notebook;
    this.selected = false;
    this.element;
    this.create_element();
    if (this.element !== undefined) {
        this.element.data("cell", this);
        this.bind_events();
    }
};

Cell.prototype.select = function () {
    this.element.addClass('ui-widget-content ui-corner-all');
    this.selected = true;
    this.element.find('textarea').trigger('focusin');
};

Cell.prototype.unselect = function () {
    this.element.removeClass('ui-widget-content ui-corner-all');
    this.selected = false;
};

Cell.prototype.bind_events = function () {
    var that = this;
    var nb = that.notebook
    that.element.click(function (event) {
        if (that.selected === false) {
            nb.select(nb.find_cell_index(that));
        };
    });
    that.element.focusin(function (event) {
        if (that.selected === false) {
            nb.select(nb.find_cell_index(that));
        };
    });
};

// Subclasses must implement create_element.
Cell.prototype.create_element = function () {};

//============================================================================
// CodeCell
//============================================================================

var CodeCell = function (notebook) {
    Cell.apply(this, arguments);
    this.input_prompt_number = ' ';
    this.output_prompt_number = ' ';
};

CodeCell.prototype = new Cell();

CodeCell.prototype.create_element = function () {
    var cell =  $('

').addClass('cell code_cell')
    var input = $('

').addClass('input').append(
                    $('
').addClass('prompt input_prompt')
                ).append(
                    $('<textarea/>').addClass('input_area').
                    attr('rows',1).
                    attr('cols',80).
                    attr('wrap','hard').
                    autoGrow()
                );
    var output = $('

').addClass('output').append(
                    $('
').addClass('prompt output_prompt')
                ).append(
                    $('
').addClass('output_area')
                );
    output.hide();
    cell.append(input).append(output);
    this.element = cell;
};

CodeCell.prototype.collapse = function () {
    this.element.find('div.output').hide();
};

CodeCell.prototype.expand = function () {
    this.element.find('div.output').show();
};

CodeCell.prototype.set_prompt = function (number) {
    this.set_input_prompt(number);
    this.set_output_prompt(number);
};

CodeCell.prototype.set_input_prompt = function (number) {
    var n = number || ' ';
    this.input_prompt_number = n
    this.element.find('div.input_prompt').html('In [' + n + ']:');
};

CodeCell.prototype.set_output_prompt = function (number) {
    var n = number || ' ';
    this.output_prompt_number = n
    this.element.find('div.output_prompt').html('Out[' + n + ']:');
};

//============================================================================
// TextCell
//============================================================================

var TextCell = function (notebook) {
    Cell.apply(this, arguments);
};

TextCell.prototype = new Cell();

TextCell.prototype.create_element = function () {
    var cell = $('
</div').addClass('cell text_cell').
               append(
                   $('<textarea>Type HTML/LaTex content here</textarea>').
                   addClass('text_cell_input').
                   attr('rows',1).
                   attr('cols',80).
                   autoGrow()
               ).append(
                   $('

').addClass('text_cell_render')
               )
    this.element = cell;
};

TextCell.prototype.config_mathjax = function () {
    var text_cell = this.element;
    var input = text_cell.find("textarea.text_cell_input");
    var output = text_cell.find("div.text_cell_render");

    text_cell.click(function () {
        output.hide();
        input.show().trigger('focus');
    }).focusout(function () {
        var text = input.val();
        output.html(text)
        input.html(text);
        MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
        input.hide();
        output.show();
    });

    text_cell.trigger("focusout");
};

$(document).ready(function () {

    MathJax.Hub.Config({
        tex2jax: {
            inlineMath: [ ['$','$'], ["\\(","\\)"] ],
            displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
        }
    });

    $("ul#main_menu").wijmenu({animation:{animated: "slide", duration: 100, easing: null}});
    IPYTHON.notebook = new Notebook('div.notebook');
    IPYTHON.notebook.insert_code_cell_after();

    $("#move_cell").buttonset();
    $("#move_up").button("option", "icons", {primary:"ui-icon-arrowthick-1-n"});
    $("#move_up").button("option", "text", false);
    $("#move_up").click(function () {IPYTHON.notebook.move_cell_up();});
    $("#move_down").button("option", "icons", {primary:"ui-icon-arrowthick-1-s"});
    $("#move_down").button("option", "text", false);
    $("#move_down").click(function () {IPYTHON.notebook.move_cell_down();});

    $("#insert_delete").buttonset();
    $("#insert_cell_before").click(function () {IPYTHON.notebook.insert_code_cell_before();});
    $("#insert_cell_after").click(function () {IPYTHON.notebook.insert_code_cell_after();});
    $("#delete_cell").button("option", "icons", {primary:"ui-icon-closethick"});
    $("#delete_cell").button("option", "text", false);
    $("#delete_cell").click(function () {IPYTHON.notebook.delete_cell();});

    $("#cell_type").buttonset();
    $("#to_code").click(function () {IPYTHON.notebook.text_to_code();});
    $("#to_text").click(function () {IPYTHON.notebook.code_to_text();});

    $("#sort").buttonset();
    $("#sort_cells").click(function () {IPYTHON.notebook.sort_cells();});

});<|endoftext|>