<commit_before>  angular
      .module('material.components.chips')
      .directive('mdChips', MdChips);

  /**
   * @ngdoc directive
   * @name mdChips
   * @module material.components.chips
   *
   * @description
   * `<md-chips>` is an input component for building lists of strings or objects. The list items are
   * displayed as 'chips'. This component can make use of an `<input>` element or an
   * `<md-autocomplete>` element.
   *
   * Custom `<md-chip-template>` template
   * A custom template may be provided to render the content of each chip. This is achieved by
   * specifying an `<md-chip-template>` element as a child of `<md-chips>`. Note: Any attributes on
   * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
   * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
   * the chip object and its index in the list of chips, respectively.
   * To override the chip delete control, include an element (ideally a button) with the attribute
   * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
   * is also placed as a sibling to the chip content (on which there are also click listeners) to
   * avoid a nested ng-click situation.
   *
   *
 Pending Features

   *

   *
   *
Style
   *
Colours for hover, press states (ripple?).

   *

   *
   *
Validation
   *
allow a validation callback

   *
hilighting style for invalid chips

   *

   *
   *
Item mutation
   *
Support `
   *       <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
   *       click?
   *

   *

   *
   *
Truncation and Disambiguation (?)
   *
Truncate chip text where possible, but do not truncate entries such that two are
   *     indistinguishable.

   *

   *
   *
Drag and Drop
   *
Drag and drop chips between related `<md-chips>` elements.
   *

   *

   *

   *
   *
   *    Warning: This component is a WORK IN PROGRESS. If you use it now,
   *    it will probably break on you in the future.
   *
   *
   * @param {string=|object=} ng-model A model to bind the list of items to
   * @param {string=} placeholder Placeholder text that will be forwarded to the input.
   * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
   *    displayed when there is at least on item in the list
   * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
   *    the input and delete buttons
   * @param {expression} md-on-append An expression that when called expects you to return an object
   *    representation of the chip input string.
   * @param {expression=} md-on-remove An expression which will be called when a chip has been
   *    removed.
   * @param {expression=} md-on-select An expression which will be called when a chip is selected.
   * @param {string=} delete-hint A string read by screen readers instructing users that pressing
   *    the delete key will remove the chip.
   * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
   *    screen readers.
   *
   * @usage
   * <hljs lang="html">
   *   <md-chips
   *       ng-model="myItems"
   *       placeholder="Add an item"
   *       readonly="isReadOnly">
   *   </md-chips>
   * </hljs>
   *
   */

  var MD_CHIPS_TEMPLATE = '\
      <md-chips-wrap\
          ng-if="!$mdChipsCtrl.readonly || $mdChipsCtrl.items.length > 0"\
          ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
          ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \'md-readonly\': !$mdChipsCtrl.ngModelCtrl }"\
          class="md-chips">\
        <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
            index="{{$index}}"\
            ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': $mdChipsCtrl.readonly}">\

\

\
        </md-chip>\

\

\
      </md-chips-wrap>';

  var CHIP_INPUT_TEMPLATE = '\
        <input\
            tabindex="0"\
            placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
            aria-label="{{$mdChipsCtrl.getPlaceholder()}}"\
            ng-model="$mdChipsCtrl.chipBuffer"\
            ng-focus="$mdChipsCtrl.onInputFocus()"\
            ng-blur="$mdChipsCtrl.onInputBlur()"\
            ng-keydown="$mdChipsCtrl.inputKeydown($event)">';

  var CHIP_DEFAULT_TEMPLATE = '\
      {{$chip}}';

  var CHIP_REMOVE_TEMPLATE = '\
      <button\
          class="md-chip-remove"\
          ng-if="!$mdChipsCtrl.readonly"\
          ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
          type="button"\
          aria-hidden="true"\
          tabindex="-1">\
        <md-icon md-svg-icon="md-close"></md-icon>\
        \
          {{$mdChipsCtrl.deleteButtonLabel}}\
        \
      </button>';

  /**
   * MDChips Directive Definition
   */
  function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout) {
    // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
    convertTemplates();

    return {
      template: function(element, attrs) {
        // Clone the element into an attribute. By prepending the attribute
        // name with '$', Angular won't write it into the DOM. The cloned
        // element propagates to the link function via the attrs argument,
        // where various contained-elements can be consumed.
        attrs['$mdUserTemplate'] = element.clone();
        return MD_CHIPS_TEMPLATE;
      },
      require: ['mdChips'],
      restrict: 'E',
      controller: 'MdChipsCtrl',
      controllerAs: '$mdChipsCtrl',
      bindToController: true,
      compile: compile,
      scope: {
        readonly: '=readonly',
        placeholder: '@',
        secondaryPlaceholder: '@',
        onAppend: '&mdOnAppend',
        onRemove: '&mdOnRemove',
        onSelect: '&mdOnSelect',
        deleteHint: '@',
        deleteButtonLabel: '@',
        requireMatch: '=?mdRequireMatch'
      }
    };

    /**
     * Builds the final template for `md-chips` and returns the postLink function.
     *
     * Building the template involves 3 key components:
     * static chips
     * chip template
     * input control
     *
     * If no `ng-model` is provided, only the static chip work needs to be done.
     *
     * If no user-passed `md-chip-template` exists, the default template is used. This resulting
     * template is appended to the chip content element.
     *
     * The remove button may be overridden by passing an element with an md-chip-remove attribute.
     *
     * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
     * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
     * If no user input is provided, a default one is appended to the input container node in the
     * template.
     *
     * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
     * transclusion in the `postLink` function.
     *
     *
     * @param element
     * @param attr
     * @returns {Function}
     */
    function compile(element, attr) {
      // Grab the user template from attr and reset the attribute to null.
      var userTemplate = attr['$mdUserTemplate'];
      attr['$mdUserTemplate'] = null;

      // Set the chip remove, chip contents and chip input templates. The link function will put
      // them on the scope for transclusion later.
      var chipRemoveTemplate   = getTemplateByQuery('md-chips>*[md-chip-remove]') || CHIP_REMOVE_TEMPLATE,
          chipContentsTemplate = getTemplateByQuery('md-chips>md-chip-template') || CHIP_DEFAULT_TEMPLATE,
          chipInputTemplate    = getTemplateByQuery('md-chips>md-autocomplete')
              || getTemplateByQuery('md-chips>input')
              || CHIP_INPUT_TEMPLATE,
          staticChips = userTemplate.find('md-chip');

      // Warn of malformed template. See #2545
      if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
        $log.warn('invalid placement of md-chip-remove within md-chip-template.');
      }

      function getTemplateByQuery (query) {
        if (!attr.ngModel) return;
        var element = userTemplate[0].querySelector(query);
        return element && element.outerHTML;
      }

      /**
       * Configures controller and transcludes.
       */
      return function postLink(scope, element, attrs, controllers) {

        $mdUtil.initOptionalProperties(scope, attr);

        $mdTheming(element);
        var mdChipsCtrl = controllers[0];
        mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
        mdChipsCtrl.chipRemoveTemplate   = chipRemoveTemplate;
        mdChipsCtrl.chipInputTemplate    = chipInputTemplate;

        element
            .attr({ ariaHidden: true, tabindex: -1 })
            .on('focus', function () { mdChipsCtrl.onFocus(); });

        if (attr.ngModel) {
          mdChipsCtrl.configureNgModel(element.controller('ngModel'));

          // If an `md-on-append` attribute was set, tell the controller to use the expression
          // when appending chips.
          if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression();

          // If an `md-on-remove` attribute was set, tell the controller to use the expression
          // when removing chips.
          if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();

          // If an `md-on-select` attribute was set, tell the controller to use the expression
          // when selecting chips.
          if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();

          // The md-autocomplete and input elements won't be compiled until after this directive
          // is complete (due to their nested nature). Wait a tick before looking for them to
          // configure the controller.
          if (chipInputTemplate != CHIP_INPUT_TEMPLATE) {
            // The autocomplete will not appear until the readonly attribute is not true (i.e.
            // false or undefined), so we have to watch the readonly and then on the next tick
            // after the chip transclusion has run, we can configure the autocomplete and user
            // input.
            scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
              if (!readonly) {
                $mdUtil.nextTick(function(){
                  if (chipInputTemplate.indexOf('<md-autocomplete') === 0)
                    mdChipsCtrl
                        .configureAutocomplete(element.find('md-autocomplete')
                            .controller('mdAutocomplete'));
                  mdChipsCtrl.configureUserInput(element.find('input'));
                });
              }
            });
          }
        }

        // Compile with the parent's scope and prepend any static chips to the wrapper.
        if (staticChips.length > 0) {
          var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
          $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
        }
      };
    }

    function convertTemplates() {
      MD_CHIPS_TEMPLATE = $mdUtil.processTemplate(MD_CHIPS_TEMPLATE);
      CHIP_INPUT_TEMPLATE = $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE);
      CHIP_DEFAULT_TEMPLATE = $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE);
      CHIP_REMOVE_TEMPLATE = $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE);
    }
  }
<commit_msg>fixes constants being modified in chips directive<commit_after>  angular
      .module('material.components.chips')
      .directive('mdChips', MdChips);

  /**
   * @ngdoc directive
   * @name mdChips
   * @module material.components.chips
   *
   * @description
   * `<md-chips>` is an input component for building lists of strings or objects. The list items are
   * displayed as 'chips'. This component can make use of an `<input>` element or an
   * `<md-autocomplete>` element.
   *
   * Custom `<md-chip-template>` template
   * A custom template may be provided to render the content of each chip. This is achieved by
   * specifying an `<md-chip-template>` element as a child of `<md-chips>`. Note: Any attributes on
   * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
   * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
   * the chip object and its index in the list of chips, respectively.
   * To override the chip delete control, include an element (ideally a button) with the attribute
   * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
   * is also placed as a sibling to the chip content (on which there are also click listeners) to
   * avoid a nested ng-click situation.
   *
   *
 Pending Features

   *

   *
   *
Style
   *
Colours for hover, press states (ripple?).

   *

   *
   *
Validation
   *
allow a validation callback

   *
hilighting style for invalid chips

   *

   *
   *
Item mutation
   *
Support `
   *       <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
   *       click?
   *

   *

   *
   *
Truncation and Disambiguation (?)
   *
Truncate chip text where possible, but do not truncate entries such that two are
   *     indistinguishable.

   *

   *
   *
Drag and Drop
   *
Drag and drop chips between related `<md-chips>` elements.
   *

   *

   *

   *
   *
   *    Warning: This component is a WORK IN PROGRESS. If you use it now,
   *    it will probably break on you in the future.
   *
   *
   * @param {string=|object=} ng-model A model to bind the list of items to
   * @param {string=} placeholder Placeholder text that will be forwarded to the input.
   * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
   *    displayed when there is at least on item in the list
   * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
   *    the input and delete buttons
   * @param {expression} md-on-append An expression that when called expects you to return an object
   *    representation of the chip input string.
   * @param {expression=} md-on-remove An expression which will be called when a chip has been
   *    removed.
   * @param {expression=} md-on-select An expression which will be called when a chip is selected.
   * @param {string=} delete-hint A string read by screen readers instructing users that pressing
   *    the delete key will remove the chip.
   * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
   *    screen readers.
   *
   * @usage
   * <hljs lang="html">
   *   <md-chips
   *       ng-model="myItems"
   *       placeholder="Add an item"
   *       readonly="isReadOnly">
   *   </md-chips>
   * </hljs>
   *
   */

  var MD_CHIPS_TEMPLATE = '\
      <md-chips-wrap\
          ng-if="!$mdChipsCtrl.readonly || $mdChipsCtrl.items.length > 0"\
          ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
          ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \'md-readonly\': !$mdChipsCtrl.ngModelCtrl }"\
          class="md-chips">\
        <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
            index="{{$index}}"\
            ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': $mdChipsCtrl.readonly}">\

\

\
        </md-chip>\

\

\
      </md-chips-wrap>';

  var CHIP_INPUT_TEMPLATE = '\
        <input\
            tabindex="0"\
            placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
            aria-label="{{$mdChipsCtrl.getPlaceholder()}}"\
            ng-model="$mdChipsCtrl.chipBuffer"\
            ng-focus="$mdChipsCtrl.onInputFocus()"\
            ng-blur="$mdChipsCtrl.onInputBlur()"\
            ng-keydown="$mdChipsCtrl.inputKeydown($event)">';

  var CHIP_DEFAULT_TEMPLATE = '\
      {{$chip}}';

  var CHIP_REMOVE_TEMPLATE = '\
      <button\
          class="md-chip-remove"\
          ng-if="!$mdChipsCtrl.readonly"\
          ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
          type="button"\
          aria-hidden="true"\
          tabindex="-1">\
        <md-icon md-svg-icon="md-close"></md-icon>\
        \
          {{$mdChipsCtrl.deleteButtonLabel}}\
        \
      </button>';

  /**
   * MDChips Directive Definition
   */
  function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout) {
    // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
    var templates = getTemplates();

    return {
      template: function(element, attrs) {
        // Clone the element into an attribute. By prepending the attribute
        // name with '$', Angular won't write it into the DOM. The cloned
        // element propagates to the link function via the attrs argument,
        // where various contained-elements can be consumed.
        attrs['$mdUserTemplate'] = element.clone();
        return templates.chips;
      },
      require: ['mdChips'],
      restrict: 'E',
      controller: 'MdChipsCtrl',
      controllerAs: '$mdChipsCtrl',
      bindToController: true,
      compile: compile,
      scope: {
        readonly: '=readonly',
        placeholder: '@',
        secondaryPlaceholder: '@',
        onAppend: '&mdOnAppend',
        onRemove: '&mdOnRemove',
        onSelect: '&mdOnSelect',
        deleteHint: '@',
        deleteButtonLabel: '@',
        requireMatch: '=?mdRequireMatch'
      }
    };

    /**
     * Builds the final template for `md-chips` and returns the postLink function.
     *
     * Building the template involves 3 key components:
     * static chips
     * chip template
     * input control
     *
     * If no `ng-model` is provided, only the static chip work needs to be done.
     *
     * If no user-passed `md-chip-template` exists, the default template is used. This resulting
     * template is appended to the chip content element.
     *
     * The remove button may be overridden by passing an element with an md-chip-remove attribute.
     *
     * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
     * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
     * If no user input is provided, a default one is appended to the input container node in the
     * template.
     *
     * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
     * transclusion in the `postLink` function.
     *
     *
     * @param element
     * @param attr
     * @returns {Function}
     */
    function compile(element, attr) {
      // Grab the user template from attr and reset the attribute to null.
      var userTemplate = attr['$mdUserTemplate'];
      attr['$mdUserTemplate'] = null;

      // Set the chip remove, chip contents and chip input templates. The link function will put
      // them on the scope for transclusion later.
      var chipRemoveTemplate   = getTemplateByQuery('md-chips>*[md-chip-remove]') || templates.remove,
          chipContentsTemplate = getTemplateByQuery('md-chips>md-chip-template') || templates.default,
          chipInputTemplate    = getTemplateByQuery('md-chips>md-autocomplete')
              || getTemplateByQuery('md-chips>input')
              || templates.input,
          staticChips = userTemplate.find('md-chip');

      // Warn of malformed template. See #2545
      if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
        $log.warn('invalid placement of md-chip-remove within md-chip-template.');
      }

      function getTemplateByQuery (query) {
        if (!attr.ngModel) return;
        var element = userTemplate[0].querySelector(query);
        return element && element.outerHTML;
      }

      /**
       * Configures controller and transcludes.
       */
      return function postLink(scope, element, attrs, controllers) {

        $mdUtil.initOptionalProperties(scope, attr);

        $mdTheming(element);
        var mdChipsCtrl = controllers[0];
        mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
        mdChipsCtrl.chipRemoveTemplate   = chipRemoveTemplate;
        mdChipsCtrl.chipInputTemplate    = chipInputTemplate;

        element
            .attr({ ariaHidden: true, tabindex: -1 })
            .on('focus', function () { mdChipsCtrl.onFocus(); });

        if (attr.ngModel) {
          mdChipsCtrl.configureNgModel(element.controller('ngModel'));

          // If an `md-on-append` attribute was set, tell the controller to use the expression
          // when appending chips.
          if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression();

          // If an `md-on-remove` attribute was set, tell the controller to use the expression
          // when removing chips.
          if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();

          // If an `md-on-select` attribute was set, tell the controller to use the expression
          // when selecting chips.
          if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();

          // The md-autocomplete and input elements won't be compiled until after this directive
          // is complete (due to their nested nature). Wait a tick before looking for them to
          // configure the controller.
          if (chipInputTemplate != templates.input) {
            // The autocomplete will not appear until the readonly attribute is not true (i.e.
            // false or undefined), so we have to watch the readonly and then on the next tick
            // after the chip transclusion has run, we can configure the autocomplete and user
            // input.
            scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
              if (!readonly) {
                $mdUtil.nextTick(function(){
                  if (chipInputTemplate.indexOf('<md-autocomplete') === 0)
                    mdChipsCtrl
                        .configureAutocomplete(element.find('md-autocomplete')
                            .controller('mdAutocomplete'));
                  mdChipsCtrl.configureUserInput(element.find('input'));
                });
              }
            });
          }
        }

        // Compile with the parent's scope and prepend any static chips to the wrapper.
        if (staticChips.length > 0) {
          var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
          $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
        }
      };
    }

    function getTemplates() {
      return {
        chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
        input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
        default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
        remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
      };
    }
  }
<|endoftext|>


<commit_before>import $ from 'jquery';
const isSupportAmd = typeof define === 'function' && define.amd; // eslint-disable-line

/**
 * returns whether font is installed or not.
 *
 * @param {String} fontName
 * @return {Boolean}
 */
function isFontInstalled(fontName) {
  const testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
  const $tester = $('<div>').css({
    position: 'absolute',
    left: '-9999px',
    top: '-9999px',
    fontSize: '200px'
  }).text('mmmmmmmmmwwwwwww').appendTo(document.body);

  const originalWidth = $tester.css('fontFamily', testFontName).width();
  const width = $tester.css('fontFamily', fontName + ',' + testFontName).width();

  $tester.remove();

  return originalWidth !== width;
}

const userAgent = navigator.userAgent;
const isMSIE = /MSIE|Trident/i.test(userAgent);
let browserVersion;
if (isMSIE) {
  let matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
  if (matches) {
    browserVersion = parseFloat(matches[1]);
  }
  matches = /Trident\/.*rv:([0-9]{1,}[.0-9]{0,})/.exec(userAgent);
  if (matches) {
    browserVersion = parseFloat(matches[1]);
  }
}

const isEdge = /Edge\/\d+/.test(userAgent);

let hasCodeMirror = !!window.CodeMirror;
if (!hasCodeMirror && isSupportAmd) {
  // Webpack
  if (typeof __webpack_require__ === 'function') { // eslint-disable-line
    try {
      // If CodeMirror can't be resolved, `require.resolve` will throw an
      // exception and `hasCodeMirror` won't be set to `true`.
      require.resolve('codemirror');
      hasCodeMirror = true;
    } catch (e) {
      // do nothing
    }
  } else if (typeof require !== 'undefined') {
    // Browserify
    if (typeof require.resolve !== 'undefined') {
      try {
        // If CodeMirror can't be resolved, `require.resolve` will throw an
        // exception and `hasCodeMirror` won't be set to `true`.
        require.resolve('codemirror');
        hasCodeMirror = true;
      } catch (e) {
        // do nothing
      }
    // Almond/Require
    } else if (typeof require.specified !== 'undefined') {
      hasCodeMirror = require.specified('codemirror');
    }
  }
}

const isSupportTouch =
  (('ontouchstart' in window) ||
   (navigator.MaxTouchPoints > 0) ||
   (navigator.msMaxTouchPoints > 0));

// [workaround] IE doesn't have input events for contentEditable
// - see: https://goo.gl/4bfIvA
const inputEventName = isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';

/**
 * @class core.env
 *
 * Object which check platform and agent
 *
 * @singleton
 * @alternateClassName env
 */
export default {
  isMac: navigator.appVersion.indexOf('Mac') > -1,
  isMSIE,
  isEdge,
  isFF: !isEdge && /firefox/i.test(userAgent),
  isPhantom: /PhantomJS/i.test(userAgent),
  isWebkit: !isEdge && /webkit/i.test(userAgent),
  isChrome: !isEdge && /chrome/i.test(userAgent),
  isSafari: !isEdge && /safari/i.test(userAgent),
  browserVersion,
  jqueryVersion: parseFloat($.fn.jquery),
  isSupportAmd,
  isSupportTouch,
  hasCodeMirror,
  isFontInstalled,
  isW3CRangeSupport: !!document.createRange,
  inputEventName
};
<commit_msg>Fix change event on input not firing in EDGE<commit_after>import $ from 'jquery';
const isSupportAmd = typeof define === 'function' && define.amd; // eslint-disable-line

/**
 * returns whether font is installed or not.
 *
 * @param {String} fontName
 * @return {Boolean}
 */
function isFontInstalled(fontName) {
    const testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
    const $tester = $('<div>').css({
        position: 'absolute',
        left: '-9999px',
        top: '-9999px',
        fontSize: '200px'
    }).text('mmmmmmmmmwwwwwww').appendTo(document.body);

    const originalWidth = $tester.css('fontFamily', testFontName).width();
    const width = $tester.css('fontFamily', fontName + ',' + testFontName).width();

    $tester.remove();

    return originalWidth !== width;
}

const userAgent = navigator.userAgent;
const isMSIE = /MSIE|Trident/i.test(userAgent);
let browserVersion;
if (isMSIE) {
    let matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
    if (matches) {
        browserVersion = parseFloat(matches[1]);
    }
    matches = /Trident\/.*rv:([0-9]{1,}[.0-9]{0,})/.exec(userAgent);
    if (matches) {
        browserVersion = parseFloat(matches[1]);
    }
}

const isEdge = /Edge\/\d+/.test(userAgent);

let hasCodeMirror = !!window.CodeMirror;
if (!hasCodeMirror && isSupportAmd) {
    // Webpack
    if (typeof __webpack_require__ === 'function') { // eslint-disable-line
        try {
            // If CodeMirror can't be resolved, `require.resolve` will throw an
            // exception and `hasCodeMirror` won't be set to `true`.
            require.resolve('codemirror');
            hasCodeMirror = true;
        } catch (e) {
            // do nothing
        }
    } else if (typeof require !== 'undefined') {
        // Browserify
        if (typeof require.resolve !== 'undefined') {
            try {
                // If CodeMirror can't be resolved, `require.resolve` will throw an
                // exception and `hasCodeMirror` won't be set to `true`.
                require.resolve('codemirror');
                hasCodeMirror = true;
            } catch (e) {
                // do nothing
            }
            // Almond/Require
        } else if (typeof require.specified !== 'undefined') {
            hasCodeMirror = require.specified('codemirror');
        }
    }
}

const isSupportTouch =
    (('ontouchstart' in window) ||
        (navigator.MaxTouchPoints > 0) ||
        (navigator.msMaxTouchPoints > 0));

// [workaround] IE doesn't have input events for contentEditable
// - see: https://goo.gl/4bfIvA
const inputEventName = (isMSIE || isEdge) ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';

/**
 * @class core.env
 *
 * Object which check platform and agent
 *
 * @singleton
 * @alternateClassName env
 */
export default {
    isMac: navigator.appVersion.indexOf('Mac') > -1,
    isMSIE,
    isEdge,
    isFF: !isEdge && /firefox/i.test(userAgent),
    isPhantom: /PhantomJS/i.test(userAgent),
    isWebkit: !isEdge && /webkit/i.test(userAgent),
    isChrome: !isEdge && /chrome/i.test(userAgent),
    isSafari: !isEdge && /safari/i.test(userAgent),
    browserVersion,
    jqueryVersion: parseFloat($.fn.jquery),
    isSupportAmd,
    isSupportTouch,
    hasCodeMirror,
    isFontInstalled,
    isW3CRangeSupport: !!document.createRange,
    inputEventName
};<|endoftext|>


<commit_before>/* global define */

define([
    'underscore',
    'backbone',
    './config',
    './channels',
    './utils',
    'plugins/js',
    'plugins/jquery-ajax-queue',
    // Conditional inclusion of json2 (for ie7)
    JSON === undefined ? 'json2' : null
], function(_, Backbone, config, channels, utils) {

    var cilantro = {
        // Version of cilantro
        version: '2.3.7-beta',

        // Defines the minimum version and maximum version Serrano that this version
        // of Cilantro is 100% compatible with. While Cilantro will attempt to run
        // normally despite the version number received from the server, the user
        // will be warned if no version number is found or if it is less than this
        // minimum to prepare them in the case of missing or broken functionality.
        minSerranoVersion: '2.0.16',
        maxSerranoVersion: '2.3.8',

        // Returns the current session's Serrano version. If there is no
        // active session or version defined, the absolute minimum is returned.
        getSerranoVersion: function() {
            if (this.session) {
                return this.utils.cleanVersionString(this.session.version);
            }
        },

        // Returns a boolean as to whether the current Serrano version is
        // supported relative to the min and max.
        isSupported: function(version) {
            var minVersion = this.minSerranoVersion,
                maxVersion = this.maxSerranoVersion,
                serranoVersion = this.getSerranoVersion();

            // Cilantro may support a version of Serrano greater than the
            // current one being accessed.
            if (utils.versionIsLt(serranoVersion, maxVersion)) {
                maxVersion = serranoVersion;
            }

            return utils.versionInRange(version, minVersion, maxVersion);
        },

        // Initialize the session manager and default configuration
        config: new config.Config(this.cilantro),

        // Attach commonly used utilities
        utils: utils,
    };

    // Give the cilantro object events!
    _.extend(cilantro, Backbone.Events, channels);

    return cilantro;
});
<commit_msg>Increase max supported Serrano version to 2.3.10<commit_after>/* global define */

define([
    'underscore',
    'backbone',
    './config',
    './channels',
    './utils',
    'plugins/js',
    'plugins/jquery-ajax-queue',
    // Conditional inclusion of json2 (for ie7)
    JSON === undefined ? 'json2' : null
], function(_, Backbone, config, channels, utils) {

    var cilantro = {
        // Version of cilantro
        version: '2.3.7-beta',

        // Defines the minimum version and maximum version Serrano that this version
        // of Cilantro is 100% compatible with. While Cilantro will attempt to run
        // normally despite the version number received from the server, the user
        // will be warned if no version number is found or if it is less than this
        // minimum to prepare them in the case of missing or broken functionality.
        minSerranoVersion: '2.0.16',
        maxSerranoVersion: '2.3.10',

        // Returns the current session's Serrano version. If there is no
        // active session or version defined, the absolute minimum is returned.
        getSerranoVersion: function() {
            if (this.session) {
                return this.utils.cleanVersionString(this.session.version);
            }
        },

        // Returns a boolean as to whether the current Serrano version is
        // supported relative to the min and max.
        isSupported: function(version) {
            var minVersion = this.minSerranoVersion,
                maxVersion = this.maxSerranoVersion,
                serranoVersion = this.getSerranoVersion();

            // Cilantro may support a version of Serrano greater than the
            // current one being accessed.
            if (utils.versionIsLt(serranoVersion, maxVersion)) {
                maxVersion = serranoVersion;
            }

            return utils.versionInRange(version, minVersion, maxVersion);
        },

        // Initialize the session manager and default configuration
        config: new config.Config(this.cilantro),

        // Attach commonly used utilities
        utils: utils,
    };

    // Give the cilantro object events!
    _.extend(cilantro, Backbone.Events, channels);

    return cilantro;
});
<|endoftext|>


<commit_before>var MBusProtocol = function(app, serialPort) {
    this.app = app;
    this.serialPort = serialPort;
    this.default = {
        bitrate: 2400,
        dataBits: "eight",
        parityBit: "even",
        stopBits: "one"
    };
};
MBusProtocol.prototype.performReadout = function(callback) {
    var self = this;
    if(self.app.options.mbus.optoWake) {
        self.optoWake(function(result) {
            self.stage1.call(self, callback);
        });
    } else {
        self.stage1.call(self, callback);
    }
};
MBusProtocol.prototype.stage1 = function(callback) {
    var self = this;
    self.final_callback = callback;
    self.serialPort.state = 'recv';
    self.serialPort.timer = 100;
    self.serialPort.eot_char = null;

    var mbus = new MBus();
    var frame = mbus.pingFrame(self.app.options.mbus.address);

    var serialOpts = {
        bitrate: self.app.options.mbus.baudrate,
        dataBits: self.default.dataBits,
        parityBit: self.default.parityBit,
        stopBits: self.default.stopBits
    };

    chrome.serial.update(self.serialPort.connectionId, serialOpts, function(r1) {
        chrome.serial.send(self.serialPort.connectionId, str2ab(frame), function(r2) {
            self.stage2.call(self, r2);
        });
    });
};
MBusProtocol.prototype.stage2 = function(result) {
    var self = this;
    self.serialPort.timer = 1000;

    self.serialPort.recvWatchdog(function(data) {
        var mbus = new MBus();
        try {
            var frame = mbus.load(data);
            // FIXME: verify if ACK frame

            self.stage3.call(self);

        } catch(err) {
            Materialize.toast(err, 2000);
            return self.final_callback.call(self);
        }
    });
};
MBusProtocol.prototype.stage3 = function(result) {
    var self = this;
    self.serialPort.state = 'recv';

    var mbus = new MBus();
    var frame = mbus.requestFrame(self.app.options.mbus.address);

    function convertToHex(str) {
        var hex = '';
        for(var i=0;i<str.length;i++) {
            hex += ''+str.charCodeAt(i).toString(16);
        }
        return hex;
    }

    chrome.serial.send(self.serialPort.connectionId, str2ab(frame.toString()), function(result) {
        self.serialPort.recvWatchdog(function(data) {
            var mbus = new MBus();
            try {
                var frame = mbus.load(data);

                self.final_callback(frame);

            } catch(err) {
                Materialize.toast(err, 2000);
                return self.final_callback.call(self);
            }
        });
    });
};
MBusProtocol.prototype.optoWake = function(callback) {
    var self = this;

    if(self.connectionId === null) {
        callback.apply(self);
    } else {
        // FIXME: Should we always use 2400 baud here? self.options.bitrate;
        var serialOpts = {
            bitrate: self.app.options.mbus.baudrate,
            dataBits: "eight",
            parityBit: "no",
            stopBits: "one"
        };

        chrome.serial.update(self.serialPort.connectionId, serialOpts, function(result) {
            var numChars = 0;
            var i1 = setInterval(function() {
                chrome.serial.send(self.serialPort.connectionId, str2ab("\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55"), function(result) {
                    numChars += 10;
                });
            }, (100/serialOpts.bitrate)*1000);

            setTimeout(function() {
                clearInterval(i1);
                chrome.serial.flush(self.serialPort.connectionId, callback);
            }, 2000);
        });
    }
};<commit_msg>Working state again...<commit_after>var MBusProtocol = function(app, serialPort) {
    this.app = app;
    this.serialPort = serialPort;
    this.default = {
        bitrate: 2400,
        dataBits: "eight",
        parityBit: "even",
        stopBits: "one"
    };
};
MBusProtocol.prototype.performReadout = function(callback) {
    var self = this;
    if(self.app.options.mbus.optoWake) {
        self.optoWake(function(result) {
            self.stage1.call(self, callback);
        });
    } else {
        self.stage1.call(self, callback);
    }
};
MBusProtocol.prototype.stage1 = function(callback) {
    var self = this;
    self.final_callback = callback;
    self.serialPort.state = 'send';
    self.serialPort.timer = 100;
    self.serialPort.eot_char = null;

    var mbus = new MBus();
    var frame = mbus.pingFrame(self.app.options.mbus.address);

    var serialOpts = {
        bitrate: self.app.options.mbus.baudrate,
        dataBits: self.default.dataBits,
        parityBit: self.default.parityBit,
        stopBits: self.default.stopBits
    };

    chrome.serial.update(self.serialPort.connectionId, serialOpts, function(r1) {
        setTimeout(function() {
            chrome.serial.send(self.serialPort.connectionId, str2ab(frame), function(r2) {
                self.stage2.call(self, r2);
            });
        }, 200);
    });
};
MBusProtocol.prototype.stage2 = function(result) {
    var self = this;
    self.serialPort.state = 'recv';
    self.serialPort.timer = 500;

    self.serialPort.recvWatchdog(function(data) {
        var mbus = new MBus();
        try {
            var frame = mbus.load(data);
            // FIXME: verify if ACK frame

            self.stage3.call(self);

        } catch(err) {
            Materialize.toast(err, 2000);
            return self.final_callback.call(self);
        }
    });
};
MBusProtocol.prototype.stage3 = function(result) {
    var self = this;
    self.serialPort.state = 'recv';
    self.serialPort.timer = 3000;

    var mbus = new MBus();
    var frame = mbus.requestFrame(self.app.options.mbus.address);

    function convertToHex(str) {
        var hex = '';
        for(var i=0;i<str.length;i++) {
            hex += ''+str.charCodeAt(i).toString(16);
        }
        return hex;
    }

    chrome.serial.send(self.serialPort.connectionId, str2ab(frame.toString()), function(result) {
        self.serialPort.recvWatchdog(function(data) {
            var mbus = new MBus();
            try {
                var frame = mbus.load(data);

                self.final_callback(frame);

            } catch(err) {
                Materialize.toast(err, 2000);
                return self.final_callback.call(self);
            }
        });
    });
};
MBusProtocol.prototype.optoWake = function(callback) {
    var self = this;
    self.serialPort.state = 'send';

    if(self.connectionId === null) {
        callback.apply(self);
    } else {
        // FIXME: Should we always use 2400 baud here? self.options.bitrate;
        var serialOpts = {
            bitrate: self.app.options.mbus.baudrate,
            dataBits: "eight",
            parityBit: "no",
            stopBits: "one"
        };

        chrome.serial.update(self.serialPort.connectionId, serialOpts, function(result) {
            var numChars = 0;
            var buff = ""
            for(var i = 0; i < 48; i++) {
                buff += "\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55";
            }

            chrome.serial.send(self.serialPort.connectionId, str2ab(buff), function(result) {
                setTimeout(function() {
                    callback.call(result);
                }, 2500);
            });
        });
    }
};<|endoftext|>


<commit_before>var appMessageQueue = {
	queue: [],
	numTries: 0,
	maxTries: 5,
	add: function(obj) {
		this.queue.push(obj);
	},
	clear: function() {
		this.queue = [];
	},
	isEmpty: function() {
		return this.queue.length === 0;
	},
	nextMessage: function() {
		return this.isEmpty() ? {} : this.queue[0];
	},
	send: function() {
		if (this.queue.length > 0) {
			var ack = function() {
				appMessageQueue.numTries = 0;
				appMessageQueue.queue.shift();
				appMessageQueue.send();
			};
			var nack = function() {
				appMessageQueue.numTries++;
				appMessageQueue.send();
			};
			if (this.numTries >= this.maxTries) {
				console.log('Failed sending AppMessage: ' + JSON.stringify(this.nextMessage()));
				ack();
			}
			console.log('Sending AppMessage: ' + JSON.stringify(this.nextMessage()));
			Pebble.sendAppMessage(this.nextMessage(), ack, nack);
		}
	}
};

var WeMo = {
	server: localStorage.getItem('server') || '',
	devices: [],
	error: function(error) {
		appMessageQueue.clear();
		appMessageQueue.add({error:error});
		appMessageQueue.send();
	},
	toggle: function(index) {
		if (!this.server) return this.error('no_server_set');
		var xhr = new XMLHttpRequest();
		xhr.open('POST', 'http://' + this.server + ':5000/api/device/' + encodeURIComponent(this.devices[index].name), true);
		xhr.onload = function() {
			appMessageQueue.clear();
			try {
				var res = JSON.parse(xhr.responseText);
				var name = res.name.substring(0,32) || '';
				var host = res.host.substring(0,32) || '';
				var type = res.type.substring(0,16) || '';
				var state = res.state ? 'ON' : 'OFF';
				appMessageQueue.add({index:index, name:name, host:host, type:type, state:state});
				appMessageQueue.add({index:WeMo.devices.length});
			} catch(e) {
				appMessageQueue.add({index:index, name:WeMo.devices[index].name, host:'Error!', type:WeMo.devices[index].type, state:''});
				appMessageQueue.add({index:WeMo.devices.length});
			}
			appMessageQueue.send();
		};
		xhr.ontimeout = function() { WeMo.error('timeout'); };
		xhr.onerror = function() { WeMo.error('error'); };
		xhr.timeout = 10000;
		xhr.send(null);
	},
	refresh: function() {
		if (!this.server) return this.error('no_server_set');
		this.devices = [];
		var xhr = new XMLHttpRequest();
		xhr.open('GET', 'http://' + this.server + ':5000/api/environment', true);
		xhr.onload = function() {
			appMessageQueue.clear();
			try {
				var res = JSON.parse(xhr.responseText);
				var i = 0;
				for (var r in res) {
					var name = res[r].name.substring(0,32) || '';
					var host = res[r].host.substring(0,32) || '';
					var type = res[r].type.substring(0,16) || '';
					var state = res[r].state ? 'ON' : 'OFF';
					appMessageQueue.add({index:i++, name:name, host:host, type:type, state:state});
					WeMo.devices.push(res[r]);
				}
				appMessageQueue.add({index:i});
			} catch(e) {
				appMessageQueue.add({error:'server_error'});
			}
			appMessageQueue.send();
		};
		xhr.ontimeout = function() { WeMo.error('timeout'); };
		xhr.onerror = function() { WeMo.error('error'); };
		xhr.timeout = 10000;
		xhr.send(null);
	}
};

Pebble.addEventListener('ready', function(e) {
	WeMo.refresh();
});

Pebble.addEventListener('appmessage', function(e) {
	if (typeof(e.payload.index) != 'undefined') WeMo.toggle(e.payload.index); else WeMo.refresh();
});

Pebble.addEventListener('showConfiguration', function() {
	var uri = 'http://neal.github.io/WeMote/configuration/index.html?server=' + encodeURIComponent(WeMo.server);
	Pebble.openURL(uri);
});

Pebble.addEventListener('webviewclosed', function(e) {
	if (e.response) {
		var data = JSON.parse(decodeURIComponent(e.response));
		WeMo.server = data.server;
		localStorage.setItem('server', WeMo.server);
		WeMo.refresh();
	}
});
<commit_msg>Update configuration page URL.<commit_after>var appMessageQueue = {
	queue: [],
	numTries: 0,
	maxTries: 5,
	add: function(obj) {
		this.queue.push(obj);
	},
	clear: function() {
		this.queue = [];
	},
	isEmpty: function() {
		return this.queue.length === 0;
	},
	nextMessage: function() {
		return this.isEmpty() ? {} : this.queue[0];
	},
	send: function() {
		if (this.queue.length > 0) {
			var ack = function() {
				appMessageQueue.numTries = 0;
				appMessageQueue.queue.shift();
				appMessageQueue.send();
			};
			var nack = function() {
				appMessageQueue.numTries++;
				appMessageQueue.send();
			};
			if (this.numTries >= this.maxTries) {
				console.log('Failed sending AppMessage: ' + JSON.stringify(this.nextMessage()));
				ack();
			}
			console.log('Sending AppMessage: ' + JSON.stringify(this.nextMessage()));
			Pebble.sendAppMessage(this.nextMessage(), ack, nack);
		}
	}
};

var WeMo = {
	server: localStorage.getItem('server') || '',
	devices: [],
	error: function(error) {
		appMessageQueue.clear();
		appMessageQueue.add({error:error});
		appMessageQueue.send();
	},
	toggle: function(index) {
		if (!this.server) return this.error('no_server_set');
		var xhr = new XMLHttpRequest();
		xhr.open('POST', 'http://' + this.server + ':5000/api/device/' + encodeURIComponent(this.devices[index].name), true);
		xhr.onload = function() {
			appMessageQueue.clear();
			try {
				var res = JSON.parse(xhr.responseText);
				var name = res.name.substring(0,32) || '';
				var host = res.host.substring(0,32) || '';
				var type = res.type.substring(0,16) || '';
				var state = res.state ? 'ON' : 'OFF';
				appMessageQueue.add({index:index, name:name, host:host, type:type, state:state});
				appMessageQueue.add({index:WeMo.devices.length});
			} catch(e) {
				appMessageQueue.add({index:index, name:WeMo.devices[index].name, host:'Error!', type:WeMo.devices[index].type, state:''});
				appMessageQueue.add({index:WeMo.devices.length});
			}
			appMessageQueue.send();
		};
		xhr.ontimeout = function() { WeMo.error('timeout'); };
		xhr.onerror = function() { WeMo.error('error'); };
		xhr.timeout = 10000;
		xhr.send(null);
	},
	refresh: function() {
		if (!this.server) return this.error('no_server_set');
		this.devices = [];
		var xhr = new XMLHttpRequest();
		xhr.open('GET', 'http://' + this.server + ':5000/api/environment', true);
		xhr.onload = function() {
			appMessageQueue.clear();
			try {
				var res = JSON.parse(xhr.responseText);
				var i = 0;
				for (var r in res) {
					var name = res[r].name.substring(0,32) || '';
					var host = res[r].host.substring(0,32) || '';
					var type = res[r].type.substring(0,16) || '';
					var state = res[r].state ? 'ON' : 'OFF';
					appMessageQueue.add({index:i++, name:name, host:host, type:type, state:state});
					WeMo.devices.push(res[r]);
				}
				appMessageQueue.add({index:i});
			} catch(e) {
				appMessageQueue.add({error:'server_error'});
			}
			appMessageQueue.send();
		};
		xhr.ontimeout = function() { WeMo.error('timeout'); };
		xhr.onerror = function() { WeMo.error('error'); };
		xhr.timeout = 10000;
		xhr.send(null);
	}
};

Pebble.addEventListener('ready', function(e) {
	WeMo.refresh();
});

Pebble.addEventListener('appmessage', function(e) {
	if (typeof(e.payload.index) != 'undefined') WeMo.toggle(e.payload.index); else WeMo.refresh();
});

Pebble.addEventListener('showConfiguration', function() {
	var uri = 'http://ineal.me/pebble/wemote/configuration?server=' + encodeURIComponent(WeMo.server);
	Pebble.openURL(uri);
});

Pebble.addEventListener('webviewclosed', function(e) {
	if (e.response) {
		var data = JSON.parse(decodeURIComponent(e.response));
		WeMo.server = data.server;
		localStorage.setItem('server', WeMo.server);
		WeMo.refresh();
	}
});
<|endoftext|>


<commit_before>/**
 * XHR wrapper
 * Usage:
 * ask({
 *   url: ...,
 *   method: null, // default depends on data: GET or POST
 *   data: null, // null => default GET
 *   headers: additional request headers (eg: {'Content-Type': 'text/plain'}
 *   success: function(text, event, xhr){...},
 *   failure: function(code, text, event, xhr){...},
 * });
 */
function ask(o) {
	function p(name, def) {
		return o[name] || def;
	}
	var req = new XMLHttpRequest();
	req.open(p('method', (o.data?'POST':'GET')), o.url, true); // open async
	headers = p('headers', {});
	for(h in headers)
		req.setRequestHeader(h, headers[h]);
	req.onload = function(e) {
		if(req.readyState == 4) {
			text = req.responseText;
			if(req.status == 200) {
				console.log("xhr:success");
				if(o.success)
					o.success(text, e, req);
			} else {
				console.log("xhr:error "+req.status+"\n"+text);
				if(o.failure)
					o.failure(req.status, text, e, req);
			}
		}
	};
	req.send(p('data', null));
}
/**
 * Usage:
 * getJson(url, success_func, failure_func)
 * or
 * getJson(url, success_func, true) to use the same function
 *
 * success(data, event, xhr)
 * failure(err_code, data, event, xhf)
 */
function getJson(url, success, failure, headers, method, data) {
	ask({
			url: url,
			headers: headers,
			method: method,
			data: data,
			success: function(text, e, xhr) {
				if(success)
					success(JSON.parse(text), e, xhr);
			},
			failure: function(code, text, e, xhr) {
				if(failure === true) {
					if(success)
						success(JSON.parse(text), e, xhr);
				} else if(failure) // function
					failure(code, JSON.parse(text), e, xhr);
			}
	});
}

var g_access_token = "";
var g_refresh_token = "";

/**
 * send query to googleTasks api.
 * Will automatically send error message to Pebble
 * if anything goes wrong.
 *
 * endpoint: e.g. users/@me/lists or lists/.../tasks
 * params: {} with all args
 * success: callback(json)
 * method: get or post or something else (defaults to Get)
 * data: n/a for get method
 */
function queryTasks(endpoint, params, success, method, data) {
	url = "https://www.googleapis.com/tasks/v1/" + endpoint;
	sep = "?";
	if(params) {
		for(p in params)
			url += sep + encodeURIComponent(p) + "=" + encodeURIComponent(params[p]);
		sep = "&";
	}
	headers = {"Authorization": "Bearer "+g_access_token};
	getJson(url, success, function(code, data) {
		if(code == 401) { // Invalid Credentials
			console.log("Renewing token and retrying...");
			renewToken(function() { // renew, and on success -
				getJson(url, success, function(code, data) {
					console.log("Renewal didn't help! "+code+": "+data.error.message);
					displayError(data.error.message, code);
				}, headers, method, data);
			});
		} else {
			displayError(data.error.message, code);
		}
	}, headers, method, data);
}

/**
 * Checks current g_access_token for validity (TODO)
 * and requests new if neccessary,
 * after which calls Success callback.
 * [Now just requests new token and saves it]
 * In case of error it will call displayError.
 */
function renewToken(success) {
	console.log("Renewing token!");
	refresh_token = localStorage["refresh_token"];
	if(!refresh_token) {
		displayError("No refresh token; please log in!", 401);
		return;
	}
	getJson("https://pebble-notes.appspot.com/v1/auth/refresh?refresh_token="+encodeURIComponent(refresh_token),
		function(data) { // success
			console.log("Renewed. "+JSON.stringify(data));
			if("access_token" in data) {
				localStorage["access_token"] = data.access_token;
				g_access_token = data.access_token;
				success(g_access_token);
			} else if("error" in data) {
				displayError(data.error);
			} else {
				displayError("No access token!");
			}
		},
		function(code, data) { // failure
			displayError(data.error.message, code);
		});
}

/**
 * Sends an error packet to Pebble
 * code may be null
 */
function displayError(text, code) {
	if(code) text += " (" + code + ")";
	console.log("Sending error msg to Pebble (Not implemented): " + text);
	// TODO
}
function assert(val, message) {
	if(!val) {
		console.log("assertion failed");
		displayError(message);
	}
}

var g_msg_buffer = [];
var g_msg_transaction = null;

/**
 * Sends appMessage to pebble; logs errors.
 */
function sendMessage(data) {
	function sendNext() {
		g_msg_transaction = null;
		next = g_msg_buffer.shift();
		if(next) { // have another msg to send
			sendMessage(next);
		}
	}
	if(g_msg_transaction) { // busy
		g_msg_buffer.push(data);
	} else { // free
		g_msg_transaction = Pebble.sendAppMessage(data,
			function(e) {
				console.log("Message sent for transactionId=" + e.data.transactionId);
				if(g_msg_transaction != e.data.transactionId)
					console.log("### Confused! Message sent which is not a current message. "+
							"Current="+g_msg_transaction+", sent="+e.data.transactionId);
				sendNext();
			},
		   	function(e) {
				console.log("Failed to send message for transactionId=" + e.data.transactionId + ", error is "+e.error.message);
				if(g_msg_transaction != e.data.transactionId)
					console.log("### Confused! Message not sent, but it is not a current message. "+
							"Current="+g_msg_transaction+", unsent="+e.data.transactionId);
				sendNext();
			});
			console.log("transactionId="+g_msg_transaction+" for msg "+JSON.stringify(data));
	}
}

var g_tasklists = [];

/* Main logic */
function doGetAllLists() {
	console.log("Querying all tasklists");
	queryTasks("users/@me/lists", null, function(d) {
		console.log("sending " + d.items.length + " items");
		sendMessage({
				code: 20, // array start/size
				scope: 0,
				count: d.items.length});
		g_tasklists = []; // TODO: use it for caching
		for(var i=0; i<d.items.length; i++) {
			var l = d.items[i];
			var lobj = {
				id: l.id,
				title: l.title,
				size: -1
			};
			var id = g_tasklists.push(lobj);
			console.log("Sending item: " + JSON.stringify(lobj));
			sendMessage({
					code: 21, // array item
					scope: 0,
					item: i,
					listId: id,
					title: lobj.title,
					size: lobj.size}); // TODO
		}
		sendMessage({
				code: 22, // array end
				scope: 0,
				count: g_tasklists.length}); // send resulting list length, just for any
		console.log("sending finished");
	});
}
function doGetOneList(listId) {
	assert(false, "Not implemented yet");
	queryTasks("lists/"+l.id+"/tasks", null, function(d) {
		console.log("result: "+d);
	});
}
function doGetTaskDetails(taskId) {
	assert(false, "Not implemented yet");
}
function doChangeTaskStatus(taskId, isDone) {
	assert(false, "Not implemented yet");
}

/* Initialization */
Pebble.addEventListener("ready", function(e) {
	console.log("JS is running. Okay.");
	g_access_token = localStorage["access_token"];
});

/* Configuration window */
Pebble.addEventListener("showConfiguration", function(e) {
	console.log("Showing config window... new url");
	Pebble.openURL("http://pebble-notes.appspot.com/v1/notes-config.html?option1=off");
});
Pebble.addEventListener("webviewclosed", function(e) {
	console.log("webview closed: "+e.response);
	result = JSON.parse(e.response);
	if(result.access_token && result.refresh_token) { // assume it was a login session
		console.log("Saving tokens");
		// save tokens
		if(result.access_token) {
			localStorage["access_token"] = result.access_token;
			console.log("Access token: " + localStorage["access_token"]);
		}
		if(result.refresh_token) {
			localStorage["refresh_token"] = result.refresh_token;
			console.log("Refresh token saved: " + localStorage.refresh_token);
		}
		// todo: maybe save expire time for later checks? (now + value)
		/*console.log("Received tokens. Testing...");
		getJson("https://www.googleapis.com/oauth2/v1/tokeninfo?id_token="+result.access_token, function(r) {
			console.log(r);
		}, true);*/
	}
});

/* Messages */
Pebble.addEventListener("appmessage", function(e) {
	console.log("Received message: " + JSON.stringify(e.payload));
	switch(e.payload.code) {
	case 10: // get info
		switch(e.payload.scope) {
		case 0: // all lists
			doGetAllLists();
			break;
		case 1: // one list
			assert(e.payload.listId, "List ID was not provided for GetOneList query");
			doGetOneList(e.payload.listId);
			break;
		case 2: // one task
			assert(e.payload.taskId, "Task ID was not provided for GetTaskDetails query");
			doGetTaskDetails(e.payload.taskId);
			break;
		default:
			console.log("Unknown message scope "+e.payload.scope);
			break;
		}
		break;
	case 11: // change info
		switch(e.payload.scope) {
		case 2: // one task (here - "done" status)
			assert(e.payload.taskId, "Task ID was not provided for ChangeTaskStatus query");
			assert(e.payload.isDone, "New task status was not provided for ChangeTaskStatus query");
			doChangeTaskStatus(e.payload.taskId, e.payload.isDone);
			break;
		case 0: // all lists
		case 1: // one list
			console.log("Cannot 'change' info for scope "+e.payload.scope);
			break;
		default:
			console.log("Unknown message scope "+e.payload.scope);
			break;
		}
		break;
	default:
		console.log("Unknown message code "+e.payload.code);
		break;
	}
});
<commit_msg>js: more correct internal tasklist ids.<commit_after>/**
 * XHR wrapper
 * Usage:
 * ask({
 *   url: ...,
 *   method: null, // default depends on data: GET or POST
 *   data: null, // null => default GET
 *   headers: additional request headers (eg: {'Content-Type': 'text/plain'}
 *   success: function(text, event, xhr){...},
 *   failure: function(code, text, event, xhr){...},
 * });
 */
function ask(o) {
	function p(name, def) {
		return o[name] || def;
	}
	var req = new XMLHttpRequest();
	req.open(p('method', (o.data?'POST':'GET')), o.url, true); // open async
	headers = p('headers', {});
	for(h in headers)
		req.setRequestHeader(h, headers[h]);
	req.onload = function(e) {
		if(req.readyState == 4) {
			text = req.responseText;
			if(req.status == 200) {
				console.log("xhr:success");
				if(o.success)
					o.success(text, e, req);
			} else {
				console.log("xhr:error "+req.status+"\n"+text);
				if(o.failure)
					o.failure(req.status, text, e, req);
			}
		}
	};
	req.send(p('data', null));
}
/**
 * Usage:
 * getJson(url, success_func, failure_func)
 * or
 * getJson(url, success_func, true) to use the same function
 *
 * success(data, event, xhr)
 * failure(err_code, data, event, xhf)
 */
function getJson(url, success, failure, headers, method, data) {
	ask({
			url: url,
			headers: headers,
			method: method,
			data: data,
			success: function(text, e, xhr) {
				if(success)
					success(JSON.parse(text), e, xhr);
			},
			failure: function(code, text, e, xhr) {
				if(failure === true) {
					if(success)
						success(JSON.parse(text), e, xhr);
				} else if(failure) // function
					failure(code, JSON.parse(text), e, xhr);
			}
	});
}

var g_access_token = "";
var g_refresh_token = "";

/**
 * send query to googleTasks api.
 * Will automatically send error message to Pebble
 * if anything goes wrong.
 *
 * endpoint: e.g. users/@me/lists or lists/.../tasks
 * params: {} with all args
 * success: callback(json)
 * method: get or post or something else (defaults to Get)
 * data: n/a for get method
 */
function queryTasks(endpoint, params, success, method, data) {
	url = "https://www.googleapis.com/tasks/v1/" + endpoint;
	sep = "?";
	if(params) {
		for(p in params)
			url += sep + encodeURIComponent(p) + "=" + encodeURIComponent(params[p]);
		sep = "&";
	}
	headers = {"Authorization": "Bearer "+g_access_token};
	getJson(url, success, function(code, data) {
		if(code == 401) { // Invalid Credentials
			console.log("Renewing token and retrying...");
			renewToken(function() { // renew, and on success -
				getJson(url, success, function(code, data) {
					console.log("Renewal didn't help! "+code+": "+data.error.message);
					displayError(data.error.message, code);
				}, headers, method, data);
			});
		} else {
			displayError(data.error.message, code);
		}
	}, headers, method, data);
}

/**
 * Checks current g_access_token for validity (TODO)
 * and requests new if neccessary,
 * after which calls Success callback.
 * [Now just requests new token and saves it]
 * In case of error it will call displayError.
 */
function renewToken(success) {
	console.log("Renewing token!");
	refresh_token = localStorage["refresh_token"];
	if(!refresh_token) {
		displayError("No refresh token; please log in!", 401);
		return;
	}
	getJson("https://pebble-notes.appspot.com/v1/auth/refresh?refresh_token="+encodeURIComponent(refresh_token),
		function(data) { // success
			console.log("Renewed. "+JSON.stringify(data));
			if("access_token" in data) {
				localStorage["access_token"] = data.access_token;
				g_access_token = data.access_token;
				success(g_access_token);
			} else if("error" in data) {
				displayError(data.error);
			} else {
				displayError("No access token!");
			}
		},
		function(code, data) { // failure
			displayError(data.error.message, code);
		});
}

/**
 * Sends an error packet to Pebble
 * code may be null
 */
function displayError(text, code) {
	if(code) text += " (" + code + ")";
	console.log("Sending error msg to Pebble (Not implemented): " + text);
	// TODO
}
function assert(val, message) {
	if(!val) {
		console.log("assertion failed");
		displayError(message);
	}
}

var g_msg_buffer = [];
var g_msg_transaction = null;

/**
 * Sends appMessage to pebble; logs errors.
 */
function sendMessage(data) {
	function sendNext() {
		g_msg_transaction = null;
		next = g_msg_buffer.shift();
		if(next) { // have another msg to send
			sendMessage(next);
		}
	}
	if(g_msg_transaction) { // busy
		g_msg_buffer.push(data);
	} else { // free
		g_msg_transaction = Pebble.sendAppMessage(data,
			function(e) {
				console.log("Message sent for transactionId=" + e.data.transactionId);
				if(g_msg_transaction != e.data.transactionId)
					console.log("### Confused! Message sent which is not a current message. "+
							"Current="+g_msg_transaction+", sent="+e.data.transactionId);
				sendNext();
			},
		   	function(e) {
				console.log("Failed to send message for transactionId=" + e.data.transactionId + ", error is "+e.error.message);
				if(g_msg_transaction != e.data.transactionId)
					console.log("### Confused! Message not sent, but it is not a current message. "+
							"Current="+g_msg_transaction+", unsent="+e.data.transactionId);
				sendNext();
			});
			console.log("transactionId="+g_msg_transaction+" for msg "+JSON.stringify(data));
	}
}

var g_tasklists = [];

/* Main logic */
function doGetAllLists() {
	console.log("Querying all tasklists");
	queryTasks("users/@me/lists", null, function(d) {
		console.log("sending " + d.items.length + " items");
		sendMessage({
				code: 20, // array start/size
				scope: 0,
				count: d.items.length});
		g_tasklists = []; // TODO: use it for caching
		for(var i=0; i<d.items.length; i++) {
			var l = d.items[i];
			var lobj = {
				id: l.id,
				title: l.title,
				size: -1
			};
			var id = g_tasklists.push(lobj) - 1;
			console.log("Sending item: " + JSON.stringify(lobj));
			sendMessage({
					code: 21, // array item
					scope: 0,
					item: i,
					listId: id,
					title: lobj.title,
					size: lobj.size}); // TODO
		}
		sendMessage({
				code: 22, // array end
				scope: 0,
				count: g_tasklists.length}); // send resulting list length, just for any
		console.log("sending finished");
	});
}
function doGetOneList(listId) {
	assert(false, "Not implemented yet");
	queryTasks("lists/"+l.id+"/tasks", null, function(d) {
		console.log("result: "+d);
	});
}
function doGetTaskDetails(taskId) {
	assert(false, "Not implemented yet");
}
function doChangeTaskStatus(taskId, isDone) {
	assert(false, "Not implemented yet");
}

/* Initialization */
Pebble.addEventListener("ready", function(e) {
	console.log("JS is running. Okay.");
	g_access_token = localStorage["access_token"];
});

/* Configuration window */
Pebble.addEventListener("showConfiguration", function(e) {
	console.log("Showing config window... new url");
	Pebble.openURL("http://pebble-notes.appspot.com/v1/notes-config.html?option1=off");
});
Pebble.addEventListener("webviewclosed", function(e) {
	console.log("webview closed: "+e.response);
	result = JSON.parse(e.response);
	if(result.access_token && result.refresh_token) { // assume it was a login session
		console.log("Saving tokens");
		// save tokens
		if(result.access_token) {
			localStorage["access_token"] = result.access_token;
			console.log("Access token: " + localStorage["access_token"]);
		}
		if(result.refresh_token) {
			localStorage["refresh_token"] = result.refresh_token;
			console.log("Refresh token saved: " + localStorage.refresh_token);
		}
		// todo: maybe save expire time for later checks? (now + value)
		/*console.log("Received tokens. Testing...");
		getJson("https://www.googleapis.com/oauth2/v1/tokeninfo?id_token="+result.access_token, function(r) {
			console.log(r);
		}, true);*/
	}
});

/* Messages */
Pebble.addEventListener("appmessage", function(e) {
	console.log("Received message: " + JSON.stringify(e.payload));
	switch(e.payload.code) {
	case 10: // get info
		switch(e.payload.scope) {
		case 0: // all lists
			doGetAllLists();
			break;
		case 1: // one list
			assert(e.payload.listId, "List ID was not provided for GetOneList query");
			doGetOneList(e.payload.listId);
			break;
		case 2: // one task
			assert(e.payload.taskId, "Task ID was not provided for GetTaskDetails query");
			doGetTaskDetails(e.payload.taskId);
			break;
		default:
			console.log("Unknown message scope "+e.payload.scope);
			break;
		}
		break;
	case 11: // change info
		switch(e.payload.scope) {
		case 2: // one task (here - "done" status)
			assert(e.payload.taskId, "Task ID was not provided for ChangeTaskStatus query");
			assert(e.payload.isDone, "New task status was not provided for ChangeTaskStatus query");
			doChangeTaskStatus(e.payload.taskId, e.payload.isDone);
			break;
		case 0: // all lists
		case 1: // one list
			console.log("Cannot 'change' info for scope "+e.payload.scope);
			break;
		default:
			console.log("Unknown message scope "+e.payload.scope);
			break;
		}
		break;
	default:
		console.log("Unknown message code "+e.payload.code);
		break;
	}
});
<|endoftext|>


<commit_before>import CModalAlert from '../../components/modal/_alert.vue'
import CModalMessage from '../../components/modal/_message.vue'
import { defer } from '../utils'

export default {
  install (Vue) {
    extendVue(Vue)
  }
}

function extendVue (Vue) {
  const { prototype } = Vue
  const createModal = (data, component) => {
    const deferred = defer()
    const div = document.createElement('div')
    document.body.appendChild(div)

    const vm = new Vue({
      components: {
        'c-portal-app': component
      },
      mounted () {
        // remove comment element
        document.body.removeChild(this.$el)
      },
      render (h) {
        return h('c-portal-app', {
          attrs: data,
          on: {
            cancel () {
              deferred.reject()
            },
            confirm () {
              deferred.resolve()
            },
            destroy () {
              vm.$destroy()
            }
          }
        })
      }
    }).$mount(div)

    return deferred.promise
  }

  prototype.$alert = function (data) {
    // data: { msg, title }
    return createModal(data, CModalAlert)
  }

  prototype.$message = function (data) {
    // data: { msg, title, type }
    return createModal(data, CModalMessage)
  }

  const messageTypes = ['success', 'error', 'info', 'warning']
  messageTypes.forEach(type => {
    prototype.$success = ({ msg, title }) => {
      return this.$message({ msg, title, type })
    }
  })
}
<commit_msg>Update modal.js<commit_after>import CModalAlert from '../../components/modal/_alert.vue'
import CModalMessage from '../../components/modal/_message.vue'
import { defer } from '../utils'

export default {
  install (Vue) {
    extendVue(Vue)
  }
}

function extendVue (Vue) {
  const { prototype } = Vue
  const createModal = (data, component) => {
    const deferred = defer()
    const div = document.createElement('div')
    document.body.appendChild(div)

    const vm = new Vue({
      components: {
        'c-portal-app': component
      },
      mounted () {
        // remove comment element
        document.body.removeChild(this.$el)
      },
      render (h) {
        return h('c-portal-app', {
          attrs: data,
          on: {
            cancel () {
              deferred.reject()
            },
            confirm () {
              deferred.resolve()
            },
            destroy () {
              vm.$destroy()
            }
          }
        })
      }
    }).$mount(div)

    return deferred.promise
  }

  prototype.$alert = function (data) {
    // data: { msg, title }
    return createModal(data, CModalAlert)
  }

  prototype.$message = function (data) {
    // data: { msg, title, type }
    return createModal(data, CModalMessage)
  }

  const messageTypes = ['success', 'error', 'info', 'warning']
  messageTypes.forEach(type => {
    prototype[$ + type] = ({ msg, title }) => {
      return this.$message({ msg, title, type })
    }
  })
}
<|endoftext|>


<commit_before>/** @jsx React.DOM */

$(document).ready(function() {
    var linkDefault = {
        'Link Area': [{
            'title': '',
            'icon': '',
            'text': '',
            'value': ''
        }]
    };
    var modeDefault = [{
            'hotkey': '',
            'indicator': '',
            'queryBefore': '',
            'queryafter': '',
            'show': '',
            'title': ''
        }];
  chrome.storage.sync.get({
    googleCalendarID: '',
    googleCalendarKey: '',
    linkData: linkDefault,
    modes: modeDefault,
    customCSS: '',
    bgImage: ''
    }, function(items) {
      console.log("calID", items.googleCalendarID);
      console.log("calKey", items.googleCalendarKey);
      console.log("linkData", items.linkData);
      console.log("modes", items.modes);
      console.log("customCSS", items.customCss);
      console.log("bgImage", items.bgImage);
    });
});

var React = require('react');

var Options = React.createClass({
    render: function() {
        return (

TABernacle Settings

                <CalendarSettings />
                <LinkSettings />
                <ModeSettings />
                <CssSettings />
                <BgSettings />

        )
    }
});

var CalendarSettings = React.createClass({
    handleChange: function(name, event) {

        //TODO: rename the keys to be the same between chrome storage and state

        var state = {};
        var stateName;
        if (name == 'googleCalendarKey') {
            stateName = "calKey";
        } else if (name == 'googleCalendarID') {
            stateName = 'calID';
        }

        console.log('calChange', event.target.value)
        state[stateName] = event.target.value;
        this.setState(state);

        var setting = {};
        setting[name] = event.target.value;
        chrome.storage.sync.set(setting);
    },
    componentDidMount: function () {
        var self = this;
        chrome.storage.sync.get({
            googleCalendarKey: '',
            googleCalendarID: ''
        }, function(items){
            self.setState({
                calKey: items.googleCalendarKey,
                calID: items.googleCalendarID
            })
        });
    },
    getInitialState: function() {
        return {
            calKey: '',
            calID: ''
        }
    },
    render: function() {
        return (
            <form id="calendar-settings" className="col-md-4">

Calendar Settings

                    Calendar ID
                    <input id="calID" className="form-control" type="text" onChange={this.handleChange.bind(this, 'googleCalendarID')} value={this.state.calID} />

                    Calendar API Key
                    <input id="calKey" className="form-control" type="text" onChange={this.handleChange.bind(this, 'googleCalendarKey')} value={this.state.calKey} />

            </form>
        )
    }
});

var LinkSettings = React.createClass({
    addArea: function (){
        console.log('add area');
        var self = this;
        var state = self.state;
        state.data['New Area'] = [];
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    removeArea: function(area){
        console.log('area name', area);
        console.log('remove area');
        var self = this;
        var state = self.state;
        delete state.data[area];
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    addLink: function(area){
        console.log('add link');
        console.log(area);
        var self = this;
        var state = self.state;
        state.data[area].push({
            'title' : '',
            'icon': '',
            'text': '',
            'value': ''
        })
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    removeLink: function(link){
        console.log('index', link.index);
        console.log('area', link.linkArea);
        console.log('remove link');
        var self = this;
        var state = self.state;
        state.data[link.linkArea].splice(link.index, 1);
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    handleChange: function(name, event) {
        var self = this;
        var linkArea = name.linkArea;
        var index = name.index;
        var value = name.value;
        var state = self.state;
        state.data[linkArea][index][value] = event.target.value;
        var stateData = state.data
        self.setState({
            stateData
        });
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    componentDidMount: function () {
        var self = this;
        var linkDefault = {
            'Link Area': [{
                'title': '',
                'icon': '',
                'text': '',
                'value': ''
            }]
        };
        chrome.storage.sync.get({
            linkData: linkDefault
        }, function(items){
            self.setState({
                data: items.linkData
            })
        });
    },
    getInitialState: function() {
        var linkDefault = {
            'Link Area': [{
                'title': '',
                'icon': '',
                'text': '',
                'value': ''
            }]
        };
        return {
            data: linkDefault
        }
    },
    render: function() {
        var modules = [];
        var i = 0;
        for (module in this.state.data) {
            modules.push(<LinkWrapper removeArea={this.removeArea} addLink={this.addLink} removeLink={this.removeLink} title={module} key={i} onChange={this.handleChange} data={this.state.data[module]} />);
            i++;
        }
        return (

                add link area

Link Settings

                <form id="link-settings">{modules}</form>

        )
    }
});

var LinkWrapper = React.createClass({
    render: function() {
        var self = this;
        var links = this.props.data.map(function(link, i) {
            return <LinkForm data={link} key={i} index={i} removeLink={self.props.removeLink} onChange={self.props.onChange} title={self.props.title} />
        });
        return (

                remove link area
                Link Area Title
                <input id="title" type="text" title={this.props.title} onChange={this.props.onChange.bind(null, this.props.data )} className="form-control" data-prop="title" value={this.props.title}></input>
                {links}
                add link

        )
    }
});

var LinkForm = React.createClass({
    render: function() {
        return (

                remove link
                Title
                <input id="title" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'title', 'index': this.props.index})} type="text" className="form-control" data-prop="title" value={this.props.data.title}></input>

                Icon
                <input id="icon" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'icon', 'index': this.props.index})} type="text" className="form-control" data-prop="icon" value={this.props.data.icon}></input>

                Text
                <input id="text" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'text', 'index': this.props.index})} type="text" className="form-control" data-prop="text" value={this.props.data.text}></input>

                Value
                <input id="value" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'value', 'index': this.props.index})} type="text" className="form-control" data-prop="value" value={this.props.data.value}></input>

        )
    }
})

var ModeSettings = React.createClass({
    addMode: function() {
        var self = this;
        var state = self.state;

        state.data.push({
            'hotkey': '',
            'indicator': '',
            'queryBefore': '',
            'queryafter': '',
            'show': '',
            'title': ''
        });

        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.modes = state.data;
        chrome.storage.sync.set(chromeData);
    },
    removeMode: function(index) {
        var self = this;
        var state = self.state;

        state.data.splice(index, 1);

        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.modes = state.data;
        chrome.storage.sync.set(chromeData);
    },
    handleChange: function(name, event) {
        var self = this;
        var index = name.index;
        var value = name.value;
        var state = self.state;
        state.data[index][value] = event.target.value;
        var stateData = state.data
        self.setState({
            stateData
        });
        var chromeData = {};
        chromeData.modes = state.data;
        chrome.storage.sync.set(chromeData);
    },
    componentDidMount: function() {
        var self = this;
        var modeDefault = [{
                'hotkey': '',
                'indicator': '',
                'queryBefore': '',
                'queryafter': '',
                'show': '',
                'title': ''
            }];
        chrome.storage.sync.get({
            modes: modeDefault
        }, function(items){
            self.setState({
                data: items.modes
            })
        });
    },
    getInitialState: function() {
        var modeDefault = [{
                'hotkey': '',
                'indicator': '',
                'queryBefore': '',
                'queryafter': '',
                'show': '',
                'title': ''
            }];
        return {
            data: [1,2]
        }
    },
    render: function() {
        var self = this;
        console.log('data', this.state);
        var modes = this.state.data.map(function(mode, i){
            return <ModeForm data={mode} removeMode={self.removeMode} onChange={self.handleChange} index={i} key={i} />
        });
        return (
            <form id="link-settings" className="col-md-4">

Search Modes

            add mode
            {modes}
            </form>
        )
    }
});

var ModeForm = React.createClass({
    render: function() {
        return (

                remove mode

                Title
                <input id="title" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'title'})} className="form-control" data-prop="title" value={this.props.data.title}></input>

                Before Search Query
                <input id="queryBefore" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'queryBefore'})} className="form-control" data-prop="queryBefore" value={this.props.data.queryBefore}></input>

                After Search Query
                <input id="queryAfter" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'queryafter'})} className="form-control" data-prop="queryAfter" value={this.props.data.queryafter}></input>

                Hotkey
                <input id="hotkey" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'hotkey'})} className="form-control" data-prop="hotkey" value={this.props.data.hotkey}></input>

                Indicator
                <input id="indicator" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'indicator'})} className="form-control" data-prop="indicator" value={this.props.data.indicator}></input>

                Show
                <input id="show" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'show'})} className="form-control" data-prop="show" value={this.props.data.show}></input>

        )
    }
});

var CssSettings = React.createClass({
    render: function() {
        return (
            <form id="css-settings" className="col-md-4">
                Custom CSS
                <textarea id="custom-css" className="form-control"></textarea>
            </form>
        )
    }
});

var BgSettings = React.createClass({
    render: function() {
        return (
            <form id="bg-settings" className="col-md-4">
                Backgound Image
                <input id="background-image" className="form-control" type="text"></input>
            </form>
        )
    }
});

React.render(
    <Options />,
    document.getElementById('options')
)<commit_msg>Link Area titles/custom css/bg image<commit_after>/** @jsx React.DOM */

$(document).ready(function() {
    var linkDefault = {
        'Link Area': [{
            'title': '',
            'icon': '',
            'text': '',
            'value': ''
        }]
    };
    var modeDefault = [{
            'hotkey': '',
            'indicator': '',
            'queryBefore': '',
            'queryafter': '',
            'show': '',
            'title': ''
        }];
  chrome.storage.sync.get({
    googleCalendarID: '',
    googleCalendarKey: '',
    linkData: linkDefault,
    modes: modeDefault,
    customCSS: '',
    bgImage: ''
    }, function(items) {
      console.log("calID", items.googleCalendarID);
      console.log("calKey", items.googleCalendarKey);
      console.log("linkData", items.linkData);
      console.log("modes", items.modes);
      console.log("customCSS", items.customCss);
      console.log("bgImage", items.bgImage);
    });
});

var React = require('react');

var Options = React.createClass({
    render: function() {
        return (

TABernacle Settings

                <CalendarSettings />
                <LinkSettings />
                <ModeSettings />
                <CssSettings />
                <BgSettings />

        )
    }
});

var CalendarSettings = React.createClass({
    handleChange: function(name, event) {

        //TODO: rename the keys to be the same between chrome storage and state

        var state = {};
        var stateName;
        if (name == 'googleCalendarKey') {
            stateName = "calKey";
        } else if (name == 'googleCalendarID') {
            stateName = 'calID';
        }

        console.log('calChange', event.target.value)
        state[stateName] = event.target.value;
        this.setState(state);

        var setting = {};
        setting[name] = event.target.value;
        chrome.storage.sync.set(setting);
    },
    componentDidMount: function () {
        var self = this;
        chrome.storage.sync.get({
            googleCalendarKey: '',
            googleCalendarID: ''
        }, function(items){
            self.setState({
                calKey: items.googleCalendarKey,
                calID: items.googleCalendarID
            })
        });
    },
    getInitialState: function() {
        return {
            calKey: '',
            calID: ''
        }
    },
    render: function() {
        return (
            <form id="calendar-settings" className="col-md-4">

Calendar Settings

                    Calendar ID
                    <input id="calID" className="form-control" type="text" onChange={this.handleChange.bind(this, 'googleCalendarID')} value={this.state.calID} />

                    Calendar API Key
                    <input id="calKey" className="form-control" type="text" onChange={this.handleChange.bind(this, 'googleCalendarKey')} value={this.state.calKey} />

            </form>
        )
    }
});

var LinkSettings = React.createClass({
    addArea: function (){
        console.log('add area');
        var self = this;
        var state = self.state;
        state.data['New Area'] = [];
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    removeArea: function(area){
        console.log('area name', area);
        console.log('remove area');
        var self = this;
        var state = self.state;
        delete state.data[area];
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    addLink: function(area){
        console.log('add link');
        console.log(area);
        var self = this;
        var state = self.state;
        state.data[area].push({
            'title' : '',
            'icon': '',
            'text': '',
            'value': ''
        })
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    removeLink: function(link){
        console.log('index', link.index);
        console.log('area', link.linkArea);
        console.log('remove link');
        var self = this;
        var state = self.state;
        state.data[link.linkArea].splice(link.index, 1);
        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    handleChange: function(name, event) {
        var self = this;
        var linkArea = name.linkArea;
        var index = name.index;
        var value = name.value;
        var state = self.state;
        state.data[linkArea][index][value] = event.target.value;
        var stateData = state.data
        self.setState({
            stateData
        });
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    handleTitle: function(name, event){

        //TODO ordering is off on page refresh?

        var self = this;
        var state = {};
        var val = event.target.value
        var state = self.state;
        var thisArea = state.data[name];
        delete state.data[name];
        state.data[event.target.value] = thisArea;
        var stateData = state.data;
        self.setState({
            stateData
        });
        var chromeData = {};
        chromeData.linkData = state.data;
        chrome.storage.sync.set(chromeData);
    },
    componentDidMount: function () {
        var self = this;
        var linkDefault = {
            'Link Area': [{
                'title': '',
                'icon': '',
                'text': '',
                'value': ''
            }]
        };
        chrome.storage.sync.get({
            linkData: linkDefault
        }, function(items){
            self.setState({
                data: items.linkData
            })
        });
    },
    getInitialState: function() {
        var linkDefault = {
            'Link Area': [{
                'title': '',
                'icon': '',
                'text': '',
                'value': ''
            }]
        };
        return {
            data: linkDefault
        }
    },
    render: function() {
        var modules = [];
        var i = 0;
        for (module in this.state.data) {
            modules.push(<LinkWrapper removeArea={this.removeArea} handleTitle={this.handleTitle} addLink={this.addLink} removeLink={this.removeLink} title={module} key={i} onChange={this.handleChange} data={this.state.data[module]} />);
            i++;
        }
        return (

                add link area

Link Settings

                <form id="link-settings">{modules}</form>

        )
    }
});

var LinkWrapper = React.createClass({
    render: function() {
        var self = this;
        var links = this.props.data.map(function(link, i) {
            return <LinkForm data={link} key={i} index={i} removeLink={self.props.removeLink} onChange={self.props.onChange} title={self.props.title} />
        });
        return (

                remove link area
                Link Area Title
                <input id="title" type="text" title={this.props.title} onChange={this.props.handleTitle.bind(null, this.props.title )} className="form-control" data-prop="title" value={this.props.title}></input>
                {links}
                add link

        )
    }
});

var LinkForm = React.createClass({
    render: function() {
        return (

                remove link
                Title
                <input id="title" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'title', 'index': this.props.index})} type="text" className="form-control" data-prop="title" value={this.props.data.title}></input>

                Icon
                <input id="icon" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'icon', 'index': this.props.index})} type="text" className="form-control" data-prop="icon" value={this.props.data.icon}></input>

                Text
                <input id="text" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'text', 'index': this.props.index})} type="text" className="form-control" data-prop="text" value={this.props.data.text}></input>

                Value
                <input id="value" onChange={this.props.onChange.bind(null, {'linkArea': this.props.title, 'value': 'value', 'index': this.props.index})} type="text" className="form-control" data-prop="value" value={this.props.data.value}></input>

        )
    }
})

var ModeSettings = React.createClass({
    addMode: function() {
        var self = this;
        var state = self.state;

        state.data.push({
            'hotkey': '',
            'indicator': '',
            'queryBefore': '',
            'queryafter': '',
            'show': '',
            'title': ''
        });

        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.modes = state.data;
        chrome.storage.sync.set(chromeData);
    },
    removeMode: function(index) {
        var self = this;
        var state = self.state;

        state.data.splice(index, 1);

        stateData = state.data;
        self.setState(stateData);
        var chromeData = {};
        chromeData.modes = state.data;
        chrome.storage.sync.set(chromeData);
    },
    handleChange: function(name, event) {
        var self = this;
        var index = name.index;
        var value = name.value;
        var state = self.state;
        state.data[index][value] = event.target.value;
        var stateData = state.data
        self.setState({
            stateData
        });
        var chromeData = {};
        chromeData.modes = state.data;
        chrome.storage.sync.set(chromeData);
    },
    componentDidMount: function() {
        var self = this;
        var modeDefault = [{
                'hotkey': '',
                'indicator': '',
                'queryBefore': '',
                'queryafter': '',
                'show': '',
                'title': ''
            }];
        chrome.storage.sync.get({
            modes: modeDefault
        }, function(items){
            self.setState({
                data: items.modes
            })
        });
    },
    getInitialState: function() {
        var modeDefault = [{
                'hotkey': '',
                'indicator': '',
                'queryBefore': '',
                'queryafter': '',
                'show': '',
                'title': ''
            }];
        return {
            data: [1,2]
        }
    },
    render: function() {
        var self = this;
        console.log('data', this.state);
        var modes = this.state.data.map(function(mode, i){
            return <ModeForm data={mode} removeMode={self.removeMode} onChange={self.handleChange} index={i} key={i} />
        });
        return (
            <form id="link-settings" className="col-md-4">

Search Modes

            add mode
            {modes}
            </form>
        )
    }
});

var ModeForm = React.createClass({
    render: function() {
        return (

                remove mode

                Title
                <input id="title" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'title'})} className="form-control" data-prop="title" value={this.props.data.title}></input>

                Before Search Query
                <input id="queryBefore" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'queryBefore'})} className="form-control" data-prop="queryBefore" value={this.props.data.queryBefore}></input>

                After Search Query
                <input id="queryAfter" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'queryafter'})} className="form-control" data-prop="queryAfter" value={this.props.data.queryafter}></input>

                Hotkey
                <input id="hotkey" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'hotkey'})} className="form-control" data-prop="hotkey" value={this.props.data.hotkey}></input>

                Indicator
                <input id="indicator" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'indicator'})} className="form-control" data-prop="indicator" value={this.props.data.indicator}></input>

                Show
                <input id="show" type="text" onChange={this.props.onChange.bind(null, {'index': this.props.index , 'value': 'show'})} className="form-control" data-prop="show" value={this.props.data.show}></input>

        )
    }
});

var CssSettings = React.createClass({
    handleChange: function(event) {

        //TODO: rename the keys to be the same between chrome storage and state
        var state = {};
        var val = event.target.value
        this.setState({ customCSS: val });

        var setting = {};
        setting.customCSS = val;
        chrome.storage.sync.set(setting);
    },
    componentDidMount: function () {
        var self = this;
        chrome.storage.sync.get({
            customCSS: ''
        }, function(items){
            self.setState({
                customCSS: items.customCSS
            })
        });
    },
    getInitialState: function() {
        return {
            customCSS: ''
        }
    },
    render: function() {
        return (
            <form id="css-settings" className="col-md-4">
                Custom CSS
                <textarea id="custom-css" onChange={this.handleChange} className="form-control" value={this.state.customCSS}></textarea>
            </form>
        )
    }
});

var BgSettings = React.createClass({
    handleChange: function(event) {

        //TODO: rename the keys to be the same between chrome storage and state
        var state = {};
        var val = event.target.value
        this.setState({ bgImage: val });

        var setting = {};
        setting.bgImage = val;
        chrome.storage.sync.set(setting);
    },
    componentDidMount: function () {
        var self = this;
        chrome.storage.sync.get({
            bgImage: ''
        }, function(items){
            self.setState({
                bgImage: items.bgImage
            })
        });
    },
    getInitialState: function() {
        return {
            customCSS: '',
        }
    },
    render: function() {
        return (
            <form id="bg-settings" className="col-md-4">
                Backgound Image
                <input id="background-image" onChange={this.handleChange} className="form-control" type="text" value={this.state.bgImage}></input>
            </form>
        )
    }
});

React.render(
    <Options />,
    document.getElementById('options')
)<|endoftext|>