<commit_before>import Ember from 'ember';
import layout from '../templates/components/power-select';
import { defaultMatcher, indexOfOption, optionAtIndex, filterOptions, countOptions } from '../utils/group-utils';
import fallbackIfUndefined from '../utils/computed-fallback-if-undefined';

const { computed, run, get, isBlank } = Ember;
const EventSender = Ember.Object.extend(Ember.Evented);

export default Ember.Component.extend({
  // HTML
  layout,
  tagName: fallbackIfUndefined(''),

  // Config
  disabled: fallbackIfUndefined(false),
  placeholder: fallbackIfUndefined(null),
  loadingMessage: fallbackIfUndefined('Loading options...'),
  noMatchesMessage: fallbackIfUndefined('No results found'),
  verticalPosition: fallbackIfUndefined('auto'),
  matcher: fallbackIfUndefined(defaultMatcher),
  searchField: fallbackIfUndefined(null),
  search: fallbackIfUndefined(null),
  closeOnSelect: fallbackIfUndefined(true),
  dropdownClass: fallbackIfUndefined(null),
  triggerClass: fallbackIfUndefined(null),
  dir: fallbackIfUndefined(null),
  initiallyOpened: fallbackIfUndefined(false),
  searchEnabled: fallbackIfUndefined(true),
  searchMessage: fallbackIfUndefined("Type to search"),
  searchPlaceholder: fallbackIfUndefined(null),
  allowClear: fallbackIfUndefined(false),
  triggerComponent: fallbackIfUndefined('power-select/trigger'),
  selectedItemComponent: fallbackIfUndefined(null),
  optionsComponent: fallbackIfUndefined('power-select/options'),
  beforeOptionsComponent: fallbackIfUndefined('power-select/before-options'),
  afterOptionsComponent: fallbackIfUndefined(null),
  matchTriggerWidth: fallbackIfUndefined(true),

  // Attrs
  searchText: '',
  lastSearchedText: '',
  expirableSearchText: '',
  activeSearch: null,
  openingEvent: null,
  loading: false,
  previousResults: null,

  // Lifecycle hooks
  didInitAttrs() {
    this._super(...arguments);
    Ember.assert('{{power-select}} requires an `onchange` function', this.get('onchange') && typeof this.get('onchange') === 'function');
  },

  willDestroy() {
    this._super(...arguments);
    this.activeSearch = null;
    run.cancel(this.expirableSearchDebounceId);
  },

  // CPs
  triggerId: computed(function() {
    return `ember-power-select-trigger-${this.elementId}`;
  }),

  optionsId: computed(function() {
    return `ember-power-select-options-${this.elementId}`;
  }),

  concatenatedClasses: computed('class', function() {
    const classes = ['ember-power-select'];
    if (this.get('class')) { classes.push(this.get('class')); }
    return classes.join(' ');
  }),

  concatenatedTriggerClasses: computed('triggerClass', function() {
    let classes = ['ember-power-select-trigger'];
    if (this.get('triggerClass')) {
      classes.push(this.get('triggerClass'));
    }
    return classes.join(' ');
  }),

  concatenatedDropdownClasses: computed('dropdownClass', function() {
    let classes = ['ember-power-select-dropdown', `ember-power-select-dropdown-${this.elementId}`];
    if (this.get('dropdownClass')) {
      classes.push(this.get('dropdownClass'));
    }
    return classes.join(' ');
  }),

  mustShowSearchMessage: computed('searchText', 'search', 'searchMessage', 'results.length', function(){
    return this.get('searchText.length') === 0 && !!this.get('search') && !!this.get('searchMessage') && this.get('results.length') === 0;
  }),

  mustShowNoMessages: computed('results.length', 'loading', 'search', 'lastSearchedText', function() {
    return !this.get('loading') && this.get('results.length') === 0 && (!this.get('search') || this.get('lastSearchedText.length') > 0);
  }),

  results: computed('options.[]', {
    get() {
      let options = this.get('options') || [];
      let searchAction = this.get('search');
      if (options.then) {
        this.set('loading', true);
        options.then(results => {
          if (this.get('isDestroyed')) { return; }
          this.set('results', results);
        });
        return this.previousResults || [];
      }
      let newResults = searchAction ? options : this.filter(options, this.get('searchText'));
      this.setProperties({ loading: false, currentlyHighlighted: undefined });
      this.previousResults = newResults;
      return newResults;
    },
    set(_, newResults) {
      this.previousResults = newResults;
      this.setProperties({ loading: false, currentlyHighlighted: undefined });
      return newResults;
    }
  }),

  optionMatcher: computed('searchField', 'matcher', function() {
    let { matcher, searchField } = this.getProperties('matcher', 'searchField');
    if (searchField) {
      return (option, text) => matcher(get(option, searchField), text);
    } else {
      return (option, text) => matcher(option, text);
    }
  }),

  highlighted: computed('results.[]', 'currentlyHighlighted', 'selected', function() {
    return this.get('currentlyHighlighted') || this.defaultHighlighted();
  }),

  resultsLength: computed('results.[]', function() {
    return countOptions(this.get('results'));
  }),

  eventSender: computed(function() {
    return EventSender.create();
  }),

  publicAPI: computed('registeredDropdown.isOpen', 'highlighted', 'searchText', function() {
    let dropdown = this.get('registeredDropdown');
    if (dropdown) {
      let ownActions = {
        search: (term, e) => this.send('search', dropdown, term, e),
        highlight: (option) => this.send('highlight', dropdown, option),
        select: (selected, e) => this.send('select', dropdown, selected, e),
        choose: (selected, e) => this.send('choose', dropdown, selected, e),
        handleKeydown: (e) => this.send('handleKeydown', dropdown, e)
      };
      return {
        isOpen: dropdown.isOpen,
        highlighted: this.get('highlighted'),
        searchText: this.get('searchText'),
        actions: Ember.merge(ownActions, dropdown.actions)
      };
    }
    return {};
  }),

  // Actions
  actions: {
    highlight(dropdown, option) {
      this._doHighlight(dropdown, option);
    },

    search(dropdown, term /*, e */) {
      this._doSearch(dropdown, term);
    },

    handleInput(e) {
      let term = e.target.value;
      let action = this.get('oninput');
      if (action) {
        let returnValue = action(e.target.value, this.get('publicAPI'), e);
        if (returnValue === false) { return; }
      }
      this.send('search', this.get('registeredDropdown'), term, e);
    },

    select(dropdown, selected, e) {
      this._doSelect(dropdown, selected, e);
    },

    choose(dropdown, selection, e) {
      if (e && e.clientY) {
        let openingEvent = this.get('openingEvent');
        if (openingEvent && openingEvent.clientY) {
          if (Math.abs(openingEvent.clientY - e.clientY) < 2) { return; }
        }
      }
      this.send('select', dropdown, this.get('buildSelection')(selection), e);
      if (this.get('closeOnSelect')) {
        dropdown.actions.close(e);
      }
    },

    handleKeydown(dropdown, e) {
      const onkeydown = this.get('onkeydown');
      if (onkeydown) { onkeydown(this.get('publicAPI'), e); }
      if (e.defaultPrevented) { return; }
      if (e.keyCode === 38 || e.keyCode === 40) { // Up & Down
        this._handleKeyUpDown(dropdown, e);
      } else if (e.keyCode === 13) {  // ENTER
        this._handleKeyEnter(dropdown, e);
      } else if (e.keyCode === 9) {   //Tab
        this._handleKeyTab(dropdown, e);
      } else if (e.keyCode === 27) {  // ESC
        this._handleKeyESC(dropdown, e);
      } else if (e.keyCode >= 48 && e.keyCode <= 90 || e.keyCode === 32) { // Keys 0-9, a-z or SPACE
        this._handleTriggerTyping(dropdown, e);
      }
    },

    handleFocus(dropdown, event) {
      const action = this.get('onfocus');
      if (action) {
        action(this.get('publicAPI'), event);
      }
      this.get('eventSender').trigger('focus');
    },

    // It is not evident what is going on here, so I'll explain why.
    //
    // As of this writting, Ember doesn allow to yield data to the "inverse" block.
    // Because of that, elements of this component rendered in the trigger can't receive the
    // yielded object contaning the public API of the ember-basic-dropdown, with actions for open,
    // close and toggle.
    //
    // The only possible workaround for this is to on initialization inject a similar object
    // to the one yielded and store it to make it available in the entire component.
    //
    // This this limitation on ember should be fixed soon, this is temporary. Because of that this
    // object will be passed to the action from the inverse block like if it was yielded.
    //
    registerDropdown(dropdown) {
      this.set('registeredDropdown', dropdown);
    },

    handleOpen(dropdown, e) {
      const action = this.get('onopen');
      if (action) { action(this.get('publicAPI'), e); }
      if (e) { this.set('openingEvent', e); }
      run.scheduleOnce('afterRender', this, this.scrollIfHighlightedIsOutOfViewport);
    },

    handleClose(dropdown, e) {
      const action = this.get('onclose');
      if (action) { action(this.get('publicAPI'), e); }
      if (e) { this.set('openingEvent', null); }
      this.send('highlight', dropdown, null, e);
    }
  },

  _handleKeyUpDown(dropdown, e) {
    if (dropdown.isOpen) {
      e.preventDefault();
      const newHighlighted = this.advanceSelectableOption(this.get('highlighted'), e.keyCode === 40 ? 1 : -1);
      this.send('highlight', dropdown, newHighlighted, e);
    } else {
      dropdown.actions.open(e);
    }
  },

  _handleKeyEnter(dropdown, e) {
    if (dropdown.isOpen) {
      this.send('choose', dropdown, this.get('highlighted'), e);
    }
  },

  _handleKeyTab(dropdown, e) {
    dropdown.actions.close(e);
  },

  _handleKeyESC(dropdown, e) {
    dropdown.actions.close(e);
  },

  // Methods
  scrollIfHighlightedIsOutOfViewport() {
    if (!self.document) { return; }
    const optionsList = document.querySelector('.ember-power-select-options');
    if (!optionsList) { return; }
    const highlightedOption = optionsList.querySelector('.ember-power-select-option[aria-current="true"]');
    if (!highlightedOption) { return; }
    const optionTopScroll = highlightedOption.offsetTop - optionsList.offsetTop;
    const optionBottomScroll = optionTopScroll + highlightedOption.offsetHeight;
    if (optionBottomScroll > optionsList.offsetHeight + optionsList.scrollTop) {
      optionsList.scrollTop = optionBottomScroll - optionsList.offsetHeight;
    } else if (optionTopScroll < optionsList.scrollTop) {
      optionsList.scrollTop = optionTopScroll;
    }
  },

  indexOfOption(option) {
    return indexOfOption(this.get('results'), option);
  },

  optionAtIndex(index) {
    return optionAtIndex(this.get('results'), index);
  },

  advanceSelectableOption(activeHighlighted, step) {
    let resultsLength = this.get('resultsLength');
    let startIndex = Math.min(Math.max(this.indexOfOption(activeHighlighted) + step, 0), resultsLength - 1);
    let nextOption = this.optionAtIndex(startIndex);
    while (nextOption && get(nextOption, 'disabled')) {
      nextOption = this.optionAtIndex(startIndex += step);
    }
    return nextOption;
  },

  filter(options, term) {
    return filterOptions(options || [], term, this.get('optionMatcher'));
  },

  defaultHighlighted() {
    const selected = this.get('selected');
    if (!selected || this.indexOfOption(selected) === -1) {
      return this.optionAtIndex(0);
    }
    return selected;
  },

  buildSelection(option) {
    return option;
  },

  _doSelect(dropdown, selected, e) {
    if (e) {
      e.preventDefault();
      e.stopPropagation();
    }
    if (this.get('selected') !== selected) {
      this.get('onchange')(selected, this.get('publicAPI'));
    }
  },

  _doHighlight(dropdown, option) {
    if (option && get(option, 'disabled')) { return; }
    run.scheduleOnce('afterRender', this, this.scrollIfHighlightedIsOutOfViewport);
    this.set('currentlyHighlighted', option);
  },

  _doSearch(dropdown, term) {
    if (isBlank(term)) {
      this._resetSearch();
    } else {
      let searchAction = this.get('search');
      if (searchAction) {
        this._performSearch(searchAction, term);
      } else {
        let options = this.get('options');
        if (options.then) {
          options.then((data) => {
            this.setProperties({ results: this.filter(data, term), searchText: term, lastSearchedText: term });
          });
        } else {
          this.setProperties({ results: this.filter(options, term), searchText: term, lastSearchedText: term });
        }
      }
    }
  },

  _resetSearch() {
    let options = this.get('options') || [];
    this.activeSearch = null;
    if (options.then) {
      options.then((data) => {
        this.setProperties({ results: data, searchText: '', lastSearchedText: '', loading: false });
      });
    } else {
      this.setProperties({ results: options, searchText: '', lastSearchedText: '', loading: false });
    }
  },

  _performSearch(searchAction, term) {
    this.set('searchText', term);
    let search = searchAction(term, this.get('publicAPI'));
    if (!search) {
      this.setProperties({ lastSearchedText: term });
    } else if (search.then) {
      this.activeSearch = search;
      this.setProperties({ loading: true });
      search.then((results) => {
        if (this.activeSearch === search) { this.setProperties({ results, lastSearchedText: term }); }
      }, () => {
        if (this.activeSearch === search) { this.set('lastSearchedText', term); }
      });
    } else {
      this.setProperties({ results: search, lastSearchedText: term });
    }
  },

  _handleTriggerTyping(dropdown, e) {
    let term = this.get('expirableSearchText') + String.fromCharCode(e.keyCode);
    this.set('expirableSearchText', term);
    this.expirableSearchDebounceId = run.debounce(this, 'set', 'expirableSearchText', '', 1000);
    let firstMatch = this.filter(this.get('results'), term)[0]; // TODO: match only words starting with this substr?
    if (firstMatch !== undefined) {
      if (dropdown.isOpen) {
        this._doHighlight(dropdown, firstMatch, e);
      } else {
        this._doSelect(dropdown, firstMatch, e);
      }
    }
  }
});
<commit_msg>fix deprecation warning for Ember 2.5.0<commit_after>import Ember from 'ember';
import layout from '../templates/components/power-select';
import { defaultMatcher, indexOfOption, optionAtIndex, filterOptions, countOptions } from '../utils/group-utils';
import fallbackIfUndefined from '../utils/computed-fallback-if-undefined';

const { computed, run, get, isBlank } = Ember;
const EventSender = Ember.Object.extend(Ember.Evented);

export default Ember.Component.extend({
  // HTML
  layout,
  tagName: fallbackIfUndefined(''),

  // Config
  disabled: fallbackIfUndefined(false),
  placeholder: fallbackIfUndefined(null),
  loadingMessage: fallbackIfUndefined('Loading options...'),
  noMatchesMessage: fallbackIfUndefined('No results found'),
  verticalPosition: fallbackIfUndefined('auto'),
  matcher: fallbackIfUndefined(defaultMatcher),
  searchField: fallbackIfUndefined(null),
  search: fallbackIfUndefined(null),
  closeOnSelect: fallbackIfUndefined(true),
  dropdownClass: fallbackIfUndefined(null),
  triggerClass: fallbackIfUndefined(null),
  dir: fallbackIfUndefined(null),
  initiallyOpened: fallbackIfUndefined(false),
  searchEnabled: fallbackIfUndefined(true),
  searchMessage: fallbackIfUndefined("Type to search"),
  searchPlaceholder: fallbackIfUndefined(null),
  allowClear: fallbackIfUndefined(false),
  triggerComponent: fallbackIfUndefined('power-select/trigger'),
  selectedItemComponent: fallbackIfUndefined(null),
  optionsComponent: fallbackIfUndefined('power-select/options'),
  beforeOptionsComponent: fallbackIfUndefined('power-select/before-options'),
  afterOptionsComponent: fallbackIfUndefined(null),
  matchTriggerWidth: fallbackIfUndefined(true),

  // Attrs
  searchText: '',
  lastSearchedText: '',
  expirableSearchText: '',
  activeSearch: null,
  openingEvent: null,
  loading: false,
  previousResults: null,

  // Lifecycle hooks
  didInitAttrs() {
    this._super(...arguments);
    Ember.assert('{{power-select}} requires an `onchange` function', this.get('onchange') && typeof this.get('onchange') === 'function');
  },

  willDestroy() {
    this._super(...arguments);
    this.activeSearch = null;
    run.cancel(this.expirableSearchDebounceId);
  },

  // CPs
  triggerId: computed(function() {
    return `ember-power-select-trigger-${this.elementId}`;
  }),

  optionsId: computed(function() {
    return `ember-power-select-options-${this.elementId}`;
  }),

  concatenatedClasses: computed('class', function() {
    const classes = ['ember-power-select'];
    if (this.get('class')) { classes.push(this.get('class')); }
    return classes.join(' ');
  }),

  concatenatedTriggerClasses: computed('triggerClass', function() {
    let classes = ['ember-power-select-trigger'];
    if (this.get('triggerClass')) {
      classes.push(this.get('triggerClass'));
    }
    return classes.join(' ');
  }),

  concatenatedDropdownClasses: computed('dropdownClass', function() {
    let classes = ['ember-power-select-dropdown', `ember-power-select-dropdown-${this.elementId}`];
    if (this.get('dropdownClass')) {
      classes.push(this.get('dropdownClass'));
    }
    return classes.join(' ');
  }),

  mustShowSearchMessage: computed('searchText', 'search', 'searchMessage', 'results.length', function(){
    return this.get('searchText.length') === 0 && !!this.get('search') && !!this.get('searchMessage') && this.get('results.length') === 0;
  }),

  mustShowNoMessages: computed('results.length', 'loading', 'search', 'lastSearchedText', function() {
    return !this.get('loading') && this.get('results.length') === 0 && (!this.get('search') || this.get('lastSearchedText.length') > 0);
  }),

  results: computed('options.[]', {
    get() {
      let options = this.get('options') || [];
      let searchAction = this.get('search');
      if (options.then) {
        this.set('loading', true);
        options.then(results => {
          if (this.get('isDestroyed')) { return; }
          this.set('results', results);
        });
        return this.previousResults || [];
      }
      let newResults = searchAction ? options : this.filter(options, this.get('searchText'));
      this.setProperties({ loading: false, currentlyHighlighted: undefined });
      this.previousResults = newResults;
      return newResults;
    },
    set(_, newResults) {
      this.previousResults = newResults;
      this.setProperties({ loading: false, currentlyHighlighted: undefined });
      return newResults;
    }
  }),

  optionMatcher: computed('searchField', 'matcher', function() {
    let { matcher, searchField } = this.getProperties('matcher', 'searchField');
    if (searchField) {
      return (option, text) => matcher(get(option, searchField), text);
    } else {
      return (option, text) => matcher(option, text);
    }
  }),

  highlighted: computed('results.[]', 'currentlyHighlighted', 'selected', function() {
    return this.get('currentlyHighlighted') || this.defaultHighlighted();
  }),

  resultsLength: computed('results.[]', function() {
    return countOptions(this.get('results'));
  }),

  eventSender: computed(function() {
    return EventSender.create();
  }),

  publicAPI: computed('registeredDropdown.isOpen', 'highlighted', 'searchText', function() {
    let dropdown = this.get('registeredDropdown');
    if (dropdown) {
      let ownActions = {
        search: (term, e) => this.send('search', dropdown, term, e),
        highlight: (option) => this.send('highlight', dropdown, option),
        select: (selected, e) => this.send('select', dropdown, selected, e),
        choose: (selected, e) => this.send('choose', dropdown, selected, e),
        handleKeydown: (e) => this.send('handleKeydown', dropdown, e)
      };
      return {
        isOpen: dropdown.isOpen,
        highlighted: this.get('highlighted'),
        searchText: this.get('searchText'),
        actions: Ember.assign(ownActions, dropdown.actions)
      };
    }
    return {};
  }),

  // Actions
  actions: {
    highlight(dropdown, option) {
      this._doHighlight(dropdown, option);
    },

    search(dropdown, term /*, e */) {
      this._doSearch(dropdown, term);
    },

    handleInput(e) {
      let term = e.target.value;
      let action = this.get('oninput');
      if (action) {
        let returnValue = action(e.target.value, this.get('publicAPI'), e);
        if (returnValue === false) { return; }
      }
      this.send('search', this.get('registeredDropdown'), term, e);
    },

    select(dropdown, selected, e) {
      this._doSelect(dropdown, selected, e);
    },

    choose(dropdown, selection, e) {
      if (e && e.clientY) {
        let openingEvent = this.get('openingEvent');
        if (openingEvent && openingEvent.clientY) {
          if (Math.abs(openingEvent.clientY - e.clientY) < 2) { return; }
        }
      }
      this.send('select', dropdown, this.get('buildSelection')(selection), e);
      if (this.get('closeOnSelect')) {
        dropdown.actions.close(e);
      }
    },

    handleKeydown(dropdown, e) {
      const onkeydown = this.get('onkeydown');
      if (onkeydown) { onkeydown(this.get('publicAPI'), e); }
      if (e.defaultPrevented) { return; }
      if (e.keyCode === 38 || e.keyCode === 40) { // Up & Down
        this._handleKeyUpDown(dropdown, e);
      } else if (e.keyCode === 13) {  // ENTER
        this._handleKeyEnter(dropdown, e);
      } else if (e.keyCode === 9) {   //Tab
        this._handleKeyTab(dropdown, e);
      } else if (e.keyCode === 27) {  // ESC
        this._handleKeyESC(dropdown, e);
      } else if (e.keyCode >= 48 && e.keyCode <= 90 || e.keyCode === 32) { // Keys 0-9, a-z or SPACE
        this._handleTriggerTyping(dropdown, e);
      }
    },

    handleFocus(dropdown, event) {
      const action = this.get('onfocus');
      if (action) {
        action(this.get('publicAPI'), event);
      }
      this.get('eventSender').trigger('focus');
    },

    // It is not evident what is going on here, so I'll explain why.
    //
    // As of this writting, Ember doesn allow to yield data to the "inverse" block.
    // Because of that, elements of this component rendered in the trigger can't receive the
    // yielded object contaning the public API of the ember-basic-dropdown, with actions for open,
    // close and toggle.
    //
    // The only possible workaround for this is to on initialization inject a similar object
    // to the one yielded and store it to make it available in the entire component.
    //
    // This this limitation on ember should be fixed soon, this is temporary. Because of that this
    // object will be passed to the action from the inverse block like if it was yielded.
    //
    registerDropdown(dropdown) {
      this.set('registeredDropdown', dropdown);
    },

    handleOpen(dropdown, e) {
      const action = this.get('onopen');
      if (action) { action(this.get('publicAPI'), e); }
      if (e) { this.set('openingEvent', e); }
      run.scheduleOnce('afterRender', this, this.scrollIfHighlightedIsOutOfViewport);
    },

    handleClose(dropdown, e) {
      const action = this.get('onclose');
      if (action) { action(this.get('publicAPI'), e); }
      if (e) { this.set('openingEvent', null); }
      this.send('highlight', dropdown, null, e);
    }
  },

  _handleKeyUpDown(dropdown, e) {
    if (dropdown.isOpen) {
      e.preventDefault();
      const newHighlighted = this.advanceSelectableOption(this.get('highlighted'), e.keyCode === 40 ? 1 : -1);
      this.send('highlight', dropdown, newHighlighted, e);
    } else {
      dropdown.actions.open(e);
    }
  },

  _handleKeyEnter(dropdown, e) {
    if (dropdown.isOpen) {
      this.send('choose', dropdown, this.get('highlighted'), e);
    }
  },

  _handleKeyTab(dropdown, e) {
    dropdown.actions.close(e);
  },

  _handleKeyESC(dropdown, e) {
    dropdown.actions.close(e);
  },

  // Methods
  scrollIfHighlightedIsOutOfViewport() {
    if (!self.document) { return; }
    const optionsList = document.querySelector('.ember-power-select-options');
    if (!optionsList) { return; }
    const highlightedOption = optionsList.querySelector('.ember-power-select-option[aria-current="true"]');
    if (!highlightedOption) { return; }
    const optionTopScroll = highlightedOption.offsetTop - optionsList.offsetTop;
    const optionBottomScroll = optionTopScroll + highlightedOption.offsetHeight;
    if (optionBottomScroll > optionsList.offsetHeight + optionsList.scrollTop) {
      optionsList.scrollTop = optionBottomScroll - optionsList.offsetHeight;
    } else if (optionTopScroll < optionsList.scrollTop) {
      optionsList.scrollTop = optionTopScroll;
    }
  },

  indexOfOption(option) {
    return indexOfOption(this.get('results'), option);
  },

  optionAtIndex(index) {
    return optionAtIndex(this.get('results'), index);
  },

  advanceSelectableOption(activeHighlighted, step) {
    let resultsLength = this.get('resultsLength');
    let startIndex = Math.min(Math.max(this.indexOfOption(activeHighlighted) + step, 0), resultsLength - 1);
    let nextOption = this.optionAtIndex(startIndex);
    while (nextOption && get(nextOption, 'disabled')) {
      nextOption = this.optionAtIndex(startIndex += step);
    }
    return nextOption;
  },

  filter(options, term) {
    return filterOptions(options || [], term, this.get('optionMatcher'));
  },

  defaultHighlighted() {
    const selected = this.get('selected');
    if (!selected || this.indexOfOption(selected) === -1) {
      return this.optionAtIndex(0);
    }
    return selected;
  },

  buildSelection(option) {
    return option;
  },

  _doSelect(dropdown, selected, e) {
    if (e) {
      e.preventDefault();
      e.stopPropagation();
    }
    if (this.get('selected') !== selected) {
      this.get('onchange')(selected, this.get('publicAPI'));
    }
  },

  _doHighlight(dropdown, option) {
    if (option && get(option, 'disabled')) { return; }
    run.scheduleOnce('afterRender', this, this.scrollIfHighlightedIsOutOfViewport);
    this.set('currentlyHighlighted', option);
  },

  _doSearch(dropdown, term) {
    if (isBlank(term)) {
      this._resetSearch();
    } else {
      let searchAction = this.get('search');
      if (searchAction) {
        this._performSearch(searchAction, term);
      } else {
        let options = this.get('options');
        if (options.then) {
          options.then((data) => {
            this.setProperties({ results: this.filter(data, term), searchText: term, lastSearchedText: term });
          });
        } else {
          this.setProperties({ results: this.filter(options, term), searchText: term, lastSearchedText: term });
        }
      }
    }
  },

  _resetSearch() {
    let options = this.get('options') || [];
    this.activeSearch = null;
    if (options.then) {
      options.then((data) => {
        this.setProperties({ results: data, searchText: '', lastSearchedText: '', loading: false });
      });
    } else {
      this.setProperties({ results: options, searchText: '', lastSearchedText: '', loading: false });
    }
  },

  _performSearch(searchAction, term) {
    this.set('searchText', term);
    let search = searchAction(term, this.get('publicAPI'));
    if (!search) {
      this.setProperties({ lastSearchedText: term });
    } else if (search.then) {
      this.activeSearch = search;
      this.setProperties({ loading: true });
      search.then((results) => {
        if (this.activeSearch === search) { this.setProperties({ results, lastSearchedText: term }); }
      }, () => {
        if (this.activeSearch === search) { this.set('lastSearchedText', term); }
      });
    } else {
      this.setProperties({ results: search, lastSearchedText: term });
    }
  },

  _handleTriggerTyping(dropdown, e) {
    let term = this.get('expirableSearchText') + String.fromCharCode(e.keyCode);
    this.set('expirableSearchText', term);
    this.expirableSearchDebounceId = run.debounce(this, 'set', 'expirableSearchText', '', 1000);
    let firstMatch = this.filter(this.get('results'), term)[0]; // TODO: match only words starting with this substr?
    if (firstMatch !== undefined) {
      if (dropdown.isOpen) {
        this._doHighlight(dropdown, firstMatch, e);
      } else {
        this._doSelect(dropdown, firstMatch, e);
      }
    }
  }
});
<|endoftext|>


<commit_before>import Ember from 'ember';
import hbs from 'htmlbars-inline-precompile';
import StateMachine from 'ember-steps/-private/state-machine';
import { MissingPropertyError } from 'ember-steps/-private/errors';

const { Component, get, set } = Ember;
const layout = hbs`
  {{yield (hash
    step=(component 'step-manager/step'
      register-step=(action 'register-step-component')
      currentStep=transitions.currentStep
    )
    transition-to=(action 'transition-to-step')
    transition-to-next=(action 'transition-to-next-step')
    transition-to-previous=(action 'transition-to-previous-step')
    currentStep=transitions.currentStep
    steps=(if _hasRendered transitions.stepArray)
  )}}
`;

export default Component.extend({
  layout,
  tagName: '',

  init() {
    this._super(...arguments);

    // Set up the state machine
    const initialStep = get(this, 'currentStep');
    if (!initialStep) {
      throw new MissingPropertyError('currentStep');
    }

    this._lastStep = initialStep;

    const stepCount = get(this, 'stepCount');
    if (!stepCount) {
      throw new MissingPropertyError('stepCount');
    }

    set(
      this,
      'transitions',
      StateMachine.create({
        initialStep
      })
    );
  },

  /**
   * @property {Ember.Object} transitions state machine for transitions
   * @private
   */
  transitions: null,

  /**
   * Whether the initial render cycle has completed
   *
   * Used to prevent a double-render-cycle when yielding an array of steps
   *
   * @property {boolean} _hasRendered
   * @private
   */
  _hasRendered: false,

  /**
   * Used internally to track the previous step
   *
   * @property {string} _lastStep
   * @private
   */
  _lastStep: undefined,

  /**
   * The `currentStep` property can be used for providing, or binding to, the
   * name of the current step.
   *
   * If you only want to provide the initial step, but do not want the target
   * object's value to be bound to it, you can either:
   *
   * - Pass it a value directly, like `currentStep='name'`
   * - Use the Unbound helper, like `currentStep=(unbound nameOfStep)`
   *
   * If you want to bind the value in both directions, you can do so by:
   *
   * - Passing a template value directly, like `currentStep=nameOfStep
   * - Using the Mut helper, like `currentStep=(mut nameOfStep)`
   *
   * Providing a mutable value is useful for cases like binding the current
   * step name to a query param.
   *
   * @property {string} currentStep the current active step
   * @public
   */
  currentStep: null,

  /**
   * If provided, this action will be called with a single POJO as the
   * argument, containing:
   *
   * - `value`-> The value passed to the transition action, or `undefined`
   * - `from` -> The name of the step being transitioned from
   * - `to`   -> The name of the step being transitioned to
   *
   * The action is called before the next step is activated.
   *
   * By returning `false` from this action, you can prevent the transition
   * from taking place.
   *
   * @property {Action} will-transition
   * @public
   */
  'will-transition': null,

  /**
   * If provided, this action will be called with a single POJO as the
   * argument, containing:
   *
   * - `value`-> The value passed to the transition action, or `undefined`
   * - `from` -> The name of the step being transitioned from
   * - `to`   -> The name of the step being transitioned to
   *
   * The action is called after the next step is activated.
   *
   * @property {Action} did-transition
   * @public
   */
  'did-transition': null,

  didUpdateAttrs() {
    this._super(...arguments);

    const oldStep = this._lastStep;
    const newStep = this.get('currentStep');

    if (typeof newStep === 'undefined') {
      const firstStep = get(this, 'transitions.firstStep');
      get(this, 'transitions').activate(firstStep);
    }

    if (newStep && oldStep !== newStep) {
      get(this, 'transitions').activate(newStep);
    }

    this._lastStep = newStep;

    this._super(...arguments);
  },

  didInsertElement() {
    this._super(...arguments);

    set(this, '_hasRendered', true);
  },

  actions: {
    /**
     * Register a step with the manager
     *
     * Adds a set to the internal registry of steps by name.  If no name is
     * provided, a name will be assigned by index.
     *
     * @method registerStep
     * @param {string} name the name of the step being registered
     * @private
     */
    'register-step-component'(stepComponent) {
      const name = get(stepComponent, 'name');
      get(this, 'transitions').addStep(name);
    },

    /**
     * Transition to a named step
     *
     * If the `currentStep` property was provided as a mutable value, like:
     *
     * ```js
     * {{#step-manager currentStep=(mut step) as |w|}}
     *   ...
     * {{/step-manager}}
     * ```
     *
     * Then the external property will be updated to the new step name.
     *
     * @method transition-to-step
     * @param {string} to the name of the step to transition to
     * @param {*} value the value to pass to the transition actions
     * @public
     */
    'transition-to-step'(to, value) {
      const from = get(this, 'transitions.currentStep');

      if (
        this['will-transition'] &&
        this['will-transition']({ value, from, to }) === false
      ) {
        return;
      }

      // Update the `currentStep` if it's mutable
      if (this.attrs.currentStep && this.attrs.currentStep.update) {
        this.attrs.currentStep.update(to);
      }

      // Activate the next step
      get(this, 'transitions').activate(to);

      if (this['did-transition']) {
        this['did-transition']({ value, from, to });
      }
    },

    /**
     * Transition to the "next" step
     *
     * When called, this action will advance from the current step to the next
     * one, as defined by the order of their insertion into the DOM (AKA, the
     * order in the template).
     *
     * The last step will transition back to the first one.
     *
     * @method transition-to-next-step
     * @param {*} value the value to pass to the transition actions
     * @public
     */
    'transition-to-next-step'(value) {
      const to = get(this, 'transitions').peek();

      this.send('transition-to-step', to, value);
    },

    /**
     * Transition to the "previous" step
     *
     * When called, this action will go back to the previous step according to
     * the step which was visited before entering the currentStep
     *
     * The first step will not transition to anything.
     *
     * @method transition-to-previous-step
     * @param {*} value the value to pass to the transition actions
     * @public
     */
    'transition-to-previous-step'(value) {
      const to = get(this, '_lastStep');

      if (to) {
        this.send('transition-to-step', to, value);
      }
    }
  }
});
<commit_msg>FIX: transition-to-previous is now the exact inverse of transition-to-next<commit_after>import Ember from 'ember';
import hbs from 'htmlbars-inline-precompile';
import StateMachine from 'ember-steps/-private/state-machine';
import { MissingPropertyError } from 'ember-steps/-private/errors';

const { Component, get, set } = Ember;
const layout = hbs`
  {{yield (hash
    step=(component 'step-manager/step'
      register-step=(action 'register-step-component')
      currentStep=transitions.currentStep
    )
    transition-to=(action 'transition-to-step')
    transition-to-next=(action 'transition-to-next-step')
    transition-to-previous=(action 'transition-to-previous-step')
    currentStep=transitions.currentStep
    steps=(if _hasRendered transitions.stepArray)
  )}}
`;

export default Component.extend({
  layout,
  tagName: '',

  init() {
    this._super(...arguments);

    // Set up the state machine
    const initialStep = get(this, 'currentStep');
    if (!initialStep) {
      throw new MissingPropertyError('currentStep');
    }

    this._lastStep = initialStep;

    const stepCount = get(this, 'stepCount');
    if (!stepCount) {
      throw new MissingPropertyError('stepCount');
    }

    set(
      this,
      'transitions',
      StateMachine.create({
        initialStep
      })
    );
  },

  /**
   * @property {Ember.Object} transitions state machine for transitions
   * @private
   */
  transitions: null,

  /**
   * Whether the initial render cycle has completed
   *
   * Used to prevent a double-render-cycle when yielding an array of steps
   *
   * @property {boolean} _hasRendered
   * @private
   */
  _hasRendered: false,

  /**
   * Used internally to track the previous step
   *
   * @property {string} _lastStep
   * @private
   */
  _lastStep: undefined,

  /**
   * The `currentStep` property can be used for providing, or binding to, the
   * name of the current step.
   *
   * If you only want to provide the initial step, but do not want the target
   * object's value to be bound to it, you can either:
   *
   * - Pass it a value directly, like `currentStep='name'`
   * - Use the Unbound helper, like `currentStep=(unbound nameOfStep)`
   *
   * If you want to bind the value in both directions, you can do so by:
   *
   * - Passing a template value directly, like `currentStep=nameOfStep
   * - Using the Mut helper, like `currentStep=(mut nameOfStep)`
   *
   * Providing a mutable value is useful for cases like binding the current
   * step name to a query param.
   *
   * @property {string} currentStep the current active step
   * @public
   */
  currentStep: null,

  /**
   * If provided, this action will be called with a single POJO as the
   * argument, containing:
   *
   * - `value`-> The value passed to the transition action, or `undefined`
   * - `from` -> The name of the step being transitioned from
   * - `to`   -> The name of the step being transitioned to
   *
   * The action is called before the next step is activated.
   *
   * By returning `false` from this action, you can prevent the transition
   * from taking place.
   *
   * @property {Action} will-transition
   * @public
   */
  'will-transition': null,

  /**
   * If provided, this action will be called with a single POJO as the
   * argument, containing:
   *
   * - `value`-> The value passed to the transition action, or `undefined`
   * - `from` -> The name of the step being transitioned from
   * - `to`   -> The name of the step being transitioned to
   *
   * The action is called after the next step is activated.
   *
   * @property {Action} did-transition
   * @public
   */
  'did-transition': null,

  didUpdateAttrs() {
    this._super(...arguments);

    const oldStep = this._lastStep;
    const newStep = this.get('currentStep');

    if (typeof newStep === 'undefined') {
      const firstStep = get(this, 'transitions.firstStep');
      get(this, 'transitions').activate(firstStep);
    }

    if (newStep && oldStep !== newStep) {
      get(this, 'transitions').activate(newStep);
    }

    this._lastStep = newStep;

    this._super(...arguments);
  },

  didInsertElement() {
    this._super(...arguments);

    set(this, '_hasRendered', true);
  },

  actions: {
    /**
     * Register a step with the manager
     *
     * Adds a set to the internal registry of steps by name.  If no name is
     * provided, a name will be assigned by index.
     *
     * @method registerStep
     * @param {string} name the name of the step being registered
     * @private
     */
    'register-step-component'(stepComponent) {
      const name = get(stepComponent, 'name');
      get(this, 'transitions').addStep(name);
    },

    /**
     * Transition to a named step
     *
     * If the `currentStep` property was provided as a mutable value, like:
     *
     * ```js
     * {{#step-manager currentStep=(mut step) as |w|}}
     *   ...
     * {{/step-manager}}
     * ```
     *
     * Then the external property will be updated to the new step name.
     *
     * @method transition-to-step
     * @param {string} to the name of the step to transition to
     * @param {*} value the value to pass to the transition actions
     * @public
     */
    'transition-to-step'(to, value) {
      const from = get(this, 'transitions.currentStep');

      if (
        this['will-transition'] &&
        this['will-transition']({ value, from, to }) === false
      ) {
        return;
      }

      // Update the `currentStep` if it's mutable
      if (this.attrs.currentStep && this.attrs.currentStep.update) {
        this.attrs.currentStep.update(to);
      }

      // Activate the next step
      get(this, 'transitions').activate(to);

      if (this['did-transition']) {
        this['did-transition']({ value, from, to });
      }
    },

    /**
     * Transition to the "next" step
     *
     * When called, this action will advance from the current step to the next
     * one, as defined by the order of their insertion into the DOM (AKA, the
     * order in the template).
     *
     * The last step will transition back to the first one.
     *
     * @method transition-to-next-step
     * @param {*} value the value to pass to the transition actions
     * @public
     */
    'transition-to-next-step'(value) {
      const to = get(this, 'transitions').peek();

      this.send('transition-to-step', to, value);
    },

    /**
     * Transition to the "previous" step
     *
     * When called, this action will go back to the previous step according to
     * the step which was visited before entering the currentStep
     *
     * The first step will not transition to anything.
     *
     * @method transition-to-previous-step
     * @param {*} value the value to pass to the transition actions
     * @public
     */
    'transition-to-previous-step'(value) {
      const to = get(this, 'transitions').previous();

      this.send('transition-to-step', to, value);
    }
  }
});
<|endoftext|>


<commit_before>/**
 * @module ember-flexberry
 */

import Ember from 'ember';

import QueryBuilder from 'ember-flexberry-data/query/builder';

// TODO: rename file, add 'controller' word into filename.
export default Ember.Mixin.create({
  // Lookup settings.
  lookupSettings: {
    controllerName: undefined,
    template: undefined,
    contentTemplate: undefined,
    loaderTemplate: undefined
  },

  /**
   * Controller to show lookup modal window.
   *
   * @property lookupController
   * @type Ember.InjectedProperty
   * @default undefined
   */
  lookupController: undefined,

  actions: {
    /**
     * Handles action from lookup choose action.
     *
     * @method showLookupDialog
     * @param {Object} chooseData Lookup parameters (projection name, relation name, etc).
     */
    showLookupDialog: function(chooseData) {
      let options = Ember.$.extend(true, {
        projection: undefined,
        relationName: undefined,
        title: undefined,
        predicate: undefined,
        modelToLookup: undefined,
        sizeClass: undefined
      }, chooseData);

      let projectionName = options.projection;
      let relationName = options.relationName;
      let title = options.title;
      let modelToLookup = options.modelToLookup;
      let sizeClass = options.sizeClass;

      if (!projectionName) {
        throw new Error('ProjectionName is undefined.');
      }

      let model = modelToLookup ? modelToLookup : this.get('model');

      // Get ember static function to get relation by name.
      let relationshipsByName = Ember.get(model.constructor, 'relationshipsByName');

      // Get relation property from model.
      let relation = relationshipsByName.get(relationName);
      if (!relation) {
        throw new Error(`No relation with '${relationName}' name defined in '${model.constructor.modelName}' model.`);
      }

      // Get property type name.
      let relatedToType = relation.type;

      // Get property type constructor by type name.
      let relatedTypeConstructor = this.store.modelFor(relatedToType);

      // Get a projection from related type model.
      let projection = Ember.get(relatedTypeConstructor, 'projections')[projectionName];
      if (!projection) {
        throw new Error(`No projection with '${projectionName}' name defined in '${relatedToType}' model. `);
      }

      // Lookup
      let lookupSettings = this.get('lookupSettings');
      if (!lookupSettings) {
        throw new Error('Lookup settings are undefined.');
      }

      // TODO: maybe default params or Ember.assert\warn?
      if (!lookupSettings.template) {
        throw new Error('Lookup template is undefined.');
      }

      if (!lookupSettings.controllerName) {
        throw new Error('Lookup controller name is undefined.');
      }

      if (!lookupSettings.contentTemplate) {
        throw new Error('Lookup content template is undefined.');
      }

      if (!lookupSettings.loaderTemplate) {
        throw new Error('Lookup loader template is undefined.');
      }

      let controller = this.get('lookupController');
      controller.clear();
      controller.setProperties({
        modelProjection: projection,
        title: title,
        sizeClass: sizeClass,
        saveTo: {
          model: model,
          propName: relationName
        }
      });

      this.send('showModalDialog', lookupSettings.template);
      let loadingParams = {
        view: lookupSettings.template,
        outlet: 'modal-content'
      };
      this.send('showModalDialog', lookupSettings.loaderTemplate, null, loadingParams);

      let builder = new QueryBuilder(this.store)
        .from(relatedToType)
        .selectByProjection(projectionName);

      if (options.predicate) {
        builder.where(options.predicate);
      }

      this.store.query(relatedToType, builder.build()).then(data => {
        this.send('removeModalDialog', loadingParams);
        this.send('showModalDialog', lookupSettings.contentTemplate, {
          controller: controller,
          model: data
        }, loadingParams);
      });
    },

    /**
     * Handles correcponding route's willTransition action.
     * It sends message about transition to showing lookup modal window controller.
     *
     * @method routeWillTransition
     */
    routeWillTransition: function() {
      this.get('lookupController').send('routeWillTransition');
    },

    /**
     * Handles action from lookup remove action.
     *
     * @method removeLookupValue
     * @param {Object} removeData Lookup parameters (projection name, etc).
     */
    removeLookupValue: function(removeData) {
      let options = Ember.$.extend(true, {
        relationName: undefined,
        modelToLookup: undefined
      }, removeData);
      let relationName = options.relationName;
      let modelToLookup = options.modelToLookup;

      let model = modelToLookup ? modelToLookup : this.get('model');
      model.set(relationName, undefined);

      // Manually make record dirty, because ember-data does not do it when relationship changes.
      model.makeDirty();
    },

    /**
     * Update relation value at model.
     *
     * @method updateLookupValue
     * @param {Object} updateData Lookup parameters to update data at model (projection name, etc).
     */
    updateLookupValue: function(updateData) {
      let options = Ember.$.extend(true, {
        relationName: undefined,
        newRelationValue: undefined,
        modelToLookup: undefined
      }, updateData);
      let modelToLookup = options.modelToLookup;
      let model = modelToLookup ? modelToLookup : this.get('model');

      Ember.Logger.debug(`Flexberry Lookup Mixin::updateLookupValue ${options.relationName}`);
      model.set(options.relationName, options.newRelationValue);

      // Manually make record dirty, because ember-data does not do it when relationship changes.
      model.makeDirty();
    }
  }
});
<commit_msg>Update FlexberryLookupController<commit_after>/**
  @module ember-flexberry
 */

import Ember from 'ember';

import QueryBuilder from 'ember-flexberry-data/query/builder';

/**
  Mixin for controller, for support FlexberryLookup component.

  TODO: Rename file, add 'controller' word into filename.

  @class FlexberryLookupController
  @uses Ember.Mixin
 */
export default Ember.Mixin.create({
  /**
    Settings for modal window.

    Structure object:
    - **controllerName** - Controller name.
    - **template** - Template name for show in modal window.
    - **contentTemplate** - Template name for show data.
    - **loaderTemplate** - Template name for show while data loading.

    @property lookupSettings
    @type Object
   */
  lookupSettings: {
    controllerName: undefined,
    template: undefined,
    contentTemplate: undefined,
    loaderTemplate: undefined
  },

  /**
    Controller to show modal window.

    @property lookupController
    @type Ember.Controller
   */
  lookupController: undefined,

  actions: {
    /**
      Handlers action from FlexberryLookup choose action.

      @method actions.showLookupDialog
      @param {Object} chooseData Lookup parameters: { projection, relationName, title, predicate, modelToLookup, sizeClass }.
     */
    showLookupDialog(chooseData) {
      let options = Ember.$.extend(true, {
        projection: undefined,
        relationName: undefined,
        title: undefined,
        predicate: undefined,
        modelToLookup: undefined,
        sizeClass: undefined
      }, chooseData);

      let projectionName = options.projection;
      let relationName = options.relationName;
      let title = options.title;
      let modelToLookup = options.modelToLookup;
      let sizeClass = options.sizeClass;

      if (!projectionName) {
        throw new Error('ProjectionName is undefined.');
      }

      let model = modelToLookup ? modelToLookup : this.get('model');

      // Get ember static function to get relation by name.
      let relationshipsByName = Ember.get(model.constructor, 'relationshipsByName');

      // Get relation property from model.
      let relation = relationshipsByName.get(relationName);
      if (!relation) {
        throw new Error(`No relation with '${relationName}' name defined in '${model.constructor.modelName}' model.`);
      }

      // Get property type name.
      let relatedToType = relation.type;

      // Get property type constructor by type name.
      let relatedTypeConstructor = this.store.modelFor(relatedToType);

      // Get a projection from related type model.
      let projection = Ember.get(relatedTypeConstructor, 'projections')[projectionName];
      if (!projection) {
        throw new Error(`No projection with '${projectionName}' name defined in '${relatedToType}' model. `);
      }

      // Lookup
      let lookupSettings = this.get('lookupSettings');
      if (!lookupSettings) {
        throw new Error('Lookup settings are undefined.');
      }

      // TODO: maybe default params or Ember.assert\warn?
      if (!lookupSettings.template) {
        throw new Error('Lookup template is undefined.');
      }

      if (!lookupSettings.controllerName) {
        throw new Error('Lookup controller name is undefined.');
      }

      if (!lookupSettings.contentTemplate) {
        throw new Error('Lookup content template is undefined.');
      }

      if (!lookupSettings.loaderTemplate) {
        throw new Error('Lookup loader template is undefined.');
      }

      let controller = this.get('lookupController');
      controller.clear();
      controller.setProperties({
        modelProjection: projection,
        title: title,
        sizeClass: sizeClass,
        saveTo: {
          model: model,
          propName: relationName
        }
      });

      this.send('showModalDialog', lookupSettings.template);
      let loadingParams = {
        view: lookupSettings.template,
        outlet: 'modal-content'
      };
      this.send('showModalDialog', lookupSettings.loaderTemplate, null, loadingParams);

      let builder = new QueryBuilder(this.store)
        .from(relatedToType)
        .selectByProjection(projectionName);

      if (options.predicate) {
        builder.where(options.predicate);
      }

      this.store.query(relatedToType, builder.build()).then(data => {
        this.send('removeModalDialog', loadingParams);
        this.send('showModalDialog', lookupSettings.contentTemplate, {
          controller: controller,
          model: data
        }, loadingParams);
      });
    },

    /**
      Handlers corresponding route's willTransition action.
      It sends message about transition to showing lookup modal window controller.

      @method actions.routeWillTransition
     */
    routeWillTransition() {
      this.get('lookupController').send('routeWillTransition');
    },

    /**
      Handlers action from FlexberryLookup remove action.

      @method actions.removeLookupValue
      @param {Object} removeData Lookup parameters: { relationName, modelToLookup }.
     */
    removeLookupValue(removeData) {
      let options = Ember.$.extend(true, {
        relationName: undefined,
        modelToLookup: undefined
      }, removeData);
      let relationName = options.relationName;
      let modelToLookup = options.modelToLookup;

      let model = modelToLookup ? modelToLookup : this.get('model');
      model.set(relationName, undefined);

      // Manually make record dirty, because ember-data does not do it when relationship changes.
      model.makeDirty();
    },

    /**
      Update relation value at model.

      @method actions.updateLookupValue
      @param {Object} updateData Lookup parameters to update data at model: { relationName, newRelationValue, modelToLookup }.
     */
    updateLookupValue(updateData) {
      let options = Ember.$.extend(true, {
        relationName: undefined,
        newRelationValue: undefined,
        modelToLookup: undefined
      }, updateData);
      let modelToLookup = options.modelToLookup;
      let model = modelToLookup ? modelToLookup : this.get('model');

      Ember.Logger.debug(`Flexberry Lookup Mixin::updateLookupValue ${options.relationName}`);
      model.set(options.relationName, options.newRelationValue);

      // Manually make record dirty, because ember-data does not do it when relationship changes.
      model.makeDirty();
    },
  },
});
<|endoftext|>


<commit_before>var path = require('path')
var request = require('request')

// # of tries for webserver to start up on specified port
// We test once per second
var RETRIES = 10

// Port on which the application-under-test should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var PORT = 8031

function prepare(ctx, cb) {
  console.log("SAUCE prepare")
  var tsh = ctx.shellWrap("npm install")
  ctx.forkProc(ctx.workingDir, tsh.cmd, tsh.args, function(exitCode) {
    if (exitCode === 0) {
      ctx.striderMessage("npm install success")
    }
    cb(exitCode)
  })
}

//
// `npm install` has succeeded at this point.
// We run `npm test` and assuming that has passed,
// then we start the Sauce test process.
// If `npm test` fails, we don't bother with the overhead of running the Sauce tests.
function test(ctx, cb) {
  console.log("SAUCE test")
  var startPhaseDone = false
  var tsh = ctx.shellWrap("npm test")
  // Run
  ctx.forkProc(ctx.workingDir, tsh.cmd, tsh.args, function(exitCode) {
    if (exitCode !== 0) {
      return cb(exitCode)
    } else {
      ctx.striderMessage("npm test success - trying Sauce tests...")

      npmTestPassed()
    }
  })
  function npmTestPassed() {
    // `npm test` succeeded, so we go through the Sauce tests.
    //
    // Start the app, suggesting a port via PORT environment variable
    var tsh = ctx.shellWrap("npm start")
    var serverProc = ctx.forkProc({
      args:tsh.args,
      cmd:tsh.cmd,
      cwd:ctx.workingDir,
      env:{PORT:PORT},
    }, function(exitCode) {
      // Could perhaps be backgrounding itself. This should be avoided.
      console.log("serverProc exited with code: %d", exitCode)
      if (exitCode !== 0 && !startPhaseDone) {
        // If we haven't already called back with completion,
        // and `npm start` exits with non-zero exit code,
        // call back with error and mark done.

        ctx.striderMessage("npm start failed - failing test")
        startPhaseDone = true
        return cb(exitCode)
      }
    })

    var tries = 0
    ctx.striderMessage("Waiting for webserver to come up on localhost:" + PORT)
    var intervalId = setInterval(function() {
      // Check for http status 200 on http://localhost:PORT/
      request("http://localhost:"+PORT+"/", function(err, response) {
        if (startPhaseDone) {
          clearInterval(intervalId)
          return
        }
        if (!err && response.statusCode == 200) {
          ctx.striderMessage("Got HTTP 200 on localhost:" + PORT + " indicating server is up")
          startPhaseDone = true
          clearInterval(intervalId)
          serverUp()
        } else {
          tries++
          console.log("Error on localhost:%d: %s", PORT, err)
          if (tries >= RETRIES) {
            ctx.striderMessage("HTTP 200 check on localhost:" + PORT + " failed after " + tries + " retries, server not up - failing test")
            clearInterval(intervalId)
            startPhaseDone = true
            return cb(1)
          }
        }
      })

    }, 1000)

    // Start the Sauce Connector. Returns childProcess object.
    function startConnector(username, apiKey, cb) {
      var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
      var jsh = ctx.shellWrap("java -jar " + jarPath + " " + username + " " + apiKey)

      ctx.striderMessage("Starting Sauce Connector")
      return ctx.forkProc(__dirname, jsh.cmd, jsh.args, cb)
    }

    function serverUp() {
      // Server is up, start Sauce Connector and run the tests via `npm sauce` invocations

      startConnector("username", "apiKey")

      ctx.striderMessage("Shutting down server")
      serverProc.kill()

      return cb(1)
    }
  }
}

module.exports = function(ctx, cb) {

  ctx.addDetectionRule({
    filename:"package.json",
    jsonKeyExists:"scripts.sauce",
    language:"node.js",
    framework:null,
    hasSauce:true,
    prepare:prepare,
    test:test
  })

  console.log("strider-sauce loaded")
  cb(null, null)

}
<commit_msg>clarity<commit_after>var path = require('path')
var request = require('request')

// # of tries for webserver to start up on specified port
// We test once per second
var RETRIES = 10

// Port on which the application-under-test should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var PORT = 8031

function prepare(ctx, cb) {
  console.log("SAUCE prepare")
  var tsh = ctx.shellWrap("npm install")
  ctx.forkProc(ctx.workingDir, tsh.cmd, tsh.args, function(exitCode) {
    if (exitCode === 0) {
      ctx.striderMessage("npm install success")
    }
    cb(exitCode)
  })
}

//
// `npm install` has succeeded at this point.
// We run `npm test` and assuming that has passed,
// then we start the Sauce test process.
// If `npm test` fails, we don't bother with the overhead of running the Sauce tests.
function test(ctx, cb) {
  console.log("SAUCE test")
  var startPhaseDone = false
  var tsh = ctx.shellWrap("npm test")
  // Run
  ctx.forkProc(ctx.workingDir, tsh.cmd, tsh.args, function(exitCode) {
    if (exitCode !== 0) {
      return cb(exitCode)
    } else {
      ctx.striderMessage("npm test success - trying Sauce tests...")

      npmTestPassed()
    }
  })
  function npmTestPassed() {
    // `npm test` succeeded, so we go through the Sauce tests.
    //
    // Start the app, suggesting a port via PORT environment variable
    var tsh = ctx.shellWrap("npm start")
    var serverProc = ctx.forkProc({
      args:tsh.args,
      cmd:tsh.cmd,
      cwd:ctx.workingDir,
      env:{PORT:PORT},
    }, function(exitCode) {
      // Could perhaps be backgrounding itself. This should be avoided.
      console.log("serverProc exited with code: %d", exitCode)
      if (exitCode !== 0 && !startPhaseDone) {
        // If we haven't already called back with completion,
        // and `npm start` exits with non-zero exit code,
        // call back with error and mark done.

        ctx.striderMessage("npm start failed - failing test")
        startPhaseDone = true
        return cb(exitCode)
      }
    })

    var tries = 0
    ctx.striderMessage("Waiting for webserver to come up on localhost:" + PORT)
    var intervalId = setInterval(function() {
      // Check for http status 200 on http://localhost:PORT/
      request("http://localhost:"+PORT+"/", function(err, response) {
        if (startPhaseDone) {
          clearInterval(intervalId)
          return
        }
        if (!err && response.statusCode == 200) {
          ctx.striderMessage("Got HTTP 200 on localhost:" + PORT + " indicating server is up")
          startPhaseDone = true
          clearInterval(intervalId)
          serverUp()
        } else {
          tries++
          console.log("Error on localhost:%d: %s", PORT, err)
          if (tries >= RETRIES) {
            ctx.striderMessage("HTTP 200 check on localhost:" + PORT + " failed after " + tries + " retries, server not up - failing test")
            clearInterval(intervalId)
            startPhaseDone = true
            return cb(1)
          }
        }
      })

    }, 1000)

    // Start the Sauce Connector. Returns childProcess object.
    function startConnector(username, apiKey, cb) {
      var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
      var jsh = ctx.shellWrap("java -jar " + jarPath + " " + username + " " + apiKey)

      ctx.striderMessage("Starting Sauce Connector")
      return ctx.forkProc(__dirname, jsh.cmd, jsh.args, cb)
    }

    function serverUp() {
      // Server is up, start Sauce Connector and run the tests via `npm sauce` invocations
      var done = false
      var connectorProc = startConnector("username", "apiKey", function(exitCode) {
        console.log("Connector exited with code: %d", exitCode)
        if (!done) {
          ctx.striderMessage("Error starting Sauce Connector - failing test")
          ctx.striderMessage("Shutting down server")
          serverProc.kill()
          done = true
          return cb(1)

        }
      })
      ctx.striderMessage("Waiting 10 seconds for Sauce Connector to come up")
      // Wait 10 seconds for Sauce Connector to come up
      setTimeout(function() {
        if (!done) {
          done = true
          ctx.striderMessage("Shutting down Sauce Connector")
          connectorProc.kill("SIGINT")
          ctx.striderMessage("Shutting down server")
          serverProc.kill()
          return cb(1)
        }
      }, 10000)

    }
  }
}

module.exports = function(ctx, cb) {

  ctx.addDetectionRule({
    filename:"package.json",
    jsonKeyExists:"scripts.sauce",
    language:"node.js",
    framework:null,
    hasSauce:true,
    prepare:prepare,
    test:test
  })

  console.log("strider-sauce loaded")
  cb(null, null)

}
<|endoftext|>


<commit_before>// This script can be sourced both from the HTML and as a web worker.

var jfxr = jfxr || {};

jfxr.Worker = function(context, $q, $rootScope) {
	this.context = context;
	this.$q = $q;
	this.$rootScope = $rootScope;

	this.renderTimeMs = null;

	this.useWebWorkers = jfxr.shims.haveWebWorkers();
};

jfxr.Worker.prototype.synth = function(str, finishedCallback) {
	var deferred = this.$q.defer();

	if (this.useWebWorkers) {
		if (this.worker) {
			this.worker.terminate();
			this.worker = null;
		}

		this.worker = new Worker('worker.js');
		this.worker.addEventListener('message', function(e) {
			this.$rootScope.$apply(function() {
				if (e.error) {
					deferred.reject(e.error);
				} else {
					deferred.resolve(e.data);
				}
			}.bind(this));
		}.bind(this));
		this.worker.postMessage(str);
	} else {
		var msg = (new jfxr.Synth()).generate(str);
		deferred.resolve(msg);
	}

	return deferred.promise.then(function(msg) {
		this.worker = null;
		this.renderTimeMs = msg.renderTimeMs;
		var array = new Float32Array(msg.arrayBuffer);
		var buffer = this.context.createBuffer(1, array.length, msg.sampleRate);
		buffer.getChannelData(0).set(array);
		return buffer;
	}.bind(this), function(error) {
		console.error('Synth failed: ' + error);
		return null;
	});
};

jfxr.Worker.main = function(worker) {
	var exception = null;
	try {
		importScripts('math.js', 'random.js', 'synth.js');
	} catch (ex) {
		exception = ex;
	}

	if (!exception) {
		worker.addEventListener('message', function(e) {
			var str = e.data;
			var msg = (new jfxr.Synth()).generate(str);
			worker.postMessage(msg, [msg.arrayBuffer]);
		});
	} else {
		worker.addEventListener('message', function(e) {
			worker.postMessage({'error': exception.toString()});
		});
	}
}

if (typeof WorkerGlobalScope == 'undefined') {
	jfxrApp.service('worker', jfxr.Worker);
} else {
	jfxr.Worker.main(this);
}
<commit_msg>Remove defunct worker<commit_after><|endoftext|>


<commit_before>/*
	A jQuery plugin to prevent "x-clicks" events.
	Version 1.0 - Released on 2013-11-18
	More info and discussion at : https://github.com/louisameline/XClick
	Released under MIT license
*/
;(function($){

	var defaultOptions = {
			// if for some reason you want to deal with x-clicks triggered only in a given branch of your DOM tree
			delegate: '',
			// to put the plugin to sleep (stops listening to mouse events)
			enable: true,
			// we do not want to waist time in browsers that do not generate x-clicks anyway. This option is mainly for debugging purposes.
			enableForAllBrowsers: false,
			// callback function called when an x-click was just triggered. If the function does not return true, the event propagation is immediately stopped (default behavior).
			// the callback will be called in the context of the common ancestor element which triggered the click event, and receives this click event as first parameter.
			onTriggered: function(){ return false; }
		},
		lastElements = {};

	// we have to know if the browser generates x-clicks. Not sure if there is another way than browser detection, suggestions are welcome.
	var XClickSupport = false;
	// look for MSIE or rv:11 (IE11)
	if(navigator.userAgent.indexOf('MSIE') !== -1 || navigator.userAgent.indexOf('rv:11') !== -1) XClickSupport = true;

	// a function to set a function as a first handler, in order to stop the x-click event before any other handler can fire
	// warning: this binds on the internals of jQuery as there is no proper method to do this. Be careful to test everytime you upgrade to a new version of jQuery.
	$.fn.xcBindFirst = function(name, fn) {
		// bind as you normally would
		this.on(name, fn);
		return this.each(function() {
			var handlers = $._data(this, 'events')[name.split('.')[0]];
			// take out the handler we just inserted from the end
			var handler = handlers.pop();
			// move it at the beginning
			handlers.splice(0, 0, handler);
		});
	};

	// set the parameter to true
	$.XClick = function(options){

		options = $.extend({}, defaultOptions, options || {});

		if(XClickSupport || options.enableForAllBrowsers){

			// enforcing the prevention system
			if(options.enable === true){

				// bind on body to track mousedown and mouseup events, it will allow us to be ready to catch the x-click
				$('body')
					// if for some reason you have to stop your mousedown/up events from bubbling up to body, trigger a custom "silentMouseup" or "silentMousedown" instead
					.on('mousedown.xc mousedownSilent.xc', options.delegate, function(e){ lastElements.mousedown = e.target; })
					.on('mouseup.xc mouseupSilent.xc', options.delegate, function(e){

						lastElements.mouseup = e.target;

						// compare with the mousedown element
						if(
							// if one of the elements is not defined, we let the event go. It happens when one of the mouse events was not triggered or not caught by our listeners and that a .click() call is made afterwards
							lastElements.mousedown && lastElements.mouseup
							// make sure the elements are different
							&& lastElements.mousedown !== lastElements.mouseup
						){
							// we determine which element is the closest ancestor
							var mdElParents = $(lastElements.mousedown).parents(),
								muElParents = $(lastElements.mouseup).parents(),
								$closestCommonAncestor = null;

							$.each(mdElParents, function(i, el){
								$.each(muElParents, function(j, el2){

									if(el === el2 || $(el).has(el2).length > 0){
										$closestCommonAncestor = $(el);
										return false;
									}
								});
								if($closestCommonAncestor) return false;
							});

							// we bind on the closest ancestor to catch the x-click
							$closestCommonAncestor.xcBindFirst('click.xc', function(event){

								// unbind itself first, in case the callback function want to trigger click events
								$(this).off('click.xc');

								// make sure the click was triggered on this element (as an x-click would be), not bubbling from children. This reduces the risk to catch a wrong click event
								if(event.target === this){

									// callback
									var letGo = options.onTriggered.call(this, {
										event: event,
										mousedownElement: lastElements.mousedown,
										mouseupElement: lastElements.mouseup
									});

									// stop the event unless the callback function returned true
									if(letGo === true) event.stopImmediatePropagation();
								}
							});

							// if the browser does not produce x-clicks, the handler we just bound will never be called and must be unbound before it catches a click it was not supposed to catch
							setTimeout(function(){
								$closestCommonAncestor.off('click.xc');
							}, 0);
						}
					});
			}
			// canceling the prevention system
			else {
				$('body').off('.xc');
			}
		}
	}
})(jQuery);<commit_msg>More for version 1<commit_after>/*
	A jQuery plugin to predict and prevent "x-clicks" events.
	Version 1.0 - Released on 2013-11-18
	More info and discussion at : https://github.com/louisameline/XClick
	Released under MIT license
*/
;(function($){

	var defaultOptions = {
			// if for some reason you want to deal with x-clicks triggered only in a given branch of your DOM tree
			delegate: '',
			// to put the plugin to sleep (stops listening to mouse events)
			enable: true,
			// we do not want to waist time in browsers that do not generate x-clicks anyway. This option is mainly for debugging purposes.
			enableForAllBrowsers: false,
			// callback function called when an x-click was just triggered. If the function does not return true, the event propagation is immediately stopped (default behavior).
			// the callback will be called in the context of the common ancestor element which triggered the click event, and receives this click event as first parameter.
			onTriggered: function(){ return false; }
		},
		lastElements = {};

	// we have to know if the browser generates x-clicks. Not sure if there is another way than browser detection (script-triggered mouse events do not trigger an x-click), suggestions are welcome.
	var xclickSupport = false;
	// look for MSIE or rv:11 (IE11)
	if(navigator.userAgent.indexOf('MSIE') !== -1 || navigator.userAgent.indexOf('rv:11') !== -1) XClickSupport = true;

	// a function to set a function as a first handler, in order to stop the x-click event before any other handler can fire
	// warning: this binds on the internals of jQuery as there is no proper method to do this. Be careful to test everytime you upgrade to a new version of jQuery.
	$.fn.xcBindFirst = function(name, fn) {
		// bind as you normally would
		this.on(name, fn);
		return this.each(function() {
			var handlers = $._data(this, 'events')[name.split('.')[0]];
			// take out the handler we just inserted from the end
			var handler = handlers.pop();
			// move it at the beginning
			handlers.splice(0, 0, handler);
		});
	};

	// set the parameter to true
	$.XClick = function(options){

		options = $.extend({}, defaultOptions, options || {});

		if(xclickSupport || options.enableForAllBrowsers){

			// enforcing the prevention system
			if(options.enable === true){

				// bind on body to track mousedown and mouseup events, it will allow us to be ready to catch the x-click
				$('body')
					// if for some reason you have to stop your mousedown/up events from bubbling up to body, trigger a custom "silentMouseup" or "silentMousedown" instead
					.on('mousedown.xc mousedownSilent.xc', options.delegate, function(e){ lastElements.mousedown = e.target; })
					.on('mouseup.xc mouseupSilent.xc', options.delegate, function(e){

						lastElements.mouseup = e.target;

						// compare with the mousedown element
						if(
							// if one of the elements is not defined, we let the event go. It happens when one of the mouse events was not triggered or not caught by our listeners and that a .click() call is made afterwards
							lastElements.mousedown && lastElements.mouseup
							// make sure the elements are different
							&& lastElements.mousedown !== lastElements.mouseup
						){
							// we determine which element is the closest ancestor
							var mdElParents = $(lastElements.mousedown).parents(),
								muElParents = $(lastElements.mouseup).parents(),
								$closestCommonAncestor = null;

							$.each(mdElParents, function(i, el){
								$.each(muElParents, function(j, el2){

									if(el === el2 || $(el).has(el2).length > 0){
										$closestCommonAncestor = $(el);
										return false;
									}
								});
								if($closestCommonAncestor) return false;
							});

							// we bind on the closest ancestor to catch the x-click
							$closestCommonAncestor.xcBindFirst('click.xc', function(event){

								// unbind itself first, in case the callback function want to trigger click events
								$(this).off('click.xc');

								// make sure the click was triggered on this element (as an x-click would be), not bubbling from children. This reduces the risk to catch a wrong click event
								if(event.target === this){

									// callback
									var letGo = options.onTriggered.call(this, {
										event: event,
										mousedownElement: lastElements.mousedown,
										mouseupElement: lastElements.mouseup
									});

									// stop the event unless the callback function returned true
									if(letGo !== true) event.stopImmediatePropagation();
								}
							});

							// if the browser does not produce x-clicks, the handler we just bound will never be called and must be unbound before it catches a click it was not supposed to catch
							setTimeout(function(){
								$closestCommonAncestor.off('click.xc');
							}, 0);
						}
					});
			}
			// canceling the prevention system
			else {
				$('body').off('.xc');
			}
		}
	}
})(jQuery);<|endoftext|>


<commit_before>// Copyright 2014 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Controller for the exploration editor feedback tab.
 *
 * @author kashida@google.com (Koji Ashida)
 */

oppia.controller('FeedbackTab', [
    '$scope', '$http', '$modal', '$timeout', '$rootScope', 'warningsData',
    'oppiaDatetimeFormatter', 'threadStatusDisplayService',
    'threadDataService', 'explorationStatesService', 'explorationData',
    'changeListService',
    function(
      $scope, $http, $modal, $timeout, $rootScope, warningsData,
      oppiaDatetimeFormatter, threadStatusDisplayService,
      threadDataService, explorationStatesService, explorationData,
      changeListService) {

  var ACTION_ACCEPT_SUGGESTION = 'accept';
  var ACTION_REJECT_SUGGESTION = 'reject';
  $scope.STATUS_CHOICES = threadStatusDisplayService.STATUS_CHOICES;
  $scope.threadData = threadDataService.data;
  $scope.getLabelClass = threadStatusDisplayService.getLabelClass;
  $scope.getHumanReadableStatus = (
    threadStatusDisplayService.getHumanReadableStatus);
  $scope.getLocaleAbbreviatedDatetimeString = (
    oppiaDatetimeFormatter.getLocaleAbbreviatedDatetimeString);

  $scope.activeThread = null;
  $rootScope.loadingMessage = 'Loading';
  $scope.tmpMessage = {
    status: null,
    text: ''
  };

  var _resetTmpMessageFields = function() {
    $scope.tmpMessage.status = $scope.activeThread ? $scope.activeThread.status : null;
    $scope.tmpMessage.text = '';
  };

  $scope.clearActiveThread = function() {
    $scope.activeThread = null;
    _resetTmpMessageFields();
  };

  $scope.showCreateThreadModal = function() {
    $modal.open({
      templateUrl: 'modals/editorFeedbackCreateThread',
      backdrop: true,
      resolve: {},
      controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
        $scope.newThreadSubject = '';
        $scope.newThreadText = '';

        $scope.create = function(newThreadSubject, newThreadText) {
          if (!newThreadSubject) {
            warningsData.addWarning('Please specify a thread subject.');
            return;
          }
          if (!newThreadText) {
            warningsData.addWarning('Please specify a message.');
            return;
          }

          $modalInstance.close({
            newThreadSubject: newThreadSubject,
            newThreadText: newThreadText
          });
        };

        $scope.cancel = function() {
          $modalInstance.dismiss('cancel');
        };
      }]
    }).result.then(function(result) {
      threadDataService.createNewThread(
        result.newThreadSubject, result.newThreadText, function() {
          $scope.clearActiveThread();
        });
    });
  };

  $scope.isSuggestionValid = function() {
    return ($scope.activeThread.status === 'open' &&
      explorationData.data.version === $scope.activeThread.suggestion.exploration_version);
  };

  $scope.hasUnsavedChanges = function() {
    return (changeListService.getChangeList().length > 0);
  };

  $scope.viewSuggestionBtnType = function () {
    return ($scope.isSuggestionValid() && !$scope.hasUnsavedChanges()
            ? 'primary' : 'default');
  };

  // TODO(Allan): Implement ability to edit suggestions before applying.
  $scope.showSuggestionModal = function() {
    $modal.open({
      templateUrl: 'modals/editorViewSuggestion',
      backdrop: true,
      size: 'lg',
      resolve: {
        suggestionInfo: function() {
          return {
            isSuggestionValid: $scope.isSuggestionValid(),
            hasUnsavedChanges: $scope.hasUnsavedChanges()
          };
        },
        stateContent: function() {
          var states = explorationData.data.states;
          var stateName = $scope.activeThread.suggestion.state_name;
          return {
            oldContent: states[stateName].content[0].value,
            newContent: $scope.activeThread.suggestion.state_content.value
          };
        }
      },
      controller: [
        '$scope', '$modalInstance', 'suggestionInfo', 'stateContent',
        function($scope, $modalInstance, suggestionInfo, stateContent) {
        var ACTION_INVALID_MSG = 'This suggestion has already been ' +
          'acted upon, or was made for an outdated version '  +
          'of this exploration.';
        var UNSAVED_CHANGES_MSG = 'You have unsaved changes to this ' +
          'exploration. Please save your changes before acting on ' +
          'a suggestion.';
        $scope.canActOnSuggestion = (
          suggestionInfo.isSuggestionValid && !suggestionInfo.hasUnsavedChanges
        );
        $scope.errorMessage = suggestionInfo.isSuggestionValid
          ? UNSAVED_CHANGES_MSG : ACTION_INVALID_MSG;
        $scope.oldContent = stateContent.oldContent;
        $scope.newContent = stateContent.newContent;
        $scope.commitMessage = '';

        $scope.acceptSuggestion = function() {
          $modalInstance.close({
            action: ACTION_ACCEPT_SUGGESTION,
            commitMsg: $scope.commitMessage
          });
        };

        $scope.rejectSuggestion = function() {
          $modalInstance.close({
            action: ACTION_REJECT_SUGGESTION
          });
        };

        $scope.cancelReview = function() {
          $modalInstance.dismiss();
        };
      }]
    }).result.then(function(result) {
      threadDataService.resolveSuggestion(
        $scope.activeThread.thread_id, result.action, result.commitMsg,
        function(res) {
          threadDataService.fetchThreads(function() {
            $scope.setActiveThread($scope.activeThread.thread_id);
          });
          // Immediately update editor to reflect accepted suggestion.
          if (result.action === ACTION_ACCEPT_SUGGESTION) {
            var suggestion = $scope.activeThread.suggestion;
            var stateName = suggestion.state_name;
            var state = angular.copy(explorationData.data.states[stateName]);
            state.content[0].value = suggestion.state_content.value;
            explorationStatesService.setState(stateName, state);
            $rootScope.$broadcast('refreshStateEditor');
          }
        }, function(res) {
          console.log('Error resolving suggestion');
        });
    });
  };

  $scope.addNewMessage = function(threadId, tmpText, tmpStatus) {
    if (threadId === null) {
      warningsData.addWarning('Cannot add message to thread with ID: null.');
      return;
    }
    if (!tmpStatus) {
      warningsData.addWarning('Invalid message status: ' + tmpStatus);
      return;
    }

    $scope.messageSendingInProgress = true;
    threadDataService.addNewMessage(threadId, tmpText, tmpStatus, function() {
      _resetTmpMessageFields();
      $scope.messageSendingInProgress = false;
    }, function() {
      $scope.messageSendingInProgress = false;
    });
  };

  $scope.setActiveThread = function(threadId) {
    threadDataService.fetchMessages(threadId);

    var combined = [].concat(
      $scope.threadData.feedbackThreads, $scope.threadData.suggestionThreads);
    for (var i = 0; i < combined.length; i++) {
      if (combined[i].thread_id === threadId) {
        $scope.activeThread = combined[i];
        break;
      }
    }

    $scope.tmpMessage.status = $scope.activeThread.status;
  };

  // Initial load of the thread list on page load.
  $scope.clearActiveThread();
  threadDataService.fetchThreads(function() {
    $timeout(function() {
      $rootScope.loadingMessage = '';
    }, 500);
  });
}]);
<commit_msg>Update exploration version in explorationData after accepting a suggestion<commit_after>// Copyright 2014 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Controller for the exploration editor feedback tab.
 *
 * @author kashida@google.com (Koji Ashida)
 */

oppia.controller('FeedbackTab', [
    '$scope', '$http', '$modal', '$timeout', '$rootScope', 'warningsData',
    'oppiaDatetimeFormatter', 'threadStatusDisplayService',
    'threadDataService', 'explorationStatesService', 'explorationData',
    'changeListService',
    function(
      $scope, $http, $modal, $timeout, $rootScope, warningsData,
      oppiaDatetimeFormatter, threadStatusDisplayService,
      threadDataService, explorationStatesService, explorationData,
      changeListService) {

  var ACTION_ACCEPT_SUGGESTION = 'accept';
  var ACTION_REJECT_SUGGESTION = 'reject';
  $scope.STATUS_CHOICES = threadStatusDisplayService.STATUS_CHOICES;
  $scope.threadData = threadDataService.data;
  $scope.getLabelClass = threadStatusDisplayService.getLabelClass;
  $scope.getHumanReadableStatus = (
    threadStatusDisplayService.getHumanReadableStatus);
  $scope.getLocaleAbbreviatedDatetimeString = (
    oppiaDatetimeFormatter.getLocaleAbbreviatedDatetimeString);

  $scope.activeThread = null;
  $rootScope.loadingMessage = 'Loading';
  $scope.tmpMessage = {
    status: null,
    text: ''
  };

  var _resetTmpMessageFields = function() {
    $scope.tmpMessage.status = $scope.activeThread ? $scope.activeThread.status : null;
    $scope.tmpMessage.text = '';
  };

  $scope.clearActiveThread = function() {
    $scope.activeThread = null;
    _resetTmpMessageFields();
  };

  $scope.showCreateThreadModal = function() {
    $modal.open({
      templateUrl: 'modals/editorFeedbackCreateThread',
      backdrop: true,
      resolve: {},
      controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
        $scope.newThreadSubject = '';
        $scope.newThreadText = '';

        $scope.create = function(newThreadSubject, newThreadText) {
          if (!newThreadSubject) {
            warningsData.addWarning('Please specify a thread subject.');
            return;
          }
          if (!newThreadText) {
            warningsData.addWarning('Please specify a message.');
            return;
          }

          $modalInstance.close({
            newThreadSubject: newThreadSubject,
            newThreadText: newThreadText
          });
        };

        $scope.cancel = function() {
          $modalInstance.dismiss('cancel');
        };
      }]
    }).result.then(function(result) {
      threadDataService.createNewThread(
        result.newThreadSubject, result.newThreadText, function() {
          $scope.clearActiveThread();
        });
    });
  };

  $scope.isSuggestionValid = function() {
    return ($scope.activeThread.status === 'open' &&
      explorationData.data.version === $scope.activeThread.suggestion.exploration_version);
  };

  $scope.hasUnsavedChanges = function() {
    return (changeListService.getChangeList().length > 0);
  };

  $scope.viewSuggestionBtnType = function () {
    return ($scope.isSuggestionValid() && !$scope.hasUnsavedChanges()
            ? 'primary' : 'default');
  };

  // TODO(Allan): Implement ability to edit suggestions before applying.
  $scope.showSuggestionModal = function() {
    $modal.open({
      templateUrl: 'modals/editorViewSuggestion',
      backdrop: true,
      size: 'lg',
      resolve: {
        suggestionInfo: function() {
          return {
            isSuggestionValid: $scope.isSuggestionValid(),
            hasUnsavedChanges: $scope.hasUnsavedChanges()
          };
        },
        stateContent: function() {
          var states = explorationData.data.states;
          var stateName = $scope.activeThread.suggestion.state_name;
          return {
            oldContent: states[stateName].content[0].value,
            newContent: $scope.activeThread.suggestion.state_content.value
          };
        }
      },
      controller: [
        '$scope', '$modalInstance', 'suggestionInfo', 'stateContent',
        function($scope, $modalInstance, suggestionInfo, stateContent) {
        var ACTION_INVALID_MSG = 'This suggestion has already been ' +
          'acted upon, or was made for an outdated version '  +
          'of this exploration.';
        var UNSAVED_CHANGES_MSG = 'You have unsaved changes to this ' +
          'exploration. Please save your changes before acting on ' +
          'a suggestion.';
        $scope.canActOnSuggestion = (
          suggestionInfo.isSuggestionValid && !suggestionInfo.hasUnsavedChanges
        );
        $scope.errorMessage = suggestionInfo.isSuggestionValid
          ? UNSAVED_CHANGES_MSG : ACTION_INVALID_MSG;
        $scope.oldContent = stateContent.oldContent;
        $scope.newContent = stateContent.newContent;
        $scope.commitMessage = '';

        $scope.acceptSuggestion = function() {
          $modalInstance.close({
            action: ACTION_ACCEPT_SUGGESTION,
            commitMsg: $scope.commitMessage
          });
        };

        $scope.rejectSuggestion = function() {
          $modalInstance.close({
            action: ACTION_REJECT_SUGGESTION
          });
        };

        $scope.cancelReview = function() {
          $modalInstance.dismiss();
        };
      }]
    }).result.then(function(result) {
      threadDataService.resolveSuggestion(
        $scope.activeThread.thread_id, result.action, result.commitMsg,
        function(res) {
          threadDataService.fetchThreads(function() {
            $scope.setActiveThread($scope.activeThread.thread_id);
          });
          // Immediately update editor to reflect accepted suggestion.
          if (result.action === ACTION_ACCEPT_SUGGESTION) {
            var suggestion = $scope.activeThread.suggestion;
            var stateName = suggestion.state_name;
            var state = explorationData.data.states[stateName];
            state.content[0].value = suggestion.state_content.value;
            explorationData.data.version += 1;
            explorationStatesService.setState(stateName, state);
            $rootScope.$broadcast('refreshStateEditor');
          }
        }, function(res) {
          console.log('Error resolving suggestion');
        });
    });
  };

  $scope.addNewMessage = function(threadId, tmpText, tmpStatus) {
    if (threadId === null) {
      warningsData.addWarning('Cannot add message to thread with ID: null.');
      return;
    }
    if (!tmpStatus) {
      warningsData.addWarning('Invalid message status: ' + tmpStatus);
      return;
    }

    $scope.messageSendingInProgress = true;
    threadDataService.addNewMessage(threadId, tmpText, tmpStatus, function() {
      _resetTmpMessageFields();
      $scope.messageSendingInProgress = false;
    }, function() {
      $scope.messageSendingInProgress = false;
    });
  };

  $scope.setActiveThread = function(threadId) {
    threadDataService.fetchMessages(threadId);

    var combined = [].concat(
      $scope.threadData.feedbackThreads, $scope.threadData.suggestionThreads);
    for (var i = 0; i < combined.length; i++) {
      if (combined[i].thread_id === threadId) {
        $scope.activeThread = combined[i];
        break;
      }
    }

    $scope.tmpMessage.status = $scope.activeThread.status;
  };

  // Initial load of the thread list on page load.
  $scope.clearActiveThread();
  threadDataService.fetchThreads(function() {
    $timeout(function() {
      $rootScope.loadingMessage = '';
    }, 500);
  });
}]);
<|endoftext|>