<commit_before>import React from 'react';

import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';

import ITEMS from 'common/ITEMS';
import ItemLink from 'common/ItemLink';

export const SPELL_CATEGORY = {
  ROTATIONAL: 'Spell',
  COOLDOWNS: 'Cooldown',
};

const CPM_ABILITIES = [
  {
    spell: SPELLS.IMMOLATION_AURA,
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 15 / (1 + haste),
    recommendedCastEfficiency: 0.9,
    extraSuggestion: This is a great Pain filler spell. Try to always cast it on cooldown, specially when using <ItemLink id={ITEMS.KIREL_NARAK.id} details={ITEMS.KIREL_NARAK}/> legendary to trigger it's passive. ,
  },
  {
    spell: SPELLS.DEMON_SPIKES,
    isActive: combatant => combatant.hasTalent(SPELLS.RAZOR_SPIKES_TALENT.id),
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 15 / (1 + haste),
    recommendedCastEfficiency: 1.0,
    extraSuggestion: This is a great physical reduction spell and it also provides a great physical damage increase in your case, giving your <SpellLink id={SPELLS.RAZOR_SPIKES_TALENT.id} /> talent choice. Try to always cast it on cooldown. ,
  },
  {
    spell: SPELLS.DEMON_SPIKES,
    isActive: combatant => !(combatant.hasTalent(SPELLS.RAZOR_SPIKES_TALENT.id)),
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 15 / (1 + haste),
    recommendedCastEfficiency: 0.75,
    extraSuggestion: This is a great physical reduction spell. Try to always cast it as soons as it gets available or when you expect a higher physical damage. ,
  },
  {
    spell: SPELLS.SOUL_CARVER,
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 40,
    recommendedCastEfficiency: 0.9,
    extraSuggestion: This is your cooldown <SpellLink id={SPELLS.SOUL_FRAGMENT.id} /> generator spell and it does the higher damage / casting time of all your abilities. The only moment you can delay it's cast is if you already have 5 unused <SpellLink id={SPELLS.SOUL_FRAGMENT.id} /> or if you are waiting for a damage burst combo with <SpellLink id={SPELLS.FIERY_BRAND.id} /> (with the <SpellLink id={SPELLS.FIERY_DEMISE.id} /> artifact trait). ,
  },
  {
    spell: SPELLS.FRACTURE_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FRACTURE_TALENT.id),
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 6,
    recommendedCastEfficiency: 0.75,
    extraSuggestion: This is your main <SpellLink id={SPELLS.SOUL_FRAGMENT.id} /> generator spell and it does a single target DPS increase by just 30 Pain per cast. The only moment you can delay it's cast is if you already have 5 unused <SpellLink id={SPELLS.SOUL_FRAGMENT.id} />. ,
  },
  {
    spell: SPELLS.FELBLADE_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FELBLADE_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 15,
    recommendedCastEfficiency: 0.90,
    extraSuggestion: This is a great Pain generator spell and it does a single target DPS increase by just 30 Pain per cast. The only moment you can delay it's cast is if you already have 5 unused <SpellLink id={SPELLS.SOUL_FRAGMENT.id} />. ,
  },
  {
    spell: SPELLS.FEL_ERUPTION_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FEL_ERUPTION_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 30,
    recommendedCastEfficiency: 0.95,
    extraSuggestion: This is a great Chaos burst damage spell and it does a huge single target DPS increase by just 10 Pain per cast. Should definitively be used as soon as it gets available. ,
  },
  {
    spell: SPELLS.FEL_DEVASTATION_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FEL_DEVASTATION_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 60,
    recommendedCastEfficiency: 0.55,
    extraSuggestion: This is a great healing and AoE damage burst spell. It costs just 30 Pain and should be definitively used as soon as it gets available. The only moment you can delay it's cast is if your <SpellLink id={SPELLS.FIERY_BRAND.id} /> (with the <SpellLink id={SPELLS.FIERY_DEMISE.id} /> artifact trait) is almost available. ,
  },
  {
    spell: SPELLS.SOUL_BARRIER_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.SOUL_BARRIER_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 30,
    recommendedCastEfficiency: 0.50,
    extraSuggestion: This usage can be improved with <SpellLink id={SPELLS.SOUL_CARVER.id} /> for maximum efficiency. Also, this can be used more to soak burst instant damage when used with <SpellLink id={SPELLS.DEMON_SPIKES.id} /> for physical damage or with <SpellLink id={SPELLS.EMPOWER_WARDS.id} /> for magical damage. ,
  },
];

export default CPM_ABILITIES;
<commit_msg>New adaptation of Fracture Talent<commit_after>import React from 'react';

import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';

import ITEMS from 'common/ITEMS';
import ItemLink from 'common/ItemLink';

export const SPELL_CATEGORY = {
  ROTATIONAL: 'Spell',
  COOLDOWNS: 'Cooldown',
};

const CPM_ABILITIES = [
  {
    spell: SPELLS.IMMOLATION_AURA,
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 15 / (1 + haste),
    recommendedCastEfficiency: 0.9,
    extraSuggestion: This is a great Pain filler spell. Try to always cast it on cooldown, specially when using <ItemLink id={ITEMS.KIREL_NARAK.id} details={ITEMS.KIREL_NARAK}/> legendary to trigger it's passive. ,
  },
  {
    spell: SPELLS.DEMON_SPIKES,
    isActive: combatant => combatant.hasTalent(SPELLS.RAZOR_SPIKES_TALENT.id),
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 15 / (1 + haste),
    recommendedCastEfficiency: 1.0,
    extraSuggestion: This is a great physical reduction spell and it also provides a great physical damage increase in your case, giving your <SpellLink id={SPELLS.RAZOR_SPIKES_TALENT.id} /> talent choice. Try to always cast it on cooldown. ,
  },
  {
    spell: SPELLS.DEMON_SPIKES,
    isActive: combatant => !(combatant.hasTalent(SPELLS.RAZOR_SPIKES_TALENT.id)),
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 15 / (1 + haste),
    recommendedCastEfficiency: 0.75,
    extraSuggestion: This is a great physical reduction spell. Try to always cast it as soons as it gets available or when you expect a higher physical damage. ,
  },
  {
    spell: SPELLS.SOUL_CARVER,
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 40,
    recommendedCastEfficiency: 0.9,
    extraSuggestion: This is your cooldown <SpellLink id={SPELLS.SOUL_FRAGMENT.id} /> generator spell and it does the higher damage / casting time of all your abilities. The only moment you can delay it's cast is if you already have 5 unused <SpellLink id={SPELLS.SOUL_FRAGMENT.id} /> or if you are waiting for a damage burst combo with <SpellLink id={SPELLS.FIERY_BRAND.id} /> (with the <SpellLink id={SPELLS.FIERY_DEMISE.id} /> artifact trait). ,
  },
  {
    spell: SPELLS.FRACTURE_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FRACTURE_TALENT.id),
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 4,
    recommendedCastEfficiency: 0.75,
    extraSuggestion: This is your main <SpellLink id={SPELLS.SOUL_FRAGMENT.id} /> generator spell and it does a single target DPS increase by just 30 Pain per cast. The only moment you can delay it's cast is if you already have 5 unused <SpellLink id={SPELLS.SOUL_FRAGMENT.id} />. ,
  },
  {
    spell: SPELLS.FELBLADE_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FELBLADE_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 15,
    recommendedCastEfficiency: 0.90,
    extraSuggestion: This is a great Pain generator spell and it does a single target DPS increase by just 30 Pain per cast. The only moment you can delay it's cast is if you already have 5 unused <SpellLink id={SPELLS.SOUL_FRAGMENT.id} />. ,
  },
  {
    spell: SPELLS.FEL_ERUPTION_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FEL_ERUPTION_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 30,
    recommendedCastEfficiency: 0.95,
    extraSuggestion: This is a great Chaos burst damage spell and it does a huge single target DPS increase by just 10 Pain per cast. Should definitively be used as soon as it gets available. ,
  },
  {
    spell: SPELLS.FEL_DEVASTATION_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.FEL_DEVASTATION_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 60,
    recommendedCastEfficiency: 0.55,
    extraSuggestion: This is a great healing and AoE damage burst spell. It costs just 30 Pain and should be definitively used as soon as it gets available. The only moment you can delay it's cast is if your <SpellLink id={SPELLS.FIERY_BRAND.id} /> (with the <SpellLink id={SPELLS.FIERY_DEMISE.id} /> artifact trait) is almost available. ,
  },
  {
    spell: SPELLS.SOUL_BARRIER_TALENT,
    isActive: combatant => combatant.hasTalent(SPELLS.SOUL_BARRIER_TALENT.id),
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 30,
    recommendedCastEfficiency: 0.50,
    extraSuggestion: This usage can be improved with <SpellLink id={SPELLS.SOUL_CARVER.id} /> for maximum efficiency. Also, this can be used more to soak burst instant damage when used with <SpellLink id={SPELLS.DEMON_SPIKES.id} /> for physical damage or with <SpellLink id={SPELLS.EMPOWER_WARDS.id} /> for magical damage. ,
  },
];

export default CPM_ABILITIES;
<|endoftext|>


<commit_before>import React from 'react';
import styled from 'styled-components';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { notes, Info } from './documentation';
import ActionPopover from '.';
import { MenuButton } from './action-popover.style';
import getDocGenInfo from '../../utils/helpers/docgen-info';
import {
  Table, TableRow, TableCell, TableHeader
} from '../table';

const StyledComponent = styled('div')``.render().type;

ActionPopover.__docgenInfo = getDocGenInfo(
  require('./docgenInfo.json'),
  /action-popover\.component(?!spec)/
);

storiesOf('Action Popover', module)
  .add('default', () => {
    return (
      <Table isZebra>
        <TableRow>
          <TableHeader>First Name</TableHeader>
          <TableHeader>Last Name</TableHeader>
          <TableHeader> </TableHeader>
        </TableRow>
        <TableRow>
          <TableCell>John</TableCell>
          <TableCell>Doe</TableCell>
          <TableCell>
            <ActionPopover>
              <ActionPopover.Item icon='email' onClick={ action('email') }>Email Invoice</ActionPopover.Item>
              <ActionPopover.Item
                disabled icon='print'
                onClick={ action('print') }
              >Print Invoice
              </ActionPopover.Item>
              <ActionPopover.Item icon='pdf' onClick={ action('pdf') }>Download PDF</ActionPopover.Item>
              <ActionPopover.Item icon='csv' onClick={ action('csv') }>Download CSV</ActionPopover.Item>
              <ActionPopover.Divider />
              <ActionPopover.Item icon='delete' onClick={ action('delete') }>Delete</ActionPopover.Item>
            </ActionPopover>
          </TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Jane</TableCell>
          <TableCell>Smith</TableCell>
          <TableCell>
            <ActionPopover>
              <ActionPopover.Item icon='csv' onClick={ action('csv') }>Download CSV</ActionPopover.Item>
            </ActionPopover>
          </TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Bob</TableCell>
          <TableCell>Jones</TableCell>
          <TableCell>
            <ActionPopover>
              <ActionPopover.Item icon='csv' onClick={ action('csv') }>Download CSV</ActionPopover.Item>
            </ActionPopover>
          </TableCell>
        </TableRow>
      </Table>
    );
  }, {
    info: {
      text: Info,
      propTablesExclude: [Table, TableRow, TableCell, TableHeader, MenuButton, StyledComponent]
    },
    notes: { markdown: notes },
    knobs: {
      escapeHTML: false
    }
  });
<commit_msg>fix: convert action-popover story to use theme selector FE-1253<commit_after>import React from 'react';
import styled from 'styled-components';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { notes, Info } from './documentation';
import ActionPopover from '.';
import { MenuButton } from './action-popover.style';
import getDocGenInfo from '../../utils/helpers/docgen-info';
import { dlsThemeSelector, classicThemeSelector } from '../../../.storybook/theme-selectors';
import {
  Table, TableRow, TableCell, TableHeader
} from '../table';

const StyledComponent = styled('div')``.render().type;

ActionPopover.__docgenInfo = getDocGenInfo(
  require('./docgenInfo.json'),
  /action-popover\.component(?!spec)/
);

function makeStory(storyName, themeSelector) {
  const component = () => (
    <Table isZebra>
      <TableRow>
        <TableHeader>First Name</TableHeader>
        <TableHeader>Last Name</TableHeader>
        <TableHeader> </TableHeader>
      </TableRow>
      <TableRow>
        <TableCell>John</TableCell>
        <TableCell>Doe</TableCell>
        <TableCell>
          <ActionPopover>
            <ActionPopover.Item icon='email' onClick={ action('email') }>Email Invoice</ActionPopover.Item>
            <ActionPopover.Item
              disabled icon='print'
              onClick={ action('print') }
            >Print Invoice
            </ActionPopover.Item>
            <ActionPopover.Item icon='pdf' onClick={ action('pdf') }>Download PDF</ActionPopover.Item>
            <ActionPopover.Item icon='csv' onClick={ action('csv') }>Download CSV</ActionPopover.Item>
            <ActionPopover.Divider />
            <ActionPopover.Item icon='delete' onClick={ action('delete') }>Delete</ActionPopover.Item>
          </ActionPopover>
        </TableCell>
      </TableRow>
      <TableRow>
        <TableCell>Jane</TableCell>
        <TableCell>Smith</TableCell>
        <TableCell>
          <ActionPopover>
            <ActionPopover.Item icon='csv' onClick={ action('csv') }>Download CSV</ActionPopover.Item>
          </ActionPopover>
        </TableCell>
      </TableRow>
      <TableRow>
        <TableCell>Bob</TableCell>
        <TableCell>Jones</TableCell>
        <TableCell>
          <ActionPopover>
            <ActionPopover.Item icon='csv' onClick={ action('csv') }>Download CSV</ActionPopover.Item>
          </ActionPopover>
        </TableCell>
      </TableRow>
    </Table>
  );

  const metadata = {
    themeSelector,
    notes: { markdown: notes },
    info: {
      text: Info,
      propTablesExclude: [Table, TableRow, TableCell, TableHeader, MenuButton, StyledComponent]
    }
  };

  return [storyName, component, metadata];
}

storiesOf('Action Popover', module)
  .add(...makeStory('default', dlsThemeSelector))
  .add(...makeStory('classic', classicThemeSelector));
<|endoftext|>


<commit_before>export default function serializeStops(response) {
  return response.stops.map(function(stop) {
    return {
      id: stop.stop_id,
      name: stop.stop_name
    };
  });
}
<commit_msg>Fix stop name deserialization<commit_after>export default function serializeStops(response) {
  return response.stops.map(function(stop) {
    return {
      id: stop.stop_id,
      name: stop.name
    };
  });
}
<|endoftext|>


<commit_before>angular
    .module('material.components.autocomplete')
    .directive('mdAutocomplete', MdAutocomplete);

/**
 * @ngdoc directive
 * @name mdAutocomplete
 * @module material.components.autocomplete
 *
 * @description
 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a custom query.
 * This component allows you to provide real-time suggestions as the user types in the input area.
 *
 * To start, you will need to specify the required parameters and provide a template for your results.
 * The content inside `md-autocomplete` will be treated as a template.
 *
 * In more complex cases, you may want to include other content such as a message to display when
 * no matches were found.  You can do this by wrapping your template in `md-item-template` and adding
 * a tag for `md-not-found`.  An example of this is shown below.
 * ### Validation
 *
 * You can use `ng-messages` to include validation the same way that you would normally validate;
 * however, if you want to replicate a standard input with a floating label, you will have to do the
 * following:
 *
 * - Make sure that your template is wrapped in `md-item-template`
 * - Add your `ng-messages` code inside of `md-autocomplete`
 * - Add your validation properties to `md-autocomplete` (ie. `required`)
 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
 *
 * There is an example below of how this should look.
 *
 *
 * @param {expression} md-items An expression in the format of `item in items` to iterate over matches for your search.
 * @param {expression=} md-selected-item-change An expression to be run each time a new item is selected
 * @param {expression=} md-search-text-change An expression to be run each time the search text updates
 * @param {string=} md-search-text A model to bind the search query text to
 * @param {object=} md-selected-item A model to bind the selected item to
 * @param {string=} md-item-text An expression that will convert your object to a single string.
 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will make suggestions
 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking for results
 * @param {boolean=} md-autofocus If true, will immediately focus the input element
 * @param {boolean=} md-autoselect If true, the first item will be selected by default
 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in `md-input-container`
 *
 * @usage
 * ###Basic Example
 * <hljs lang="html">
 *   <md-autocomplete
 *       md-selected-item="selectedItem"
 *       md-search-text="searchText"
 *       md-items="item in getMatches(searchText)"
 *       md-item-text="item.display">
 *     {{item.display}}
 *   </md-autocomplete>
 * </hljs>
 *
 * ###Example with "not found" message
 * <hljs lang="html">
 * <md-autocomplete
 *     md-selected-item="selectedItem"
 *     md-search-text="searchText"
 *     md-items="item in getMatches(searchText)"
 *     md-item-text="item.display">
 *   <md-item-template>
 *     {{item.display}}
 *   </md-item-template>
 *   <md-not-found>
 *     No matches found.
 *   </md-not-found>
 * </md-autocomplete>
 * </hljs>
 *
 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
 * parts that make up our component.
 *
 * ### Example with validation
 * <hljs lang="html">
 * <form name="autocompleteForm">
 *   <md-autocomplete
 *       required
 *       input-name="autocomplete"
 *       md-selected-item="selectedItem"
 *       md-search-text="searchText"
 *       md-items="item in getMatches(searchText)"
 *       md-item-text="item.display">
 *     <md-item-template>
 *       {{item.display}}
 *     </md-item-template>
 *

 *
This field is required

 *

 *   </md-autocomplete>
 * </form>
 * </hljs>
 *
 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
 * parts that make up our component.
 */

function MdAutocomplete ($mdTheming, $mdUtil) {
  return {
    controller:   'MdAutocompleteCtrl',
    controllerAs: '$mdAutocompleteCtrl',
    scope:        {
      inputName:      '@mdInputName',
      inputMinlength: '@mdInputMinlength',
      inputMaxlength: '@mdInputMaxlength',
      searchText:     '=?mdSearchText',
      selectedItem:   '=?mdSelectedItem',
      itemsExpr:      '@mdItems',
      itemText:       '&mdItemText',
      placeholder:    '@placeholder',
      noCache:        '=?mdNoCache',
      itemChange:     '&?mdSelectedItemChange',
      textChange:     '&?mdSearchTextChange',
      minLength:      '=?mdMinLength',
      delay:          '=?mdDelay',
      autofocus:      '=?mdAutofocus',
      floatingLabel:  '@?mdFloatingLabel',
      autoselect:     '=?mdAutoselect',
      menuClass:      '@?mdMenuClass'
    },
    template: function (element, attr) {
      var noItemsTemplate = getNoItemsTemplate(),
          itemTemplate = getItemTemplate(),
          leftover = element.html();
      return '\
        <md-autocomplete-wrap\
            layout="row"\
            ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }"\
            role="listbox">\
          ' + getInputElement() + '\
          <md-progress-linear\
              ng-if="$mdAutocompleteCtrl.loading && !$mdAutocompleteCtrl.hidden"\
              md-mode="indeterminate"></md-progress-linear>\

\

\
                ' + itemTemplate + '\

\
            ' + noItemsTemplate + '\

\
        </md-autocomplete-wrap>\
        <aria-status\
            class="md-visually-hidden"\
            role="status"\
            aria-live="assertive">\

{{message}}
\
        </aria-status>';

      function getItemTemplate() {
        var templateTag = element.find('md-item-template').remove(),
            html = templateTag.length ? templateTag.html() : element.html();
        if (!templateTag.length) element.empty();
        return html;
      }

      function getNoItemsTemplate() {
        var templateTag = element.find('md-not-found').remove(),
            template = templateTag.length ? templateTag.html() : '';
        return template
            ? '
' + template + '
'
            : '';

      }

      function getInputElement() {
        if (attr.mdFloatingLabel) {
          return '\
            <md-input-container flex ng-if="floatingLabel">\
              {{floatingLabel}}\
              <input type="search"\
                  id="fl-input-{{$mdAutocompleteCtrl.id}}"\
                  name="{{inputName}}"\
                  autocomplete="off"\
                  ng-required="$mdAutocompleteCtrl.isRequired"\
                  ng-minlength="inputMinlength"\
                  ng-maxlength="inputMaxlength"\
                  ng-disabled="$mdAutocompleteCtrl.isDisabled"\
                  ng-model="$mdAutocompleteCtrl.scope.searchText"\
                  ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
                  ng-blur="$mdAutocompleteCtrl.blur()"\
                  ng-focus="$mdAutocompleteCtrl.focus()"\
                  aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
                  aria-label="{{floatingLabel}}"\
                  aria-autocomplete="list"\
                  aria-haspopup="true"\
                  aria-activedescendant=""\
                  aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\

' + leftover + '
\
            </md-input-container>';
        } else {
          return '\
            <input flex type="search"\
                id="input-{{$mdAutocompleteCtrl.id}}"\
                name="{{inputName}}"\
                ng-if="!floatingLabel"\
                autocomplete="off"\
                ng-required="$mdAutocompleteCtrl.isRequired"\
                ng-disabled="$mdAutocompleteCtrl.isDisabled"\
                ng-model="$mdAutocompleteCtrl.scope.searchText"\
                ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
                ng-blur="$mdAutocompleteCtrl.blur()"\
                ng-focus="$mdAutocompleteCtrl.focus()"\
                placeholder="{{placeholder}}"\
                aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
                aria-label="{{placeholder}}"\
                aria-autocomplete="list"\
                aria-haspopup="true"\
                aria-activedescendant=""\
                aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
            <button\
                type="button"\
                tabindex="-1"\
                ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
                ng-click="$mdAutocompleteCtrl.clear()">\
              <md-icon md-svg-icon="md-close"></md-icon>\
              Clear\
            </button>\
                ';
        }
      }
    }
  };
}
<commit_msg>update(docs): Fix autocomplete input-name example.<commit_after>angular
    .module('material.components.autocomplete')
    .directive('mdAutocomplete', MdAutocomplete);

/**
 * @ngdoc directive
 * @name mdAutocomplete
 * @module material.components.autocomplete
 *
 * @description
 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a custom query.
 * This component allows you to provide real-time suggestions as the user types in the input area.
 *
 * To start, you will need to specify the required parameters and provide a template for your results.
 * The content inside `md-autocomplete` will be treated as a template.
 *
 * In more complex cases, you may want to include other content such as a message to display when
 * no matches were found.  You can do this by wrapping your template in `md-item-template` and adding
 * a tag for `md-not-found`.  An example of this is shown below.
 * ### Validation
 *
 * You can use `ng-messages` to include validation the same way that you would normally validate;
 * however, if you want to replicate a standard input with a floating label, you will have to do the
 * following:
 *
 * - Make sure that your template is wrapped in `md-item-template`
 * - Add your `ng-messages` code inside of `md-autocomplete`
 * - Add your validation properties to `md-autocomplete` (ie. `required`)
 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
 *
 * There is an example below of how this should look.
 *
 *
 * @param {expression} md-items An expression in the format of `item in items` to iterate over matches for your search.
 * @param {expression=} md-selected-item-change An expression to be run each time a new item is selected
 * @param {expression=} md-search-text-change An expression to be run each time the search text updates
 * @param {string=} md-search-text A model to bind the search query text to
 * @param {object=} md-selected-item A model to bind the selected item to
 * @param {string=} md-item-text An expression that will convert your object to a single string.
 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will make suggestions
 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking for results
 * @param {boolean=} md-autofocus If true, will immediately focus the input element
 * @param {boolean=} md-autoselect If true, the first item will be selected by default
 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in `md-input-container`
 * @param {string=} md-input-name The name attribute given to the input element to be used with FormController
 * @param {number=} md-input-minlength The minimum length for the input's value for validation
 * @param {number=} md-input-maxlength The maximum length for the input's value for validation
 *
 * @usage
 * ###Basic Example
 * <hljs lang="html">
 *   <md-autocomplete
 *       md-selected-item="selectedItem"
 *       md-search-text="searchText"
 *       md-items="item in getMatches(searchText)"
 *       md-item-text="item.display">
 *     {{item.display}}
 *   </md-autocomplete>
 * </hljs>
 *
 * ###Example with "not found" message
 * <hljs lang="html">
 * <md-autocomplete
 *     md-selected-item="selectedItem"
 *     md-search-text="searchText"
 *     md-items="item in getMatches(searchText)"
 *     md-item-text="item.display">
 *   <md-item-template>
 *     {{item.display}}
 *   </md-item-template>
 *   <md-not-found>
 *     No matches found.
 *   </md-not-found>
 * </md-autocomplete>
 * </hljs>
 *
 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
 * parts that make up our component.
 *
 * ### Example with validation
 * <hljs lang="html">
 * <form name="autocompleteForm">
 *   <md-autocomplete
 *       required
 *       md-input-name="autocomplete"
 *       md-selected-item="selectedItem"
 *       md-search-text="searchText"
 *       md-items="item in getMatches(searchText)"
 *       md-item-text="item.display">
 *     <md-item-template>
 *       {{item.display}}
 *     </md-item-template>
 *

 *
This field is required

 *

 *   </md-autocomplete>
 * </form>
 * </hljs>
 *
 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
 * parts that make up our component.
 */

function MdAutocomplete ($mdTheming, $mdUtil) {
  return {
    controller:   'MdAutocompleteCtrl',
    controllerAs: '$mdAutocompleteCtrl',
    scope:        {
      inputName:      '@mdInputName',
      inputMinlength: '@mdInputMinlength',
      inputMaxlength: '@mdInputMaxlength',
      searchText:     '=?mdSearchText',
      selectedItem:   '=?mdSelectedItem',
      itemsExpr:      '@mdItems',
      itemText:       '&mdItemText',
      placeholder:    '@placeholder',
      noCache:        '=?mdNoCache',
      itemChange:     '&?mdSelectedItemChange',
      textChange:     '&?mdSearchTextChange',
      minLength:      '=?mdMinLength',
      delay:          '=?mdDelay',
      autofocus:      '=?mdAutofocus',
      floatingLabel:  '@?mdFloatingLabel',
      autoselect:     '=?mdAutoselect',
      menuClass:      '@?mdMenuClass'
    },
    template: function (element, attr) {
      var noItemsTemplate = getNoItemsTemplate(),
          itemTemplate = getItemTemplate(),
          leftover = element.html();
      return '\
        <md-autocomplete-wrap\
            layout="row"\
            ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }"\
            role="listbox">\
          ' + getInputElement() + '\
          <md-progress-linear\
              ng-if="$mdAutocompleteCtrl.loading && !$mdAutocompleteCtrl.hidden"\
              md-mode="indeterminate"></md-progress-linear>\

\

\
                ' + itemTemplate + '\

\
            ' + noItemsTemplate + '\

\
        </md-autocomplete-wrap>\
        <aria-status\
            class="md-visually-hidden"\
            role="status"\
            aria-live="assertive">\

{{message}}
\
        </aria-status>';

      function getItemTemplate() {
        var templateTag = element.find('md-item-template').remove(),
            html = templateTag.length ? templateTag.html() : element.html();
        if (!templateTag.length) element.empty();
        return html;
      }

      function getNoItemsTemplate() {
        var templateTag = element.find('md-not-found').remove(),
            template = templateTag.length ? templateTag.html() : '';
        return template
            ? '
' + template + '
'
            : '';

      }

      function getInputElement() {
        if (attr.mdFloatingLabel) {
          return '\
            <md-input-container flex ng-if="floatingLabel">\
              {{floatingLabel}}\
              <input type="search"\
                  id="fl-input-{{$mdAutocompleteCtrl.id}}"\
                  name="{{inputName}}"\
                  autocomplete="off"\
                  ng-required="$mdAutocompleteCtrl.isRequired"\
                  ng-minlength="inputMinlength"\
                  ng-maxlength="inputMaxlength"\
                  ng-disabled="$mdAutocompleteCtrl.isDisabled"\
                  ng-model="$mdAutocompleteCtrl.scope.searchText"\
                  ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
                  ng-blur="$mdAutocompleteCtrl.blur()"\
                  ng-focus="$mdAutocompleteCtrl.focus()"\
                  aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
                  aria-label="{{floatingLabel}}"\
                  aria-autocomplete="list"\
                  aria-haspopup="true"\
                  aria-activedescendant=""\
                  aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\

' + leftover + '
\
            </md-input-container>';
        } else {
          return '\
            <input flex type="search"\
                id="input-{{$mdAutocompleteCtrl.id}}"\
                name="{{inputName}}"\
                ng-if="!floatingLabel"\
                autocomplete="off"\
                ng-required="$mdAutocompleteCtrl.isRequired"\
                ng-disabled="$mdAutocompleteCtrl.isDisabled"\
                ng-model="$mdAutocompleteCtrl.scope.searchText"\
                ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
                ng-blur="$mdAutocompleteCtrl.blur()"\
                ng-focus="$mdAutocompleteCtrl.focus()"\
                placeholder="{{placeholder}}"\
                aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
                aria-label="{{placeholder}}"\
                aria-autocomplete="list"\
                aria-haspopup="true"\
                aria-activedescendant=""\
                aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
            <button\
                type="button"\
                tabindex="-1"\
                ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
                ng-click="$mdAutocompleteCtrl.clear()">\
              <md-icon md-svg-icon="md-close"></md-icon>\
              Clear\
            </button>\
                ';
        }
      }
    }
  };
}
<|endoftext|>


<commit_before>
  /**
   *  Carto CSS editor module
   *
   *  new cdb.admin.mod.CartoCSSEditor({
   *    model: dataLayer
   *    table: table
   *  })
   *
   */

cdb.admin.mod = cdb.admin.mod || {};
cdb.admin.mod.CartoCSSEditor = cdb.admin.Module.extend({

  _TEXTS: {
    tip: 'Ctrl + SPACE to autocomplete. <%= key %> + S to apply your styles.'
  },

  _ACTION: {
    type: 'show',
    width: 600
  },

  buttonClass: 'cartocss_mod',
  type: 'tool',

  events: {
    'click .actions button':  'applyStyle',
    'click .actions a.next':  '_do',
    'click .actions a.back':  '_undo',
    'click .doc_info':        '_showDoc'
  },

  initialize: function() {
    _.bindAll(this, '_onKeyUpEditor');

    this.template = this.getTemplate('table/menu_modules/views/carto_editor');

    this.model.bind('change',      this._updateStyle, this);
    this.add_related_model(this.model);
    this.add_related_model(this.model.table);

    // Set query position from history array and last sql applied
    var history   = this.model.get('tile_style_history')
      , position  = this.model.tile_style_history_position
      , style     = this.model.get('tile_style');

    // Model doesn't persist last change, let's add in the history
    if (style && style != "" && history && _.indexOf(history, style) === -1) {
      history.push(style);
      this.model.set({ "tile_style_history": history }, { silent:true });
    }

    // Get history position
    this.model.tile_style_history_position =
      _.indexOf(history, style) !== -1
      ? _.indexOf(history, style)
      : 0;

    this.model.bind('parseError', this._showErrorFromServer, this);
    /*
    this.model.bind('tileError', this._renderError, this);
    */
    this.model.bind('tileOk', this._checkLocalErrors, this);
    this.model.table.bind('change:schema', this._checkLocalErrors, this);

    //this.buildAutocomplete();
    this._initBinds();

    cdb.god.bind('end_show', this.activated, this)
    this.add_related_model(cdb.god);
  },

  /** builds autocomplete from cartcss reference */
  buildAutocomplete: function() {
    this.autocomplete = [];
    if (typeof(window._mapnik_reference_latest) !== 'undefined') {
      var symbolizers = _mapnik_reference_latest.symbolizers;
      for (var s in symbolizers) {
        var sym = symbolizers[s];
        for (var p in sym) {
          var css = sym[p].css;
          if (css && css.length) {
            this.autocomplete.push(css);
          }
        }
      }
    }
  },

  activated: function() {
    if(this.codeEditor) {
      this.codeEditor.refresh();
      this.codeEditor.focus();
      this._adjustCodeEditorSize();
    }
  },

  render: function() {
    var self = this;
    this.clearSubViews();

    this.$el.append(this.template({}));

    this._initHelp();
    this._initEditor();

    this._updateStyle();
    this._adjustCodeEditorSize();

    return this;
  },

  _initBinds: function() {
    // Codemirror extrakey
    // Add save keymap
    // PC & LINUX -> Ctrl + s
    // MAC        -> Cmd + s
    var ua      = navigator.userAgent.toLowerCase()
      , so      = "rest"
      , keymap  = "ctrl+s"
      , self    = this;

    if (/mac os/.test(ua)) {
      keymap = "meta+s";
      so = "mac";
    }

    this.$el.bind('keydown', keymap, function(ev) {
      if (((so=="mac" && ev.metaKey) || (so=="rest" && ev.ctrlKey)) && ev.keyCode == 83 ) {
        ev.preventDefault();
        self.applyStyle();
      }
    });
  },

  _initEditor: function() {
    var self = this;
    this.codeEditor = CodeMirror.fromTextArea(this.$('textarea')[0], {
      mode: "text/x-carto",
      tabMode: "indent",
      matchBrackets: true,
      lineNumbers: true,
      lineWrapping: true,
      onKeyEvent: this._onKeyUpEditor,
      extraKeys: {
        "Ctrl-Space": function(cm) { self._showAutocomplete(cm) }
      }
    });

    var color_picker = new cdb.admin.CodemirrorColorPicker({
      editor: this.codeEditor,
      model:  this.model
    });
    color_picker.bind('colorChosen', this.applyStyle, this);
    this.addView(color_picker);

    // Add tooltip for undo/redo buttons
    this.$("a.next, a.back").tipsy({
      gravity: "s",
      fade: true
    });
  },

  _initHelp: function() {
    var so = "rest";
    var ua = navigator.userAgent.toLowerCase();

    if (/mac os/.test(ua)) {
      so = "mac";
    }

    var help = new cdb.admin.mod.HTMLEditorHelp({
      localStorageKey: this._STORAGE_NAMESPACE + this.model.table.get('id'),
      text: _.template(this._TEXTS.tip)({ key: (so == "mac") ? "CMD" : "Ctrl" })
    }).bind("hide show", this._adjustCodeEditorSize, this);
    this.$el.append(help.render().$el);
    this.addView(help);
  },

  _showAutocomplete: function(cm) {
    CodeMirror.showHint(cm, CodeMirror.hint['custom-list-with-type'], {
      completeSingle: false,
      list: _.union( this._getTableName(), this._getSQLColumns())
    });
  },

  _getTableName: function() {
    return [ [ this.model.table.get('name'), "T" ] ]
  },

  _getSQLColumns: function() {
    return _.map(
      this.model.table.get('schema'),
      function(pair) {
        // Column name and type
        return [pair[0], "C"]
      });
  },

  _onKeyUpEditor: function(cm, e) {
    var code = (e.keyCode ? e.keyCode : e.which);

    if (e.type == "keyup" && code != 27 ) {
      var self = this;

      if (this.autocomplete_timeout) clearTimeout(this.autocomplete_timeout);

      this.autocomplete_timeout = setTimeout(function() {
        var cur = cm.getCursor();
        var str = cm.getTokenAt(cur).string;
        var schema = self.model.table.get('schema');

        if (schema && str.length > 2) {
          var arr = _.union(self.model.table.get('schema'), self._getTableName());
          var list = _.compact(_.map(arr, function(pair) {
            if (pair[0].search(str) != -1)
              return pair[0];
            return null;
          }));

          if (!cm.state.completionActive && str.length > 2 && list.length > 0) {
            self._showAutocomplete(cm)
          }
        }

      }, 150);
    }
  },

  /** hack used to format the old styles transformed to cartodb 2.0*/
  formatStyle: function(s) {
    try {
      if (s && s.length) {
        s = s.replace(/{/g,'{\n')
            .replace(/}/g,'}\n')
            .replace(/;/g,';\n')
        var t = s.split('\n');
        var lines = [];
        var c = 0;
        for(var i = 0; i < t.length; ++i) {
          lines.push(c);
          if (t[i].indexOf('{')  != -1) {
            ++c;
          }
          if (t[i].indexOf('}')  != -1) {
            --c;
          }
        }
        var r = [];
        for(var i = 0; i < t.length; ++i) {
          var spaces = '';
          if(t[i].indexOf('}') >= 0) lines[i]-=1;
          for(var j = 0; j < lines[i]; ++j) {
            spaces = spaces + '  ';
          }
          r.push(spaces + t[i]);
        }
        return '/** this cartoCSS has been processed in order to be compatible with the new cartodb 2.0 */\n\n' + r.join('\n');
      }
    } catch(e) {
    }
    return s;
  },

  _updateStyle: function(){
    var st        = this.model.get('tile_style')
      , editor_st = this.codeEditor.getValue();

      if(this.codeEditor && st && st != editor_st) {
        if(st.indexOf('\n') === -1) {
          st = this.formatStyle(st);
        }
        this.codeEditor.setValue(st);
        this.codeEditor.refresh();
      }

    // If model is using history, check buttons
    if (this.model.get('tile_style_history'))
      this._checkDoButtons();
  },

  /**
   * gets an array of parse errors from windshaft
   * and returns an array of {line:1, error: 'string'] with user friendly
   * strings. Parses errors in format:
   *
   *  'style.mss:7:2 Invalid code: asdasdasda'
   */
  _parseError: function(errors) {
    var parsedErrors = [];
    for(var i in errors) {
      var err = errors[i];
      if(err) {
        if (err.length > 0) {
          var g = err.match(/.*:(\d+):(\d+)\s*(.*)/);
          if(g) {
            parsedErrors.push({
              line: parseInt(g[1], 10),
              message: g[3]
            });
          } else {
            parsedErrors.push({
              line: null,
              message: err
            })
          }
        } else if(err.line) {
          parsedErrors.push(err);
        }
      }
    }
    // sort by line
    parsedErrors.sort(function(a, b) { return a.line - b.line; });
    parsedErrors = _.uniq(parsedErrors, true, function(a) { return a.line + a.message; });
    return parsedErrors;
  },

  _showErrorFromServer: function(err) {
    this._showError(this._parseError(err));
  },

  _showError: function(err) {
    var parsedErrors = err;
    if(parsedErrors.length > 0) {
      var errors = _(parsedErrors).map(function(e) {
        if(e.line) {
          return "line " + e.line + ": " + e.message;
        }
        return e.error || e.message;
      })
      this._renderError(errors.join('
'));
    }
  },

  _renderError: function(errors) {
    this.trigger('hasErrors');

    // Get actions block height
    var actions_h = this.$('.actions').outerHeight();

    // Add error text
    this.$('.info')
      .addClass('error')
      .html("
" + errors + "
")
      // If layer is not visible, we need to move error message
      .css({
        bottom: actions_h + (!this.model.get('visible') ? 57 : 0)
      })
      .show();

    this._adjustCodeEditorSize();
  },

  _adjustCodeEditorSize: function() {
    // Fit editor with the error
    var info_h = this.$('.info').is(':visible') ? this.$('.info').outerHeight() : 0;
    var help_h = this.$('.help-tip').is(':visible') ? 36 : 0 ;
    // If layer is not visible, we need to take into account
    var vis_msg_h = !this.model.get('visible') ? 57 : 0 ;

    this.$('.CodeMirror-wrap').css({
      bottom: info_h + vis_msg_h + 80, /* the space we need to show the action buttons */
      top: help_h
    });
  },

  _checkLocalErrors: function() {
    var style = this.model.get('tile_style');
    var cartoParser = new cdb.admin.CartoParser(style);
    if(cartoParser.errors().length) {
      this._showError(this._parseError(cartoParser.errors()));
    } else {
      // check variables used
      var err = this.checkVariables(cartoParser.variablesUsed());
      if(err.length) {
        this._showError(err);
        return;
      }
    }
    if(style && style.indexOf(this.model.get('table_name')) == -1) {
      this._renderError("style name should be #" + this.model.get('table_name') + " otherwise all tiles will be blank");
    } else {
      this._clearErrors();
    }
  },

  _clearErrors: function() {
    this.trigger('clearError');

    // Hide info
    this.$('.info')
      .html('')
      .removeClass('error')
      .hide();

    this._adjustCodeEditorSize();
  },

  _do: function(e) {
    e.preventDefault();
    var newCarto = this.model.redoHistory('tile_style');
    if(this.codeEditor) this.codeEditor.setValue(newCarto);
    this._checkDoButtons();
    return false;
  },

  _undo: function(e) {
    e.preventDefault();
    var newCarto = this.model.undoHistory('tile_style');
    if(this.codeEditor) this.codeEditor.setValue(newCarto);
    this._checkDoButtons();
    return false;
  },

  /**
   * checks variabels used in cartocss are in the schem
   */
  checkVariables: function(vars) {
    var columns = this.model.table.columnNames();
    var err = [];
    for(var i in vars) {
      if(!_.contains(columns, vars[i])) {
        err.push({
          error: "sql/table must contain " + vars[i] + " variable"
        });
      }
    }
    return err;
  },

  applyStyle: function() {
    this._clearErrors();
    var style = this.codeEditor.getValue();
    var cartoParser = new cdb.admin.CartoParser(style);
    if(cartoParser.errors().length) {
      this._showError(this._parseError(cartoParser.errors()));
    } else {
      // check variables used
      var err = this.checkVariables(cartoParser.variablesUsed());
      if(err.length) {
        this._showError(err)
        return;
      }
      this.model.addToHistory('tile_style', style);
      //TODO: check if the style has been changed
      this.model.save({
        tile_style: style,
        tile_style_custom: true
      });

      // we save the new applied query on the history array
      this.trigger('applyStyle', style);
    }

    // Send mixpanel event applying a new cartocss style
    cdb.god.trigger('mixpanel', "Applying a CartoCSS style manually", { style: style });
  },

  /**
   * Check if the editor is different from the saved value
   * @return {Boolean}
   */
  hasChanges: function() {
    return this.model.get('tile_style') != this.codeEditor.getValue();
  },

  _checkDoButtons: function() {
      // Redo
      if (!this.model.isHistoryAtLastPosition('tile_style')) {
        this.$el.find('a.next').removeClass("disabled")
      } else {
        this.$el.find('a.next').addClass("disabled")
      }
      // Undo
      if (!this.model.isHistoryAtFirstPosition('tile_style')) {
        this.$el.find('a.back').removeClass("disabled")
      } else {
        this.$el.find('a.back').addClass("disabled")
      }
  },

  _showDoc: function(ev) {
    ev.preventDefault();
    var dialog = new cdb.admin.CartoCSSInfo();
    $("body").append(dialog.render().el);
    dialog.open();
  }
});<commit_msg>removed cartoccs error message<commit_after>
  /**
   *  Carto CSS editor module
   *
   *  new cdb.admin.mod.CartoCSSEditor({
   *    model: dataLayer
   *    table: table
   *  })
   *
   */

cdb.admin.mod = cdb.admin.mod || {};
cdb.admin.mod.CartoCSSEditor = cdb.admin.Module.extend({

  _TEXTS: {
    tip: 'Ctrl + SPACE to autocomplete. <%= key %> + S to apply your styles.'
  },

  _ACTION: {
    type: 'show',
    width: 600
  },

  buttonClass: 'cartocss_mod',
  type: 'tool',

  events: {
    'click .actions button':  'applyStyle',
    'click .actions a.next':  '_do',
    'click .actions a.back':  '_undo',
    'click .doc_info':        '_showDoc'
  },

  initialize: function() {
    _.bindAll(this, '_onKeyUpEditor');

    this.template = this.getTemplate('table/menu_modules/views/carto_editor');

    this.model.bind('change',      this._updateStyle, this);
    this.add_related_model(this.model);
    this.add_related_model(this.model.table);

    // Set query position from history array and last sql applied
    var history   = this.model.get('tile_style_history')
      , position  = this.model.tile_style_history_position
      , style     = this.model.get('tile_style');

    // Model doesn't persist last change, let's add in the history
    if (style && style != "" && history && _.indexOf(history, style) === -1) {
      history.push(style);
      this.model.set({ "tile_style_history": history }, { silent:true });
    }

    // Get history position
    this.model.tile_style_history_position =
      _.indexOf(history, style) !== -1
      ? _.indexOf(history, style)
      : 0;

    this.model.bind('parseError', this._showErrorFromServer, this);
    /*
    this.model.bind('tileError', this._renderError, this);
    */
    this.model.bind('tileOk', this._checkLocalErrors, this);
    this.model.table.bind('change:schema', this._checkLocalErrors, this);

    //this.buildAutocomplete();
    this._initBinds();

    cdb.god.bind('end_show', this.activated, this)
    this.add_related_model(cdb.god);
  },

  /** builds autocomplete from cartcss reference */
  buildAutocomplete: function() {
    this.autocomplete = [];
    if (typeof(window._mapnik_reference_latest) !== 'undefined') {
      var symbolizers = _mapnik_reference_latest.symbolizers;
      for (var s in symbolizers) {
        var sym = symbolizers[s];
        for (var p in sym) {
          var css = sym[p].css;
          if (css && css.length) {
            this.autocomplete.push(css);
          }
        }
      }
    }
  },

  activated: function() {
    if(this.codeEditor) {
      this.codeEditor.refresh();
      this.codeEditor.focus();
      this._adjustCodeEditorSize();
    }
  },

  render: function() {
    var self = this;
    this.clearSubViews();

    this.$el.append(this.template({}));

    this._initHelp();
    this._initEditor();

    this._updateStyle();
    this._adjustCodeEditorSize();

    return this;
  },

  _initBinds: function() {
    // Codemirror extrakey
    // Add save keymap
    // PC & LINUX -> Ctrl + s
    // MAC        -> Cmd + s
    var ua      = navigator.userAgent.toLowerCase()
      , so      = "rest"
      , keymap  = "ctrl+s"
      , self    = this;

    if (/mac os/.test(ua)) {
      keymap = "meta+s";
      so = "mac";
    }

    this.$el.bind('keydown', keymap, function(ev) {
      if (((so=="mac" && ev.metaKey) || (so=="rest" && ev.ctrlKey)) && ev.keyCode == 83 ) {
        ev.preventDefault();
        self.applyStyle();
      }
    });
  },

  _initEditor: function() {
    var self = this;
    this.codeEditor = CodeMirror.fromTextArea(this.$('textarea')[0], {
      mode: "text/x-carto",
      tabMode: "indent",
      matchBrackets: true,
      lineNumbers: true,
      lineWrapping: true,
      onKeyEvent: this._onKeyUpEditor,
      extraKeys: {
        "Ctrl-Space": function(cm) { self._showAutocomplete(cm) }
      }
    });

    var color_picker = new cdb.admin.CodemirrorColorPicker({
      editor: this.codeEditor,
      model:  this.model
    });
    color_picker.bind('colorChosen', this.applyStyle, this);
    this.addView(color_picker);

    // Add tooltip for undo/redo buttons
    this.$("a.next, a.back").tipsy({
      gravity: "s",
      fade: true
    });
  },

  _initHelp: function() {
    var so = "rest";
    var ua = navigator.userAgent.toLowerCase();

    if (/mac os/.test(ua)) {
      so = "mac";
    }

    var help = new cdb.admin.mod.HTMLEditorHelp({
      localStorageKey: this._STORAGE_NAMESPACE + this.model.table.get('id'),
      text: _.template(this._TEXTS.tip)({ key: (so == "mac") ? "CMD" : "Ctrl" })
    }).bind("hide show", this._adjustCodeEditorSize, this);
    this.$el.append(help.render().$el);
    this.addView(help);
  },

  _showAutocomplete: function(cm) {
    CodeMirror.showHint(cm, CodeMirror.hint['custom-list-with-type'], {
      completeSingle: false,
      list: _.union( this._getTableName(), this._getSQLColumns())
    });
  },

  _getTableName: function() {
    return [ [ this.model.table.get('name'), "T" ] ]
  },

  _getSQLColumns: function() {
    return _.map(
      this.model.table.get('schema'),
      function(pair) {
        // Column name and type
        return [pair[0], "C"]
      });
  },

  _onKeyUpEditor: function(cm, e) {
    var code = (e.keyCode ? e.keyCode : e.which);

    if (e.type == "keyup" && code != 27 ) {
      var self = this;

      if (this.autocomplete_timeout) clearTimeout(this.autocomplete_timeout);

      this.autocomplete_timeout = setTimeout(function() {
        var cur = cm.getCursor();
        var str = cm.getTokenAt(cur).string;
        var schema = self.model.table.get('schema');

        if (schema && str.length > 2) {
          var arr = _.union(self.model.table.get('schema'), self._getTableName());
          var list = _.compact(_.map(arr, function(pair) {
            if (pair[0].search(str) != -1)
              return pair[0];
            return null;
          }));

          if (!cm.state.completionActive && str.length > 2 && list.length > 0) {
            self._showAutocomplete(cm)
          }
        }

      }, 150);
    }
  },

  /** hack used to format the old styles transformed to cartodb 2.0*/
  formatStyle: function(s) {
    try {
      if (s && s.length) {
        s = s.replace(/{/g,'{\n')
            .replace(/}/g,'}\n')
            .replace(/;/g,';\n')
        var t = s.split('\n');
        var lines = [];
        var c = 0;
        for(var i = 0; i < t.length; ++i) {
          lines.push(c);
          if (t[i].indexOf('{')  != -1) {
            ++c;
          }
          if (t[i].indexOf('}')  != -1) {
            --c;
          }
        }
        var r = [];
        for(var i = 0; i < t.length; ++i) {
          var spaces = '';
          if(t[i].indexOf('}') >= 0) lines[i]-=1;
          for(var j = 0; j < lines[i]; ++j) {
            spaces = spaces + '  ';
          }
          r.push(spaces + t[i]);
        }
        return '/** this cartoCSS has been processed in order to be compatible with the new cartodb 2.0 */\n\n' + r.join('\n');
      }
    } catch(e) {
    }
    return s;
  },

  _updateStyle: function(){
    var st        = this.model.get('tile_style')
      , editor_st = this.codeEditor.getValue();

      if(this.codeEditor && st && st != editor_st) {
        if(st.indexOf('\n') === -1) {
          st = this.formatStyle(st);
        }
        this.codeEditor.setValue(st);
        this.codeEditor.refresh();
      }

    // If model is using history, check buttons
    if (this.model.get('tile_style_history'))
      this._checkDoButtons();
  },

  /**
   * gets an array of parse errors from windshaft
   * and returns an array of {line:1, error: 'string'] with user friendly
   * strings. Parses errors in format:
   *
   *  'style.mss:7:2 Invalid code: asdasdasda'
   */
  _parseError: function(errors) {
    var parsedErrors = [];
    for(var i in errors) {
      var err = errors[i];
      if(err) {
        if (err.length > 0) {
          var g = err.match(/.*:(\d+):(\d+)\s*(.*)/);
          if(g) {
            parsedErrors.push({
              line: parseInt(g[1], 10),
              message: g[3]
            });
          } else {
            parsedErrors.push({
              line: null,
              message: err
            })
          }
        } else if(err.line) {
          parsedErrors.push(err);
        }
      }
    }
    // sort by line
    parsedErrors.sort(function(a, b) { return a.line - b.line; });
    parsedErrors = _.uniq(parsedErrors, true, function(a) { return a.line + a.message; });
    return parsedErrors;
  },

  _showErrorFromServer: function(err) {
    this._showError(this._parseError(err));
  },

  _showError: function(err) {
    var parsedErrors = err;
    if(parsedErrors.length > 0) {
      var errors = _(parsedErrors).map(function(e) {
        if(e.line) {
          return "line " + e.line + ": " + e.message;
        }
        return e.error || e.message;
      })
      this._renderError(errors.join('
'));
    }
  },

  _renderError: function(errors) {
    this.trigger('hasErrors');

    // Get actions block height
    var actions_h = this.$('.actions').outerHeight();

    // Add error text
    this.$('.info')
      .addClass('error')
      .html("
" + errors + "
")
      // If layer is not visible, we need to move error message
      .css({
        bottom: actions_h + (!this.model.get('visible') ? 57 : 0)
      })
      .show();

    this._adjustCodeEditorSize();
  },

  _adjustCodeEditorSize: function() {
    // Fit editor with the error
    var info_h = this.$('.info').is(':visible') ? this.$('.info').outerHeight() : 0;
    var help_h = this.$('.help-tip').is(':visible') ? 36 : 0 ;
    // If layer is not visible, we need to take into account
    var vis_msg_h = !this.model.get('visible') ? 57 : 0 ;

    this.$('.CodeMirror-wrap').css({
      bottom: info_h + vis_msg_h + 80, /* the space we need to show the action buttons */
      top: help_h
    });
  },

  _checkLocalErrors: function() {
    var style = this.model.get('tile_style');
    var cartoParser = new cdb.admin.CartoParser(style);
    if(cartoParser.errors().length) {
      this._showError(this._parseError(cartoParser.errors()));
    } else {
      // check variables used
      var err = this.checkVariables(cartoParser.variablesUsed());
      if(err.length) {
        this._showError(err);
        return;
      }
    }
    this._clearErrors();
  },

  _clearErrors: function() {
    this.trigger('clearError');

    // Hide info
    this.$('.info')
      .html('')
      .removeClass('error')
      .hide();

    this._adjustCodeEditorSize();
  },

  _do: function(e) {
    e.preventDefault();
    var newCarto = this.model.redoHistory('tile_style');
    if(this.codeEditor) this.codeEditor.setValue(newCarto);
    this._checkDoButtons();
    return false;
  },

  _undo: function(e) {
    e.preventDefault();
    var newCarto = this.model.undoHistory('tile_style');
    if(this.codeEditor) this.codeEditor.setValue(newCarto);
    this._checkDoButtons();
    return false;
  },

  /**
   * checks variabels used in cartocss are in the schem
   */
  checkVariables: function(vars) {
    var columns = this.model.table.columnNames();
    var err = [];
    for(var i in vars) {
      if(!_.contains(columns, vars[i])) {
        err.push({
          error: "sql/table must contain " + vars[i] + " variable"
        });
      }
    }
    return err;
  },

  applyStyle: function() {
    this._clearErrors();
    var style = this.codeEditor.getValue();
    var cartoParser = new cdb.admin.CartoParser(style);
    if(cartoParser.errors().length) {
      this._showError(this._parseError(cartoParser.errors()));
    } else {
      // check variables used
      var err = this.checkVariables(cartoParser.variablesUsed());
      if(err.length) {
        this._showError(err)
        return;
      }
      this.model.addToHistory('tile_style', style);
      //TODO: check if the style has been changed
      this.model.save({
        tile_style: style,
        tile_style_custom: true
      });

      // we save the new applied query on the history array
      this.trigger('applyStyle', style);
    }

    // Send mixpanel event applying a new cartocss style
    cdb.god.trigger('mixpanel', "Applying a CartoCSS style manually", { style: style });
  },

  /**
   * Check if the editor is different from the saved value
   * @return {Boolean}
   */
  hasChanges: function() {
    return this.model.get('tile_style') != this.codeEditor.getValue();
  },

  _checkDoButtons: function() {
      // Redo
      if (!this.model.isHistoryAtLastPosition('tile_style')) {
        this.$el.find('a.next').removeClass("disabled")
      } else {
        this.$el.find('a.next').addClass("disabled")
      }
      // Undo
      if (!this.model.isHistoryAtFirstPosition('tile_style')) {
        this.$el.find('a.back').removeClass("disabled")
      } else {
        this.$el.find('a.back').addClass("disabled")
      }
  },

  _showDoc: function(ev) {
    ev.preventDefault();
    var dialog = new cdb.admin.CartoCSSInfo();
    $("body").append(dialog.render().el);
    dialog.open();
  }
});<|endoftext|>


<commit_before>var ConfigCheckbox = Ember.Checkbox.extend({
	classNames: ['config-checkbox'],
	elementInserted: false,
	bindingPath: function(){
		var path = this.get('config.path');
		return 'targetModel.' + path;
	}.property('config.path'),
	valueChanged: function(){
		if (!this.get('elementInserted')) return;
		var path = this.get('config.path');
		var target = this.get('targetModel');
		target.set(path, this.get('checked') ? 1 : 0);
	}.observes('checked'),
	updateCurrentValue: function() {
		var that = this;
		var current = that.get(that.get('bindingPath'));
		that.set('checked', !!current);
	},
	didInsertElement: function(){
		var that = this;
		this.updateCurrentValue();
		that.addObserver(that.get('bindingPath'), this.updateCurrentValue);

		Em.run.next(this, function(){
			this.set('elementInserted', true);
		});
	},
	willDestroyElement: function(){
		this.removeObserver(this.get('bindingPath'), this.updateCurrentValue);
	},
});

export default ConfigCheckbox;<commit_msg>ConfigCheckbox now supports automatic disable<commit_after>var ConfigCheckbox = Ember.Checkbox.extend({
	classNames: ['config-checkbox'],
	elementInserted: false,
	attributeBindings: ['disabled'],
	bindingPath: function(){
		var path = this.get('config.path');
		return 'targetModel.' + path;
	}.property('config.path'),
	disablePath: function(){
		var path = this.get('config.disable.path');
		return 'targetModel.' + path;
	}.property('config.disable.path'),
	valueChanged: function(){
		if (!this.get('elementInserted')) return;
		var path = this.get('config.path');
		var target = this.get('targetModel');
		target.set(path, this.get('checked') ? 1 : 0);
	}.observes('checked'),
	updateCurrentValue: function() {
		var that = this;
		var current = that.get(that.get('bindingPath'));
		that.set('checked', !!current);
	},
	updateDisableValue: function(){
		var current = this.get(this.get('disablePath'));
		var value = this.get('config.disable.value');
		this.set('disabled', current === value);
	},
	didInsertElement: function(){
		var that = this;
		this.updateCurrentValue();
		that.addObserver(that.get('bindingPath'), this.updateCurrentValue);

		if (this.get('config.disable')) {
			this.updateDisableValue();
			this.addObserver(this.get('disablePath'), this.updateDisableValue);
		}

		Em.run.next(this, function(){
			this.set('elementInserted', true);
		});
	},
	willDestroyElement: function(){
		this.removeObserver(this.get('bindingPath'), this.updateCurrentValue);
		this.removeObserver(this.get('disablePath'), this.updateDisableValue);
	},
});

export default ConfigCheckbox;<|endoftext|>


<commit_before>const warningHTML = `

      This record uses a term that has since fallen from usage and is now considered offensive. The term is repeated in this record in its original historical context.

      <button class="b-search-results__offensive-warning-button">
        View record
      </button>

`;

const toggleSort = (el) => {
  const { value } = el.dataset;
  const orderBy = document.querySelector('.b-search-results__hidden-input-order-by');
  const orderSort = document.querySelector('.b-search-results__hidden-input-order-sort');
  const defaultClass = 'b-search-results__head-cell--sort-asc';
  const currentClass = Array.from(el.classList)[2];

  const newSort = {
    'b-search-results__head-cell--sort-asc': 'b-search-results__head-cell--sort-desc',
    'b-search-results__head-cell--sort-desc': 'b-search-results__head-cell--sort-none'
  }[currentClass];

  Array.from(document.querySelectorAll('.b-search-results__head-cell')).forEach((e) => {
    e.classList.remove('b-search-results__head-cell--sort-desc');
    e.classList.remove('b-search-results__head-cell--sort-asc');
    e.classList.remove('b-search-results__head-cell--sort-none');
  });

  if (
    !newSort
  ) {
    el.classList.add(defaultClass);
    orderBy.value = value;
  } else if (newSort == ('b-search-results__head-cell--sort-desc')) {
    orderSort.value = 'desc';
  } else if (newSort == ('b-search-results__head-cell--sort-none')) {
    orderBy.value = '';
    orderSort.value ='asc';
  }

  el.classList.add(newSort || defaultClass);
}

const reAdjustWarnings = () => {
  const table = document.querySelector('.b-search-results__table');

  Array.from(document.querySelectorAll('.b-search-results__offensive-warning')).forEach((el) => {
    const row = Array.from(document.querySelectorAll('.b-search-results__body-row'))[el.getAttribute('data-row-index')];
    el.style.top = `${row.offsetTop + (row.offsetHeight / 5)}px`;

    if (table.clientWidth < 769) {
      el.style.left = '5px';
    } else {
      el.style.left = `${el.getBoundingClientRect().left / 5}px`;
    }
  });
};

const offensiveWarningInitializer = () => {
  Array.from(document.querySelectorAll('.b-search-results__body-row')).forEach((el, i) => {
    if (el.classList.contains('b-search-results__body-row--offensive')) {
      const warningEl = document.createElement('DIV');
      warningEl.className = 'b-search-results__offensive-warning';
      warningEl.innerHTML = warningHTML;
      warningEl.style.top = `${el.offsetTop + (el.offsetHeight / 5)}px`;
      warningEl.setAttribute('data-row-index', i);
      warningEl.onclick = (e) => {
        Array.from(document.querySelectorAll('.b-search-results__body-row'))[
          e.target.parentElement.getAttribute('data-row-index')
        ].classList.remove('b-search-results__body-row--offensive');
        e.target.parentElement.remove();
        reAdjustWarnings();
        e.stopPropagation();
        return false;
      };
      el.appendChild(warningEl);
      reAdjustWarnings();
    }
  });

  reAdjustWarnings();
};

window.addEventListener('resize', () => {
  reAdjustWarnings();
});

offensiveWarningInitializer();

if (document.querySelector('.etc-template__results-container')) {
  document.querySelector('.etc-template__results-container').addEventListener('initWarnings', () => {
    offensiveWarningInitializer();
  });
}

Array.from(document.querySelectorAll('.b-search-results__head-cell')).forEach((el) => {
  if (Boolean(parseInt(el.dataset.sortable))) {
    el.onclick = e => toggleSort(e.target);
  }
});

<commit_msg>dispatch input event<commit_after>const warningHTML = `

      This record uses a term that has since fallen from usage and is now considered offensive. The term is repeated in this record in its original historical context.

      <button class="b-search-results__offensive-warning-button">
        View record
      </button>

`;

const toggleSort = (el) => {
  const { value } = el.dataset;
  const orderBy = document.querySelector('.b-search-results__hidden-input-order-by');
  const orderSort = document.querySelector('.b-search-results__hidden-input-order-sort');
  const defaultClass = 'b-search-results__head-cell--sort-asc';
  const currentClass = Array.from(el.classList)[2];

  const newSort = {
    'b-search-results__head-cell--sort-asc': 'b-search-results__head-cell--sort-desc',
    'b-search-results__head-cell--sort-desc': 'b-search-results__head-cell--sort-none'
  }[currentClass];

  Array.from(document.querySelectorAll('.b-search-results__head-cell')).forEach((e) => {
    e.classList.remove('b-search-results__head-cell--sort-desc');
    e.classList.remove('b-search-results__head-cell--sort-asc');
    e.classList.remove('b-search-results__head-cell--sort-none');
  });

  if (
    !newSort
  ) {
    el.classList.add(defaultClass);
    orderBy.value = value;
  } else if (newSort == ('b-search-results__head-cell--sort-desc')) {
    orderSort.value = 'desc';
  } else if (newSort == ('b-search-results__head-cell--sort-none')) {
    orderBy.value = '';
    orderSort.value ='asc';
  }

  el.classList.add(newSort || defaultClass);

  el.dispatchEvent(new Event('input', { bubbles: true }))
}

const reAdjustWarnings = () => {
  const table = document.querySelector('.b-search-results__table');

  Array.from(document.querySelectorAll('.b-search-results__offensive-warning')).forEach((el) => {
    const row = Array.from(document.querySelectorAll('.b-search-results__body-row'))[el.getAttribute('data-row-index')];
    el.style.top = `${row.offsetTop + (row.offsetHeight / 5)}px`;

    if (table.clientWidth < 769) {
      el.style.left = '5px';
    } else {
      el.style.left = `${el.getBoundingClientRect().left / 5}px`;
    }
  });
};

const offensiveWarningInitializer = () => {
  Array.from(document.querySelectorAll('.b-search-results__body-row')).forEach((el, i) => {
    if (el.classList.contains('b-search-results__body-row--offensive')) {
      const warningEl = document.createElement('DIV');
      warningEl.className = 'b-search-results__offensive-warning';
      warningEl.innerHTML = warningHTML;
      warningEl.style.top = `${el.offsetTop + (el.offsetHeight / 5)}px`;
      warningEl.setAttribute('data-row-index', i);
      warningEl.onclick = (e) => {
        Array.from(document.querySelectorAll('.b-search-results__body-row'))[
          e.target.parentElement.getAttribute('data-row-index')
        ].classList.remove('b-search-results__body-row--offensive');
        e.target.parentElement.remove();
        reAdjustWarnings();
        e.stopPropagation();
        return false;
      };
      el.appendChild(warningEl);
      reAdjustWarnings();
    }
  });

  reAdjustWarnings();
};

window.addEventListener('resize', () => {
  reAdjustWarnings();
});

offensiveWarningInitializer();

if (document.querySelector('.etc-template__results-container')) {
  document.querySelector('.etc-template__results-container').addEventListener('initWarnings', () => {
    offensiveWarningInitializer();
  });
}

Array.from(document.querySelectorAll('.b-search-results__head-cell')).forEach((el) => {
  if (Boolean(parseInt(el.dataset.sortable))) {
    el.onclick = e => toggleSort(e.target);
  }
});

<|endoftext|>


<commit_before>/**
 * Copyright (C) 2018 Mailvelope GmbH
 * Licensed under the GNU Affero General Public License version 3
 */

import React from 'react';
import PropTypes from 'prop-types';

export default class FormSandbox extends React.Component {
  constructor(props) {
    super(props);
    this.sandbox = null;
    this.form = null;
    this.files = [];
  }

  shouldComponentUpdate() {
    // create sandbox iframe only once
    return false;
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.needSubmit === true) {
      this.form.dispatchEvent(new Event('submit'));
    }
  }

  removeInputSubmit() {
    // TODO add ability to remove tags by type using DOMPurify
    $(this.form).find('input[type=submit]').each(function() {
      $(this).remove();
    });
  }

  checkForEmptyForm() {
    // check that there is at least a valid input field to send
    const validInput = $(this.form).find('input[name], select[name], textarea[name]').length;
    if (!validInput) {
      this.onError(new Error('There should be at least one input field with name property set.'));
    }
  }

  resizeIframe() {
    const height = `${this.sandbox.contentDocument.body.scrollHeight}px`;
    if (height !== this.sandbox.style.height) {
      let offset = 0;
      if (typeof browser !== 'undefined') {
        offset = 16;
      }
      const newHeight = this.sandbox.contentDocument.body.scrollHeight + offset;
      this.sandbox.style.height = `${newHeight}px`;
      this.props.onResize();
    }
  }

  onFormSubmit() {
    if (this.form.checkValidity()) {
      this.getFilesValues().then(() => {
        const data = this.serializeFormData(this.props.formEncoding);
        this.props.onValidated(data);
      });
    }
    this.form.classList.add('was-validated');
    this.resizeIframe();
  }

  onError(error) {
    this.props.onError(error);
  }

  handleLoad() {
    this.resizeIframe();
    this.form = this.sandbox.contentDocument.getElementsByTagName('form')[0];

    // Remove all input type submit
    this.removeInputSubmit();

    // Check that form has at least one valid input
    this.checkForEmptyForm();

    // Prevent default behavior on form submit event
    $(this.form).on('submit', event => {
      this.onFormSubmit(event);
      event.preventDefault();
      event.stopPropagation();
    });

    // Pressing enter in an input field also triggers a submit
    $(this.form).on('keypress', 'input', event => {
      const code = event.keyCode || event.which;
      if (code === 13) {
        this.onFormSubmit(event);
        event.preventDefault();
        return false;
      }
    });

    // Keyup and focus out can trigger validation and therefore change height
    $(this.form).on('focusout keyup', 'input, textarea, select', () => {
      this.resizeIframe();
    });
  }

  serializeFormData(mode) {
    switch (mode) {
      case 'array': {
        let result = $(this.form).serializeArray();
        if (this.files.length) {
          result = result.concat(this.files);
        }
        return result;
      }
      case 'json':
        return JSON.stringify(this.serializeFormData('array'));
      case 'url':
        return $.param(this.serializeFormData('array'));
      case 'html':
        this.updateFormValues();
        return this.form.outerHTML;
      default:
        throw new Error('This response mode format is not supported.');
    }
  }

  getFilesValues() {
    return new Promise(resolve => {
      const that = this;
      const promises = [];
      $(this.form).find('input[type=file]').each(function() {
        const name = $(this).prop('name');
        if (name) {
          for (let i = 0; i < this.files.length; i++) {
            const reader = new FileReader();
            promises.push(new Promise((resolve => {
              reader.addEventListener('load', () => {
                that.files.push({name, 'value': reader.result});
                resolve();
              });
              reader.readAsDataURL(this.files[i]);
            })));
          }
        }
      });
      Promise.all(promises).then(resolve);
    });
  }

  updateFormValues() {
    $(this.form).find('[name]').each(function() {
      switch ($(this).prop('tagName').toLowerCase()) {
        case 'textarea':
          // <textarea>value</textarea>
          $(this).text($(this).val());
          break;
        case 'select':
          // <select><option value='1' selected /></select>
          $(this).find('option').each(function() {
            $(this).attr('selected', $(this).is(':selected'));
          });
          break;
        case 'input': {
          switch ($(this).prop('type').toLowerCase()) {
            case 'radio':
            case 'checkbox':
              // <input type='checkbox|radio' checked=true|false />
              $(this).attr('checked', $(this).is(':checked'));
              break;
            case 'file': {
              // TODO ignore value in this case or reuse values in this.files?
              break;
            }
            default:
              // <input type='text|url|email|etc.' value='val' />
              $(this).attr('value', $(this).val());
              break;
          }
          break;
        }
      }
    });
  }

  render() {
    const sandboxContent = `
      <!DOCTYPE html>

          <meta charset="utf-8">
          <link rel="stylesheet" href="../../dep/bootstrap4/css/bootstrap.css">

         ${this.props.formDefinition}

    `;
    return (
       this.sandbox = node}
        onLoad={() => this.handleLoad()} />
    );
  }
}

FormSandbox.propTypes = {
  formDefinition: PropTypes.string,
  formEncoding: PropTypes.string,
  needSubmit: PropTypes.bool,
  onValidated: PropTypes.func,
  onResize: PropTypes.func,
  onError: PropTypes.func
};

FormSandbox.defaultProps = {
  formEncoding: 'json',
};
<commit_msg>Fix reuse mvelo for browser detection<commit_after>/**
 * Copyright (C) 2018 Mailvelope GmbH
 * Licensed under the GNU Affero General Public License version 3
 */

import React from 'react';
import PropTypes from 'prop-types';
import mvelo from '../../../mvelo';

export default class FormSandbox extends React.Component {
  constructor(props) {
    super(props);
    this.sandbox = null;
    this.form = null;
    this.files = [];
  }

  shouldComponentUpdate() {
    // create sandbox iframe only once
    return false;
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.needSubmit === true) {
      this.form.dispatchEvent(new Event('submit'));
    }
  }

  removeInputSubmit() {
    $(this.form).find('input[type=submit]').each(function() {
      $(this).remove();
    });
  }

  checkForEmptyForm() {
    // check that there is at least a valid input field to send
    const validInput = $(this.form).find('input[name], select[name], textarea[name]').length;
    if (!validInput) {
      this.onError(new Error('There should be at least one input field with name property set.'));
    }
  }

  resizeIframe() {
    const height = `${this.sandbox.contentDocument.body.scrollHeight}px`;
    if (height !== this.sandbox.style.height) {
      let offset = 0;
      if (!mvelo.crx) {
        offset = 16;
      }
      const newHeight = this.sandbox.contentDocument.body.scrollHeight + offset;
      this.sandbox.style.height = `${newHeight}px`;
      this.props.onResize();
    }
  }

  onFormSubmit() {
    if (this.form.checkValidity()) {
      this.getFilesValues().then(() => {
        const data = this.serializeFormData(this.props.formEncoding);
        this.props.onValidated(data);
      });
    }
    this.form.classList.add('was-validated');
    this.resizeIframe();
  }

  onError(error) {
    this.props.onError(error);
  }

  handleLoad() {
    this.resizeIframe();
    this.form = this.sandbox.contentDocument.getElementsByTagName('form')[0];

    // Remove all input type submit
    this.removeInputSubmit();

    // Check that form has at least one valid input
    this.checkForEmptyForm();

    // Prevent default behavior on form submit event
    $(this.form).on('submit', event => {
      this.onFormSubmit(event);
      event.preventDefault();
      event.stopPropagation();
    });

    // Pressing enter in an input field also triggers a submit
    $(this.form).on('keypress', 'input', event => {
      const code = event.keyCode || event.which;
      if (code === 13) {
        this.onFormSubmit(event);
        event.preventDefault();
        return false;
      }
    });

    // Keyup and focus out can trigger validation and therefore change height
    $(this.form).on('focusout keyup', 'input, textarea, select', () => {
      this.resizeIframe();
    });
  }

  serializeFormData(mode) {
    switch (mode) {
      case 'array': {
        let result = $(this.form).serializeArray();
        if (this.files.length) {
          result = result.concat(this.files);
        }
        return result;
      }
      case 'json':
        return JSON.stringify(this.serializeFormData('array'));
      case 'url':
        return $.param(this.serializeFormData('array'));
      case 'html':
        this.updateFormValues();
        return this.form.outerHTML;
      default:
        throw new Error('This response mode format is not supported.');
    }
  }

  getFilesValues() {
    return new Promise(resolve => {
      const that = this;
      const promises = [];
      $(this.form).find('input[type=file]').each(function() {
        const name = $(this).prop('name');
        if (name) {
          for (let i = 0; i < this.files.length; i++) {
            const reader = new FileReader();
            promises.push(new Promise((resolve => {
              reader.addEventListener('load', () => {
                that.files.push({name, 'value': reader.result});
                resolve();
              });
              reader.readAsDataURL(this.files[i]);
            })));
          }
        }
      });
      Promise.all(promises).then(resolve);
    });
  }

  updateFormValues() {
    $(this.form).find('[name]').each(function() {
      switch ($(this).prop('tagName').toLowerCase()) {
        case 'textarea':
          // <textarea>value</textarea>
          $(this).text($(this).val());
          break;
        case 'select':
          // <select><option value='1' selected /></select>
          $(this).find('option').each(function() {
            $(this).attr('selected', $(this).is(':selected'));
          });
          break;
        case 'input': {
          switch ($(this).prop('type').toLowerCase()) {
            case 'radio':
            case 'checkbox':
              // <input type='checkbox|radio' checked=true|false />
              $(this).attr('checked', $(this).is(':checked'));
              break;
            case 'file': {
              // TODO ignore value in this case or reuse values in this.files?
              break;
            }
            default:
              // <input type='text|url|email|etc.' value='val' />
              $(this).attr('value', $(this).val());
              break;
          }
          break;
        }
      }
    });
  }

  render() {
    const sandboxContent = `
      <!DOCTYPE html>

          <meta charset="utf-8">
          <link rel="stylesheet" href="../../dep/bootstrap4/css/bootstrap.css">

         ${this.props.formDefinition}

    `;
    return (
       this.sandbox = node}
        onLoad={() => this.handleLoad()} />
    );
  }
}

FormSandbox.propTypes = {
  formDefinition: PropTypes.string,
  formEncoding: PropTypes.string,
  needSubmit: PropTypes.bool,
  onValidated: PropTypes.func,
  onResize: PropTypes.func,
  onError: PropTypes.func
};

FormSandbox.defaultProps = {
  formEncoding: 'json',
};
<|endoftext|>