<commit_before>// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// @ts-nocheck
// TODO(crbug.com/1011811): Enable TypeScript compiler checks

import * as ObjectUI from '../object_ui/object_ui.js';
import * as Root from '../root/root.js';
import * as UI from '../ui/ui.js';

export class BreakpointEditDialog extends UI.Widget.Widget {
  /**
   * @param {number} editorLineNumber
   * @param {string} oldCondition
   * @param {boolean} preferLogpoint
   * @param {function({committed: boolean, condition: string})} onFinish
   */
  constructor(editorLineNumber, oldCondition, preferLogpoint, onFinish) {
    super(true);
    this.registerRequiredCSS('sources/breakpointEditDialog.css');
    this._onFinish = onFinish;
    this._finished = false;
    /** @type {?UI.TextEditor.TextEditor} */
    this._editor = null;
    this.element.tabIndex = -1;

    const logpointPrefix = LogpointPrefix;
    const logpointSuffix = LogpointSuffix;
    this._isLogpoint = oldCondition.startsWith(logpointPrefix) && oldCondition.endsWith(logpointSuffix);
    if (this._isLogpoint) {
      oldCondition = oldCondition.substring(logpointPrefix.length, oldCondition.length - logpointSuffix.length);
    }
    this._isLogpoint = this._isLogpoint || preferLogpoint;

    this.element.classList.add('sources-edit-breakpoint-dialog');
    const toolbar = new UI.Toolbar.Toolbar('source-frame-breakpoint-toolbar', this.contentElement);
    toolbar.appendText(`Line ${editorLineNumber + 1}:`);

    this._typeSelector = new UI.Toolbar.ToolbarComboBox(this._onTypeChanged.bind(this), ls`Breakpoint type`);
    this._typeSelector.createOption(ls`Breakpoint`, BreakpointType.Breakpoint);
    const conditionalOption = this._typeSelector.createOption(ls`Conditional breakpoint`, BreakpointType.Conditional);
    const logpointOption = this._typeSelector.createOption(ls`Logpoint`, BreakpointType.Logpoint);
    this._typeSelector.select(this._isLogpoint ? logpointOption : conditionalOption);
    toolbar.appendToolbarItem(this._typeSelector);

    Root.Runtime.Runtime.instance().extension(UI.TextEditor.TextEditorFactory).instance().then(factory => {
      const editorOptions = {lineNumbers: false, lineWrapping: true, mimeType: 'javascript', autoHeight: true};
      this._editor = factory.createEditor(editorOptions);
      this._updatePlaceholder();
      this._editor.widget().element.classList.add('condition-editor');
      this._editor.configureAutocomplete(
          ObjectUI.JavaScriptAutocomplete.JavaScriptAutocompleteConfig.createConfigForEditor(this._editor));
      if (oldCondition) {
        this._editor.setText(oldCondition);
      }
      this._editor.widget().markAsExternallyManaged();
      this._editor.widget().show(this.contentElement);
      this._editor.setSelection(this._editor.fullRange());
      this._editor.widget().focus();
      this._editor.widget().element.addEventListener('keydown', this._onKeyDown.bind(this), true);
      this.element.addEventListener('blur', event => {
        if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this.element)) {
          this._finishEditing(true);
        }
      }, true);
    });
  }

  /**
   * @param {string} condition
   * @return {string}
   */
  static _conditionForLogpoint(condition) {
    return `${LogpointPrefix}${condition}${LogpointSuffix}`;
  }

  _onTypeChanged() {
    const value = this._typeSelector.selectedOption().value;
    this._isLogpoint = value === BreakpointType.Logpoint;
    this._updatePlaceholder();
    if (value === BreakpointType.Breakpoint) {
      this._editor.setText('');
      this._finishEditing(true);
    }
  }

  _updatePlaceholder() {
    const selectedValue = this._typeSelector.selectedOption().value;
    if (selectedValue === BreakpointType.Conditional) {
      this._editor.setPlaceholder(ls`Expression to check before pausing, e.g. x > 5`);
      this._typeSelector.element.title = ls`Pause only when the condition is true`;
    } else if (selectedValue === BreakpointType.Logpoint) {
      this._editor.setPlaceholder(ls`Log message, e.g. 'x is', x`);
      this._typeSelector.element.title = ls`Log a message to Console, do not break`;
    }
  }

  /**
   * @param {boolean} committed
   */
  _finishEditing(committed) {
    if (this._finished) {
      return;
    }
    this._finished = true;
    this._editor.widget().detach();
    let condition = this._editor.text();
    if (this._isLogpoint) {
      condition = BreakpointEditDialog._conditionForLogpoint(condition);
    }
    this._onFinish({committed, condition});
  }

  /**
   * @param {!Event} event
   */
  async _onKeyDown(event) {
    if (isEnterKey(event) && !event.shiftKey) {
      event.consume(true);
      const expression = this._editor.text();
      if (event.ctrlKey ||
          await ObjectUI.JavaScriptAutocomplete.JavaScriptAutocomplete.isExpressionComplete(expression)) {
        this._finishEditing(true);
      } else {
        this._editor.newlineAndIndent();
      }
    }
    if (isEscKey(event)) {
      this._finishEditing(false);
      event.stopImmediatePropagation();
    }
  }
}

export const LogpointPrefix = '/** DEVTOOLS_LOGPOINT */ console.log(';
export const LogpointSuffix = ')';

export const BreakpointType = {
  Breakpoint: 'Breakpoint',
  Conditional: 'Conditional',
  Logpoint: 'Logpoint',
};
<commit_msg>TypeScriptify BreakpointEditDialog<commit_after>// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as ObjectUI from '../object_ui/object_ui.js';
import * as Root from '../root/root.js';
import * as UI from '../ui/ui.js';

export class BreakpointEditDialog extends UI.Widget.Widget {
  /**
   * @param {number} editorLineNumber
   * @param {string} oldCondition
   * @param {boolean} preferLogpoint
   * @param {function({committed: boolean, condition: string}): !Promise<void>} onFinish
   */
  constructor(editorLineNumber, oldCondition, preferLogpoint, onFinish) {
    super(true);
    this.registerRequiredCSS('sources/breakpointEditDialog.css');
    this._onFinish = onFinish;
    this._finished = false;
    /** @type {?UI.TextEditor.TextEditor} */
    this._editor = null;
    this.element.tabIndex = -1;

    const logpointPrefix = LogpointPrefix;
    const logpointSuffix = LogpointSuffix;
    this._isLogpoint = oldCondition.startsWith(logpointPrefix) && oldCondition.endsWith(logpointSuffix);
    if (this._isLogpoint) {
      oldCondition = oldCondition.substring(logpointPrefix.length, oldCondition.length - logpointSuffix.length);
    }
    this._isLogpoint = this._isLogpoint || preferLogpoint;

    this.element.classList.add('sources-edit-breakpoint-dialog');
    const toolbar = new UI.Toolbar.Toolbar('source-frame-breakpoint-toolbar', this.contentElement);
    toolbar.appendText(`Line ${editorLineNumber + 1}:`);

    this._typeSelector = new UI.Toolbar.ToolbarComboBox(this._onTypeChanged.bind(this), ls`Breakpoint type`);
    this._typeSelector.createOption(ls`Breakpoint`, BreakpointType.Breakpoint);
    const conditionalOption = this._typeSelector.createOption(ls`Conditional breakpoint`, BreakpointType.Conditional);
    const logpointOption = this._typeSelector.createOption(ls`Logpoint`, BreakpointType.Logpoint);
    this._typeSelector.select(this._isLogpoint ? logpointOption : conditionalOption);
    toolbar.appendToolbarItem(this._typeSelector);

    const extension = Root.Runtime.Runtime.instance().extension(UI.TextEditor.TextEditorFactory);
    if (extension) {
      extension.instance().then(factory => {
        const editorOptions = {
          lineNumbers: false,
          lineWrapping: true,
          mimeType: 'javascript',
          autoHeight: true,
          bracketMatchingSetting: undefined,
          devtoolsAccessibleName: undefined,
          padBottom: undefined,
          maxHighlightLength: undefined,
          placeholder: undefined,
          lineWiseCopyCut: undefined,
        };
        this._editor = /** @type {!UI.TextEditor.TextEditorFactory} */ (factory).createEditor(editorOptions);
        this._updatePlaceholder();
        this._editor.widget().element.classList.add('condition-editor');
        this._editor.configureAutocomplete(
            ObjectUI.JavaScriptAutocomplete.JavaScriptAutocompleteConfig.createConfigForEditor(this._editor));
        if (oldCondition) {
          this._editor.setText(oldCondition);
        }
        this._editor.widget().markAsExternallyManaged();
        this._editor.widget().show(this.contentElement);
        this._editor.setSelection(this._editor.fullRange());
        this._editor.widget().focus();
        this._editor.widget().element.addEventListener('keydown', this._onKeyDown.bind(this), true);
        this.element.addEventListener('blur', event => {
          if (event.relatedTarget && !/** @type {!Node} */ (event.relatedTarget).isSelfOrDescendant(this.element)) {
            this._finishEditing(true);
          }
        }, true);
      });
    }
  }

  /**
   * @param {string} condition
   * @return {string}
   */
  static _conditionForLogpoint(condition) {
    return `${LogpointPrefix}${condition}${LogpointSuffix}`;
  }

  _onTypeChanged() {
    const option = this._typeSelector.selectedOption();
    if (!option || !this._editor) {
      return;
    }
    const value = option.value;
    this._isLogpoint = value === BreakpointType.Logpoint;
    this._updatePlaceholder();
    if (value === BreakpointType.Breakpoint) {
      this._editor.setText('');
      this._finishEditing(true);
    }
  }

  _updatePlaceholder() {
    const option = this._typeSelector.selectedOption();
    if (!option || !this._editor) {
      return;
    }
    const selectedValue = option.value;
    if (selectedValue === BreakpointType.Conditional) {
      this._editor.setPlaceholder(ls`Expression to check before pausing, e.g. x > 5`);
      /** @type {!HTMLSpanElement} */ (this._typeSelector.element).title = ls`Pause only when the condition is true`;
    } else if (selectedValue === BreakpointType.Logpoint) {
      this._editor.setPlaceholder(ls`Log message, e.g. 'x is', x`);
      /** @type {!HTMLSpanElement} */ (this._typeSelector.element).title = ls`Log a message to Console, do not break`;
    }
  }

  /**
   * @param {boolean} committed
   */
  _finishEditing(committed) {
    if (this._finished) {
      return;
    }
    this._finished = true;
    if (!this._editor) {
      return;
    }
    this._editor.widget().detach();
    let condition = this._editor.text();
    if (this._isLogpoint) {
      condition = BreakpointEditDialog._conditionForLogpoint(condition);
    }
    this._onFinish({committed, condition});
  }

  /**
   * @param {!Event} event
   */
  async _onKeyDown(event) {
    if (!(event instanceof KeyboardEvent) || !this._editor) {
      return;
    }
    if (isEnterKey(event) && !event.shiftKey) {
      event.consume(true);
      const expression = this._editor.text();
      if (event.ctrlKey ||
          await ObjectUI.JavaScriptAutocomplete.JavaScriptAutocomplete.isExpressionComplete(expression)) {
        this._finishEditing(true);
      } else {
        this._editor.newlineAndIndent();
      }
    }
    if (isEscKey(event)) {
      this._finishEditing(false);
      event.stopImmediatePropagation();
    }
  }
}

export const LogpointPrefix = '/** DEVTOOLS_LOGPOINT */ console.log(';
export const LogpointSuffix = ')';

export const BreakpointType = {
  Breakpoint: 'Breakpoint',
  Conditional: 'Conditional',
  Logpoint: 'Logpoint',
};
<|endoftext|>


<commit_before>ihr.factory('playerFactory', ['$q', '$http', 'config', function($q, $http, config) {
	return {
		url: config.apiServer+'/players',
		getPlayers: function(){
			var deferred = $q.defer();
				$http.get(this.url).then(function(response) {
				// Check data is not null
				if(!response.data) {
					deferred.reject('Bad object');
				} else {
					deferred.resolve(response.data);
				}
			}, function(error) {
				deferred.reject('Error in request: ' + error.statusText);
			});

			return deferred.promise;
		},

	};
}]);<commit_msg>fix playerFactory<commit_after>ihr.factory('playerFactory', ['$q', '$http', 'config', function($q, $http, config) {
	return {
		url: config.apiServer+'/players',
		getPlayers: function(){
			var deferred = $q.defer();
				$http.get(this.url).then(function(response) {
				// Check data is not null
				if(!response.data) {
					deferred.reject('Bad object');
				} else {
					deferred.resolve(response.data);
				}
			}, function(error) {
				deferred.reject('Error in request: ' + error.statusText);
			});

			return deferred.promise;
		},
		updatePlayer: function(){
			var deferred = $q.defer();

			$http.post(this.url + '/' + player.id, player).then(function(responseData) {
				if(responseData.data.success) {
					deferred.resolve(responseData);
				} else {
					deferred.reject(responseData.data.error);
				}
			}, function (error) {
				deferred.reject('Error in update: ' + error.responseData.statusText);
			});
			return deferred.promise;
		},
	};
}]);<|endoftext|>


<commit_before>import Ember from 'ember';
const {
  $,
  run,
  computed,
  computed: { alias, sort }
} = Ember;

export default Ember.Component.extend({
  remodal: Ember.inject.service(),
  isFullyEditable: alias('surveyTemplate.fullyEditable'),
  showAnswerChoices: alias('question.answerType.hasAnswerChoices'),
  sortTypesBy: ['displayName'],
  sortAnswerChoicesBy: ['sortOrder'],
  sortedAnswerTypes: sort('filteredAnswerTypes', 'sortTypesBy'),
  sortedAnswerChoices: sort('question.answerChoices', 'sortAnswerChoicesBy'),
  filteredAnswerTypes: computed('answerTypes', function() {
    let answerTypes = this.get('answerTypes');
    if(this.get('isSuperUser')){
      return answerTypes;
    }else{
      return answerTypes.filter((answerType) => {
        return !answerType.get('name').includes('taxon');
      });
    }
  }),
  ancestryQuestions: computed('questions', function() {
    return this.get('questions').filter((question) => {
      let allowedTypes = ['section','repeater'];
      return allowedTypes.includes(question.get('answerType.name'));
    });
  }),
  ancestryQuestion: computed('question.parentId', function() {
    return this.get('questions').findBy('id',this.get('question.parentId'));
  }),

  isRequiredDisabled: computed('question.{rule.isNew,answerType.name}','conditionsPendingSave.[]', function() {
    let question = this.get('question'),
        newRule = question.get('rule.isNew'),
        notTypes = ['section','repeater','helperabove','helperbelow'],
        pendingConditions = this.get('conditionsPendingSave.length') > 0;
    if(newRule === undefined){
      newRule = true;
    }
    let isDisabled = !newRule || pendingConditions || notTypes.includes(question.get('answerType.name'));
    if(isDisabled){
      run.later(this, ()=> { this.set('question.required', false); }, 0);
    }
    return isDisabled;
  }),

  showQuestionTypePlaceholder: computed('question.{isNew,answerType.id}', function() {
    let question = this.get('question');
    return question.get('isNew') && question.get('answerType.id') === undefined;
  }),

  init() {
    this._super(...arguments);
    this.setProperties({
      answerChoicesPendingSave: [],
      conditionsPendingSave: []
    });
  },

  didInsertElement() {
    this._super(...arguments);
    Ember.run.scheduleOnce('afterRender', this, function () {
      this.get('remodal').open('question-modal');
    });
    // Tabs
    $('a[data-toggle="tab"]').on('click', function(e) {
      e.preventDefault();
      $(this).tab('show');
    });
  },

  _removeAnswerChoices() {
    this.get('question.answerChoices').then((answerChoices)=>{
      answerChoices.forEach(function(answerChoice) {
        answerChoice.deleteRecord();
        answerChoice.save();
      });
    });
  },

  _pendingObjectsPromises(pendingObjects, toSet, objTo){
    for (var pendingObject of pendingObjects) {
      pendingObject.set(toSet, objTo);
    }
    return pendingObjects.invoke('save');
  },

  _saveSuccess(question, promises, keepOpen){
    question.reload().then((question)=>{
      let answerChoicesPendingSave = this.get('answerChoicesPendingSave'),
          conditionsPendingSave = this.get('conditionsPendingSave');

      if(!question.get('answerType.hasAnswerChoices')){
        this._removeAnswerChoices();
      }
      promises = $.makeArray(promises);
      Ember.RSVP.all(promises).then(()=>{
        while (answerChoicesPendingSave.length > 0) {
          answerChoicesPendingSave.popObject();
        }
        while (conditionsPendingSave.length > 0) {
          conditionsPendingSave.popObject();
        }
        if(keepOpen){
          if(!question.get('rule')){
            let store = question.get('store');
            question.set('rule', store.createRecord('rule'));
          }
        }else{
          this.send('closeModal');
        }
      });
    });
  },

  _sortOrder(question){
    let surveyTemplate = this.get('surveyTemplate'),
        lastQuestion = surveyTemplate.get('questions').sortBy('sortOrder').get('lastObject');
    question.set('sortOrder',1);
    if(lastQuestion && lastQuestion !== question){
      question.set('sortOrder',lastQuestion.get('sortOrder') + 1);
    }
  },

  actions: {
    sortAnswerChoices(){
      let answerChoices = this.get('question.answerChoices');
      answerChoices.forEach((answerChoice, index) => {
        let oldSortOrder = answerChoice.get('sortOrder'),
            newSortOrder = index + 1;
        if(oldSortOrder !== newSortOrder){
          answerChoice.set('sortOrder', newSortOrder);
          if(!answerChoice.get('isNew')){
            answerChoice.save();
          }
        }
      });
    },

    ancestryChange(newAncestryId){
      let question = this.get('question');
      if(Ember.isBlank(newAncestryId)){
        newAncestryId = null;
      }
      question.set('parentId',newAncestryId);
    },

    setAnswerType(answerTypeId) {
      const answerType = this.get('answerTypes').findBy('id', answerTypeId);
      this.set('question.answerType', answerType);
    },

    setRuleMatchType(matchType) {
      this.set('question.rule.matchType', matchType);
    },

    save(_e, keepOpen = false) {
      let question = this.get('question'),
          surveyTemplate = this.get('surveyTemplate');
      question.set('surveyTemplate', surveyTemplate);
      if(question.validate()){
        if(question.get('isNew')){
          question.set('wasNew', true);
          this._sortOrder(question);
        }
        question.save().then(
          (question)=>{
            let promises = [],
                answerChoicesPendingSave = this.get('answerChoicesPendingSave'),
                answerChoicesPromises = this._pendingObjectsPromises(answerChoicesPendingSave, 'question', question);

            promises = promises.concat(answerChoicesPromises);

            // We can't save the rule until there is at least one condition associated with the rule
            if(question.get('rule') && (!question.get('rule.isNew') || this.get('conditionsPendingSave.length') > 0)){
              let conditionsPendingSave = this.get('conditionsPendingSave'),
                  rule = question.get('rule');
              rule.set('question',question);
              rule.save().then(
                (rule) =>{
                  let conditionsPromises = this._pendingObjectsPromises(conditionsPendingSave, 'rule', rule);
                  promises = promises.concat(conditionsPromises);
                  this._saveSuccess(question, promises, keepOpen);
                },
                // Rule was deleted on the server
                () =>{
                  question.get('store').unloadRecord(rule);
                  question.reload().then((question)=>{
                    this._saveSuccess(question, [], keepOpen);
                  });
                }
              );
            }else{
              this._saveSuccess(question, promises, keepOpen);
            }
          },
          (error)=>{
            console.error(`An error has occured: ${error}.`);
            surveyTemplate.get('questions').removeObject(question);
          }
        );
      }
    },

    saveCondition(condition){
      if(this.get('question.isNew') || this.get('question.rule.isNew')){
        if(this.get('conditionsPendingSave').indexOf(condition) === -1){
          this.get('conditionsPendingSave').pushObject(condition);
        }
      }else{
        condition.save();
      }
    },

    removeCondition(condition){
      if(this.get('question.rule.isNew') || condition.get('isNew')){
        this.get('conditionsPendingSave').removeObject(condition);
        condition.deleteRecord();
      }else{
        condition.deleteRecord();
        condition.save();
      }
    },

    saveAnswerChoice(answerChoice){
      if(this.get('question.isNew')){
        if(this.get('answerChoicesPendingSave').indexOf(answerChoice) === -1){
          this.get('answerChoicesPendingSave').pushObject(answerChoice);
        }
      }else{
        answerChoice.save();
      }
    },

    closeModal() {
      if(this.get('question.wasNew')){
        run.later(this, ()=> { $('html, body').animate({ scrollTop: $(document).height() }, 500); }, 500);
      }
      this.get('remodal').close('question-modal');
      this.sendAction('transitionToSurveyStep');
    }
  }
});
<commit_msg>add back popover initialization that got deleted<commit_after>import Ember from 'ember';
const {
  $,
  run,
  computed,
  computed: { alias, sort }
} = Ember;

export default Ember.Component.extend({
  remodal: Ember.inject.service(),
  isFullyEditable: alias('surveyTemplate.fullyEditable'),
  showAnswerChoices: alias('question.answerType.hasAnswerChoices'),
  sortTypesBy: ['displayName'],
  sortAnswerChoicesBy: ['sortOrder'],
  sortedAnswerTypes: sort('filteredAnswerTypes', 'sortTypesBy'),
  sortedAnswerChoices: sort('question.answerChoices', 'sortAnswerChoicesBy'),
  filteredAnswerTypes: computed('answerTypes', function() {
    let answerTypes = this.get('answerTypes');
    if(this.get('isSuperUser')){
      return answerTypes;
    }else{
      return answerTypes.filter((answerType) => {
        return !answerType.get('name').includes('taxon');
      });
    }
  }),
  ancestryQuestions: computed('questions', function() {
    return this.get('questions').filter((question) => {
      let allowedTypes = ['section','repeater'];
      return allowedTypes.includes(question.get('answerType.name'));
    });
  }),
  ancestryQuestion: computed('question.parentId', function() {
    return this.get('questions').findBy('id',this.get('question.parentId'));
  }),

  isRequiredDisabled: computed('question.{rule.isNew,answerType.name}','conditionsPendingSave.[]', function() {
    let question = this.get('question'),
        newRule = question.get('rule.isNew'),
        notTypes = ['section','repeater','helperabove','helperbelow'],
        pendingConditions = this.get('conditionsPendingSave.length') > 0;
    if(newRule === undefined){
      newRule = true;
    }
    let isDisabled = !newRule || pendingConditions || notTypes.includes(question.get('answerType.name'));
    if(isDisabled){
      run.later(this, ()=> { this.set('question.required', false); }, 0);
    }
    return isDisabled;
  }),

  showQuestionTypePlaceholder: computed('question.{isNew,answerType.id}', function() {
    let question = this.get('question');
    return question.get('isNew') && question.get('answerType.id') === undefined;
  }),

  init() {
    this._super(...arguments);
    this.setProperties({
      answerChoicesPendingSave: [],
      conditionsPendingSave: []
    });
  },

  didInsertElement() {
    this._super(...arguments);
    Ember.run.scheduleOnce('afterRender', this, function () {
      this.get('remodal').open('question-modal');
      $('[data-toggle="popover"]').popover({});

    });
    // Tabs
    $('a[data-toggle="tab"]').on('click', function(e) {
      e.preventDefault();
      $(this).tab('show');
    });
  },

  _removeAnswerChoices() {
    this.get('question.answerChoices').then((answerChoices)=>{
      answerChoices.forEach(function(answerChoice) {
        answerChoice.deleteRecord();
        answerChoice.save();
      });
    });
  },

  _pendingObjectsPromises(pendingObjects, toSet, objTo){
    for (var pendingObject of pendingObjects) {
      pendingObject.set(toSet, objTo);
    }
    return pendingObjects.invoke('save');
  },

  _saveSuccess(question, promises, keepOpen){
    question.reload().then((question)=>{
      let answerChoicesPendingSave = this.get('answerChoicesPendingSave'),
          conditionsPendingSave = this.get('conditionsPendingSave');

      if(!question.get('answerType.hasAnswerChoices')){
        this._removeAnswerChoices();
      }
      promises = $.makeArray(promises);
      Ember.RSVP.all(promises).then(()=>{
        while (answerChoicesPendingSave.length > 0) {
          answerChoicesPendingSave.popObject();
        }
        while (conditionsPendingSave.length > 0) {
          conditionsPendingSave.popObject();
        }
        if(keepOpen){
          if(!question.get('rule')){
            let store = question.get('store');
            question.set('rule', store.createRecord('rule'));
          }
        }else{
          this.send('closeModal');
        }
      });
    });
  },

  _sortOrder(question){
    let surveyTemplate = this.get('surveyTemplate'),
        lastQuestion = surveyTemplate.get('questions').sortBy('sortOrder').get('lastObject');
    question.set('sortOrder',1);
    if(lastQuestion && lastQuestion !== question){
      question.set('sortOrder',lastQuestion.get('sortOrder') + 1);
    }
  },

  actions: {
    sortAnswerChoices(){
      let answerChoices = this.get('question.answerChoices');
      answerChoices.forEach((answerChoice, index) => {
        let oldSortOrder = answerChoice.get('sortOrder'),
            newSortOrder = index + 1;
        if(oldSortOrder !== newSortOrder){
          answerChoice.set('sortOrder', newSortOrder);
          if(!answerChoice.get('isNew')){
            answerChoice.save();
          }
        }
      });
    },

    ancestryChange(newAncestryId){
      let question = this.get('question');
      if(Ember.isBlank(newAncestryId)){
        newAncestryId = null;
      }
      question.set('parentId',newAncestryId);
    },

    setAnswerType(answerTypeId) {
      const answerType = this.get('answerTypes').findBy('id', answerTypeId);
      this.set('question.answerType', answerType);
    },

    setRuleMatchType(matchType) {
      this.set('question.rule.matchType', matchType);
    },

    save(_e, keepOpen = false) {
      let question = this.get('question'),
          surveyTemplate = this.get('surveyTemplate');
      question.set('surveyTemplate', surveyTemplate);
      if(question.validate()){
        if(question.get('isNew')){
          question.set('wasNew', true);
          this._sortOrder(question);
        }
        question.save().then(
          (question)=>{
            let promises = [],
                answerChoicesPendingSave = this.get('answerChoicesPendingSave'),
                answerChoicesPromises = this._pendingObjectsPromises(answerChoicesPendingSave, 'question', question);

            promises = promises.concat(answerChoicesPromises);

            // We can't save the rule until there is at least one condition associated with the rule
            if(question.get('rule') && (!question.get('rule.isNew') || this.get('conditionsPendingSave.length') > 0)){
              let conditionsPendingSave = this.get('conditionsPendingSave'),
                  rule = question.get('rule');
              rule.set('question',question);
              rule.save().then(
                (rule) =>{
                  let conditionsPromises = this._pendingObjectsPromises(conditionsPendingSave, 'rule', rule);
                  promises = promises.concat(conditionsPromises);
                  this._saveSuccess(question, promises, keepOpen);
                },
                // Rule was deleted on the server
                () =>{
                  question.get('store').unloadRecord(rule);
                  question.reload().then((question)=>{
                    this._saveSuccess(question, [], keepOpen);
                  });
                }
              );
            }else{
              this._saveSuccess(question, promises, keepOpen);
            }
          },
          (error)=>{
            console.error(`An error has occured: ${error}.`);
            surveyTemplate.get('questions').removeObject(question);
          }
        );
      }
    },

    saveCondition(condition){
      if(this.get('question.isNew') || this.get('question.rule.isNew')){
        if(this.get('conditionsPendingSave').indexOf(condition) === -1){
          this.get('conditionsPendingSave').pushObject(condition);
        }
      }else{
        condition.save();
      }
    },

    removeCondition(condition){
      if(this.get('question.rule.isNew') || condition.get('isNew')){
        this.get('conditionsPendingSave').removeObject(condition);
        condition.deleteRecord();
      }else{
        condition.deleteRecord();
        condition.save();
      }
    },

    saveAnswerChoice(answerChoice){
      if(this.get('question.isNew')){
        if(this.get('answerChoicesPendingSave').indexOf(answerChoice) === -1){
          this.get('answerChoicesPendingSave').pushObject(answerChoice);
        }
      }else{
        answerChoice.save();
      }
    },

    closeModal() {
      if(this.get('question.wasNew')){
        run.later(this, ()=> { $('html, body').animate({ scrollTop: $(document).height() }, 500); }, 500);
      }
      this.get('remodal').close('question-modal');
      this.sendAction('transitionToSurveyStep');
    }
  }
});
<|endoftext|>


<commit_before>pc.extend(pc, function () {
    /**
     * @component
     * @name pc.LightComponent
     * @class The Light Component enables the Entity to light the scene. There are three types
     * of light: directional, point and spot. Directional lights are global in that they are
     * considered to be infinitely far away and light the entire scene. Point and spot lights
     * are local in that they have a position and a range. A spot light is a specialization of
     * a point light where light is emitted in a cone rather than in all directions. Lights
     * also have the ability to cast shadows to add realism to your scenes.
     * @description Creates a new Light Component.
     * @param {pc.LightComponentSystem} system The ComponentSystem that created this Component.
     * @param {pc.Entity} entity The Entity that this Component is attached to.
     * @example
     * // Add a pc.LightComponent to an entity
     * var entity = new pc.Entity();
     * entity.addComponent('light', {
     *     type: "point",
     *     color: new pc.Color(1, 0, 0),
     *     range: 10
     * });
     * @example
     * // Get the pc.LightComponent on an entity
     * var lightComponent = entity.light;
     * @example
     * // Update a property on a light component
     * entity.light.range = 20;
     * @property {String} type The type of light. Can be:
     *

     *
"directional": A light that is infinitely far away and lights the entire scene from one direction.

     *
"point": A light that illuminates in all directions from a point.

     *
"spot": A light that illuminates in all directions from a point and is bounded by a cone.

     *

     * Defaults to "directional".
     * @property {pc.Color} color The Color of the light. The alpha component of the color is
     * ignored. Defaults to white (1, 1, 1).
     * @property {Number} intensity The brightness of the light. Defaults to 1.
     * @property {Boolean} castShadows If enabled the light will cast shadows. Defaults to false.
     * @property {Number} shadowDistance The distance from the viewpoint beyond which shadows
     * are no longer rendered. Affects directional lights only. Defaults to 40.
     * @property {Number} shadowResolution The size of the texture used for the shadow map.
     * Valid sizes are 64, 128, 256, 512, 1024, 2048. Defaults to 1024.
     * @property {Number} shadowBias The depth bias for tuning the appearance of the shadow
     * mapping generated by this light. Defaults to 0.05.
     * @property {Number} normalOffsetBias Normal offset depth bias. Defaults to 0.
     * @property {Number} range The range of the light. Affects point and spot lights only.
     * Defaults to 10.
     * @property {Number} innerConeAngle The angle at which the spotlight cone starts
     * to fade off. The angle is specified in degrees. Affects spot lights only. Defaults
     * to 40.
     * @property {Number} outerConeAngle The angle at which the spotlight cone has faded
     * to nothing. The angle is specified in degrees. Affects spot lights only. Defaults
     * to 45.
     * @property {Number} falloffMode Controls the rate at which a light attentuates from
     * its position. Can be:
     *

     *
{@link pc.LIGHTFALLOFF_LINEAR}: Linear.

     *
{@link pc.LIGHTFALLOFF_INVERSESQUARED}: Inverse squared.

     *

     * Affects point and spot lights only. Defaults to pc.LIGHTFALLOFF_LINEAR.
     * @property {Number} mask Defines a mask to determine which {@link pc.MeshInstance}s are
     * lit by this light. Defaults to 1.
     * @property {Boolean} dynamic If enabled the light will affect non-lightmapped objects
     * @property {Boolean} baked If enabled the light will affect lightmapped objects
     * @property {Boolean} lightMap If enabled the light will be rendered into lightMaps
     * @extends pc.Component
     */
    var LightComponent = function LightComponent(system, entity) {
        this.on("set_type", this.onSetType, this);
        this.on("set_color", this.onSetColor, this);
        this.on("set_intensity", this.onSetIntensity, this);
        this.on("set_castShadows", this.onSetCastShadows, this);
        this.on("set_shadowDistance", this.onSetShadowDistance, this);
        this.on("set_shadowResolution", this.onSetShadowResolution, this);
        this.on("set_shadowBias", this.onSetShadowBias, this);
        this.on("set_normalOffsetBias", this.onSetNormalOffsetBias, this);
        this.on("set_range", this.onSetRange, this);
        this.on("set_innerConeAngle", this.onSetInnerConeAngle, this);
        this.on("set_outerConeAngle", this.onSetOuterConeAngle, this);
        this.on("set_falloffMode", this.onSetFalloffMode, this);
        this.on("set_shadowType", this.onSetShadowType, this);
        this.on("set_shadowUpdateMode", this.onSetShadowUpdateMode, this);
        this.on("set_mask", this.onSetMask, this);
        this.on("set_dynamic", this.onSetDynamic, this);
        this.on("set_baked", this.onSetBaked, this);
        this.on("set_lightMap", this.onSetLightMap, this);
    };

    LightComponent = pc.inherits(LightComponent, pc.Component);

    Object.defineProperty(LightComponent.prototype, "enable", {
        get: function() {
            console.warn("WARNING: enable: Property is deprecated. Query enabled property instead.");
            return this.enabled;
        },
        set: function(value) {
            console.warn("WARNING: enable: Property is deprecated. Set enabled property instead.");
            this.enabled = value;
        },
    });

    pc.extend(LightComponent.prototype, {
        onSetType: function (name, oldValue, newValue) {
            if (oldValue === newValue)
                return;

            this.system.changeType(this, oldValue, newValue);

            // refresh light properties because changing the type does not reset the
            // light properties
            this.refreshProperties();
        },

        refreshProperties: function() {
            this.onSetCastShadows("castShadows", this.castShadows, this.castShadows);
            this.onSetColor("color", this.color, this.color);
            this.onSetIntensity("intensity", this.intensity, this.intensity);
            this.onSetShadowDistance("shadowDistance", this.shadowDistance, this.shadowDistance);
            this.onSetShadowResolution("shadowResolution", this.shadowResolution, this.shadowResolution);
            this.onSetShadowBias("shadowBias", this.shadowBias, this.shadowBias);
            this.onSetNormalOffsetBias("normalOffsetBias", this.normalOffsetBias, this.normalOffsetBias);
            this.onSetRange("range", this.range, this.range);
            this.onSetInnerConeAngle("innerConeAngle", this.innerConeAngle, this.innerConeAngle);
            this.onSetOuterConeAngle("outerConeAngle", this.outerConeAngle, this.outerConeAngle);
            this.onSetFalloffMode("falloffMode", this.falloffMode, this.falloffMode);
            this.onSetShadowType("shadowType", this.shadowType, this.shadowType);
            this.onSetShadowUpdateMode("shadowUpdateMode", this.shadowUpdateMode, this.shadowUpdateMode);
            this.onSetMask("mask", this.mask, this.mask);
            this.onSetDynamic("dynamic", this.dynamic, this.dynamic);
            this.onSetBaked("baked", this.baked, this.baked);
            this.onSetLightMap("lightMap", this.lightMap, this.lightMap);

            if (this.enabled && this.entity.enabled)
                this.onEnable();
        },

        updateShadow: function() {
            this.light.updateShadow();
        },

        onSetCastShadows: function (name, oldValue, newValue) {
            this.light.setCastShadows(newValue);
        },

        onSetColor: function (name, oldValue, newValue) {
            this.light.setColor(newValue);
        },

        onSetIntensity: function (name, oldValue, newValue) {
            this.light.setIntensity(newValue);
        },

        onSetShadowDistance: function (name, oldValue, newValue) {
            if (this.data.type !== 'directional')
                return;

            this.light.setShadowDistance(newValue);
        },

        onSetShadowResolution: function (name, oldValue, newValue) {
            this.light.setShadowResolution(newValue);
        },

        onSetShadowBias: function (name, oldValue, newValue) {
            // remap the value to the range needed by the shaders
            this.light.setShadowBias(-0.01 * newValue);
        },

        onSetNormalOffsetBias: function (name, oldValue, newValue) {
            this.light.setNormalOffsetBias(newValue);
        },

        onSetRange: function (name, oldValue, newValue) {
            if (this.data.type !== 'point' && this.data.type !== 'spot')
                return;

            this.light.setAttenuationEnd(newValue);
        },

        onSetInnerConeAngle: function (name, oldValue, newValue) {
            if (this.data.type !== 'spot')
                return;

            this.light.setInnerConeAngle(newValue);
        },

        onSetOuterConeAngle: function (name, oldValue, newValue) {
            if (this.data.type !== 'spot')
                return;

            this.light.setOuterConeAngle(newValue);
        },

        onSetFalloffMode: function (name, oldValue, newValue) {
            if (this.data.type !== 'point' && this.data.type !== 'spot')
                return;

            this.light.setFalloffMode(newValue);
        },

        onSetShadowType: function (name, oldValue, newValue) {
            this.light.setShadowType(newValue);
        },

        onSetShadowUpdateMode: function (name, oldValue, newValue) {
            this.light.shadowUpdateMode = newValue;
        },

        onSetMask: function (name, oldValue, newValue) {
            this.light.setMask(newValue);
        },

        onSetDynamic: function (name, oldValue, newValue) {
            if (!oldValue && newValue) {
                this.light.mask |= pc.MASK_DYNAMIC;
            } else if (oldValue && !newValue) {
                this.light.mask &= ~pc.MASK_DYNAMIC;
            }
        },

        onSetBaked: function (name, oldValue, newValue) {

            if ((this.light.mask & pc.MASK_LIGHTMAP)!==0) newValue = false;

            if (!oldValue && newValue) {
                this.light.mask |= pc.MASK_BAKED;
            } else if (oldValue && !newValue) {
                this.light.mask &= ~pc.MASK_BAKED;
            }
        },

        onSetLightMap: function (name, oldValue, newValue) {
            if (!oldValue && newValue) {
                this.light.mask |= pc.MASK_LIGHTMAP;
            } else if (oldValue && !newValue) {
                this.light.mask &= ~pc.MASK_LIGHTMAP;
            }

            if (newValue && (this.light.mask & pc.MASK_BAKED)!==0) this.baked = false;
        },

        onEnable: function () {
            LightComponent._super.onEnable.call(this);

            this.light.setEnabled(true);

            var model = this.data.model;
            if (! model)
                return;

            var scene = this.system.app.scene;
            if (scene.containsModel(model))
                return;

            scene.addModel(model);
        },

        onDisable: function () {
            LightComponent._super.onDisable.call(this);

            this.light.setEnabled(false);

            var model = this.data.model;
            if (! model)
                return;

            this.system.app.scene.removeModel(model);
        }
    });

    return {
        LightComponent: LightComponent
    };
}());
<commit_msg>fixes4<commit_after>pc.extend(pc, function () {
    /**
     * @component
     * @name pc.LightComponent
     * @class The Light Component enables the Entity to light the scene. There are three types
     * of light: directional, point and spot. Directional lights are global in that they are
     * considered to be infinitely far away and light the entire scene. Point and spot lights
     * are local in that they have a position and a range. A spot light is a specialization of
     * a point light where light is emitted in a cone rather than in all directions. Lights
     * also have the ability to cast shadows to add realism to your scenes.
     * @description Creates a new Light Component.
     * @param {pc.LightComponentSystem} system The ComponentSystem that created this Component.
     * @param {pc.Entity} entity The Entity that this Component is attached to.
     * @example
     * // Add a pc.LightComponent to an entity
     * var entity = new pc.Entity();
     * entity.addComponent('light', {
     *     type: "point",
     *     color: new pc.Color(1, 0, 0),
     *     range: 10
     * });
     * @example
     * // Get the pc.LightComponent on an entity
     * var lightComponent = entity.light;
     * @example
     * // Update a property on a light component
     * entity.light.range = 20;
     * @property {String} type The type of light. Can be:
     *

     *
"directional": A light that is infinitely far away and lights the entire scene from one direction.

     *
"point": A light that illuminates in all directions from a point.

     *
"spot": A light that illuminates in all directions from a point and is bounded by a cone.

     *

     * Defaults to "directional".
     * @property {pc.Color} color The Color of the light. The alpha component of the color is
     * ignored. Defaults to white (1, 1, 1).
     * @property {Number} intensity The brightness of the light. Defaults to 1.
     * @property {Boolean} castShadows If enabled the light will cast shadows. Defaults to false.
     * @property {Number} shadowDistance The distance from the viewpoint beyond which shadows
     * are no longer rendered. Affects directional lights only. Defaults to 40.
     * @property {Number} shadowResolution The size of the texture used for the shadow map.
     * Valid sizes are 64, 128, 256, 512, 1024, 2048. Defaults to 1024.
     * @property {Number} shadowBias The depth bias for tuning the appearance of the shadow
     * mapping generated by this light. Defaults to 0.05.
     * @property {Number} normalOffsetBias Normal offset depth bias. Defaults to 0.
     * @property {Number} range The range of the light. Affects point and spot lights only.
     * Defaults to 10.
     * @property {Number} innerConeAngle The angle at which the spotlight cone starts
     * to fade off. The angle is specified in degrees. Affects spot lights only. Defaults
     * to 40.
     * @property {Number} outerConeAngle The angle at which the spotlight cone has faded
     * to nothing. The angle is specified in degrees. Affects spot lights only. Defaults
     * to 45.
     * @property {Number} falloffMode Controls the rate at which a light attentuates from
     * its position. Can be:
     *

     *
{@link pc.LIGHTFALLOFF_LINEAR}: Linear.

     *
{@link pc.LIGHTFALLOFF_INVERSESQUARED}: Inverse squared.

     *

     * Affects point and spot lights only. Defaults to pc.LIGHTFALLOFF_LINEAR.
     * @property {Number} mask Defines a mask to determine which {@link pc.MeshInstance}s are
     * lit by this light. Defaults to 1.
     * @property {Boolean} dynamic If enabled the light will affect non-lightmapped objects
     * @property {Boolean} baked If enabled the light will affect lightmapped objects
     * @property {Boolean} lightMap If enabled the light will be rendered into lightMaps
     * @extends pc.Component
     */
    var LightComponent = function LightComponent(system, entity) {
        this.on("set_type", this.onSetType, this);
        this.on("set_color", this.onSetColor, this);
        this.on("set_intensity", this.onSetIntensity, this);
        this.on("set_castShadows", this.onSetCastShadows, this);
        this.on("set_shadowDistance", this.onSetShadowDistance, this);
        this.on("set_shadowResolution", this.onSetShadowResolution, this);
        this.on("set_shadowBias", this.onSetShadowBias, this);
        this.on("set_normalOffsetBias", this.onSetNormalOffsetBias, this);
        this.on("set_range", this.onSetRange, this);
        this.on("set_innerConeAngle", this.onSetInnerConeAngle, this);
        this.on("set_outerConeAngle", this.onSetOuterConeAngle, this);
        this.on("set_falloffMode", this.onSetFalloffMode, this);
        this.on("set_shadowType", this.onSetShadowType, this);
        this.on("set_shadowUpdateMode", this.onSetShadowUpdateMode, this);
        this.on("set_mask", this.onSetMask, this);
        this.on("set_dynamic", this.onSetDynamic, this);
        this.on("set_baked", this.onSetBaked, this);
        this.on("set_lightMap", this.onSetLightMap, this);
    };

    LightComponent = pc.inherits(LightComponent, pc.Component);

    Object.defineProperty(LightComponent.prototype, "enable", {
        get: function() {
            console.warn("WARNING: enable: Property is deprecated. Query enabled property instead.");
            return this.enabled;
        },
        set: function(value) {
            console.warn("WARNING: enable: Property is deprecated. Set enabled property instead.");
            this.enabled = value;
        },
    });

    pc.extend(LightComponent.prototype, {
        onSetType: function (name, oldValue, newValue) {
            if (oldValue === newValue)
                return;

            this.system.changeType(this, oldValue, newValue);

            // refresh light properties because changing the type does not reset the
            // light properties
            this.refreshProperties();
        },

        refreshProperties: function() {
            this.onSetCastShadows("castShadows", this.castShadows, this.castShadows);
            this.onSetColor("color", this.color, this.color);
            this.onSetIntensity("intensity", this.intensity, this.intensity);
            this.onSetShadowDistance("shadowDistance", this.shadowDistance, this.shadowDistance);
            this.onSetShadowResolution("shadowResolution", this.shadowResolution, this.shadowResolution);
            this.onSetShadowBias("shadowBias", this.shadowBias, this.shadowBias);
            this.onSetNormalOffsetBias("normalOffsetBias", this.normalOffsetBias, this.normalOffsetBias);
            this.onSetRange("range", this.range, this.range);
            this.onSetInnerConeAngle("innerConeAngle", this.innerConeAngle, this.innerConeAngle);
            this.onSetOuterConeAngle("outerConeAngle", this.outerConeAngle, this.outerConeAngle);
            this.onSetFalloffMode("falloffMode", this.falloffMode, this.falloffMode);
            this.onSetShadowType("shadowType", this.shadowType, this.shadowType);
            this.onSetShadowUpdateMode("shadowUpdateMode", this.shadowUpdateMode, this.shadowUpdateMode);
            this.onSetMask("mask", this.mask, this.mask);
            this.onSetDynamic("dynamic", this.dynamic, this.dynamic);
            this.onSetBaked("baked", this.baked, this.baked);
            this.onSetLightMap("lightMap", this.lightMap, this.lightMap);

            if (this.enabled && this.entity.enabled)
                this.onEnable();
        },

        updateShadow: function() {
            this.light.updateShadow();
        },

        onSetCastShadows: function (name, oldValue, newValue) {
            this.light.setCastShadows(newValue);
        },

        onSetColor: function (name, oldValue, newValue) {
            this.light.setColor(newValue);
        },

        onSetIntensity: function (name, oldValue, newValue) {
            this.light.setIntensity(newValue);
        },

        onSetShadowDistance: function (name, oldValue, newValue) {
            if (this.data.type !== 'directional')
                return;

            this.light.setShadowDistance(newValue);
        },

        onSetShadowResolution: function (name, oldValue, newValue) {
            this.light.setShadowResolution(newValue);
        },

        onSetShadowBias: function (name, oldValue, newValue) {
            // remap the value to the range needed by the shaders
            this.light.setShadowBias(-0.01 * newValue);
        },

        onSetNormalOffsetBias: function (name, oldValue, newValue) {
            this.light.setNormalOffsetBias(newValue);
        },

        onSetRange: function (name, oldValue, newValue) {
            if (this.data.type !== 'point' && this.data.type !== 'spot')
                return;

            this.light.setAttenuationEnd(newValue);
        },

        onSetInnerConeAngle: function (name, oldValue, newValue) {
            if (this.data.type !== 'spot')
                return;

            this.light.setInnerConeAngle(newValue);
        },

        onSetOuterConeAngle: function (name, oldValue, newValue) {
            if (this.data.type !== 'spot')
                return;

            this.light.setOuterConeAngle(newValue);
        },

        onSetFalloffMode: function (name, oldValue, newValue) {
            if (this.data.type !== 'point' && this.data.type !== 'spot')
                return;

            this.light.setFalloffMode(newValue);
        },

        onSetShadowType: function (name, oldValue, newValue) {
            this.light.setShadowType(newValue);
        },

        onSetShadowUpdateMode: function (name, oldValue, newValue) {
            this.light.shadowUpdateMode = newValue;
        },

        onSetMask: function (name, oldValue, newValue) {
            this.light.setMask(newValue);
        },

        onSetDynamic: function (name, oldValue, newValue) {
            if (!oldValue && newValue) {
                this.light.mask |= pc.MASK_DYNAMIC;
            } else if (oldValue && !newValue) {
                this.light.mask &= ~pc.MASK_DYNAMIC;
            }
            this.light.setMask(this.light.mask);
        },

        onSetBaked: function (name, oldValue, newValue) {

            if ((this.light.mask & pc.MASK_LIGHTMAP)!==0) newValue = false;

            if (!oldValue && newValue) {
                this.light.mask |= pc.MASK_BAKED;
            } else if (oldValue && !newValue) {
                this.light.mask &= ~pc.MASK_BAKED;
            }
            this.light.setMask(this.light.mask);
        },

        onSetLightMap: function (name, oldValue, newValue) {
            if (!oldValue && newValue) {
                this.light.mask |= pc.MASK_LIGHTMAP;
            } else if (oldValue && !newValue) {
                this.light.mask &= ~pc.MASK_LIGHTMAP;
            }

            if (newValue && (this.light.mask & pc.MASK_BAKED)!==0) this.baked = false;

            this.light.setMask(this.light.mask);
        },

        onEnable: function () {
            LightComponent._super.onEnable.call(this);

            this.light.setEnabled(true);

            var model = this.data.model;
            if (! model)
                return;

            var scene = this.system.app.scene;
            if (scene.containsModel(model))
                return;

            scene.addModel(model);
        },

        onDisable: function () {
            LightComponent._super.onDisable.call(this);

            this.light.setEnabled(false);

            var model = this.data.model;
            if (! model)
                return;

            this.system.app.scene.removeModel(model);
        }
    });

    return {
        LightComponent: LightComponent
    };
}());
<|endoftext|>


<commit_before>(function ($) {
    var KEditor = $.keditor;
    var flog = KEditor.log;

    KEditor.components['audio'] = {
        getContent: function (component, keditor) {
            flog('getContent "audio" component, component');

            var componentContent = component.children('.keditor-component-content');
            var audio = componentContent.find('audio');
            audio.unwrap();

            return componentContent.html();
        },

        settingEnabled: true,

        settingTitle: 'Audio Settings',

        initSettingForm: function (form, keditor) {
            flog('init "audio" settings', form);

            form.append(
                '<form class="form-horizontal">' +
                '
' +
                'Audio file' +
                '
' +
                '
' +
                '' +
                '<input id="audioFileInput" type="file" style="display: none">' +
                '
' +
                '
' +
                '
' +
                '
' +
                'Autoplay' +
                '
' +
                '<input type="checkbox" id="audio-autoplay" />' +
                '
' +
                '
' +
                '
' +
                'Show Controls' +
                '
' +
                '<input type="checkbox" id="audio-showcontrols" checked />' +
                '
' +
                '
' +
                '
' +
                'Width (%)' +
                '
' +
                '<input type="number" id="audio-width" min="20" max="100" class="form-control" value="100" />' +
                '
' +
                '
' +
                '</form>'
            );
        },

        showSettingForm: function (form, component, keditor) {
            flog('showSettingForm "audio" component', form, component);

            var options = keditor.options;

            var audio = component.find('audio');
            var fileInput = form.find('#audioFileInput');
            var btnAudioFileInput = form.find('.btn-audioFileInput');
            btnAudioFileInput.off('click').on('click', function (e) {
                e.preventDefault();

                fileInput.trigger('click');
            });
            fileInput.off('change').on('change', function () {
                var file = this.files[0];
                if (/audio/.test(file.type)) {
                    // Todo: Upload to your server :)

                    audio.attr('src', URL.createObjectURL(file));

                    audio.load(function () {
                        keditor.showSettingPanel(component, options);
                    });
                } else {
                    alert('Your selected file is not an audio file!');
                }
            });

            var autoplayToggle = form.find('#audio-autoplay');
            autoplayToggle.off('click').on('click', function (e) {
                if (this.checked) {
                    audio.attr('autoplay', 'autoplay');
                } else {
                    audio.removeAttr('autoplay');
                }
            });

            var showcontrolsToggle = form.find('#audio-showcontrols');
            showcontrolsToggle.off('click').on('click', function (e) {
                if (this.checked) {
                    audio.attr('controls', 'controls');
                } else {
                    audio.removeAttr('controls');
                }
            });

            var audioWidth = form.find('#audio-width');
            audioWidth.off('change').on('change', function () {
                audio.css('width', this.value + '%');
            });
        }
    };
})(jQuery);
<commit_msg>Reformat source<commit_after>(function ($) {
    var KEditor = $.keditor;
    var flog = KEditor.log;

    KEditor.components['audio'] = {
        getContent: function (component, keditor) {
            flog('getContent "audio" component, component');

            var componentContent = component.children('.keditor-component-content');
            var audio = componentContent.find('audio');
            audio.unwrap();

            return componentContent.html();
        },

        settingEnabled: true,

        settingTitle: 'Audio Settings',

        initSettingForm: function (form, keditor) {
            flog('init "audio" settings', form);

            form.append(
                '<form class="form-horizontal">' +
                '
' +
                '         Audio file' +
                '
' +
                '
' +
                '                 ' +
                '                 <input id="audioFileInput" type="file" style="display: none" />' +
                '
' +
                '
' +
                '
' +
                '
' +
                '         Autoplay' +
                '
' +
                '             <input type="checkbox" id="audio-autoplay" />' +
                '
' +
                '
' +
                '
' +
                '         Show Controls' +
                '
' +
                '             <input type="checkbox" id="audio-showcontrols" checked />' +
                '
' +
                '
' +
                '
' +
                '         Width (%)' +
                '
' +
                '             <input type="number" id="audio-width" min="20" max="100" class="form-control" value="100" />' +
                '
' +
                '
' +
                '</form>'
            );
        },

        showSettingForm: function (form, component, keditor) {
            flog('showSettingForm "audio" component', form, component);

            var options = keditor.options;

            var audio = component.find('audio');
            var fileInput = form.find('#audioFileInput');
            var btnAudioFileInput = form.find('.btn-audioFileInput');
            btnAudioFileInput.off('click').on('click', function (e) {
                e.preventDefault();

                fileInput.trigger('click');
            });
            fileInput.off('change').on('change', function () {
                var file = this.files[0];
                if (/audio/.test(file.type)) {
                    // Todo: Upload to your server :)

                    audio.attr('src', URL.createObjectURL(file));

                    audio.load(function () {
                        keditor.showSettingPanel(component, options);
                    });
                } else {
                    alert('Your selected file is not an audio file!');
                }
            });

            var autoplayToggle = form.find('#audio-autoplay');
            autoplayToggle.off('click').on('click', function (e) {
                if (this.checked) {
                    audio.attr('autoplay', 'autoplay');
                } else {
                    audio.removeAttr('autoplay');
                }
            });

            var showcontrolsToggle = form.find('#audio-showcontrols');
            showcontrolsToggle.off('click').on('click', function (e) {
                if (this.checked) {
                    audio.attr('controls', 'controls');
                } else {
                    audio.removeAttr('controls');
                }
            });

            var audioWidth = form.find('#audio-width');
            audioWidth.off('change').on('change', function () {
                audio.css('width', this.value + '%');
            });
        }
    };
})(jQuery);
<|endoftext|>


<commit_before>/*eslint-env mocha */

const expect = require('expect')
const { pushPath, replacePath, UPDATE_PATH, routeReducer, syncReduxAndRouter } = require('../src/index')
const { createStore, combineReducers, compose } = require('redux')
const { devTools } = require('redux-devtools')
const { ActionCreators } = require('redux-devtools/lib/devTools')
const { useBasename } = require('history')

expect.extend({
  toContainRoute({
    path,
    state = undefined,
    replace = false,
    changeId = undefined
  }) {
    const routing = this.actual.getState().routing

    expect(routing.path).toEqual(path)
    expect(routing.state).toEqual(state)
    expect(routing.replace).toEqual(replace)

    if (changeId !== undefined) {
      expect(routing.changeId).toEqual(changeId)
    }
  }
})

function createSyncedHistoryAndStore(createHistory) {
  const store = createStore(combineReducers({
    routing: routeReducer
  }))
  const history = createHistory()
  const unsubscribe = syncReduxAndRouter(history, store)
  return { history, store, unsubscribe }
}

const defaultReset = () => {}

module.exports = function createTests(createHistory, name, reset = defaultReset) {
  describe(name, () => {

    beforeEach(reset)

    describe('pushPath', () => {
      it('creates actions', () => {
        expect(pushPath('/foo', { bar: 'baz' })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            replace: false,
            state: { bar: 'baz' },
            avoidRouterUpdate: false
          }
        })

        expect(pushPath('/foo', undefined, { avoidRouterUpdate: true })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            state: undefined,
            replace: false,
            avoidRouterUpdate: true
          }
        })
      })
    })

    describe('replacePath', () => {
      it('creates actions', () => {
        expect(replacePath('/foo', { bar: 'baz' })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            replace: true,
            state: { bar: 'baz' },
            avoidRouterUpdate: false
          }
        })

        expect(replacePath('/foo', undefined, { avoidRouterUpdate: true })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            state: undefined,
            replace: true,
            avoidRouterUpdate: true
          }
        })

        expect(replacePath('/foo', undefined, { avoidRouterUpdate: false })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            state: undefined,
            replace: true,
            avoidRouterUpdate: false
          }
        })
      })
    })

    describe('routeReducer', () => {
      const state = {
        path: '/foo',
        changeId: 1
      }

      it('updates the path', () => {
        expect(routeReducer(state, {
          type: UPDATE_PATH,
          payload: {
            path: '/bar',
            replace: false
          }
        })).toEqual({
          path: '/bar',
          replace: false,
          state: undefined,
          changeId: 2
        })
      })

      it('respects replace', () => {
        expect(routeReducer(state, {
          type: UPDATE_PATH,
          payload: {
            path: '/bar',
            replace: true,
            avoidRouterUpdate: false
          }
        })).toEqual({
          path: '/bar',
          replace: true,
          state: undefined,
          changeId: 2
        })
      })

      it('respects `avoidRouterUpdate` flag', () => {
        expect(routeReducer(state, {
          type: UPDATE_PATH,
          payload: {
            path: '/bar',
            replace: false,
            avoidRouterUpdate: true
          }
        })).toEqual({
          path: '/bar',
          replace: false,
          state: undefined,
          changeId: 1
        })
      })
    })

    // To ensure that "Revert" and toggling actions work as expected in
    // Redux DevTools we need a couple of tests for it. In these tests we
    // rely directly on the DevTools, as they implement these actions as
    // middleware, and we don't want to implement this ourselves.
    describe('devtools', () => {
      let history, store, devToolsStore, unsubscribe

      beforeEach(() => {
        history = createHistory()
        const finalCreateStore = compose(devTools())(createStore)
        store = finalCreateStore(combineReducers({
          routing: routeReducer
        }))
        devToolsStore = store.devToolsStore

        // Set initial URL before syncing
        history.pushState(null, '/foo')

        unsubscribe = syncReduxAndRouter(history, store)
      })

      afterEach(() => {
        unsubscribe()
      })

      it('resets to the initial url', () => {
        let currentPath
        const historyUnsubscribe = history.listen(location => {
          currentPath = location.pathname
        })

        history.pushState(null, '/bar')
        store.dispatch(pushPath('/baz'))

        // By calling reset we expect DevTools to re-play the initial state
        // and the history to update to the initial path
        devToolsStore.dispatch(ActionCreators.reset())

        expect(store.getState().routing.path).toEqual('/foo')
        expect(currentPath).toEqual('/foo')

        historyUnsubscribe()
      })

      it('handles toggle after store change', () => {
        let currentPath
        const historyUnsubscribe = history.listen(location => {
          currentPath = location.pathname
        })

        // DevTools action #2
        history.pushState(null, '/foo2')
        // DevTools action #3
        history.pushState(null, '/foo3')

        // When we toggle an action, the devtools will revert the action
        // and we therefore expect the history to update to the previous path
        devToolsStore.dispatch(ActionCreators.toggleAction(3))
        expect(currentPath).toEqual('/foo2')

        historyUnsubscribe()
      })

      it('handles toggle after store change', () => {
        let currentPath
        const historyUnsubscribe = history.listen(location => {
          currentPath = location.pathname
        })

        // DevTools action #2
        store.dispatch(pushPath('/foo2'))
        // DevTools action #3
        store.dispatch(pushPath('/foo3'))

        // When we toggle an action, the devtools will revert the action
        // and we therefore expect the history to update to the previous path
        devToolsStore.dispatch(ActionCreators.toggleAction(3))
        expect(currentPath).toEqual('/foo2')

        historyUnsubscribe()
      })
    })

    describe('syncReduxAndRouter', () => {
      let history, store, unsubscribe

      beforeEach(() => {
        let synced = createSyncedHistoryAndStore(createHistory)
        history = synced.history
        store = synced.store
        unsubscribe = synced.unsubscribe
      })

      afterEach(() => {
        unsubscribe()
      })

      it('syncs router -> redux', () => {
        expect(store).toContainRoute({
          path: '/'
        })

        history.pushState(null, '/foo')
        expect(store).toContainRoute({
          path: '/foo',
          replace: false,
          state: null
        })

        history.pushState({ bar: 'baz' }, '/foo')
        expect(store).toContainRoute({
          path: '/foo',
          replace: true,
          state: { bar: 'baz' }
        })

        history.replaceState(null, '/bar')
        expect(store).toContainRoute({
          path: '/bar',
          replace: true,
          state: null
        })

        history.pushState(null, '/bar')
        expect(store).toContainRoute({
          path: '/bar',
          replace: true,
          state: null
        })

        history.pushState(null, '/bar?query=1')
        expect(store).toContainRoute({
          path: '/bar?query=1',
          replace: false,
          state: null
        })

        history.replaceState({ bar: 'baz' }, '/bar?query=1')
        expect(store).toContainRoute({
          path: '/bar?query=1',
          replace: true,
          state: { bar: 'baz' }
        })

        history.pushState({ bar: 'baz' }, '/bar?query=1#hash=2')
        expect(store).toContainRoute({
          path: '/bar?query=1#hash=2',
          replace: true,
          state: { bar: 'baz' }
        })
      })

      it('syncs redux -> router', () => {
        expect(store).toContainRoute({
          path: '/',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo', { bar: 'baz' }))
        expect(store).toContainRoute({
          path: '/foo',
          replace: false,
          state: { bar: 'baz' }
        })

        store.dispatch(replacePath('/bar', { bar: 'foo' }))
        expect(store).toContainRoute({
          path: '/bar',
          replace: true,
          state: { bar: 'foo' }
        })

        store.dispatch(pushPath('/bar'))
        expect(store).toContainRoute({
          path: '/bar',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/bar?query=1'))
        expect(store).toContainRoute({
          path: '/bar?query=1',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/bar?query=1#hash=2'))
        expect(store).toContainRoute({
          path: '/bar?query=1#hash=2',
          replace: false,
          state: undefined
        })
      })

      it('updates the router even if path is the same', () => {
        expect(store).toContainRoute({
          path: '/',
          changeId: 1,
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 2,
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 3,
          replace: false,
          state: undefined
        })

        store.dispatch(replacePath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 4,
          replace: true,
          state: undefined
        })
      })

      it('does not update the router for other state changes', () => {
        store.dispatch({
          type: 'RANDOM_ACTION',
          payload: {
            payload: {
              value: 5
            }
          }
        })

        expect(store).toContainRoute({
          path: '/',
          changeId: 1,
          replace: false,
          state: undefined
        })
      })

      it('only updates the router once when dispatching from `listenBefore`', () => {
        expect(store).toContainRoute({
          path: '/',
          changeId: 1,
          replace: false,
          state: undefined
        })

        history.listenBefore(location => {
          expect(location.pathname).toEqual('/foo')
          store.dispatch({
            type: 'RANDOM_ACTION',
            payload: {
              payload: {
                value: 5
              }
            }
          })
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 2,
          replace: false,
          state: undefined
        })
      })

      it('does not unnecessarily update the store', () => {
        const updates = []

        const unsubscribeFromStore = store.subscribe(() => {
          updates.push(store.getState())
        })

        store.dispatch(pushPath('/foo'))
        store.dispatch(pushPath('/foo'))
        store.dispatch(pushPath('/foo', { bar: 'baz' }))
        history.pushState({ foo: 'bar' }, '/foo')
        store.dispatch(replacePath('/bar'))
        store.dispatch(replacePath('/bar', { bar: 'foo' }))

        unsubscribeFromStore()

        expect(updates.length).toBe(6)
        expect(updates).toEqual([
          {
            routing: {
              changeId: 2,
              path: '/foo',
              state: undefined,
              replace: false
            }
          },
          {
            routing: {
              changeId: 3,
              path: '/foo',
              state: undefined,
              replace: false
            }
          },
          {
            routing: {
              changeId: 4,
              path: '/foo',
              state: { bar: 'baz' },
              replace: false
            }
          },
          {
            routing: {
              changeId: 4,
              path: '/foo',
              state: { foo: 'bar' },
              replace: true
            }
          },
          {
            routing: {
              changeId: 5,
              path: '/bar',
              state: undefined,
              replace: true
            }
          },
          {
            routing: {
              changeId: 6,
              path: '/bar',
              state: { bar: 'foo' },
              replace: true
            }
          }
        ])
      })

      it('allows updating the route from within `listenBefore`', () => {
        expect(store).toContainRoute({
          path: '/'
        })

        history.listenBefore(location => {
          if(location.pathname === '/foo') {
            expect(store).toContainRoute({
              path: '/foo',
              changeId: 2,
              replace: false,
              state: undefined
            })
            store.dispatch(pushPath('/bar'))
          }
          else if(location.pathname === '/replace') {
            expect(store).toContainRoute({
              path: '/replace',
              changeId: 4,
              replace: false,
              state: { bar: 'baz' }
            })
            store.dispatch(replacePath('/baz', { foo: 'bar' }))
          }
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/bar',
          changeId: 3,
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/replace', { bar: 'baz' }))
        expect(store).toContainRoute({
          path: '/baz',
          changeId: 5,
          replace: true,
          state: { foo: 'bar' }
        })
      })

      it('throws if "routing" key is missing with default selectRouteState', () => {
        const store = createStore(combineReducers({
          notRouting: routeReducer
        }))
        const history = createHistory()
        expect(
          () => syncReduxAndRouter(history, store)
        ).toThrow(/Cannot sync router: route state does not exist/)
      })

      it('accepts custom selectRouterState', () => {
        const store = createStore(combineReducers({
          notRouting: routeReducer
        }))
        const history = createHistory()
        syncReduxAndRouter(history, store, state => state.notRouting)
        history.pushState(null, '/bar')
        expect(store.getState().notRouting.path).toEqual('/bar')
      })

      it('returns unsubscribe to stop listening to history and store', () => {
        const store = createStore(combineReducers({
          routing: routeReducer
        }))
        const history = createHistory()
        const unsubscribe = syncReduxAndRouter(history, store)

        history.pushState(null, '/foo')
        expect(store).toContainRoute({
          path: '/foo'
        })

        store.dispatch(pushPath('/bar'))
        expect(store).toContainRoute({
          path: '/bar',
          changeId: 2,
          replace: false,
          state: undefined
        })

        unsubscribe()

        history.pushState(null, '/foo')
        expect(store).toContainRoute({
          path: '/bar'
        })

        history.listenBefore(() => {
          throw new Error()
        })
        expect(
          () => store.dispatch(pushPath('/foo'))
        ).toNotThrow()
      })
    })

    it('handles basename history option', () => {
      const store = createStore(combineReducers({
        routing: routeReducer
      }))
      const history = useBasename(createHistory)({ basename:'/foobar' })
      syncReduxAndRouter(history, store)

      store.dispatch(pushPath('/bar'))
      expect(store).toContainRoute({
        path: '/bar',
        changeId: 2,
        replace: false,
        state: undefined
      })
    })
  })
}
<commit_msg>Add pushState test for basename<commit_after>/*eslint-env mocha */

const expect = require('expect')
const { pushPath, replacePath, UPDATE_PATH, routeReducer, syncReduxAndRouter } = require('../src/index')
const { createStore, combineReducers, compose } = require('redux')
const { devTools } = require('redux-devtools')
const { ActionCreators } = require('redux-devtools/lib/devTools')
const { useBasename } = require('history')

expect.extend({
  toContainRoute({
    path,
    state = undefined,
    replace = false,
    changeId = undefined
  }) {
    const routing = this.actual.getState().routing

    expect(routing.path).toEqual(path)
    expect(routing.state).toEqual(state)
    expect(routing.replace).toEqual(replace)

    if (changeId !== undefined) {
      expect(routing.changeId).toEqual(changeId)
    }
  }
})

function createSyncedHistoryAndStore(createHistory) {
  const store = createStore(combineReducers({
    routing: routeReducer
  }))
  const history = createHistory()
  const unsubscribe = syncReduxAndRouter(history, store)
  return { history, store, unsubscribe }
}

const defaultReset = () => {}

module.exports = function createTests(createHistory, name, reset = defaultReset) {
  describe(name, () => {

    beforeEach(reset)

    describe('pushPath', () => {
      it('creates actions', () => {
        expect(pushPath('/foo', { bar: 'baz' })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            replace: false,
            state: { bar: 'baz' },
            avoidRouterUpdate: false
          }
        })

        expect(pushPath('/foo', undefined, { avoidRouterUpdate: true })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            state: undefined,
            replace: false,
            avoidRouterUpdate: true
          }
        })
      })
    })

    describe('replacePath', () => {
      it('creates actions', () => {
        expect(replacePath('/foo', { bar: 'baz' })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            replace: true,
            state: { bar: 'baz' },
            avoidRouterUpdate: false
          }
        })

        expect(replacePath('/foo', undefined, { avoidRouterUpdate: true })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            state: undefined,
            replace: true,
            avoidRouterUpdate: true
          }
        })

        expect(replacePath('/foo', undefined, { avoidRouterUpdate: false })).toEqual({
          type: UPDATE_PATH,
          payload: {
            path: '/foo',
            state: undefined,
            replace: true,
            avoidRouterUpdate: false
          }
        })
      })
    })

    describe('routeReducer', () => {
      const state = {
        path: '/foo',
        changeId: 1
      }

      it('updates the path', () => {
        expect(routeReducer(state, {
          type: UPDATE_PATH,
          payload: {
            path: '/bar',
            replace: false
          }
        })).toEqual({
          path: '/bar',
          replace: false,
          state: undefined,
          changeId: 2
        })
      })

      it('respects replace', () => {
        expect(routeReducer(state, {
          type: UPDATE_PATH,
          payload: {
            path: '/bar',
            replace: true,
            avoidRouterUpdate: false
          }
        })).toEqual({
          path: '/bar',
          replace: true,
          state: undefined,
          changeId: 2
        })
      })

      it('respects `avoidRouterUpdate` flag', () => {
        expect(routeReducer(state, {
          type: UPDATE_PATH,
          payload: {
            path: '/bar',
            replace: false,
            avoidRouterUpdate: true
          }
        })).toEqual({
          path: '/bar',
          replace: false,
          state: undefined,
          changeId: 1
        })
      })
    })

    // To ensure that "Revert" and toggling actions work as expected in
    // Redux DevTools we need a couple of tests for it. In these tests we
    // rely directly on the DevTools, as they implement these actions as
    // middleware, and we don't want to implement this ourselves.
    describe('devtools', () => {
      let history, store, devToolsStore, unsubscribe

      beforeEach(() => {
        history = createHistory()
        const finalCreateStore = compose(devTools())(createStore)
        store = finalCreateStore(combineReducers({
          routing: routeReducer
        }))
        devToolsStore = store.devToolsStore

        // Set initial URL before syncing
        history.pushState(null, '/foo')

        unsubscribe = syncReduxAndRouter(history, store)
      })

      afterEach(() => {
        unsubscribe()
      })

      it('resets to the initial url', () => {
        let currentPath
        const historyUnsubscribe = history.listen(location => {
          currentPath = location.pathname
        })

        history.pushState(null, '/bar')
        store.dispatch(pushPath('/baz'))

        // By calling reset we expect DevTools to re-play the initial state
        // and the history to update to the initial path
        devToolsStore.dispatch(ActionCreators.reset())

        expect(store.getState().routing.path).toEqual('/foo')
        expect(currentPath).toEqual('/foo')

        historyUnsubscribe()
      })

      it('handles toggle after store change', () => {
        let currentPath
        const historyUnsubscribe = history.listen(location => {
          currentPath = location.pathname
        })

        // DevTools action #2
        history.pushState(null, '/foo2')
        // DevTools action #3
        history.pushState(null, '/foo3')

        // When we toggle an action, the devtools will revert the action
        // and we therefore expect the history to update to the previous path
        devToolsStore.dispatch(ActionCreators.toggleAction(3))
        expect(currentPath).toEqual('/foo2')

        historyUnsubscribe()
      })

      it('handles toggle after store change', () => {
        let currentPath
        const historyUnsubscribe = history.listen(location => {
          currentPath = location.pathname
        })

        // DevTools action #2
        store.dispatch(pushPath('/foo2'))
        // DevTools action #3
        store.dispatch(pushPath('/foo3'))

        // When we toggle an action, the devtools will revert the action
        // and we therefore expect the history to update to the previous path
        devToolsStore.dispatch(ActionCreators.toggleAction(3))
        expect(currentPath).toEqual('/foo2')

        historyUnsubscribe()
      })
    })

    describe('syncReduxAndRouter', () => {
      let history, store, unsubscribe

      beforeEach(() => {
        let synced = createSyncedHistoryAndStore(createHistory)
        history = synced.history
        store = synced.store
        unsubscribe = synced.unsubscribe
      })

      afterEach(() => {
        unsubscribe()
      })

      it('syncs router -> redux', () => {
        expect(store).toContainRoute({
          path: '/'
        })

        history.pushState(null, '/foo')
        expect(store).toContainRoute({
          path: '/foo',
          replace: false,
          state: null
        })

        history.pushState({ bar: 'baz' }, '/foo')
        expect(store).toContainRoute({
          path: '/foo',
          replace: true,
          state: { bar: 'baz' }
        })

        history.replaceState(null, '/bar')
        expect(store).toContainRoute({
          path: '/bar',
          replace: true,
          state: null
        })

        history.pushState(null, '/bar')
        expect(store).toContainRoute({
          path: '/bar',
          replace: true,
          state: null
        })

        history.pushState(null, '/bar?query=1')
        expect(store).toContainRoute({
          path: '/bar?query=1',
          replace: false,
          state: null
        })

        history.replaceState({ bar: 'baz' }, '/bar?query=1')
        expect(store).toContainRoute({
          path: '/bar?query=1',
          replace: true,
          state: { bar: 'baz' }
        })

        history.pushState({ bar: 'baz' }, '/bar?query=1#hash=2')
        expect(store).toContainRoute({
          path: '/bar?query=1#hash=2',
          replace: true,
          state: { bar: 'baz' }
        })
      })

      it('syncs redux -> router', () => {
        expect(store).toContainRoute({
          path: '/',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo', { bar: 'baz' }))
        expect(store).toContainRoute({
          path: '/foo',
          replace: false,
          state: { bar: 'baz' }
        })

        store.dispatch(replacePath('/bar', { bar: 'foo' }))
        expect(store).toContainRoute({
          path: '/bar',
          replace: true,
          state: { bar: 'foo' }
        })

        store.dispatch(pushPath('/bar'))
        expect(store).toContainRoute({
          path: '/bar',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/bar?query=1'))
        expect(store).toContainRoute({
          path: '/bar?query=1',
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/bar?query=1#hash=2'))
        expect(store).toContainRoute({
          path: '/bar?query=1#hash=2',
          replace: false,
          state: undefined
        })
      })

      it('updates the router even if path is the same', () => {
        expect(store).toContainRoute({
          path: '/',
          changeId: 1,
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 2,
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 3,
          replace: false,
          state: undefined
        })

        store.dispatch(replacePath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 4,
          replace: true,
          state: undefined
        })
      })

      it('does not update the router for other state changes', () => {
        store.dispatch({
          type: 'RANDOM_ACTION',
          payload: {
            payload: {
              value: 5
            }
          }
        })

        expect(store).toContainRoute({
          path: '/',
          changeId: 1,
          replace: false,
          state: undefined
        })
      })

      it('only updates the router once when dispatching from `listenBefore`', () => {
        expect(store).toContainRoute({
          path: '/',
          changeId: 1,
          replace: false,
          state: undefined
        })

        history.listenBefore(location => {
          expect(location.pathname).toEqual('/foo')
          store.dispatch({
            type: 'RANDOM_ACTION',
            payload: {
              payload: {
                value: 5
              }
            }
          })
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/foo',
          changeId: 2,
          replace: false,
          state: undefined
        })
      })

      it('does not unnecessarily update the store', () => {
        const updates = []

        const unsubscribeFromStore = store.subscribe(() => {
          updates.push(store.getState())
        })

        store.dispatch(pushPath('/foo'))
        store.dispatch(pushPath('/foo'))
        store.dispatch(pushPath('/foo', { bar: 'baz' }))
        history.pushState({ foo: 'bar' }, '/foo')
        store.dispatch(replacePath('/bar'))
        store.dispatch(replacePath('/bar', { bar: 'foo' }))

        unsubscribeFromStore()

        expect(updates.length).toBe(6)
        expect(updates).toEqual([
          {
            routing: {
              changeId: 2,
              path: '/foo',
              state: undefined,
              replace: false
            }
          },
          {
            routing: {
              changeId: 3,
              path: '/foo',
              state: undefined,
              replace: false
            }
          },
          {
            routing: {
              changeId: 4,
              path: '/foo',
              state: { bar: 'baz' },
              replace: false
            }
          },
          {
            routing: {
              changeId: 4,
              path: '/foo',
              state: { foo: 'bar' },
              replace: true
            }
          },
          {
            routing: {
              changeId: 5,
              path: '/bar',
              state: undefined,
              replace: true
            }
          },
          {
            routing: {
              changeId: 6,
              path: '/bar',
              state: { bar: 'foo' },
              replace: true
            }
          }
        ])
      })

      it('allows updating the route from within `listenBefore`', () => {
        expect(store).toContainRoute({
          path: '/'
        })

        history.listenBefore(location => {
          if(location.pathname === '/foo') {
            expect(store).toContainRoute({
              path: '/foo',
              changeId: 2,
              replace: false,
              state: undefined
            })
            store.dispatch(pushPath('/bar'))
          }
          else if(location.pathname === '/replace') {
            expect(store).toContainRoute({
              path: '/replace',
              changeId: 4,
              replace: false,
              state: { bar: 'baz' }
            })
            store.dispatch(replacePath('/baz', { foo: 'bar' }))
          }
        })

        store.dispatch(pushPath('/foo'))
        expect(store).toContainRoute({
          path: '/bar',
          changeId: 3,
          replace: false,
          state: undefined
        })

        store.dispatch(pushPath('/replace', { bar: 'baz' }))
        expect(store).toContainRoute({
          path: '/baz',
          changeId: 5,
          replace: true,
          state: { foo: 'bar' }
        })
      })

      it('throws if "routing" key is missing with default selectRouteState', () => {
        const store = createStore(combineReducers({
          notRouting: routeReducer
        }))
        const history = createHistory()
        expect(
          () => syncReduxAndRouter(history, store)
        ).toThrow(/Cannot sync router: route state does not exist/)
      })

      it('accepts custom selectRouterState', () => {
        const store = createStore(combineReducers({
          notRouting: routeReducer
        }))
        const history = createHistory()
        syncReduxAndRouter(history, store, state => state.notRouting)
        history.pushState(null, '/bar')
        expect(store.getState().notRouting.path).toEqual('/bar')
      })

      it('returns unsubscribe to stop listening to history and store', () => {
        const store = createStore(combineReducers({
          routing: routeReducer
        }))
        const history = createHistory()
        const unsubscribe = syncReduxAndRouter(history, store)

        history.pushState(null, '/foo')
        expect(store).toContainRoute({
          path: '/foo'
        })

        store.dispatch(pushPath('/bar'))
        expect(store).toContainRoute({
          path: '/bar',
          changeId: 2,
          replace: false,
          state: undefined
        })

        unsubscribe()

        history.pushState(null, '/foo')
        expect(store).toContainRoute({
          path: '/bar'
        })

        history.listenBefore(() => {
          throw new Error()
        })
        expect(
          () => store.dispatch(pushPath('/foo'))
        ).toNotThrow()
      })
    })

    it('handles basename history option', () => {
      const store = createStore(combineReducers({
        routing: routeReducer
      }))
      const history = useBasename(createHistory)({ basename:'/foobar' })
      syncReduxAndRouter(history, store)

      store.dispatch(pushPath('/bar'))
      expect(store).toContainRoute({
        path: '/bar',
        changeId: 2,
        replace: false,
        state: undefined
      })

      history.pushState(undefined, '/baz');
      expect(store).toContainRoute({
        path: '/baz',
        changeId: 2,
        replace: false,
        state: undefined
      })
    })
  })
}
<|endoftext|>