<commit_before>/**
 * @flow
 *
 * Building Hours "report a problem" screen.
 */

import React from 'react'
import {ScrollView, View, StyleSheet, Text} from 'react-native'
import moment from 'moment-timezone'
import {CellTextField} from '../../components/cells/textfield'
import {CellToggle} from '../../components/cells/toggle'
import {DeleteButtonCell} from '../../components/cells/delete-button'
import {ButtonCell} from '../../components/cells/button'
import * as c from '../../components/colors'
import {TableView, Section, Cell} from 'react-native-tableview-simple'
import type {
  BuildingType,
  NamedBuildingScheduleType,
  SingleBuildingScheduleType,
} from '../types'
import type {TopLevelViewPropsType} from '../../types'
import {summarizeDays, formatBuildingTimes, blankSchedule} from '../lib'

const styles = StyleSheet.create({
  helpWrapper: {
    backgroundColor: c.white,
    borderWidth: StyleSheet.hairlineWidth,
    borderColor: '#ebebeb',
    marginBottom: 10,
  },
  helpTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    paddingTop: 15,
    paddingHorizontal: 15,
  },
  helpDescription: {
    fontSize: 14,
    paddingTop: 5,
    paddingBottom: 15,
    paddingHorizontal: 15,
  },
})

export class BuildingHoursProblemReportView extends React.PureComponent {
  static navigationOptions = {
    title: 'Report a Problem',
  }

  state: {
    building: BuildingType,
  } = {
    building: this.props.navigation.state.params.initialBuilding,
  }

  props: TopLevelViewPropsType & {
    navigation: {state: {params: {initialBuilding: BuildingType}}},
  }

  openEditor = (
    scheduleIdx: number,
    setIdx: number,
    set: ?SingleBuildingScheduleType = null,
  ) => {
    this.props.navigation.navigate('BuildingHoursScheduleEditorView', {
      initialSet: set,
      onEditSet: (editedData: SingleBuildingScheduleType) =>
        this.editHoursRow(scheduleIdx, setIdx, editedData),
      onDeleteSet: () => this.deleteHoursRow(scheduleIdx, setIdx),
    })
  }

  editSchedule = (idx: number, newSchedule: NamedBuildingScheduleType) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]
      schedules.splice(idx, 1, newSchedule)

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  deleteSchedule = (idx: number) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]
      schedules.splice(idx, 1)

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  addSchedule = () => {
    this.setState(state => {
      return {
        ...state,
        building: {
          ...state.building,
          schedule: [
            ...state.building.schedule,
            {
              title: 'Hours',
              hours: [blankSchedule()],
            },
          ],
        },
      }
    })
  }

  addHoursRow = (idx: number) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]

      schedules[idx] = {
        ...schedules[idx],
        hours: [...schedules[idx].hours, blankSchedule()],
      }

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  editHoursRow = (
    scheduleIdx: number,
    setIdx: number,
    newData: SingleBuildingScheduleType,
  ) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]

      const hours = [...schedules[scheduleIdx].hours]
      hours.splice(setIdx, 1, newData)

      schedules[scheduleIdx] = {...schedules[scheduleIdx], hours}

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  deleteHoursRow = (scheduleIdx: number, setIdx: number) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]

      const hours = [...schedules[scheduleIdx].hours]
      hours.splice(setIdx, 1)

      schedules[scheduleIdx] = {...schedules[scheduleIdx], hours}

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  submit = () => {
    console.log(JSON.stringify(this.state.building))
  }

  render() {
    const {schedule: schedules = []} = this.state.building

    return (
      <ScrollView>
        <View style={styles.helpWrapper}>
          <Text style={styles.helpTitle}>Thanks for spotting a problem!</Text>
          <Text style={styles.helpDescription}>
            If you could tell us what the new times are, we’d greatly
            appreciate it.
          </Text>
        </View>

        <TableView>
          {schedules.map((s, i) =>
            <EditableSchedule
              key={i}
              schedule={s}
              scheduleIndex={i}
              addRow={this.addHoursRow}
              editRow={this.openEditor}
              onEditSchedule={this.editSchedule}
              onDelete={this.deleteSchedule}
            />,
          )}

            <Cell
              title="Add New Schedule"
              accessory="DisclosureIndicator"
              onPress={this.addSchedule}
            />

            <ButtonCell title="Submit Report" onPress={this.submit} />

        </TableView>
      </ScrollView>
    )
  }
}

class EditableSchedule extends React.PureComponent {
  props: {
    schedule: NamedBuildingScheduleType,
    scheduleIndex: number,
    addRow: (idx: number) => any,
    editRow: (
      schedIdx: number,
      setIdx: number,
      set: SingleBuildingScheduleType,
    ) => any,
    onEditSchedule: (idx: number, set: NamedBuildingScheduleType) => any,
    onDelete: (idx: number) => any,
  }

  onEdit = data => {
    const idx = this.props.scheduleIndex
    this.props.onEditSchedule(idx, {
      ...this.props.schedule,
      ...data,
    })
  }

  editTitle = (newValue: string) => {
    this.onEdit({title: newValue})
  }

  editNotes = (newValue: string) => {
    this.onEdit({notes: newValue})
  }

  toggleChapel = (newValue: boolean) => {
    this.onEdit({closedForChapelTime: newValue})
  }

  addHoursRow = () => {
    this.props.addRow(this.props.scheduleIndex)
  }

  delete = () => {
    this.props.onDelete(this.props.scheduleIndex)
  }

  openEditor = (setIndex: number, hoursSet: SingleBuildingScheduleType) => {
    this.props.editRow(this.props.scheduleIndex, setIndex, hoursSet)
  }

  render() {
    const {schedule} = this.props
    const now = moment()

    return (
      <View>

          <TitleCell text={schedule.title} onChange={this.editTitle} />
          <NotesCell text={schedule.notes} onChange={this.editNotes} />

          <CellToggle
            label="Closes for Chapel"
            value={Boolean(schedule.closedForChapelTime)}
            onChange={this.toggleChapel}
          />

          {schedule.hours.map((set, i) =>
            <TimesCell
              key={i}
              set={set}
              setIndex={i}
              onPress={this.openEditor}
              now={now}
            />,
          )}

          <Cell
            title="Add More Hours"
            accessory="DisclosureIndicator"
            onPress={this.addHoursRow}
          />

          <DeleteButtonCell title="Delete Schedule" onPress={this.delete} />

      </View>
    )
  }
}

// "Title" will become a textfield like the login form
const TitleCell = ({text, onChange = () => {}}) =>
  <CellTextField
    hideLabel={true}
    returnKeyType="done"
    placeholder="Title"
    value={text}
    onChangeText={onChange}
    onSubmitEditing={onChange}
  />

// "Notes" will become a big textarea
const NotesCell = ({text, onChange}) =>
  <CellTextField
    hideLabel={true}
    returnKeyType="done"
    placeholder="Notes"
    value={text}
    onChangeText={onChange}
    onSubmitEditing={onChange}
  />

class TimesCell extends React.PureComponent {
  props: {
    set: SingleBuildingScheduleType,
    setIndex: number,
    onPress: (setIdx: number, set: SingleBuildingScheduleType) => any,
    now: moment,
  }

  onPress = () => {
    this.props.onPress(this.props.setIndex, this.props.set)
  }

  render() {
    const {set, now} = this.props

    return (
      <Cell
        title={set.days.length ? summarizeDays(set.days) : 'Days'}
        accessory="DisclosureIndicator"
        cellStyle="RightDetail"
        detail={formatBuildingTimes(set, now)}
        onPress={this.onPress}
      />
    )
  }
}
<commit_msg>change out colors for borders<commit_after>/**
 * @flow
 *
 * Building Hours "report a problem" screen.
 */

import React from 'react'
import {ScrollView, View, StyleSheet, Text} from 'react-native'
import moment from 'moment-timezone'
import {CellTextField} from '../../components/cells/textfield'
import {CellToggle} from '../../components/cells/toggle'
import {DeleteButtonCell} from '../../components/cells/delete-button'
import {ButtonCell} from '../../components/cells/button'
import * as c from '../../components/colors'
import {TableView, Section, Cell} from 'react-native-tableview-simple'
import type {
  BuildingType,
  NamedBuildingScheduleType,
  SingleBuildingScheduleType,
} from '../types'
import type {TopLevelViewPropsType} from '../../types'
import {summarizeDays, formatBuildingTimes, blankSchedule} from '../lib'

const styles = StyleSheet.create({
  helpWrapper: {
    backgroundColor: c.white,
    borderWidth: StyleSheet.hairlineWidth,
    borderTopColor: c.iosHeaderTopBorder,
    borderBottomColor: c.iosHeaderBottomBorder,
    marginBottom: 10,
  },
  helpTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    paddingTop: 15,
    paddingHorizontal: 15,
  },
  helpDescription: {
    fontSize: 14,
    paddingTop: 5,
    paddingBottom: 15,
    paddingHorizontal: 15,
  },
})

export class BuildingHoursProblemReportView extends React.PureComponent {
  static navigationOptions = {
    title: 'Report a Problem',
  }

  state: {
    building: BuildingType,
  } = {
    building: this.props.navigation.state.params.initialBuilding,
  }

  props: TopLevelViewPropsType & {
    navigation: {state: {params: {initialBuilding: BuildingType}}},
  }

  openEditor = (
    scheduleIdx: number,
    setIdx: number,
    set: ?SingleBuildingScheduleType = null,
  ) => {
    this.props.navigation.navigate('BuildingHoursScheduleEditorView', {
      initialSet: set,
      onEditSet: (editedData: SingleBuildingScheduleType) =>
        this.editHoursRow(scheduleIdx, setIdx, editedData),
      onDeleteSet: () => this.deleteHoursRow(scheduleIdx, setIdx),
    })
  }

  editSchedule = (idx: number, newSchedule: NamedBuildingScheduleType) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]
      schedules.splice(idx, 1, newSchedule)

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  deleteSchedule = (idx: number) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]
      schedules.splice(idx, 1)

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  addSchedule = () => {
    this.setState(state => {
      return {
        ...state,
        building: {
          ...state.building,
          schedule: [
            ...state.building.schedule,
            {
              title: 'Hours',
              hours: [blankSchedule()],
            },
          ],
        },
      }
    })
  }

  addHoursRow = (idx: number) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]

      schedules[idx] = {
        ...schedules[idx],
        hours: [...schedules[idx].hours, blankSchedule()],
      }

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  editHoursRow = (
    scheduleIdx: number,
    setIdx: number,
    newData: SingleBuildingScheduleType,
  ) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]

      const hours = [...schedules[scheduleIdx].hours]
      hours.splice(setIdx, 1, newData)

      schedules[scheduleIdx] = {...schedules[scheduleIdx], hours}

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  deleteHoursRow = (scheduleIdx: number, setIdx: number) => {
    this.setState(state => {
      const schedules = [...state.building.schedule]

      const hours = [...schedules[scheduleIdx].hours]
      hours.splice(setIdx, 1)

      schedules[scheduleIdx] = {...schedules[scheduleIdx], hours}

      return {
        ...state,
        building: {
          ...state.building,
          schedule: schedules,
        },
      }
    })
  }

  submit = () => {
    console.log(JSON.stringify(this.state.building))
  }

  render() {
    const {schedule: schedules = []} = this.state.building

    return (
      <ScrollView>
        <View style={styles.helpWrapper}>
          <Text style={styles.helpTitle}>Thanks for spotting a problem!</Text>
          <Text style={styles.helpDescription}>
            If you could tell us what the new times are, we’d greatly
            appreciate it.
          </Text>
        </View>

        <TableView>
          {schedules.map((s, i) =>
            <EditableSchedule
              key={i}
              schedule={s}
              scheduleIndex={i}
              addRow={this.addHoursRow}
              editRow={this.openEditor}
              onEditSchedule={this.editSchedule}
              onDelete={this.deleteSchedule}
            />,
          )}

            <Cell
              title="Add New Schedule"
              accessory="DisclosureIndicator"
              onPress={this.addSchedule}
            />

            <ButtonCell title="Submit Report" onPress={this.submit} />

        </TableView>
      </ScrollView>
    )
  }
}

class EditableSchedule extends React.PureComponent {
  props: {
    schedule: NamedBuildingScheduleType,
    scheduleIndex: number,
    addRow: (idx: number) => any,
    editRow: (
      schedIdx: number,
      setIdx: number,
      set: SingleBuildingScheduleType,
    ) => any,
    onEditSchedule: (idx: number, set: NamedBuildingScheduleType) => any,
    onDelete: (idx: number) => any,
  }

  onEdit = data => {
    const idx = this.props.scheduleIndex
    this.props.onEditSchedule(idx, {
      ...this.props.schedule,
      ...data,
    })
  }

  editTitle = (newValue: string) => {
    this.onEdit({title: newValue})
  }

  editNotes = (newValue: string) => {
    this.onEdit({notes: newValue})
  }

  toggleChapel = (newValue: boolean) => {
    this.onEdit({closedForChapelTime: newValue})
  }

  addHoursRow = () => {
    this.props.addRow(this.props.scheduleIndex)
  }

  delete = () => {
    this.props.onDelete(this.props.scheduleIndex)
  }

  openEditor = (setIndex: number, hoursSet: SingleBuildingScheduleType) => {
    this.props.editRow(this.props.scheduleIndex, setIndex, hoursSet)
  }

  render() {
    const {schedule} = this.props
    const now = moment()

    return (
      <View>

          <TitleCell text={schedule.title} onChange={this.editTitle} />
          <NotesCell text={schedule.notes} onChange={this.editNotes} />

          <CellToggle
            label="Closes for Chapel"
            value={Boolean(schedule.closedForChapelTime)}
            onChange={this.toggleChapel}
          />

          {schedule.hours.map((set, i) =>
            <TimesCell
              key={i}
              set={set}
              setIndex={i}
              onPress={this.openEditor}
              now={now}
            />,
          )}

          <Cell
            title="Add More Hours"
            accessory="DisclosureIndicator"
            onPress={this.addHoursRow}
          />

          <DeleteButtonCell title="Delete Schedule" onPress={this.delete} />

      </View>
    )
  }
}

// "Title" will become a textfield like the login form
const TitleCell = ({text, onChange = () => {}}) =>
  <CellTextField
    hideLabel={true}
    returnKeyType="done"
    placeholder="Title"
    value={text}
    onChangeText={onChange}
    onSubmitEditing={onChange}
  />

// "Notes" will become a big textarea
const NotesCell = ({text, onChange}) =>
  <CellTextField
    hideLabel={true}
    returnKeyType="done"
    placeholder="Notes"
    value={text}
    onChangeText={onChange}
    onSubmitEditing={onChange}
  />

class TimesCell extends React.PureComponent {
  props: {
    set: SingleBuildingScheduleType,
    setIndex: number,
    onPress: (setIdx: number, set: SingleBuildingScheduleType) => any,
    now: moment,
  }

  onPress = () => {
    this.props.onPress(this.props.setIndex, this.props.set)
  }

  render() {
    const {set, now} = this.props

    return (
      <Cell
        title={set.days.length ? summarizeDays(set.days) : 'Days'}
        accessory="DisclosureIndicator"
        cellStyle="RightDetail"
        detail={formatBuildingTimes(set, now)}
        onPress={this.onPress}
      />
    )
  }
}
<|endoftext|>


<commit_before>(function($) {
// Models
var Document = Backbone.Model.extend({ url: function() { return "/api/1.0/document/" + this.id; } });
var DocumentCollection = Backbone.Collection.extend({ model: Document, url: "/api/1.0/document" });

var Docket = Backbone.Model.extend({ url: function() { return "/api/1.0/docket/" + this.id; } });
var DocketCollection = Backbone.Collection.extend({ model: Docket, url: "/api/1.0/docket" });

var Agency = Backbone.Model.extend({ url: function() { return "/api/1.0/agency/" + this.id; } });
var AgencyCollection = Backbone.Collection.extend({ model: Agency, url: "/api/1.0/agency" });

var Entity = Backbone.Model.extend({ url: function() { return "/api/1.0/entity/" + this.id; } });
var EntityCollection = Backbone.Collection.extend({ model: Entity, url: "/api/1.0/entity" });

var SearchResults = Backbone.Model.extend({ idAttribute: "query", url: function() { return "/api/1.0/search/" + (this.get('level') ? this.get('level') + '/' : '') + encodeURIComponent(this.id) + (this.get('in_page') ? "?page=" + this.get('in_page') : ''); } });
var SearchResultsCollection = Backbone.Collection.extend({ model: SearchResults, url: "/api/1.0/search" });

// Template helpers
var helpers = {
    'formatDate': function(iso_date) {
        var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
        var date = new Date(iso_date);
        return (months[date.getUTCMonth()] + " " + date.getUTCDate() + ", " + date.getUTCFullYear());
    },
    'capitalize': function(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    },
    'prettifyLabel': function(string) {
        return _.map(string.split('_'), helpers.capitalize).join(' ');
    },
    'getIcon': function(file_type) {
        var icons = {
            'html':   'html',
            'xml':    'html',
            'crtext': 'html',

            'msw':    'msw',
            'msw6':   'msw',
            'msw8':   'msw',
            'msw12':  'msw',

            'pdf':    'pdf',
            'rtf':    'rtf',
            'txt':    'txt',
            'wp8':    'wp8',
            '?':      'unknown'
        }
        return '/static/img/icons/64x64/icon_' + (typeof icons[file_type] == "undefined" ? icons['?'] : icons[file_type]) + '.png';
    }
}
// Views
var SearchView = Backbone.View.extend({
    tagName: 'div',
    className: 'search-view',

    events: {
        'submit form': 'search',
        'click ul.results a': 'showTopSearch'
    },

    template: _.template($('#search-tpl').html()),
    render: function() {
        $(this.el).html(this.template(this));

        // if we're in the main area, hide the top area
        if (this.id == 'main-search-form') {
            $('#top-search').hide().find('input[type=text]').val('');
        }

        return this;
    },

    search: function(evt) {
        evt.preventDefault();
        app.navigate('/search/' + encodeURIComponent($(this.el).find('.search-query').val()), {trigger: true});
        return false;
    },

    showTopSearch: function(evt) {
        $('#top-search').show()
    }
})

var ResultsView = Backbone.View.extend({
    tagName: 'div',
    id: 'results-view',

    template: _.template($('#results-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());
                    $(this.el).html(this.template(context));

                    // update the URL for the right type
                    if (!this.model.get('level')) {
                        app.navigate('/search-' + this.model.attributes.search.aggregation_level + '/' + encodeURIComponent(this.model.attributes.search.raw_query) + (this.model.get('in_page') ? '/' + this.model.get('in_page') : ''), {trigger: false, replace: true});
                    }

                    // populate the search input
                    this.$el.closest('.search-view').find('form input.search-query').val(context.search.raw_query);
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    }
})

var AggregatedDetailView = Backbone.View.extend({
    tagName: 'div',
    id: 'docket-view',

    template: _.template($('#aggregated-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());
                    $(this.el).html(this.template(context));

                    // charts
                    SpareribCharts.type_breakdown_piechart('type-breakdown', context.stats.type_breakdown);

                    var timeGranularity = this.model.get('type') == 'docket' ? 'weeks' : 'months';
                    var timeline_data = [{
                        'name': 'Submission Timline',
                        'href': '',
                        'timeline': _.map(context.stats[timeGranularity], function(period) {
                            return period.count;
                        })
                    }];
                    SpareribCharts.timeline_chart('submission-timeline', timeline_data);
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    }
})

var DocumentDetailView = Backbone.View.extend({
    tagName: 'div',
    id: 'document-view',

    events: {
        'click .tab': 'switchTab',
        'click .attachment-name': 'toggleAttachment'
    },

    template: _.template($('#document-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());

                    // tweak attachments a bit
                    context['full_attachments'] = [{'title': 'Main Views', 'attachment': false, 'views': context['views']}].concat(_.map(context['attachments'], function(attachment) {
                        attachment['attachment'] = true;
                        return attachment;
                    }));
                    $(this.el).html(this.template(context));

                    // make the first attachment visible
                    $(this.el).find('.attachment-name').eq(0).click()
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    },

    switchTab: function(evt) {
        var $tab = $(evt.target).closest('.tab');
        var $this = $(this.el);
        var $area = $tab.closest('.tab-area');
        if (!$tab.hasClass('active')) {
            $area.find('.tab').removeClass('active');
            $tab.addClass('active');

            var view = $area.find('.tab-view').hide().filter('[data-tab-id=' + $tab.attr('data-tab-id') + ']').show();
            var iframe = view.find('iframe');
            if (!iframe.attr('src')) {
                iframe.attr('src', iframe.attr('data-src'));
            }
        }
    },

    toggleAttachment: function(evt) {
        var $name = $(evt.target).closest('.attachment-name');
        var $attachment = $name.closest('.attachment');
        var $area = $attachment.find('.tab-area');
        if (!$name.hasClass('active')) {
            // first make sure something is visible in the hidden area
            var tabs = $area.find('.tab');
            if (tabs.filter('.active').length == 0) {
                tabs.eq(0).click();
            }

            // then show the whole thing
            $name.addClass('active');
            $area.slideDown('fast');
        } else {
            $name.removeClass('active');
            $area.slideUp('fast');
        }
    }
})

var EntityDetailView = Backbone.View.extend({
    tagName: 'div',
    id: 'entity-view',

    template: _.template($('#entity-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());
                    $(this.el).html(this.template(context));

                    // charts
                    _.each(['submitter_mentions', 'text_mentions'], function(submission_type) {
                        if (context.stats[submission_type].count == 0) {
                            return;
                        }

                        var timeline_data = [{
                            'name': 'Submission Timline',
                            'href': '',
                            'timeline': _.map(context.stats[submission_type].months, function(month) {
                                return month.count;
                            })
                        }];
                        SpareribCharts.timeline_chart(({'submitter_mentions': 'submission', 'text_mentions': 'mention'})[submission_type] + '-timeline', timeline_data);
                    });
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    }
})

// Router
var AppRouter = Backbone.Router.extend({
    initialize: function() {
        // routes

        // resource pages
        this.route("document/:id", "documentDetail");
        this.route("docket/:id", "docketDetail");
        this.route("agency/:id", "agencyDetail");
        this.route(/^(organization|individual|politician|entity)\/[a-zA-Z0-9-]*\/([a-z0-9-]*)$/, "entityDetail");

        // search
        this.route("", "searchLanding");
        this.route("search/:term/:page", "defaultSearchResults");
        this.route("search/:term", "defaultSearchResults");
        this.route("search-:type/:term/:page", "searchResults");
        this.route("search-:type/:term", "searchResults");

        // load the upper search box at the beginning
        var topSearchView = new SearchView({'id': 'top-search-form'});
        $('#top-search').html(topSearchView.render().el);
    },

    searchLanding: function() {
        var searchView = new SearchView({'id': 'main-search-form'});
        $('#main').html(searchView.render().el);
    },

    defaultSearchResults: function(query, page) {
        this.searchResults(null, query, page);
    },
    searchResults: function(type, query, page) {
        console.log(query, page);
        // are we on a search page?
        var resultSet = $('#main .result-set');
        if (resultSet.length == 0) {
            this.searchLanding();
            resultSet = $('#main .result-set');
        }

        if (typeof page == "undefined") {
            page = null;
        }

        var results = new SearchResults({'query': query, 'in_page': page, 'level': type});
        var resultsView = new ResultsView({model: results});

        resultSet.html(resultsView.render().el);
    },

    documentDetail: function(id) {
        var doc = new Document({'id': id});
        var view = new DocumentDetailView({model: doc});
        $('#main').html(view.render().el);
    },

    docketDetail: function(id) {
        var docket = new Docket({'id': id});
        var view = new AggregatedDetailView({model: docket});
        $('#main').html(view.render().el);
    },

    agencyDetail: function(id) {
        var agency = new Agency({'id': id});
        var view = new AggregatedDetailView({model: agency});
        $('#main').html(view.render().el);
    },

    entityDetail: function(type, id) {
        var entity = new Entity({'id': id});
        var entityView = new EntityDetailView({model: entity});
        $('#main').html(entityView.render().el);
    }
});

var app = new AppRouter();
window.app = app;

Backbone.history.start({pushState: true});

/* assume backbone link handling, from Tim Branyen */
$(document).on("click", "a:not([data-bypass])", function(evt) {
    var href = $(this).attr("href");
    var protocol = this.protocol + "//";

    if (href && href.slice(0, protocol.length) !== protocol &&
        href.indexOf("javascript:") !== 0) {
        evt.preventDefault();
        Backbone.history.navigate(href, true);
    }
});

})(jQuery);<commit_msg>A better way of doing the search box showing and hiding.<commit_after>(function($) {
// Models
var Document = Backbone.Model.extend({ url: function() { return "/api/1.0/document/" + this.id; } });
var DocumentCollection = Backbone.Collection.extend({ model: Document, url: "/api/1.0/document" });

var Docket = Backbone.Model.extend({ url: function() { return "/api/1.0/docket/" + this.id; } });
var DocketCollection = Backbone.Collection.extend({ model: Docket, url: "/api/1.0/docket" });

var Agency = Backbone.Model.extend({ url: function() { return "/api/1.0/agency/" + this.id; } });
var AgencyCollection = Backbone.Collection.extend({ model: Agency, url: "/api/1.0/agency" });

var Entity = Backbone.Model.extend({ url: function() { return "/api/1.0/entity/" + this.id; } });
var EntityCollection = Backbone.Collection.extend({ model: Entity, url: "/api/1.0/entity" });

var SearchResults = Backbone.Model.extend({ idAttribute: "query", url: function() { return "/api/1.0/search/" + (this.get('level') ? this.get('level') + '/' : '') + encodeURIComponent(this.id) + (this.get('in_page') ? "?page=" + this.get('in_page') : ''); } });
var SearchResultsCollection = Backbone.Collection.extend({ model: SearchResults, url: "/api/1.0/search" });

// Template helpers
var helpers = {
    'formatDate': function(iso_date) {
        var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
        var date = new Date(iso_date);
        return (months[date.getUTCMonth()] + " " + date.getUTCDate() + ", " + date.getUTCFullYear());
    },
    'capitalize': function(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    },
    'prettifyLabel': function(string) {
        return _.map(string.split('_'), helpers.capitalize).join(' ');
    },
    'getIcon': function(file_type) {
        var icons = {
            'html':   'html',
            'xml':    'html',
            'crtext': 'html',

            'msw':    'msw',
            'msw6':   'msw',
            'msw8':   'msw',
            'msw12':  'msw',

            'pdf':    'pdf',
            'rtf':    'rtf',
            'txt':    'txt',
            'wp8':    'wp8',
            '?':      'unknown'
        }
        return '/static/img/icons/64x64/icon_' + (typeof icons[file_type] == "undefined" ? icons['?'] : icons[file_type]) + '.png';
    }
}
// Views
var SearchView = Backbone.View.extend({
    tagName: 'div',
    className: 'search-view',

    events: {
        'submit form': 'search'
    },

    template: _.template($('#search-tpl').html()),
    render: function() {
        $(this.el).html(this.template(this));
        return this;
    },

    search: function(evt) {
        evt.preventDefault();
        app.navigate('/search/' + encodeURIComponent($(this.el).find('.search-query').val()), {trigger: true});
        return false;
    }
})

var ResultsView = Backbone.View.extend({
    tagName: 'div',
    id: 'results-view',

    template: _.template($('#results-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());
                    $(this.el).html(this.template(context));

                    // update the URL for the right type
                    if (!this.model.get('level')) {
                        app.navigate('/search-' + this.model.attributes.search.aggregation_level + '/' + encodeURIComponent(this.model.attributes.search.raw_query) + (this.model.get('in_page') ? '/' + this.model.get('in_page') : ''), {trigger: false, replace: true});
                    }

                    // populate the search input
                    this.$el.closest('.search-view').find('form input.search-query').val(context.search.raw_query);
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    }
})

var AggregatedDetailView = Backbone.View.extend({
    tagName: 'div',
    id: 'docket-view',

    template: _.template($('#aggregated-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());
                    $(this.el).html(this.template(context));

                    // charts
                    SpareribCharts.type_breakdown_piechart('type-breakdown', context.stats.type_breakdown);

                    var timeGranularity = this.model.get('type') == 'docket' ? 'weeks' : 'months';
                    var timeline_data = [{
                        'name': 'Submission Timline',
                        'href': '',
                        'timeline': _.map(context.stats[timeGranularity], function(period) {
                            return period.count;
                        })
                    }];
                    SpareribCharts.timeline_chart('submission-timeline', timeline_data);
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    }
})

var DocumentDetailView = Backbone.View.extend({
    tagName: 'div',
    id: 'document-view',

    events: {
        'click .tab': 'switchTab',
        'click .attachment-name': 'toggleAttachment'
    },

    template: _.template($('#document-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());

                    // tweak attachments a bit
                    context['full_attachments'] = [{'title': 'Main Views', 'attachment': false, 'views': context['views']}].concat(_.map(context['attachments'], function(attachment) {
                        attachment['attachment'] = true;
                        return attachment;
                    }));
                    $(this.el).html(this.template(context));

                    // make the first attachment visible
                    $(this.el).find('.attachment-name').eq(0).click()
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    },

    switchTab: function(evt) {
        var $tab = $(evt.target).closest('.tab');
        var $this = $(this.el);
        var $area = $tab.closest('.tab-area');
        if (!$tab.hasClass('active')) {
            $area.find('.tab').removeClass('active');
            $tab.addClass('active');

            var view = $area.find('.tab-view').hide().filter('[data-tab-id=' + $tab.attr('data-tab-id') + ']').show();
            var iframe = view.find('iframe');
            if (!iframe.attr('src')) {
                iframe.attr('src', iframe.attr('data-src'));
            }
        }
    },

    toggleAttachment: function(evt) {
        var $name = $(evt.target).closest('.attachment-name');
        var $attachment = $name.closest('.attachment');
        var $area = $attachment.find('.tab-area');
        if (!$name.hasClass('active')) {
            // first make sure something is visible in the hidden area
            var tabs = $area.find('.tab');
            if (tabs.filter('.active').length == 0) {
                tabs.eq(0).click();
            }

            // then show the whole thing
            $name.addClass('active');
            $area.slideDown('fast');
        } else {
            $name.removeClass('active');
            $area.slideUp('fast');
        }
    }
})

var EntityDetailView = Backbone.View.extend({
    tagName: 'div',
    id: 'entity-view',

    template: _.template($('#entity-tpl').html()),
    render: function() {
        this.model.fetch(
            {
                'success': $.proxy(function() {
                    var context = _.extend({}, helpers, this.model.toJSON());
                    $(this.el).html(this.template(context));

                    // charts
                    _.each(['submitter_mentions', 'text_mentions'], function(submission_type) {
                        if (context.stats[submission_type].count == 0) {
                            return;
                        }

                        var timeline_data = [{
                            'name': 'Submission Timline',
                            'href': '',
                            'timeline': _.map(context.stats[submission_type].months, function(month) {
                                return month.count;
                            })
                        }];
                        SpareribCharts.timeline_chart(({'submitter_mentions': 'submission', 'text_mentions': 'mention'})[submission_type] + '-timeline', timeline_data);
                    });
                }, this),
                'error': function() {
                    console.log('failed');
                }
            }
        );
        return this;
    }
})

// Router
var AppRouter = Backbone.Router.extend({
    initialize: function() {
        // routes

        // resource pages
        this.route("document/:id", "documentDetail");
        this.route("docket/:id", "docketDetail");
        this.route("agency/:id", "agencyDetail");
        this.route(/^(organization|individual|politician|entity)\/[a-zA-Z0-9-]*\/([a-z0-9-]*)$/, "entityDetail");

        // search
        this.route("", "searchLanding");
        this.route("search/:term/:page", "defaultSearchResults");
        this.route("search/:term", "defaultSearchResults");
        this.route("search-:type/:term/:page", "searchResults");
        this.route("search-:type/:term", "searchResults");

        // load the upper search box at the beginning
        var topSearchView = new SearchView({'id': 'top-search-form'});
        $('#top-search').html(topSearchView.render().el);

        // on all navigation, check to show/hide the search box
        this.on('all', function () {
            if ($('#main .search-view').length != 0) {
                $('#top-search').hide();
            } else {
                $('#top-search').show().find('input[type=text]').val('');
            }
        });
    },

    searchLanding: function() {
        var searchView = new SearchView({'id': 'main-search-form'});
        $('#main').html(searchView.render().el);
    },

    defaultSearchResults: function(query, page) {
        this.searchResults(null, query, page);
    },
    searchResults: function(type, query, page) {
        console.log(query, page);
        // are we on a search page?
        var resultSet = $('#main .result-set');
        if (resultSet.length == 0) {
            this.searchLanding();
            resultSet = $('#main .result-set');
        }

        if (typeof page == "undefined") {
            page = null;
        }

        var results = new SearchResults({'query': query, 'in_page': page, 'level': type});
        var resultsView = new ResultsView({model: results});

        resultSet.html(resultsView.render().el);
    },

    documentDetail: function(id) {
        var doc = new Document({'id': id});
        var view = new DocumentDetailView({model: doc});
        $('#main').html(view.render().el);
    },

    docketDetail: function(id) {
        var docket = new Docket({'id': id});
        var view = new AggregatedDetailView({model: docket});
        $('#main').html(view.render().el);
    },

    agencyDetail: function(id) {
        var agency = new Agency({'id': id});
        var view = new AggregatedDetailView({model: agency});
        $('#main').html(view.render().el);
    },

    entityDetail: function(type, id) {
        var entity = new Entity({'id': id});
        var entityView = new EntityDetailView({model: entity});
        $('#main').html(entityView.render().el);
    }
});

var app = new AppRouter();
window.app = app;
console.log(app);

Backbone.history.start({pushState: true});

/* assume backbone link handling, from Tim Branyen */
$(document).on("click", "a:not([data-bypass])", function(evt) {
    var href = $(this).attr("href");
    var protocol = this.protocol + "//";

    if (href && href.slice(0, protocol.length) !== protocol &&
        href.indexOf("javascript:") !== 0) {
        evt.preventDefault();
        Backbone.history.navigate(href, true);
    }
});

})(jQuery);<|endoftext|>


<commit_before>import * as d3 from 'd3';

function isEmpty(data) {
	return data.every((item) => item.value === 0);
}

function rand(min, max) {
	return min + Math.random() * (max - min);
}

function getRandomColor() {
	const h = rand(1, 360);
	const s = rand(0, 100);
	const l = rand(0, 100);
	return `hsl(${h},${s}%,${l}%)`;
}

/**
 * When the charts are lined horizontally
 * @returns {boolean}
 */
function chartsHorizontal() {
	return window.innerWidth > 671
}

export function initVisualisation(container = '#results') {
	window.pieSvg = d3.select(container).append('svg');
	window.pieTooltip = d3.select(container).append('div').attr('class', 'toolTip');
	window.barSvg = d3.select(container).append('svg');
	window.barTooltip = d3.select(container).append('div').attr('class', 'toolTip');
}

/**
 * Visualise pollistics data
 * @param  {Array} data      Data which has object with value and label
 * @param  {String} container a d3 selector
 */
export function visualise(data, container = '#results') {
	if (isEmpty(data)) {
		const containerEl = document.querySelector(container);
		const item = document.createElement('h2');
		item.style.margin = '0 auto';
		item.innerText = 'No data available';
		containerEl.appendChild(item);
		return undefined;
	}

	function drawPie(data, svg, tooltip) {
		const containerEl = document.querySelector(container);
		let width = containerEl.getBoundingClientRect().width;
		let height = (data.length + 1) * 100;
		if (chartsHorizontal()) {
			width = containerEl.getBoundingClientRect().width / 2;
		}
		const radius = Math.min(width, height) / 2;

		const arc = d3.arc()
			.outerRadius(radius - 10)
			.innerRadius(radius - 70);

		const pie = d3.pie()
			.sort(null)
			.value(d => d.value);

		svg.attr('width', width)
			.attr('height', height);

		// todo: without removing xdd
		svg.selectAll("*").remove();

		const g = svg.selectAll('.arc')
			.data(pie(data))
			.enter().append('g')
			.attr('class', 'arc')
			.attr('transform', `translate(${width / 2},${height / 2})`);

		if (window.disco) {
			g.append('path')
				.attr('d', arc)
				.style('fill', d => getRandomColor(d.data.label));
		} else {
			g.append('path')
				.attr('d', arc);
		}

		g.on('mousemove', d => {
			tooltip.style('left', `${d3.event.pageX+10}px`);
			tooltip.style('top', `${d3.event.pageY-25}px`);
			tooltip.style('display', 'inline-block');
			tooltip.html(`${d.data.label}<br>${d.value} vote${d.data.value === 1 ? '' : 's'}`);
		});

		g.on('mouseout', () => {
			tooltip.style('display', 'none');
		});

		g.append('text')
			.attr('transform', (d) => {
				if (d.value == 0) {
					return null;
				} else {
					return `translate(${arc.centroid(d)})`;
				}
			})
			.attr('dy', '.35em')
			.text((d) => d.data.label)
			.attr('class', 'value');
	}

	function drawBar(data, svg, tooltip) {
		const axisMargin = 20;
		const margin = 40;
		const valueMargin = 4;

		const containerEl = document.querySelector(container);
		let width = containerEl.getBoundingClientRect().width;
		let height = (data.length + 1) * 100;

		if (chartsHorizontal()) {
			width = containerEl.getBoundingClientRect().width / 2;
		}

		const barHeight = (height - axisMargin - margin * 2) * 0.4 / data.length;
		const barPadding = (height - axisMargin - margin * 2) * 0.4 / data.length;
		let labelWidth = 0;

		const max = d3.max(data, d => d.value);

		svg.attr('width', width)
			.attr('height', height);

		// todo: without removing xdd
		svg.selectAll("*").remove();

		const bar = svg.selectAll('g')
			.data(data)
			.enter()
			.append('g');

		bar.attr('class', 'bar')
			.attr('cx', 0)
			.attr('transform', (d, i) => `translate(${margin},${i * (barHeight + barPadding) + barPadding})`);

		bar.append('text')
			.attr('class', 'value2')
			.attr('y', barHeight / 2)
			.attr('dy', '.35em') //vertical align middle
			.text(d => d.label).each(function() {
			if (data.value == 0) {
				return null;
			} else {
				labelWidth = Math.ceil(Math.max(labelWidth, this.getBBox().width));
			}
		});

		const scale = d3.scaleLinear()
			.domain([0, max * 1.2])
			.range([0, width - margin * 2 - labelWidth]);

		const xAxis = d3.axisBottom()
			.scale(scale)
			.tickSize(-height + 2 * margin + axisMargin);

		bar.append('rect')
			.attr('transform', `translate(${labelWidth}, 0)`)
			.attr('height', barHeight)
			.attr('width', d => scale(d.value));

		bar.append('text')
			.attr('class', 'value')
			.attr('y', barHeight / 2)
			.attr('dx', -valueMargin + labelWidth) //margin right
			.attr('dy', '.35em') //vertical align middle
			.attr('text-anchor', 'end')
			.text(d => `${d.value} vote${d.value === 1 ? '' : 's'}`)
			.attr('x', function(d) {
				const width = this.getBBox().width;
				return Math.max(width + valueMargin, scale(d.value));
			});

		bar.on('mousemove', d => {
				tooltip.style('left', `${d3.event.pageX + 10}px`);
				tooltip.style('top', `${d3.event.pageY - 25}px`);
				tooltip.style('display', 'inline-block');
				tooltip.html(`${d.label}<br>${d.value} vote${d.value === 1 ? '' : 's'}`);
			}
		});

		bar.on('mouseout', () => {
				tooltip.style('display', 'none');
			});

		svg.insert('g', ':first-child')
			.attr('class', 'axisHorizontal')
			.attr('transform', `translate(${margin + labelWidth},${height - axisMargin - margin})`)
			.call(xAxis);
	}

	const dataFiltered = data.filter(d => d.value > 0);

	drawPie(dataFiltered, window.pieSvg, window.pieTooltip);
	drawBar(dataFiltered, window.barSvg, window.barTooltip);
}
<commit_msg>fix error in visualisation.js<commit_after>import * as d3 from 'd3';

function isEmpty(data) {
	return data.every((item) => item.value === 0);
}

function rand(min, max) {
	return min + Math.random() * (max - min);
}

function getRandomColor() {
	const h = rand(1, 360);
	const s = rand(0, 100);
	const l = rand(0, 100);
	return `hsl(${h},${s}%,${l}%)`;
}

/**
 * When the charts are lined horizontally
 * @returns {boolean}
 */
function chartsHorizontal() {
	return window.innerWidth > 671
}

export function initVisualisation(container = '#results') {
	window.pieSvg = d3.select(container).append('svg');
	window.pieTooltip = d3.select(container).append('div').attr('class', 'toolTip');
	window.barSvg = d3.select(container).append('svg');
	window.barTooltip = d3.select(container).append('div').attr('class', 'toolTip');
}

/**
 * Visualise pollistics data
 * @param  {Array} data      Data which has object with value and label
 * @param  {String} container a d3 selector
 */
export function visualise(data, container = '#results') {
	if (isEmpty(data)) {
		const containerEl = document.querySelector(container);
		const item = document.createElement('h2');
		item.style.margin = '0 auto';
		item.innerText = 'No data available';
		containerEl.appendChild(item);
		return undefined;
	}

	function drawPie(data, svg, tooltip) {
		const containerEl = document.querySelector(container);
		let width = containerEl.getBoundingClientRect().width;
		let height = (data.length + 1) * 100;
		if (chartsHorizontal()) {
			width = containerEl.getBoundingClientRect().width / 2;
		}
		const radius = Math.min(width, height) / 2;

		const arc = d3.arc()
			.outerRadius(radius - 10)
			.innerRadius(radius - 70);

		const pie = d3.pie()
			.sort(null)
			.value(d => d.value);

		svg.attr('width', width)
			.attr('height', height);

		// todo: without removing xdd
		svg.selectAll("*").remove();

		const g = svg.selectAll('.arc')
			.data(pie(data))
			.enter().append('g')
			.attr('class', 'arc')
			.attr('transform', `translate(${width / 2},${height / 2})`);

		if (window.disco) {
			g.append('path')
				.attr('d', arc)
				.style('fill', d => getRandomColor(d.data.label));
		} else {
			g.append('path')
				.attr('d', arc);
		}

		g.on('mousemove', d => {
			tooltip.style('left', `${d3.event.pageX+10}px`);
			tooltip.style('top', `${d3.event.pageY-25}px`);
			tooltip.style('display', 'inline-block');
			tooltip.html(`${d.data.label}<br>${d.value} vote${d.data.value === 1 ? '' : 's'}`);
		});

		g.on('mouseout', () => {
			tooltip.style('display', 'none');
		});

		g.append('text')
			.attr('transform', (d) => {
				if (d.value == 0) {
					return null;
				} else {
					return `translate(${arc.centroid(d)})`;
				}
			})
			.attr('dy', '.35em')
			.text((d) => d.data.label)
			.attr('class', 'value');
	}

	function drawBar(data, svg, tooltip) {
		const axisMargin = 20;
		const margin = 40;
		const valueMargin = 4;

		const containerEl = document.querySelector(container);
		let width = containerEl.getBoundingClientRect().width;
		let height = (data.length + 1) * 100;

		if (chartsHorizontal()) {
			width = containerEl.getBoundingClientRect().width / 2;
		}

		const barHeight = (height - axisMargin - margin * 2) * 0.4 / data.length;
		const barPadding = (height - axisMargin - margin * 2) * 0.4 / data.length;
		let labelWidth = 0;

		const max = d3.max(data, d => d.value);

		svg.attr('width', width)
			.attr('height', height);

		// todo: without removing xdd
		svg.selectAll("*").remove();

		const bar = svg.selectAll('g')
			.data(data)
			.enter()
			.append('g');

		bar.attr('class', 'bar')
			.attr('cx', 0)
			.attr('transform', (d, i) => `translate(${margin},${i * (barHeight + barPadding) + barPadding})`);

		bar.append('text')
			.attr('class', 'value2')
			.attr('y', barHeight / 2)
			.attr('dy', '.35em') //vertical align middle
			.text(d => d.label).each(function() {
			if (data.value == 0) {
				return null;
			} else {
				labelWidth = Math.ceil(Math.max(labelWidth, this.getBBox().width));
			}
		});

		const scale = d3.scaleLinear()
			.domain([0, max * 1.2])
			.range([0, width - margin * 2 - labelWidth]);

		const xAxis = d3.axisBottom()
			.scale(scale)
			.tickSize(-height + 2 * margin + axisMargin);

		bar.append('rect')
			.attr('transform', `translate(${labelWidth}, 0)`)
			.attr('height', barHeight)
			.attr('width', d => scale(d.value));

		bar.append('text')
			.attr('class', 'value')
			.attr('y', barHeight / 2)
			.attr('dx', -valueMargin + labelWidth) //margin right
			.attr('dy', '.35em') //vertical align middle
			.attr('text-anchor', 'end')
			.text(d => `${d.value} vote${d.value === 1 ? '' : 's'}`)
			.attr('x', function(d) {
				const width = this.getBBox().width;
				return Math.max(width + valueMargin, scale(d.value));
			});

		bar.on('mousemove', d => {
			tooltip.style('left', `${d3.event.pageX + 10}px`);
			tooltip.style('top', `${d3.event.pageY - 25}px`);
			tooltip.style('display', 'inline-block');
			tooltip.html(`${d.label}<br>${d.value} vote${d.value === 1 ? '' : 's'}`);
		});

		bar.on('mouseout', () => {
			tooltip.style('display', 'none');
		});

		svg.insert('g', ':first-child')
			.attr('class', 'axisHorizontal')
			.attr('transform', `translate(${margin + labelWidth},${height - axisMargin - margin})`)
			.call(xAxis);
	}

	const dataFiltered = data.filter(d => d.value > 0);

	drawPie(dataFiltered, window.pieSvg, window.pieTooltip);
	drawBar(dataFiltered, window.barSvg, window.barTooltip);
}
<|endoftext|>


<commit_before>describe("app.views.Stream", function() {
  beforeEach(function() {
    loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}});

    this.posts = $.parseJSON(spec.readFixture("stream_json"));

    this.stream = new app.models.Stream();
    this.stream.add(this.posts);

    this.view = new app.views.Stream({model : this.stream});

    // do this manually because we've moved loadMore into render??
    this.view.render();
    _.each(this.view.collection.models, function(post) {
      this.view.addPostView(post);
    }, this);
  });

  describe("initialize", function() {
    it("binds an infinite scroll listener", function() {
      spyOn($.fn, "scroll");
      new app.views.Stream({model : this.stream});
      expect($.fn.scroll).toHaveBeenCalled();
    });
  });

  describe("#render", function() {
    beforeEach(function() {
      this.statusMessage = this.stream.items.models[0];
      this.statusElement = $(this.view.$(".stream-element")[0]);
    });

    context("when rendering a status message", function() {
      it("shows the message in the content area", function() {
        expect(this.statusElement.find(".post-content p").text()).toContain("LONG POST"); //markdown'ed
      });
    });
  });

  describe("infScroll", function() {
    // NOTE: inf scroll happens at 500px
    beforeEach(function(){
      spyOn($.fn, "height").and.returnValue(0);
      spyOn($.fn, "scrollTop").and.returnValue(100);
      spyOn(this.view.model, "fetch");

    });

    describe('fetching more', function() {
      beforeEach(function(done) {
        this.view.on('loadMore', function() {
          done();
        });
        this.view.infScroll();
      });

      it("fetches moar when the user is at the bottom of the page", function() {
        expect(this.view.model.fetch).toHaveBeenCalled();
      });
    });

    it("shows the loader while fetching new posts", function() {
      spyOn(this.view, "showLoader");
      this.view.infScroll();

      expect(this.view.showLoader).toHaveBeenCalled();
    });

    it("doesnt try to fetch more content if already fetched all", function() {
      spyOn($.fn, "unbind");

      this.stream.trigger("allItemsLoaded", this.view);

      expect($.fn.unbind).toHaveBeenCalledWith("scroll");
    });
  });

  describe("unbindInfScroll", function() {
    it("unbinds scroll", function() {
      spyOn($.fn, "unbind");
      this.view.unbindInfScroll();
      expect($.fn.unbind).toHaveBeenCalledWith("scroll");
    });
  });
});
<commit_msg>Fix order-dependent failure of stream_view_spec.js<commit_after>describe("app.views.Stream", function() {
  beforeEach(function() {
    // This puts `app.page` into the proper state.
    new app.Router().stream();

    loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}});

    this.posts = $.parseJSON(spec.readFixture("stream_json"));

    this.stream = new app.models.Stream();
    this.stream.add(this.posts);

    this.view = new app.views.Stream({model : this.stream});

    // do this manually because we've moved loadMore into render??
    this.view.render();
    _.each(this.view.collection.models, function(post) {
      this.view.addPostView(post);
    }, this);
  });

  describe("initialize", function() {
    it("binds an infinite scroll listener", function() {
      spyOn($.fn, "scroll");
      new app.views.Stream({model : this.stream});
      expect($.fn.scroll).toHaveBeenCalled();
    });
  });

  describe("#render", function() {
    beforeEach(function() {
      this.statusMessage = this.stream.items.models[0];
      this.statusElement = $(this.view.$(".stream-element")[0]);
    });

    context("when rendering a status message", function() {
      it("shows the message in the content area", function() {
        expect(this.statusElement.find(".post-content p").text()).toContain("LONG POST"); //markdown'ed
      });
    });
  });

  describe("infScroll", function() {
    // NOTE: inf scroll happens at 500px
    beforeEach(function(){
      spyOn($.fn, "height").and.returnValue(0);
      spyOn($.fn, "scrollTop").and.returnValue(100);
      spyOn(this.view.model, "fetch");

    });

    describe('fetching more', function() {
      beforeEach(function(done) {
        this.view.on('loadMore', function() {
          done();
        });
        this.view.infScroll();
      });

      it("fetches moar when the user is at the bottom of the page", function() {
        expect(this.view.model.fetch).toHaveBeenCalled();
      });
    });

    it("shows the loader while fetching new posts", function() {
      spyOn(this.view, "showLoader");
      this.view.infScroll();

      expect(this.view.showLoader).toHaveBeenCalled();
    });

    it("doesnt try to fetch more content if already fetched all", function() {
      spyOn($.fn, "unbind");

      this.stream.trigger("allItemsLoaded", this.view);

      expect($.fn.unbind).toHaveBeenCalledWith("scroll");
    });
  });

  describe("unbindInfScroll", function() {
    it("unbinds scroll", function() {
      spyOn($.fn, "unbind");
      this.view.unbindInfScroll();
      expect($.fn.unbind).toHaveBeenCalledWith("scroll");
    });
  });
});
<|endoftext|>


<commit_before>///////////////////////////////////////////////////////////////////////////////
//
//  AutobahnJS - http://autobahn.ws, http://wamp.ws
//
//  A JavaScript library for WAMP ("The Web Application Messaging Protocol").
//
//  Copyright (C) 2011-2014 Tavendo GmbH, http://tavendo.com
//
//  Licensed under the MIT License.
//  http://www.opensource.org/licenses/mit-license.php
//
///////////////////////////////////////////////////////////////////////////////

var when = require('when');

var session = require('./session.js');
var websocket = require('./websocket.js');
var util = require('./util.js');
var log = require('./log.js');

var Connection = function (options) {

   var self = this;

   self._options = options;

   // Deferred factory
   //
   if (options && options.use_es6_promises) {

      if ('Promise' in global) {
         // ES6-based deferred factory
         //
         self._defer = function () {
            var deferred = {};

            deferred.promise = new Promise(function (resolve, reject) {
               deferred.resolve = resolve;
               deferred.reject = reject;
            });

            return deferred;
         };
      } else {

         log.debug("Warning: ES6 promises requested, but not found! Falling back to whenjs.");

         // whenjs-based deferred factory
         //
         self._defer = when.defer;
      }

   } else if (options && options.use_deferred) {

      // use explicit deferred factory, e.g. jQuery.Deferred or Q.defer
      //
      self._defer = options.use_deferred;

   } else {

      // whenjs-based deferred factory
      //
      self._defer = when.defer;
   }

   // WAMP transport
   //
   self._websocket_factory = new websocket.WebSocket(self._options.url, ['wamp.2.json']);
   self._websocket = null;

   // WAMP session
   //
   self._session = null;
   self._session_close_reason = null;
   self._session_close_message = null;

   // automatic reconnection configuration
   //

   // maximum number of reconnection attempts
   self._max_retries = self._options.max_retries || 15;

   // initial retry delay in seconds
   self._initial_retry_delay = self._options.initial_retry_delay || 1.5;

   // maximum seconds between reconnection attempts
   self._max_retry_delay = self._options.max_retry_delay || 300;

   // the growth factor applied to the retry delay on each retry cycle
   self._retry_delay_growth = self._options.retry_delay_growth || 1.5;

   // the SD of a Gaussian to jitter the delay on each retry cycle
   // as a fraction of the mean
   self._retry_delay_jitter = self._options.retry_delay_jitter || 0.1;

   // reconnection tracking
   //

   // total number of successful connections
   self._connect_successes = 0;

   // controls if we should try to reconnect
   self._retry = false;

   // current number of reconnect cycles we went through
   self._retry_count = 0;

   // the current retry delay
   self._retry_delay = self._initial_retry_delay;

   // flag indicating if we are currently in a reconnect cycle
   self._is_retrying = false;
};

Connection.prototype.open = function () {

   var self = this;

   if (self._websocket || self._is_retrying) {
      throw "connection already open (or opening or retrying)";
   }

   self._retry = true;
   self._retry_count = 0;

   function retry () {

      // let the WebSocket factory produce a new WebSocket connection
      // which will automatically connect
      self._websocket = self._websocket_factory.create();
      if (!self._websocket) {
         self._retry = false;
         if (self.onclose) {
            self.onclose("unsupported", "WebSocket transport unsupported");
         }
         return;
      }

      // create a new WAMP session using the WebSocket connection as transport
      self._session = new session.Session(self._websocket, self._defer, self._options.onchallenge);
      self._session_close_reason = null;
      self._session_close_message = null;

      self._websocket.onopen = function () {

         self._is_retrying = false;
         self._retry_count = 0;
         self._retry_delay = self._initial_retry_delay;

         self._connect_successes += 1;

         self._session.join(self._options.realm, self._options.authmethods);
      };

      self._session.onjoin = function (details) {
         if (self.onopen) {
            self.onopen(self._session, details);
         }
      };

      //
      // ... WAMP session is now attached to realm.
      //

      self._session.onleave = function (reason, details) {
         self._session_close_reason = reason;
         self._session_close_message = details.message;
         self._retry = false;
         self._websocket.close(1000);
      };

      self._websocket.onclose = function (evt) {

         self._websocket = null;

         var reason = null;
         if (self._connect_successes === 0) {
            reason = "unreachable";
            self._retry = false;

         } else if (!evt.wasClean) {
            reason = "lost";

         } else {
            reason = "closed";
         }

         var stop_retrying = false;

         if (self.onclose) {
            var details = {
               reason: self._session_close_reason,
               message: self._session_close_message
            };
            stop_retrying = self.onclose(reason, details);
         }

         if (self._session) {
            self._session._id = null;
            self._session = null;
            self._session_close_reason = null;
            self._session_close_message = null;
         }

         // automatic reconnection
         //
         if (self._retry && !stop_retrying) {
            self._retry_count += 1;
            if (self._retry_count <= self._max_retries) {

               self._is_retrying = true;

               // jitter retry delay
               if (self._retry_delay_jitter) {
                  self._retry_delay = util.rand_normal(self._retry_delay, self._retry_delay * self._retry_delay_jitter);
               }

               // cap the retry delay
               if (self._retry_delay > self._max_retry_delay) {
                  self._retry_delay = self._max_retry_delay;
               }

               log.debug("retrying in " + self._retry_delay + " s");
               setTimeout(retry, self._retry_delay * 1000);

               // retry delay growth for next retry cycle
               if (self._retry_delay_growth) {
                  self._retry_delay = self._retry_delay * self._retry_delay_growth;
               }

            } else {
               log.debug("giving up trying to reconnect");
            }
         }
      }
   }

   retry();
};

Connection.prototype.close = function (reason, message) {
   var self = this;

   if (!self._websocket && !self._is_retrying) {
      throw "connection already closed";
   }

   // the app wants to close .. don't retry
   self._retry = false;

   if (self._session && self._session.isOpen) {
      // if there is an open session, close that first.
      self._session.leave(reason, message);
   } else if (self._websocket) {
      // no session active: just close the transport
      self._websocket.close(1000);
   }
};

Object.defineProperty(Connection.prototype, "defer", {
   get: function () {
      return this._defer;
   }
});

Object.defineProperty(Connection.prototype, "session", {
   get: function () {
      return this._session;
   }
});

Object.defineProperty(Connection.prototype, "isOpen", {
   get: function () {
      if (self._websocket) {
         return true;
      } else {
         return false;
      }
   }
});

Object.defineProperty(Connection.prototype, "isRetrying", {
   get: function () {
      return self._is_retrying;
   }
});

exports.Connection = Connection;
<commit_msg>fixed connection.isOpen + connection.isRetrying<commit_after>///////////////////////////////////////////////////////////////////////////////
//
//  AutobahnJS - http://autobahn.ws, http://wamp.ws
//
//  A JavaScript library for WAMP ("The Web Application Messaging Protocol").
//
//  Copyright (C) 2011-2014 Tavendo GmbH, http://tavendo.com
//
//  Licensed under the MIT License.
//  http://www.opensource.org/licenses/mit-license.php
//
///////////////////////////////////////////////////////////////////////////////

var when = require('when');

var session = require('./session.js');
var websocket = require('./websocket.js');
var util = require('./util.js');
var log = require('./log.js');

var Connection = function (options) {

   var self = this;

   self._options = options;

   // Deferred factory
   //
   if (options && options.use_es6_promises) {

      if ('Promise' in global) {
         // ES6-based deferred factory
         //
         self._defer = function () {
            var deferred = {};

            deferred.promise = new Promise(function (resolve, reject) {
               deferred.resolve = resolve;
               deferred.reject = reject;
            });

            return deferred;
         };
      } else {

         log.debug("Warning: ES6 promises requested, but not found! Falling back to whenjs.");

         // whenjs-based deferred factory
         //
         self._defer = when.defer;
      }

   } else if (options && options.use_deferred) {

      // use explicit deferred factory, e.g. jQuery.Deferred or Q.defer
      //
      self._defer = options.use_deferred;

   } else {

      // whenjs-based deferred factory
      //
      self._defer = when.defer;
   }

   // WAMP transport
   //
   self._websocket_factory = new websocket.WebSocket(self._options.url, ['wamp.2.json']);
   self._websocket = null;

   // WAMP session
   //
   self._session = null;
   self._session_close_reason = null;
   self._session_close_message = null;

   // automatic reconnection configuration
   //

   // maximum number of reconnection attempts
   self._max_retries = self._options.max_retries || 15;

   // initial retry delay in seconds
   self._initial_retry_delay = self._options.initial_retry_delay || 1.5;

   // maximum seconds between reconnection attempts
   self._max_retry_delay = self._options.max_retry_delay || 300;

   // the growth factor applied to the retry delay on each retry cycle
   self._retry_delay_growth = self._options.retry_delay_growth || 1.5;

   // the SD of a Gaussian to jitter the delay on each retry cycle
   // as a fraction of the mean
   self._retry_delay_jitter = self._options.retry_delay_jitter || 0.1;

   // reconnection tracking
   //

   // total number of successful connections
   self._connect_successes = 0;

   // controls if we should try to reconnect
   self._retry = false;

   // current number of reconnect cycles we went through
   self._retry_count = 0;

   // the current retry delay
   self._retry_delay = self._initial_retry_delay;

   // flag indicating if we are currently in a reconnect cycle
   self._is_retrying = false;
};

Connection.prototype.open = function () {

   var self = this;

   if (self._websocket || self._is_retrying) {
      throw "connection already open (or opening or retrying)";
   }

   self._retry = true;
   self._retry_count = 0;

   function retry () {

      // let the WebSocket factory produce a new WebSocket connection
      // which will automatically connect
      self._websocket = self._websocket_factory.create();
      if (!self._websocket) {
         self._retry = false;
         if (self.onclose) {
            self.onclose("unsupported", "WebSocket transport unsupported");
         }
         return;
      }

      // create a new WAMP session using the WebSocket connection as transport
      self._session = new session.Session(self._websocket, self._defer, self._options.onchallenge);
      self._session_close_reason = null;
      self._session_close_message = null;

      self._websocket.onopen = function () {

         self._is_retrying = false;
         self._retry_count = 0;
         self._retry_delay = self._initial_retry_delay;

         self._connect_successes += 1;

         self._session.join(self._options.realm, self._options.authmethods);
      };

      self._session.onjoin = function (details) {
         if (self.onopen) {
            self.onopen(self._session, details);
         }
      };

      //
      // ... WAMP session is now attached to realm.
      //

      self._session.onleave = function (reason, details) {
         self._session_close_reason = reason;
         self._session_close_message = details.message;
         self._retry = false;
         self._websocket.close(1000);
      };

      self._websocket.onclose = function (evt) {

         self._websocket = null;

         var reason = null;
         if (self._connect_successes === 0) {
            reason = "unreachable";
            self._retry = false;

         } else if (!evt.wasClean) {
            reason = "lost";

         } else {
            reason = "closed";
         }

         var stop_retrying = false;

         if (self.onclose) {
            var details = {
               reason: self._session_close_reason,
               message: self._session_close_message
            };
            stop_retrying = self.onclose(reason, details);
         }

         if (self._session) {
            self._session._id = null;
            self._session = null;
            self._session_close_reason = null;
            self._session_close_message = null;
         }

         // automatic reconnection
         //
         if (self._retry && !stop_retrying) {
            self._retry_count += 1;
            if (self._retry_count <= self._max_retries) {

               self._is_retrying = true;

               // jitter retry delay
               if (self._retry_delay_jitter) {
                  self._retry_delay = util.rand_normal(self._retry_delay, self._retry_delay * self._retry_delay_jitter);
               }

               // cap the retry delay
               if (self._retry_delay > self._max_retry_delay) {
                  self._retry_delay = self._max_retry_delay;
               }

               log.debug("retrying in " + self._retry_delay + " s");
               setTimeout(retry, self._retry_delay * 1000);

               // retry delay growth for next retry cycle
               if (self._retry_delay_growth) {
                  self._retry_delay = self._retry_delay * self._retry_delay_growth;
               }

            } else {
               log.debug("giving up trying to reconnect");
            }
         }
      }
   }

   retry();
};

Connection.prototype.close = function (reason, message) {
   var self = this;

   if (!self._websocket && !self._is_retrying) {
      throw "connection already closed";
   }

   // the app wants to close .. don't retry
   self._retry = false;

   if (self._session && self._session.isOpen) {
      // if there is an open session, close that first.
      self._session.leave(reason, message);
   } else if (self._websocket) {
      // no session active: just close the transport
      self._websocket.close(1000);
   }
};

Object.defineProperty(Connection.prototype, "defer", {
   get: function () {
      return this._defer;
   }
});

Object.defineProperty(Connection.prototype, "session", {
   get: function () {
      return this._session;
   }
});

Object.defineProperty(Connection.prototype, "isOpen", {
   get: function () {
      if (this._websocket) {
         return true;
      } else {
         return false;
      }
   }
});

Object.defineProperty(Connection.prototype, "isRetrying", {
   get: function () {
      return this._is_retrying;
   }
});

exports.Connection = Connection;
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { uniqueId } from 'lodash';
import SelectList from './select-list.component';
import Textbox from '../textbox';
import Pill from '../../../components/pill';
import Events from '../../../utils/helpers/events';
import tagComponent from '../../../utils/helpers/tags';
import StyledSelectPillContainer from './select.style';

/**
 * Basic example:
 *
 *   <Select>
 *     <Option text='Approve' />
 *     <Option text='Configure' />
 *     <Option text='Deny' />
 *   </Select>
 *
 * Custom JSX:
 *
 *   <Select>
 *     <Option text='Approve'><Icon type='tick' /></Option>
 *     <Option text='Configure'><Icon type='settings' /></Option>
 *     <Option text='Deny'><Icon type='cross' /></Option>
 *   </Select>
 */

class Select extends React.Component {
  constructor(props) {
    super(props);
    this.listboxId = uniqueId('listbox-');
  }

  state = {
    filter: undefined,
    open: false
  }

  blurBlocked = false // stops the blur callback from triggering (closing the list) when we don't want it to

  input = {} // this will store a reference to the input html element

  assignInput = (input) => { this.input = input; }

  blockBlur() {
    this.blurBlocked = true;
  }

  unblockBlur() {
    this.blurBlocked = false;
  }

  handleBlur = (ev) => {
    if (this.blurBlocked) return;
    this.setState({ filter: undefined, open: false });
    if (this.props.onBlur) this.props.onBlur(ev);
  }

  // opens the dropdown and ensures the input has focus
  // (this fixes a bug in which rapidly clicking the label or dropdown icon would break the list open state)
  handleFocus = (ev) => {
    this.openList();
    if (this.props.onFocus) this.props.onFocus(ev);
  }

  handleMouseEnter = () => this.blockBlur()

  handleMouseLeave = () => this.unblockBlur()

  handleClick = () => this.openList()

  handleChange = (newValue) => {
    let updatedValue = newValue;
    // if the component is multi value then we need to push the new value into the array of values
    if (this.isMultiValue(this.props.value)) {
      // do not allow the same value twice
      if (this.props.value.find(item => item.value === newValue.value)) return;
      const value = this.props.value.slice();
      value.push(newValue);
      updatedValue = value;
    }
    this.triggerChange(updatedValue);
  }

  handleFilter = (ev) => {
    const { value: filter } = ev.target;
    this.setState({ filter });

    if (this.props.onFilter) this.props.onFilter(filter);
  }

  handleKeyDown = (ev) => {
    // order of event checking is important here!

    // if tab key then allow normal behaviour
    if (Events.isTabKey(ev)) {
      this.unblockBlur();
      return;
    }
    // if the dropdown is not open then block regular activity and open it
    if (!this.state.open) {
      ev.preventDefault();
      this.openList();
      return;
    }
    // if esc key then close the dropdown
    if (Events.isEscKey(ev)) {
      this.setState({ open: false });
      return;
    }
    // if backspace key and multi value and no filter, remove the last item in the array
    if (Events.isBackspaceKey(ev)) {
      if (!this.isMultiValue(this.props.value) || this.state.filter || !this.props.value.length) return;
      this.removeItem(this.props.value.length - 1);
    }

    if (!this.props.filterable) ev.preventDefault();
  }

  openWhenTypeAhead(typeAhead, value) {
    if (!this.props.filterable) return false;

    return typeAhead && value && value.length >= 3;
  }

  openList() {
    if (this.state.open) return;

    this.setState({ open: true });
    if (this.props.onOpen) this.props.onOpen();
  }

  triggerChange(value) {
    const newState = {};
    if (!this.isMultiValue(value)) {
      // only closes the dropdown if not multi-value
      newState.open = false;
      this.unblockBlur();
    }

    newState.filter = undefined;
    this.setState(newState);

    if (this.props.onChange) this.props.onChange({ target: { value } });
  }

  removeItem(index) {
    const newValue = this.props.value.slice(); // copies the array first to not mutate original value
    newValue.splice(index, 1);
    this.triggerChange(newValue);
  }

  // returns the human readable value for the user
  formattedValue(filterValue, value) {
    let visibleValue = '';
    // if not multi-value then fetch the text key on the value
    if (!this.isMultiValue(value) && value) visibleValue = value.text;
    // if there is a filter then return that over the selected visible value
    return (typeof filterValue === 'string') ? filterValue : visibleValue;
  }

  // returns the correct value to feed into the textbox component
  value(value) {
    if (!this.props.filterable) return undefined;
    if (this.isMultiValue(value)) {
      return value; // if multi value the returns the full array
    }
    if (value) return value.value; // if single value then returns the id/value

    return undefined; // otherwise return undefined
  }

  renderMultiValues(values) {
    const canDelete = !this.props.disabled && !this.props.readOnly;

    return (
      values.map((value, index) => (
        <StyledSelectPillContainer key={ value.value }>
          <Pill
            onDelete={ canDelete ? () => this.removeItem(index) : undefined }
            title={ value.text }
          >
            { value.text }
          </Pill>
        </StyledSelectPillContainer>
      ))
    );
  }

  isMultiValue(value) { return Array.isArray(value); }

  placeholder(placeholder, value) {
    let displayedPlaceHolder = this.props.typeAhead ? 'Type to Search...' : 'Please Select...';

    if (placeholder) displayedPlaceHolder = placeholder;
    if (this.isMultiValue(value)) {
      // if multi-value then only show placeholder if nothing is currently selected
      return value.length ? null : displayedPlaceHolder;
    }
    return displayedPlaceHolder;
  }

  // data attributes used for automation
  dataAttributes() {
    return tagComponent(this.props['data-component'], this.props);
  }

  inputIcon(typeAhead, value) {
    if (this.openWhenTypeAhead(typeAhead, this.state.filter)) return 'cross';
    if (typeAhead) return 'search';
    return this.isMultiValue(value) ? undefined : 'dropdown';
  }

  eventProps() {
    let events = {};

    if (!this.props.disabled && !this.props.readOnly) {
      events = {
        onBlur: this.handleBlur,
        onFocus: this.handleFocus,
        onKeyDown: this.handleKeyDown,
        onClick: this.handleClick,
        onChange: this.handleFilter
      };
    }
    return events;
  }

  textboxProps() {
    const {
      typeAhead,
      value,
      placeholder
    } = this.props;

    const placeholderText = this.placeholder(placeholder, value);

    const props = {
      'data-component': 'carbon-select',
      inputIcon: this.inputIcon(typeAhead, value),
      inputRef: this.assignInput,
      placeholder: placeholderText,
      'aria-label': placeholderText,
      leftChildren: this.isMultiValue(value) && this.renderMultiValues(value),
      value: this.value(value),
      formattedValue: this.formattedValue(this.state.filter, value)
    };

    return props;
  }

  listDisplayable(isTypeAhead) {
    const { filter, open } = this.state;
    return (this.openWhenTypeAhead((isTypeAhead), filter) || !isTypeAhead) ? open : false;
  }

  render() {
    const {
      children,
      customFilter,
      placeholder,
      value,
      onLazyLoad,
      onFilter,
      onOpen,
      typeAhead,
      filterable,
      ...props
    } = this.props;

    const { filter, open } = this.state;

    const allowTypeAhead = filterable ? typeAhead : false;
    return (
      <div
        role='combobox'
        aria-haspopup='listbox'
        aria-expanded={ open }
        aria-controls={ open ? this.listboxId : '' }
      >
        <Textbox
          { ...props } // this needs to send all of the original props
          { ...this.dataAttributes() }
          { ...this.textboxProps() }
          { ...this.eventProps() }
        >
          { this.listDisplayable(allowTypeAhead, filter, open) && (
            <SelectList
              alwaysHighlight={ !!filter } // always ensure something is highlighted only if there's a filter
              customFilter={ customFilter }
              filterValue={ filter }
              onLazyLoad={ onLazyLoad }
              onMouseEnter={ this.handleMouseEnter }
              onMouseLeave={ this.handleMouseLeave }
              onSelect={ this.handleChange }
              open={ open }
              target={ this.input.current && this.input.current.parentElement }
              role='listbox'
              id={ this.listboxId }
            >
              { children }
            </SelectList>
          ) }
        </Textbox>
      </div>
    );
  }
}

const optionShape = PropTypes.shape({
  value: PropTypes.string.isRequired,
  text: PropTypes.string.isRequired
});

Select.propTypes = {
  children: PropTypes.node,
  className: PropTypes.string,
  customFilter: PropTypes.func,
  disabled: PropTypes.bool,
  label: PropTypes.string,
  onBlur: PropTypes.func,
  onChange: PropTypes.func,
  onFocus: PropTypes.func,
  onOpen: PropTypes.func,
  onLazyLoad: PropTypes.func,
  onFilter: PropTypes.func,
  placeholder: PropTypes.string,
  readOnly: PropTypes.bool,
  value: PropTypes.oneOfType([
    optionShape,
    PropTypes.arrayOf(optionShape)
  ]),
  'data-component': PropTypes.string,
  typeAhead: PropTypes.bool,
  filterable: PropTypes.bool
};

Select.defaultProps = {
  filterable: true,
  typeAhead: false
};

export default Select;
<commit_msg>(FE-1420) select.component.js: remove className propType<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { uniqueId } from 'lodash';
import SelectList from './select-list.component';
import Textbox from '../textbox';
import Pill from '../../../components/pill';
import Events from '../../../utils/helpers/events';
import tagComponent from '../../../utils/helpers/tags';
import StyledSelectPillContainer from './select.style';

/**
 * Basic example:
 *
 *   <Select>
 *     <Option text='Approve' />
 *     <Option text='Configure' />
 *     <Option text='Deny' />
 *   </Select>
 *
 * Custom JSX:
 *
 *   <Select>
 *     <Option text='Approve'><Icon type='tick' /></Option>
 *     <Option text='Configure'><Icon type='settings' /></Option>
 *     <Option text='Deny'><Icon type='cross' /></Option>
 *   </Select>
 */

class Select extends React.Component {
  constructor(props) {
    super(props);
    this.listboxId = uniqueId('listbox-');
  }

  state = {
    filter: undefined,
    open: false
  }

  blurBlocked = false // stops the blur callback from triggering (closing the list) when we don't want it to

  input = {} // this will store a reference to the input html element

  assignInput = (input) => { this.input = input; }

  blockBlur() {
    this.blurBlocked = true;
  }

  unblockBlur() {
    this.blurBlocked = false;
  }

  handleBlur = (ev) => {
    if (this.blurBlocked) return;
    this.setState({ filter: undefined, open: false });
    if (this.props.onBlur) this.props.onBlur(ev);
  }

  // opens the dropdown and ensures the input has focus
  // (this fixes a bug in which rapidly clicking the label or dropdown icon would break the list open state)
  handleFocus = (ev) => {
    this.openList();
    if (this.props.onFocus) this.props.onFocus(ev);
  }

  handleMouseEnter = () => this.blockBlur()

  handleMouseLeave = () => this.unblockBlur()

  handleClick = () => this.openList()

  handleChange = (newValue) => {
    let updatedValue = newValue;
    // if the component is multi value then we need to push the new value into the array of values
    if (this.isMultiValue(this.props.value)) {
      // do not allow the same value twice
      if (this.props.value.find(item => item.value === newValue.value)) return;
      const value = this.props.value.slice();
      value.push(newValue);
      updatedValue = value;
    }
    this.triggerChange(updatedValue);
  }

  handleFilter = (ev) => {
    const { value: filter } = ev.target;
    this.setState({ filter });

    if (this.props.onFilter) this.props.onFilter(filter);
  }

  handleKeyDown = (ev) => {
    // order of event checking is important here!

    // if tab key then allow normal behaviour
    if (Events.isTabKey(ev)) {
      this.unblockBlur();
      return;
    }
    // if the dropdown is not open then block regular activity and open it
    if (!this.state.open) {
      ev.preventDefault();
      this.openList();
      return;
    }
    // if esc key then close the dropdown
    if (Events.isEscKey(ev)) {
      this.setState({ open: false });
      return;
    }
    // if backspace key and multi value and no filter, remove the last item in the array
    if (Events.isBackspaceKey(ev)) {
      if (!this.isMultiValue(this.props.value) || this.state.filter || !this.props.value.length) return;
      this.removeItem(this.props.value.length - 1);
    }

    if (!this.props.filterable) ev.preventDefault();
  }

  openWhenTypeAhead(typeAhead, value) {
    if (!this.props.filterable) return false;

    return typeAhead && value && value.length >= 3;
  }

  openList() {
    if (this.state.open) return;

    this.setState({ open: true });
    if (this.props.onOpen) this.props.onOpen();
  }

  triggerChange(value) {
    const newState = {};
    if (!this.isMultiValue(value)) {
      // only closes the dropdown if not multi-value
      newState.open = false;
      this.unblockBlur();
    }

    newState.filter = undefined;
    this.setState(newState);

    if (this.props.onChange) this.props.onChange({ target: { value } });
  }

  removeItem(index) {
    const newValue = this.props.value.slice(); // copies the array first to not mutate original value
    newValue.splice(index, 1);
    this.triggerChange(newValue);
  }

  // returns the human readable value for the user
  formattedValue(filterValue, value) {
    let visibleValue = '';
    // if not multi-value then fetch the text key on the value
    if (!this.isMultiValue(value) && value) visibleValue = value.text;
    // if there is a filter then return that over the selected visible value
    return (typeof filterValue === 'string') ? filterValue : visibleValue;
  }

  // returns the correct value to feed into the textbox component
  value(value) {
    if (!this.props.filterable) return undefined;
    if (this.isMultiValue(value)) {
      return value; // if multi value the returns the full array
    }
    if (value) return value.value; // if single value then returns the id/value

    return undefined; // otherwise return undefined
  }

  renderMultiValues(values) {
    const canDelete = !this.props.disabled && !this.props.readOnly;

    return (
      values.map((value, index) => (
        <StyledSelectPillContainer key={ value.value }>
          <Pill
            onDelete={ canDelete ? () => this.removeItem(index) : undefined }
            title={ value.text }
          >
            { value.text }
          </Pill>
        </StyledSelectPillContainer>
      ))
    );
  }

  isMultiValue(value) { return Array.isArray(value); }

  placeholder(placeholder, value) {
    let displayedPlaceHolder = this.props.typeAhead ? 'Type to Search...' : 'Please Select...';

    if (placeholder) displayedPlaceHolder = placeholder;
    if (this.isMultiValue(value)) {
      // if multi-value then only show placeholder if nothing is currently selected
      return value.length ? null : displayedPlaceHolder;
    }
    return displayedPlaceHolder;
  }

  // data attributes used for automation
  dataAttributes() {
    return tagComponent(this.props['data-component'], this.props);
  }

  inputIcon(typeAhead, value) {
    if (this.openWhenTypeAhead(typeAhead, this.state.filter)) return 'cross';
    if (typeAhead) return 'search';
    return this.isMultiValue(value) ? undefined : 'dropdown';
  }

  eventProps() {
    let events = {};

    if (!this.props.disabled && !this.props.readOnly) {
      events = {
        onBlur: this.handleBlur,
        onFocus: this.handleFocus,
        onKeyDown: this.handleKeyDown,
        onClick: this.handleClick,
        onChange: this.handleFilter
      };
    }
    return events;
  }

  textboxProps() {
    const {
      typeAhead,
      value,
      placeholder
    } = this.props;

    const placeholderText = this.placeholder(placeholder, value);

    const props = {
      'data-component': 'carbon-select',
      inputIcon: this.inputIcon(typeAhead, value),
      inputRef: this.assignInput,
      placeholder: placeholderText,
      'aria-label': placeholderText,
      leftChildren: this.isMultiValue(value) && this.renderMultiValues(value),
      value: this.value(value),
      formattedValue: this.formattedValue(this.state.filter, value)
    };

    return props;
  }

  listDisplayable(isTypeAhead) {
    const { filter, open } = this.state;
    return (this.openWhenTypeAhead((isTypeAhead), filter) || !isTypeAhead) ? open : false;
  }

  render() {
    const {
      children,
      customFilter,
      placeholder,
      value,
      onLazyLoad,
      onFilter,
      onOpen,
      typeAhead,
      filterable,
      ...props
    } = this.props;

    const { filter, open } = this.state;

    const allowTypeAhead = filterable ? typeAhead : false;
    return (
      <div
        role='combobox'
        aria-haspopup='listbox'
        aria-expanded={ open }
        aria-controls={ open ? this.listboxId : '' }
      >
        <Textbox
          { ...props } // this needs to send all of the original props
          { ...this.dataAttributes() }
          { ...this.textboxProps() }
          { ...this.eventProps() }
        >
          { this.listDisplayable(allowTypeAhead, filter, open) && (
            <SelectList
              alwaysHighlight={ !!filter } // always ensure something is highlighted only if there's a filter
              customFilter={ customFilter }
              filterValue={ filter }
              onLazyLoad={ onLazyLoad }
              onMouseEnter={ this.handleMouseEnter }
              onMouseLeave={ this.handleMouseLeave }
              onSelect={ this.handleChange }
              open={ open }
              target={ this.input.current && this.input.current.parentElement }
              role='listbox'
              id={ this.listboxId }
            >
              { children }
            </SelectList>
          ) }
        </Textbox>
      </div>
    );
  }
}

const optionShape = PropTypes.shape({
  value: PropTypes.string.isRequired,
  text: PropTypes.string.isRequired
});

Select.propTypes = {
  children: PropTypes.node,
  customFilter: PropTypes.func,
  disabled: PropTypes.bool,
  label: PropTypes.string,
  onBlur: PropTypes.func,
  onChange: PropTypes.func,
  onFocus: PropTypes.func,
  onOpen: PropTypes.func,
  onLazyLoad: PropTypes.func,
  onFilter: PropTypes.func,
  placeholder: PropTypes.string,
  readOnly: PropTypes.bool,
  value: PropTypes.oneOfType([
    optionShape,
    PropTypes.arrayOf(optionShape)
  ]),
  'data-component': PropTypes.string,
  typeAhead: PropTypes.bool,
  filterable: PropTypes.bool
};

Select.defaultProps = {
  filterable: true,
  typeAhead: false
};

export default Select;
<|endoftext|>