<commit_before>// @flow
import {
  WALLET_RECOVERY_PHRASE_WORD_COUNT,
  PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT,
} from './cryptoConfig';

export const CREATE_WALLET_STEPS = [
  'instructions',
  'template',
  'mnemonics',
  'validate',
  'hashImage',
  'config',
];

export const WALLET_RESTORE_TYPES = {
  REGULAR: 'regular', // Shelley wallet
  CERTIFICATE: 'certificate', // Paper wallet
  LEGACY: 'legacy', // Byron wallet
  YOROI_REGULAR: 'yoroi-regular', // Yoroi regular (rewards) wallet
  YOROI_LEGACY: 'yoroi-legacy', // Yoroi legacy (balance) wallet
};

export const RECOVERY_PHRASE_WORD_COUNT_OPTIONS = {
  [WALLET_RESTORE_TYPES.REGULAR]: WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.CERTIFICATE]: PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.LEGACY]: LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.YOROI_REGULAR]: YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.YOROI_LEGACY]: YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT,
};

export const WALLET_PUBLIC_KEY_NOTIFICATION_SEGMENT_LENGTH = 15;
export const IS_WALLET_PUBLIC_KEY_SHARING_ENABLED = true;

// Automatic wallet migration from pre Daedalus 1.0.0 versions has been disabled
export const IS_AUTOMATIC_WALLET_MIGRATION_ENABLED = false;

// Wallet assets feature toggle enable/disable
export const WALLET_ASSETS_ENABLED = true;

export const IS_WALLET_ASSETS_AMOUNT_FORMATTING_ENABLED = true;

// Byron wallet migration has been temporarily disabled due to missing Api support after Mary HF
export const IS_BYRON_WALLET_MIGRATION_ENABLED = false;

export const IS_WALLET_UNDELEGATION_ENABLED = false;

export const TRANSACTION_MIN_ADA_VALUE = 1;
<commit_msg>Disable native token amount formatting<commit_after>// @flow
import {
  WALLET_RECOVERY_PHRASE_WORD_COUNT,
  PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT,
} from './cryptoConfig';

export const CREATE_WALLET_STEPS = [
  'instructions',
  'template',
  'mnemonics',
  'validate',
  'hashImage',
  'config',
];

export const WALLET_RESTORE_TYPES = {
  REGULAR: 'regular', // Shelley wallet
  CERTIFICATE: 'certificate', // Paper wallet
  LEGACY: 'legacy', // Byron wallet
  YOROI_REGULAR: 'yoroi-regular', // Yoroi regular (rewards) wallet
  YOROI_LEGACY: 'yoroi-legacy', // Yoroi legacy (balance) wallet
};

export const RECOVERY_PHRASE_WORD_COUNT_OPTIONS = {
  [WALLET_RESTORE_TYPES.REGULAR]: WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.CERTIFICATE]: PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.LEGACY]: LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.YOROI_REGULAR]: YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT,
  [WALLET_RESTORE_TYPES.YOROI_LEGACY]: YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT,
};

export const WALLET_PUBLIC_KEY_NOTIFICATION_SEGMENT_LENGTH = 15;
export const IS_WALLET_PUBLIC_KEY_SHARING_ENABLED = true;

// Automatic wallet migration from pre Daedalus 1.0.0 versions has been disabled
export const IS_AUTOMATIC_WALLET_MIGRATION_ENABLED = false;

// Wallet assets feature toggle enable/disable
export const WALLET_ASSETS_ENABLED = true;

export const IS_WALLET_ASSETS_AMOUNT_FORMATTING_ENABLED = false;

// Byron wallet migration has been temporarily disabled due to missing Api support after Mary HF
export const IS_BYRON_WALLET_MIGRATION_ENABLED = false;

export const IS_WALLET_UNDELEGATION_ENABLED = false;

export const TRANSACTION_MIN_ADA_VALUE = 1;
<|endoftext|>


<commit_before>const WAIT = 1000;
const NODE_PORT = process.env.NODE_PORT || 3000;

module.exports = {
  'Smoketest' (browser) {
    browser
      .url(`http://localhost:${NODE_PORT}/`)
      .waitForElementVisible('body', WAIT)
      .assert.containsText('body', 'Client render')
      .end();
  }
};
<commit_msg>Add test delay for CI<commit_after>const WAIT = 1000;
const NODE_PORT = process.env.NODE_PORT || 3000;

module.exports = {
  'Smoketest' (browser) {
    browser
      .url(`http://localhost:${NODE_PORT}/`)
      .waitForElementVisible('body', WAIT)
      .pause(100)
      .assert.containsText('body', 'Client render')
      .end();
  }
};
<|endoftext|>


<commit_before>/**
 * Check if JQuery is defined
 * is not the user is opening the file with Electron
 */

let hasBackend = false

/**
 * Check if JQuery is imported, if not create the auxiliary script
 * If the script throw an exception,
 *
 * By default the app is tought as if it's opened with the browser
 */
try {

  /**
   * If this JQuery operation throw and exception, Jquery isn't imported.
   * So the project is opened with Electron.js
   */
  $('body').length

}

//The app is opened with Electron.js
catch (exception) {

  // Create the helper script
  let requiredScript = document.createElement("script")
  requiredScript.innerHTML = `window.$ = window.jQuery = require(__dirname + '/js/jquery.min.js');`

  // Add the attribute used to remove the script
  requiredScript.setAttributeNode(document.createAttribute('data-rash-original-content'))
  document.head.appendChild(requiredScript);

  hasBackend = true
}

/**
 * Here the script try to call the backend to know if it exists
 */
try {

  // Init the ipcRenderer and ask for the existency of backend
  let ipcRenderer = require('electron').ipcRenderer
  hasbackend = ipcRenderer.sendSync('hasBackendSync')

} catch (exception) {

  hasBackend = false
}

$(document).ready(function () {
  if (hasBackend) {
    let thisScript = $('script[src="js/rajemce/init_rajemce.js"]')

    // Remove and re-add RASH
    $('script[src="js/rash.js"]').remove()
    thisScript.after('<script src="js/rash.js"></script>')

    // Add TinyMCE in the right position
    thisScript.after('<script src="js/tinymce/tinymce.js" data-rash-original-content=""></script>')
    thisScript.after('<script src="js/rajemce/raje_core.js" data-rash-original-content=""></script>')

    // Add CSS stylesheets
    thisScript.after('<link rel="stylesheet" href="css/font-awesome.min.css" data-rash-original-content="">')
    thisScript.after('<link rel="stylesheet" href="css/rajemce.css" data-rash-original-content="">')
  }
})<commit_msg>Rash is not removed when an article is opened<commit_after>/**
 * Check if JQuery is defined
 * is not the user is opening the file with Electron
 */

let hasBackend = false

/**
 * Check if JQuery is imported, if not create the auxiliary script
 * If the script throw an exception,
 *
 * By default the app is tought as if it's opened with the browser
 */
try {

  /**
   * If this JQuery operation throw and exception, Jquery isn't imported.
   * So the project is opened with Electron.js
   */
  $('body').length

}

//The app is opened with Electron.js
catch (exception) {

  // Create the helper script
  let requiredScript = document.createElement("script")
  requiredScript.innerHTML = `window.$ = window.jQuery = require(__dirname + '/js/jquery.min.js');`

  // Add the attribute used to remove the script
  requiredScript.setAttributeNode(document.createAttribute('data-rash-original-content'))
  document.head.appendChild(requiredScript);

  hasBackend = true
}

/**
 * Here the script try to call the backend to know if it exists
 */
try {

  // Init the ipcRenderer and ask for the existency of backend
  let ipcRenderer = require('electron').ipcRenderer
  hasbackend = ipcRenderer.sendSync('hasBackendSync')

} catch (exception) {

  hasBackend = false
}

$(document).ready(function () {
  if (hasBackend) {
    let thisScript = $('script[src="js/rajemce/init_rajemce.js"]')

    // Remove and re-add RASH
    $('script[src="js/rash.js"]').remove()
    //thisScript.after('<script src="js/rash.js"></script>')

    // Add TinyMCE in the right position
    thisScript.after('<script src="js/tinymce/tinymce.js" data-rash-original-content=""></script>')
    thisScript.after('<script src="js/rajemce/raje_core.js" data-rash-original-content=""></script>')

    // Add CSS stylesheets
    thisScript.after('<link rel="stylesheet" href="css/font-awesome.min.css" data-rash-original-content="">')
    thisScript.after('<link rel="stylesheet" href="css/rajemce.css" data-rash-original-content="">')
  }
})<|endoftext|>


<commit_before>(function() {
  'use strict';

  angular.module('material.components.fabShared', ['material.core'])
    .controller('FabController', FabController);

  function FabController($scope, $element, $animate, $mdUtil, $mdConstant) {
    var vm = this;

    // NOTE: We use async evals below to avoid conflicts with any existing digest loops

    vm.open = function() {
      $scope.$evalAsync("vm.isOpen = true");
    };

    vm.close = function() {
      // Async eval to avoid conflicts with existing digest loops
      $scope.$evalAsync("vm.isOpen = false");

      // Focus the trigger when the element closes so users can still tab to the next item
      $element.find('md-fab-trigger')[0].focus();
    };

    // Toggle the open/close state when the trigger is clicked
    vm.toggle = function() {
      $scope.$evalAsync("vm.isOpen = !vm.isOpen");
    };

    setupDefaults();
    setupListeners();
    setupWatchers();
    fireInitialAnimations();

    function setupDefaults() {
      // Set the default direction to 'down' if none is specified
      vm.direction = vm.direction || 'down';

      // Set the default to be closed
      vm.isOpen = vm.isOpen || false;

      // Start the keyboard interaction at the first action
      resetActionIndex();
    }

    var events = [];

    function setupListeners() {
      var eventTypes = [
        'mousedown', 'mouseup', 'click', 'touchstart', 'touchend', 'focusin', 'focusout'
      ];

      // Add our listeners
      angular.forEach(eventTypes, function(eventType) {
        $element.on(eventType, parseEvents);
      });

      // Remove our listeners when destroyed
      $scope.$on('$destroy', function() {
        angular.forEach(eventTypes, function(eventType) {
          $element.off(eventType, parseEvents);
        });
      });
    }

    function resetEvents() {
      events = [];
    }

    function equalsEvents(toCheck) {
      var isEqual, strippedCheck, moreToCheck;

      // Quick check to make sure we don't get stuck in an infinite loop
      var numTests = 0;

      do {
        // Strip out the question mark
        strippedCheck = toCheck.map(function(event) {
          return event.replace('?', '')
        });

        // Check if they are equal
        isEqual = angular.equals(events, strippedCheck);

        // If not, check to see if removing an optional event makes them equal
        if (!isEqual) {
          toCheck = removeOptionalEvent(toCheck);
          moreToCheck = (toCheck.length >= events.length && toCheck.length !== strippedCheck.length);
        }
      }
      while (numTests < 10 && !isEqual && moreToCheck);

      return isEqual;
    }

    function removeOptionalEvent(events) {
      var foundOptional = false;

      return events.filter(function(event) {
        // If we have not found an optional one, keep searching
        if (!foundOptional && event.indexOf('?') !== -1) {
          foundOptional = true;

          // If we find an optional one, remove only that one and keep going
          return false;
        }

        return true;
      });
    }

    function parseEvents(latestEvent) {
      events.push(latestEvent.type);

      // Handle desktop click
      if (equalsEvents(['mousedown', 'focusout?', 'focusin?', 'mouseup', 'click'])) {
        handleItemClick(latestEvent);
        resetEvents();
        return;
      }

      // Handle mobile click/tap (and keyboard enter)
      if (equalsEvents(['touchstart?', 'touchend?', 'click'])) {
        handleItemClick(latestEvent);
        resetEvents();
        return;
      }

      // Handle tab keys (focusin)
      if (equalsEvents(['focusin'])) {
        vm.open();
        resetEvents();
        return;
      }

      // Handle tab keys (focusout)
      if (equalsEvents(['focusout'])) {
        vm.close();
        resetEvents();
        return;
      }

      eventUnhandled();
    }

    /*
     * No event was handled, so setup a timeout to clear the events
     *
     * TODO: Use $mdUtil.debounce()?
     */
    var resetEventsTimeout;

    function eventUnhandled() {
      if (resetEventsTimeout) {
        window.clearTimeout(resetEventsTimeout);
      }

      resetEventsTimeout = window.setTimeout(function() {
        resetEvents();
      }, 250);
    }

    function resetActionIndex() {
      vm.currentActionIndex = -1;
    }

    function setupWatchers() {
      // Watch for changes to the direction and update classes/attributes
      $scope.$watch('vm.direction', function(newDir, oldDir) {
        // Add the appropriate classes so we can target the direction in the CSS
        $animate.removeClass($element, 'md-' + oldDir);
        $animate.addClass($element, 'md-' + newDir);

        // Reset the action index since it may have changed
        resetActionIndex();
      });

      var trigger, actions;

      // Watch for changes to md-open
      $scope.$watch('vm.isOpen', function(isOpen) {
        // Reset the action index since it may have changed
        resetActionIndex();

        // We can't get the trigger/actions outside of the watch because the component hasn't been
        // linked yet, so we wait until the first watch fires to cache them.
        if (!trigger || !actions) {
          trigger = getTriggerElement();
          actions = getActionsElement();
        }

        if (isOpen) {
          enableKeyboard();
        } else {
          disableKeyboard();
        }

        var toAdd = isOpen ? 'md-is-open' : '';
        var toRemove = isOpen ? '' : 'md-is-open';

        // Set the proper ARIA attributes
        trigger.attr('aria-haspopup', true);
        trigger.attr('aria-expanded', isOpen);
        actions.attr('aria-hidden', !isOpen);

        // Animate the CSS classes
        $animate.setClass($element, toAdd, toRemove);
      });
    }

    // Fire the animations once in a separate digest loop to initialize them
    function fireInitialAnimations() {
      $mdUtil.nextTick(function() {
        $animate.addClass($element, 'md-noop');
      });
    }

    function enableKeyboard() {
      angular.element(document).on('keydown', keyPressed);
    }

    function disableKeyboard() {
      angular.element(document).off('keydown', keyPressed);
    }

    function keyPressed(event) {
      switch (event.which) {
        case $mdConstant.KEY_CODE.SPACE: event.preventDefault(); return false;
        case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false;
        case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false;
        case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false;
        case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false;
        case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false;
      }
    }

    function doActionPrev(event) {
      focusAction(event, -1);
    }

    function doActionNext(event) {
      focusAction(event, 1);
    }

    function focusAction(event, direction) {
      // Grab all of the actions
      var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');

      // Disable all other actions for tabbing
      angular.forEach(actions, function(action) {
        angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
      });

      // Increment/decrement the counter with restrictions
      vm.currentActionIndex = vm.currentActionIndex + direction;
      vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex);
      vm.currentActionIndex = Math.max(0, vm.currentActionIndex);

      // Focus the element
      var focusElement =  angular.element(actions[vm.currentActionIndex]).children()[0];
      angular.element(focusElement).attr('tabindex', 0);
      focusElement.focus();

      // Make sure the event doesn't bubble and cause something else
      event.preventDefault();
      event.stopImmediatePropagation();
    }

    function doKeyLeft(event) {
      if (vm.direction === 'left') {
        doActionNext(event);
      } else {
        doActionPrev(event);
      }
    }

    function doKeyUp(event) {
      if (vm.direction === 'down') {
        doActionPrev(event);
      } else {
        doActionNext(event);
      }
    }

    function doKeyRight(event) {
      if (vm.direction === 'left') {
        doActionPrev(event);
      } else {
        doActionNext(event);
      }
    }

    function doKeyDown(event) {
      if (vm.direction === 'up') {
        doActionPrev(event);
      } else {
        doActionNext(event);
      }
    }

    function isTrigger(element) {
      return $mdUtil.getClosest(element, 'md-fab-trigger');
    }

    function isAction(element) {
      return $mdUtil.getClosest(element, 'md-fab-actions');
    }

    function handleItemClick(event) {
      if (isTrigger(event.target)) {
        vm.toggle();
      }

      if (isAction(event.target)) {
        vm.close();
      }
    }

    function getTriggerElement() {
      return $element.find('md-fab-trigger');
    }

    function getActionsElement() {
      return $element.find('md-fab-actions');
    }
  }
})();<commit_msg>fix(fabSpeekDial): remove keyboard handlers on scope destroy<commit_after>(function() {
  'use strict';

  angular.module('material.components.fabShared', ['material.core'])
    .controller('FabController', FabController);

  function FabController($scope, $element, $animate, $mdUtil, $mdConstant) {
    var vm = this;

    // NOTE: We use async evals below to avoid conflicts with any existing digest loops

    vm.open = function() {
      $scope.$evalAsync("vm.isOpen = true");
    };

    vm.close = function() {
      // Async eval to avoid conflicts with existing digest loops
      $scope.$evalAsync("vm.isOpen = false");

      // Focus the trigger when the element closes so users can still tab to the next item
      $element.find('md-fab-trigger')[0].focus();
    };

    // Toggle the open/close state when the trigger is clicked
    vm.toggle = function() {
      $scope.$evalAsync("vm.isOpen = !vm.isOpen");
    };

    setupDefaults();
    setupListeners();
    setupWatchers();
    fireInitialAnimations();

    function setupDefaults() {
      // Set the default direction to 'down' if none is specified
      vm.direction = vm.direction || 'down';

      // Set the default to be closed
      vm.isOpen = vm.isOpen || false;

      // Start the keyboard interaction at the first action
      resetActionIndex();
    }

    var events = [];

    function setupListeners() {
      var eventTypes = [
        'mousedown', 'mouseup', 'click', 'touchstart', 'touchend', 'focusin', 'focusout'
      ];

      // Add our listeners
      angular.forEach(eventTypes, function(eventType) {
        $element.on(eventType, parseEvents);
      });

      // Remove our listeners when destroyed
      $scope.$on('$destroy', function() {
        angular.forEach(eventTypes, function(eventType) {
          $element.off(eventType, parseEvents);
        });
        // remove any attached keyboard handlers in case element is removed while
        // speed dial is open
        disableKeyboard();
      });
    }

    function resetEvents() {
      events = [];
    }

    function equalsEvents(toCheck) {
      var isEqual, strippedCheck, moreToCheck;

      // Quick check to make sure we don't get stuck in an infinite loop
      var numTests = 0;

      do {
        // Strip out the question mark
        strippedCheck = toCheck.map(function(event) {
          return event.replace('?', '')
        });

        // Check if they are equal
        isEqual = angular.equals(events, strippedCheck);

        // If not, check to see if removing an optional event makes them equal
        if (!isEqual) {
          toCheck = removeOptionalEvent(toCheck);
          moreToCheck = (toCheck.length >= events.length && toCheck.length !== strippedCheck.length);
        }
      }
      while (numTests < 10 && !isEqual && moreToCheck);

      return isEqual;
    }

    function removeOptionalEvent(events) {
      var foundOptional = false;

      return events.filter(function(event) {
        // If we have not found an optional one, keep searching
        if (!foundOptional && event.indexOf('?') !== -1) {
          foundOptional = true;

          // If we find an optional one, remove only that one and keep going
          return false;
        }

        return true;
      });
    }

    function parseEvents(latestEvent) {
      events.push(latestEvent.type);

      // Handle desktop click
      if (equalsEvents(['mousedown', 'focusout?', 'focusin?', 'mouseup', 'click'])) {
        handleItemClick(latestEvent);
        resetEvents();
        return;
      }

      // Handle mobile click/tap (and keyboard enter)
      if (equalsEvents(['touchstart?', 'touchend?', 'click'])) {
        handleItemClick(latestEvent);
        resetEvents();
        return;
      }

      // Handle tab keys (focusin)
      if (equalsEvents(['focusin'])) {
        vm.open();
        resetEvents();
        return;
      }

      // Handle tab keys (focusout)
      if (equalsEvents(['focusout'])) {
        vm.close();
        resetEvents();
        return;
      }

      eventUnhandled();
    }

    /*
     * No event was handled, so setup a timeout to clear the events
     *
     * TODO: Use $mdUtil.debounce()?
     */
    var resetEventsTimeout;

    function eventUnhandled() {
      if (resetEventsTimeout) {
        window.clearTimeout(resetEventsTimeout);
      }

      resetEventsTimeout = window.setTimeout(function() {
        resetEvents();
      }, 250);
    }

    function resetActionIndex() {
      vm.currentActionIndex = -1;
    }

    function setupWatchers() {
      // Watch for changes to the direction and update classes/attributes
      $scope.$watch('vm.direction', function(newDir, oldDir) {
        // Add the appropriate classes so we can target the direction in the CSS
        $animate.removeClass($element, 'md-' + oldDir);
        $animate.addClass($element, 'md-' + newDir);

        // Reset the action index since it may have changed
        resetActionIndex();
      });

      var trigger, actions;

      // Watch for changes to md-open
      $scope.$watch('vm.isOpen', function(isOpen) {
        // Reset the action index since it may have changed
        resetActionIndex();

        // We can't get the trigger/actions outside of the watch because the component hasn't been
        // linked yet, so we wait until the first watch fires to cache them.
        if (!trigger || !actions) {
          trigger = getTriggerElement();
          actions = getActionsElement();
        }

        if (isOpen) {
          enableKeyboard();
        } else {
          disableKeyboard();
        }

        var toAdd = isOpen ? 'md-is-open' : '';
        var toRemove = isOpen ? '' : 'md-is-open';

        // Set the proper ARIA attributes
        trigger.attr('aria-haspopup', true);
        trigger.attr('aria-expanded', isOpen);
        actions.attr('aria-hidden', !isOpen);

        // Animate the CSS classes
        $animate.setClass($element, toAdd, toRemove);
      });
    }

    // Fire the animations once in a separate digest loop to initialize them
    function fireInitialAnimations() {
      $mdUtil.nextTick(function() {
        $animate.addClass($element, 'md-noop');
      });
    }

    function enableKeyboard() {
      angular.element(document).on('keydown', keyPressed);
    }

    function disableKeyboard() {
      angular.element(document).off('keydown', keyPressed);
    }

    function keyPressed(event) {
      switch (event.which) {
        case $mdConstant.KEY_CODE.SPACE: event.preventDefault(); return false;
        case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false;
        case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false;
        case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false;
        case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false;
        case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false;
      }
    }

    function doActionPrev(event) {
      focusAction(event, -1);
    }

    function doActionNext(event) {
      focusAction(event, 1);
    }

    function focusAction(event, direction) {
      // Grab all of the actions
      var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');

      // Disable all other actions for tabbing
      angular.forEach(actions, function(action) {
        angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
      });

      // Increment/decrement the counter with restrictions
      vm.currentActionIndex = vm.currentActionIndex + direction;
      vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex);
      vm.currentActionIndex = Math.max(0, vm.currentActionIndex);

      // Focus the element
      var focusElement =  angular.element(actions[vm.currentActionIndex]).children()[0];
      angular.element(focusElement).attr('tabindex', 0);
      focusElement.focus();

      // Make sure the event doesn't bubble and cause something else
      event.preventDefault();
      event.stopImmediatePropagation();
    }

    function doKeyLeft(event) {
      if (vm.direction === 'left') {
        doActionNext(event);
      } else {
        doActionPrev(event);
      }
    }

    function doKeyUp(event) {
      if (vm.direction === 'down') {
        doActionPrev(event);
      } else {
        doActionNext(event);
      }
    }

    function doKeyRight(event) {
      if (vm.direction === 'left') {
        doActionPrev(event);
      } else {
        doActionNext(event);
      }
    }

    function doKeyDown(event) {
      if (vm.direction === 'up') {
        doActionPrev(event);
      } else {
        doActionNext(event);
      }
    }

    function isTrigger(element) {
      return $mdUtil.getClosest(element, 'md-fab-trigger');
    }

    function isAction(element) {
      return $mdUtil.getClosest(element, 'md-fab-actions');
    }

    function handleItemClick(event) {
      if (isTrigger(event.target)) {
        vm.toggle();
      }

      if (isAction(event.target)) {
        vm.close();
      }
    }

    function getTriggerElement() {
      return $element.find('md-fab-trigger');
    }

    function getActionsElement() {
      return $element.find('md-fab-actions');
    }
  }
})();
<|endoftext|>


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

import {CSS} from '../../../build/amp-selector-0.1.css';
import {Keycodes} from '../../../src/utils/keycodes';
import {actionServiceForDoc} from '../../../src/services';
import {closestBySelector, tryFocus} from '../../../src/dom';
import {createCustomEvent} from '../../../src/event-helper';
import {dev, user} from '../../../src/log';
import {isEnumValue} from '../../../src/types';

/**
 * Set of namespaces that can be set for lifecycle reporters.
 *
 * @enum {string}
 */
const KEYBOARD_SELECT_MODES = {
  NONE: 'none',
  FOCUS: 'focus',
  SELECT: 'select',
};

export class AmpSelector extends AMP.BaseElement {

  /** @param {!AmpElement} element */
  constructor(element) {
    super(element);

    /** @private {boolean} */
    this.isMultiple_ = false;

    /** @private {!Array<!Element>} */
    this.selectedOptions_ = [];

    /** @private {!Array<!Element>} */
    this.options_ = [];

    /** @private {!Array<!Element>} */
    this.inputs_ = [];

    /** @private {boolean} */
    this.isDisabled_ = false;

    /** @private {?../../../src/service/action-impl.ActionService} */
    this.action_ = null;

    /**
     * The index of the option that should receive tab focus. Only one
     * option should ever receive tab focus, with the other options reachable
     * by arrow keys when the option is in focus.
     * @private {number}
     */
    this.focusedIndex_ = 0;

    /** @private {!KEYBOARD_SELECT_MODES} */
    this.kbSelectMode_ = KEYBOARD_SELECT_MODES.NONE;
  }

  /** @override */
  isLayoutSupported() {
    return true;
  }

  /** @override */
  buildCallback() {
    this.action_ = actionServiceForDoc(this.element);
    this.isMultiple_ = this.element.hasAttribute('multiple');
    this.isDisabled_ = this.element.hasAttribute('disabled');

    this.element.setAttribute('role', 'listbox');

    if (this.isMultiple_) {
      this.element.setAttribute('aria-multiselectable', 'true');
    }

    if (this.isDisabled_) {
      this.element.setAttribute('aria-disabled', 'true');
    }

    let kbSelectMode = this.element.getAttribute('keyboard-select-mode');
    if (!kbSelectMode) {
      kbSelectMode = KEYBOARD_SELECT_MODES.NONE;
    }
    user().assert(isEnumValue(KEYBOARD_SELECT_MODES, kbSelectMode),
        'Invalid keyboard-select-mode');
    user().assert(
        !(this.isMultiple_ && kbSelectMode == KEYBOARD_SELECT_MODES.SELECT),
        '[keyboard-select-mode=select] not supported for multiple ' +
        'selection amp-selector');
    this.kbSelectMode_ = kbSelectMode;

    this.init_();
    if (!this.isDisabled_) {
      this.element.addEventListener('click', this.clickHandler_.bind(this));
      this.element.addEventListener('keydown', this.keyDownHandler_.bind(this));
    }
  }

  /** @override */
  mutatedAttributesCallback(mutations) {
    const selected = mutations['selected'];
    if (selected !== undefined) {
      this.selectedAttributeMutated_(selected);
    }
  }

  /**
   * Handles mutation of the `selected` attribute.
   * @param {null|boolean|string|number|Array|Object} newValue
   * @private
   */
  selectedAttributeMutated_(newValue) {
    if (newValue === null) {
      this.clearAllSelections_();
      return;
    }
    let selectedArray = Array.isArray(newValue) ? newValue : [newValue];
    // Only use first value if multiple selection is disabled.
    if (!this.isMultiple_) {
      selectedArray = selectedArray.slice(0, 1);
    }
    // Convert array values to strings and create map for fast lookup.
    const selectedMap = selectedArray.reduce((map, value) => {
      map[value] = true;
      return map;
    }, Object.create(null));
    // Iterate through elements and toggle selection as necessary.
    for (let i = 0; i < this.options_.length; i++) {
      const element = this.options_[i];
      const option = element.getAttribute('option');
      if (selectedMap[option]) {
        this.setSelection_(element);
      } else {
        this.clearSelection_(element);
      }
    }
    this.updateFocus_();
    // Update inputs.
    this.setInputs_();
  }

  /**
   * Update focus such that only one option in the selector can receive focus.
   * When keyboard-select-mode is not none, this function handles focus as if
   * the selector options are set of radio buttons. Otherwise, this function
   * is a no-op.
   *
   * If no element is provided, this function will determine which option should
   * receive focus.
   *
   * In multi-select selectors, focus should go to the first option.
   * In single-select selectors, focus should go to the initially selected
   * option, or to the first option if none are initially selected.
   * @param {Element=} opt_focusEl Element to put focus on
   * @private
   */
  updateFocus_(opt_focusEl) {
    if (this.kbSelectMode_ == KEYBOARD_SELECT_MODES.NONE) {
      // Don't manage focus.
      return;
    }

    this.options_.forEach(option => {
      option.tabIndex = -1;
    });

    let focusElement = opt_focusEl;
    if (!focusElement) {
      if (this.isMultiple_) {
        focusElement = this.options_[0];
      } else {
        focusElement = this.selectedOptions_[0] || this.options_[0];
      }
    }
    if (focusElement) {
      this.focusedIndex_ = this.options_.indexOf(focusElement);
      focusElement.tabIndex = 0;
    }
  }

  /**
   * @private
   */
  init_() {
    const options = [].slice.call(this.element.querySelectorAll('[option]'));
    options.forEach(option => {
      option.setAttribute('role', 'option');
      if (option.hasAttribute('disabled')) {
        option.setAttribute('aria-disabled', 'true');
      }
      if (option.hasAttribute('selected')) {
        this.setSelection_(option);
      } else {
        this.clearSelection_(option);
      }
      option.tabIndex = 0;
      this.options_.push(option);
    });
    this.updateFocus_();
    this.setInputs_();
  }

  /**
   * Creates inputs for the currently selected elements and returns a string
   * array of their option values.
   * @note Ignores elements that have `disabled` attribute set.
   * @return {!Array<string>}
   * @private
   */
  setInputs_() {
    const selectedValues = [];
    const elementName = this.element.getAttribute('name');
    if (!elementName || this.isDisabled_) {
      return selectedValues;
    }
    const formId = this.element.getAttribute('form');

    this.inputs_.forEach(input => {
      this.element.removeChild(input);
    });
    this.inputs_ = [];
    const doc = this.win.document;
    const fragment = doc.createDocumentFragment();
    this.selectedOptions_.forEach(option => {
      if (!option.hasAttribute('disabled')) {
        const hidden = doc.createElement('input');
        const value = option.getAttribute('option');
        hidden.setAttribute('type', 'hidden');
        hidden.setAttribute('name', elementName);
        hidden.setAttribute('value', value);
        if (formId) {
          hidden.setAttribute('form', formId);
        }
        this.inputs_.push(hidden);
        fragment.appendChild(hidden);
        selectedValues.push(value);
      }
    });
    this.element.appendChild(fragment);
    return selectedValues;
  }

  /**
   * Handles user selection on an option.
   * @param {!Element} el The element selected.
   */
  onOptionPicked_(el) {
    if (el.hasAttribute('disabled')) {
      return;
    }

    this.mutateElement(() => {
      /** @type {?Array<string>} */
      let selectedValues;
      if (el.hasAttribute('selected')) {
        if (this.isMultiple_) {
          this.clearSelection_(el);
          selectedValues = this.setInputs_();
        }
      } else {
        this.setSelection_(el);
        selectedValues = this.setInputs_();
      }

      // Don't trigger action or update focus if
      // selected values haven't changed.
      if (selectedValues) {
        // Newly picked option should always have focus.
        this.updateFocus_(el);

        // Trigger 'select' event with two data params:
        // 'targetOption' - option value of the selected or deselected element.
        // 'selectedOptions' - array of option values of selected elements.
        const name = 'select';
        const selectEvent =
            createCustomEvent(this.win, `amp-selector.${name}`, {
              targetOption: el.getAttribute('option'),
              selectedOptions: selectedValues,
            });
        this.action_.trigger(this.element, name, selectEvent);
      }
    });
  }

  /**
   * Handles click events for the selectables.
   * @param {!Event} event
   */
  clickHandler_(event) {
    let el = dev().assertElement(event.target);
    if (!el) {
      return;
    }
    if (!el.hasAttribute('option')) {
      el = closestBySelector(el, '[option]');
    }
    if (el) {
      this.onOptionPicked_(el);
    }
  }

  /**
   * Handles keyboard events.
   * @param {!Event} event
   */
  keyDownHandler_(event) {
    const keyCode = event.keyCode;
    switch (keyCode) {
      case Keycodes.LEFT_ARROW: /* fallthrough */
      case Keycodes.UP_ARROW: /* fallthrough */
      case Keycodes.RIGHT_ARROW: /* fallthrough */
      case Keycodes.DOWN_ARROW: /* fallthrough */
        if (this.kbSelectMode_ != KEYBOARD_SELECT_MODES.NONE) {
          this.navigationKeyDownHandler_(event);
        }
        return;
      case Keycodes.ENTER: /* fallthrough */
      case Keycodes.SPACE:
        this.selectionKeyDownHandler_(event);
        return;
    }
  }

  /**
   * Handles keyboard navigation events. Should not be called if
   * keyboard selection is disabled.
   * @param {!Event} event
   */
  navigationKeyDownHandler_(event) {
    const isLtr = this.win.document.body.getAttribute('dir') != 'rtl';
    let dir = 0;
    switch (event.keyCode) {
      case Keycodes.LEFT_ARROW:
        // Left is considered 'previous' in LTR and 'next' in RTL.
        dir = isLtr ? -1 : 1;
        break;
      case Keycodes.UP_ARROW:
        // Up is considered 'previous' in both LTR and RTL.
        dir = -1;
        break;
      case Keycodes.RIGHT_ARROW:
        // Right is considered 'next' in LTR and 'previous' in RTL.
        dir = isLtr ? 1 : -1;
        break;
      case Keycodes.DOWN_ARROW:
        // Down is considered 'next' in both LTR and RTL.
        dir = 1;
        break;
      default:
        return;
    }

    event.preventDefault();

    // Make currently selected option unfocusable
    this.options_[this.focusedIndex_].tabIndex = -1;

    // Change the focus to the next element in the specified direction.
    // The selection should loop around if the user attempts to go one
    // past the beginning or end.
    this.focusedIndex_ = (this.focusedIndex_ + dir) % this.options_.length;
    if (this.focusedIndex_ < 0) {
      this.focusedIndex_ = this.focusedIndex_ + this.options_.length;
    }

    // Focus newly selected option
    const newSelectedOption = this.options_[this.focusedIndex_];
    newSelectedOption.tabIndex = 0;
    tryFocus(newSelectedOption);

    const focusedOption = this.options_[this.focusedIndex_];
    if (this.kbSelectMode_ == KEYBOARD_SELECT_MODES.SELECT) {
      this.onOptionPicked_(focusedOption);
    }
  }

  /**
   * Handles keyboard selection events.
   * @param {!Event} event
   */
  selectionKeyDownHandler_(event) {
    const keyCode = event.keyCode;
    if (keyCode == Keycodes.SPACE || keyCode == Keycodes.ENTER) {
      if (this.options_.includes(event.target)) {
        event.preventDefault();
        const el = dev().assertElement(event.target);
        this.onOptionPicked_(el);
      }
    }
  }

  /**
   * Clears a given element from the list of selected options.
   * @param {!Element} element.
   * @private
   */
  clearSelection_(element) {
    element.removeAttribute('selected');
    element.setAttribute('aria-selected', 'false');
    const selIndex = this.selectedOptions_.indexOf(element);
    if (selIndex !== -1) {
      this.selectedOptions_.splice(selIndex, 1);
    }
  }

  /**
   * Clears all selected options.
   * @private
   */
  clearAllSelections_() {
    while (this.selectedOptions_.length > 0) {
      // Clear selected options for single select.
      const el = this.selectedOptions_.pop();
      this.clearSelection_(el);
    }
  }

  /**
   * Marks a given element as selected and clears the others if required.
   * @param {!Element} element.
   * @private
   */
  setSelection_(element) {
    // Exit if `element` is already selected.
    if (this.selectedOptions_.includes(element)) {
      return;
    }
    if (!this.isMultiple_) {
      this.clearAllSelections_();
    }
    element.setAttribute('selected', '');
    element.setAttribute('aria-selected', 'true');
    this.selectedOptions_.push(element);
  }
}

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

import {CSS} from '../../../build/amp-selector-0.1.css';
import {Keycodes} from '../../../src/utils/keycodes';
import {actionServiceForDoc} from '../../../src/services';
import {closestBySelector, tryFocus} from '../../../src/dom';
import {createCustomEvent} from '../../../src/event-helper';
import {dev, user} from '../../../src/log';

/**
 * Set of namespaces that can be set for lifecycle reporters.
 *
 * @enum {string}
 */
const KEYBOARD_SELECT_MODES = {
  NONE: 'none',
  FOCUS: 'focus',
  SELECT: 'select',
};

export class AmpSelector extends AMP.BaseElement {

  /** @param {!AmpElement} element */
  constructor(element) {
    super(element);

    /** @private {boolean} */
    this.isMultiple_ = false;

    /** @private {!Array<!Element>} */
    this.selectedOptions_ = [];

    /** @private {!Array<!Element>} */
    this.options_ = [];

    /** @private {!Array<!Element>} */
    this.inputs_ = [];

    /** @private {boolean} */
    this.isDisabled_ = false;

    /** @private {?../../../src/service/action-impl.ActionService} */
    this.action_ = null;

    /**
     * The index of the option that should receive tab focus. Only one
     * option should ever receive tab focus, with the other options reachable
     * by arrow keys when the option is in focus.
     * @private {number}
     */
    this.focusedIndex_ = 0;

    /** @private {!KEYBOARD_SELECT_MODES} */
    this.kbSelectMode_ = KEYBOARD_SELECT_MODES.NONE;
  }

  /** @override */
  isLayoutSupported() {
    return true;
  }

  /** @override */
  buildCallback() {
    this.action_ = actionServiceForDoc(this.element);
    this.isMultiple_ = this.element.hasAttribute('multiple');
    this.isDisabled_ = this.element.hasAttribute('disabled');

    this.element.setAttribute('role', 'listbox');

    if (this.isMultiple_) {
      this.element.setAttribute('aria-multiselectable', 'true');
    }

    if (this.isDisabled_) {
      this.element.setAttribute('aria-disabled', 'true');
    }

    let kbSelectMode = this.element.getAttribute('keyboard-select-mode');
    if (kbSelectMode) {
      kbSelectMode = kbSelectMode.toLowerCase();
      user().assertEnumValue(KEYBOARD_SELECT_MODES, kbSelectMode);
      user().assert(
        !(this.isMultiple_ && kbSelectMode == KEYBOARD_SELECT_MODES.SELECT),
        '[keyboard-select-mode=select] not supported for multiple ' +
        'selection amp-selector');
    } else {
      kbSelectMode = KEYBOARD_SELECT_MODES.NONE;
    }
    this.kbSelectMode_ = kbSelectMode;

    this.init_();
    if (!this.isDisabled_) {
      this.element.addEventListener('click', this.clickHandler_.bind(this));
      this.element.addEventListener('keydown', this.keyDownHandler_.bind(this));
    }
  }

  /** @override */
  mutatedAttributesCallback(mutations) {
    const selected = mutations['selected'];
    if (selected !== undefined) {
      this.selectedAttributeMutated_(selected);
    }
  }

  /**
   * Handles mutation of the `selected` attribute.
   * @param {null|boolean|string|number|Array|Object} newValue
   * @private
   */
  selectedAttributeMutated_(newValue) {
    if (newValue === null) {
      this.clearAllSelections_();
      return;
    }
    let selectedArray = Array.isArray(newValue) ? newValue : [newValue];
    // Only use first value if multiple selection is disabled.
    if (!this.isMultiple_) {
      selectedArray = selectedArray.slice(0, 1);
    }
    // Convert array values to strings and create map for fast lookup.
    const selectedMap = selectedArray.reduce((map, value) => {
      map[value] = true;
      return map;
    }, Object.create(null));
    // Iterate through elements and toggle selection as necessary.
    for (let i = 0; i < this.options_.length; i++) {
      const element = this.options_[i];
      const option = element.getAttribute('option');
      if (selectedMap[option]) {
        this.setSelection_(element);
      } else {
        this.clearSelection_(element);
      }
    }
    this.updateFocus_();
    // Update inputs.
    this.setInputs_();
  }

  /**
   * Update focus such that only one option in the selector can receive focus.
   * When keyboard-select-mode is not none, this function handles focus as if
   * the selector options are set of radio buttons. Otherwise, this function
   * is a no-op.
   *
   * If no element is provided, this function will determine which option should
   * receive focus.
   *
   * In multi-select selectors, focus should go to the first option.
   * In single-select selectors, focus should go to the initially selected
   * option, or to the first option if none are initially selected.
   * @param {Element=} opt_focusEl Element to put focus on
   * @private
   */
  updateFocus_(opt_focusEl) {
    if (this.kbSelectMode_ == KEYBOARD_SELECT_MODES.NONE) {
      // Don't manage focus.
      return;
    }

    this.options_.forEach(option => {
      option.tabIndex = -1;
    });

    let focusElement = opt_focusEl;
    if (!focusElement) {
      if (this.isMultiple_) {
        focusElement = this.options_[0];
      } else {
        focusElement = this.selectedOptions_[0] || this.options_[0];
      }
    }
    if (focusElement) {
      this.focusedIndex_ = this.options_.indexOf(focusElement);
      focusElement.tabIndex = 0;
    }
  }

  /**
   * @private
   */
  init_() {
    const options = [].slice.call(this.element.querySelectorAll('[option]'));
    options.forEach(option => {
      option.setAttribute('role', 'option');
      if (option.hasAttribute('disabled')) {
        option.setAttribute('aria-disabled', 'true');
      }
      if (option.hasAttribute('selected')) {
        this.setSelection_(option);
      } else {
        this.clearSelection_(option);
      }
      option.tabIndex = 0;
      this.options_.push(option);
    });
    this.updateFocus_();
    this.setInputs_();
  }

  /**
   * Creates inputs for the currently selected elements and returns a string
   * array of their option values.
   * @note Ignores elements that have `disabled` attribute set.
   * @return {!Array<string>}
   * @private
   */
  setInputs_() {
    const selectedValues = [];
    const elementName = this.element.getAttribute('name');
    if (!elementName || this.isDisabled_) {
      return selectedValues;
    }
    const formId = this.element.getAttribute('form');

    this.inputs_.forEach(input => {
      this.element.removeChild(input);
    });
    this.inputs_ = [];
    const doc = this.win.document;
    const fragment = doc.createDocumentFragment();
    this.selectedOptions_.forEach(option => {
      if (!option.hasAttribute('disabled')) {
        const hidden = doc.createElement('input');
        const value = option.getAttribute('option');
        hidden.setAttribute('type', 'hidden');
        hidden.setAttribute('name', elementName);
        hidden.setAttribute('value', value);
        if (formId) {
          hidden.setAttribute('form', formId);
        }
        this.inputs_.push(hidden);
        fragment.appendChild(hidden);
        selectedValues.push(value);
      }
    });
    this.element.appendChild(fragment);
    return selectedValues;
  }

  /**
   * Handles user selection on an option.
   * @param {!Element} el The element selected.
   */
  onOptionPicked_(el) {
    if (el.hasAttribute('disabled')) {
      return;
    }

    this.mutateElement(() => {
      /** @type {?Array<string>} */
      let selectedValues;
      if (el.hasAttribute('selected')) {
        if (this.isMultiple_) {
          this.clearSelection_(el);
          selectedValues = this.setInputs_();
        }
      } else {
        this.setSelection_(el);
        selectedValues = this.setInputs_();
      }

      // Don't trigger action or update focus if
      // selected values haven't changed.
      if (selectedValues) {
        // Newly picked option should always have focus.
        this.updateFocus_(el);

        // Trigger 'select' event with two data params:
        // 'targetOption' - option value of the selected or deselected element.
        // 'selectedOptions' - array of option values of selected elements.
        const name = 'select';
        const selectEvent =
            createCustomEvent(this.win, `amp-selector.${name}`, {
              targetOption: el.getAttribute('option'),
              selectedOptions: selectedValues,
            });
        this.action_.trigger(this.element, name, selectEvent);
      }
    });
  }

  /**
   * Handles click events for the selectables.
   * @param {!Event} event
   */
  clickHandler_(event) {
    let el = dev().assertElement(event.target);
    if (!el) {
      return;
    }
    if (!el.hasAttribute('option')) {
      el = closestBySelector(el, '[option]');
    }
    if (el) {
      this.onOptionPicked_(el);
    }
  }

  /**
   * Handles keyboard events.
   * @param {!Event} event
   */
  keyDownHandler_(event) {
    const keyCode = event.keyCode;
    switch (keyCode) {
      case Keycodes.LEFT_ARROW: /* fallthrough */
      case Keycodes.UP_ARROW: /* fallthrough */
      case Keycodes.RIGHT_ARROW: /* fallthrough */
      case Keycodes.DOWN_ARROW: /* fallthrough */
        if (this.kbSelectMode_ != KEYBOARD_SELECT_MODES.NONE) {
          this.navigationKeyDownHandler_(event);
        }
        return;
      case Keycodes.ENTER: /* fallthrough */
      case Keycodes.SPACE:
        this.selectionKeyDownHandler_(event);
        return;
    }
  }

  /**
   * Handles keyboard navigation events. Should not be called if
   * keyboard selection is disabled.
   * @param {!Event} event
   */
  navigationKeyDownHandler_(event) {
    const isLtr = this.win.document.body.getAttribute('dir') != 'rtl';
    let dir = 0;
    switch (event.keyCode) {
      case Keycodes.LEFT_ARROW:
        // Left is considered 'previous' in LTR and 'next' in RTL.
        dir = isLtr ? -1 : 1;
        break;
      case Keycodes.UP_ARROW:
        // Up is considered 'previous' in both LTR and RTL.
        dir = -1;
        break;
      case Keycodes.RIGHT_ARROW:
        // Right is considered 'next' in LTR and 'previous' in RTL.
        dir = isLtr ? 1 : -1;
        break;
      case Keycodes.DOWN_ARROW:
        // Down is considered 'next' in both LTR and RTL.
        dir = 1;
        break;
      default:
        return;
    }

    event.preventDefault();

    // Make currently selected option unfocusable
    this.options_[this.focusedIndex_].tabIndex = -1;

    // Change the focus to the next element in the specified direction.
    // The selection should loop around if the user attempts to go one
    // past the beginning or end.
    this.focusedIndex_ = (this.focusedIndex_ + dir) % this.options_.length;
    if (this.focusedIndex_ < 0) {
      this.focusedIndex_ = this.focusedIndex_ + this.options_.length;
    }

    // Focus newly selected option
    const newSelectedOption = this.options_[this.focusedIndex_];
    newSelectedOption.tabIndex = 0;
    tryFocus(newSelectedOption);

    const focusedOption = this.options_[this.focusedIndex_];
    if (this.kbSelectMode_ == KEYBOARD_SELECT_MODES.SELECT) {
      this.onOptionPicked_(focusedOption);
    }
  }

  /**
   * Handles keyboard selection events.
   * @param {!Event} event
   */
  selectionKeyDownHandler_(event) {
    const keyCode = event.keyCode;
    if (keyCode == Keycodes.SPACE || keyCode == Keycodes.ENTER) {
      if (this.options_.includes(event.target)) {
        event.preventDefault();
        const el = dev().assertElement(event.target);
        this.onOptionPicked_(el);
      }
    }
  }

  /**
   * Clears a given element from the list of selected options.
   * @param {!Element} element.
   * @private
   */
  clearSelection_(element) {
    element.removeAttribute('selected');
    element.setAttribute('aria-selected', 'false');
    const selIndex = this.selectedOptions_.indexOf(element);
    if (selIndex !== -1) {
      this.selectedOptions_.splice(selIndex, 1);
    }
  }

  /**
   * Clears all selected options.
   * @private
   */
  clearAllSelections_() {
    while (this.selectedOptions_.length > 0) {
      // Clear selected options for single select.
      const el = this.selectedOptions_.pop();
      this.clearSelection_(el);
    }
  }

  /**
   * Marks a given element as selected and clears the others if required.
   * @param {!Element} element.
   * @private
   */
  setSelection_(element) {
    // Exit if `element` is already selected.
    if (this.selectedOptions_.includes(element)) {
      return;
    }
    if (!this.isMultiple_) {
      this.clearAllSelections_();
    }
    element.setAttribute('selected', '');
    element.setAttribute('aria-selected', 'true');
    this.selectedOptions_.push(element);
  }
}

AMP.registerElement('amp-selector', AmpSelector, CSS);
<|endoftext|>


<commit_before>test(
  'ValueSchemaRawTest',

  [
    'ephox.agar.api.Logger',
    'ephox.agar.api.RawAssertions',
    'ephox.boulder.api.FieldPresence',
    'ephox.boulder.api.FieldSchema',
    'ephox.boulder.api.ValueSchema',
    'ephox.numerosity.api.JSON',
    'ephox.perhaps.Result'
  ],

  function (Logger, RawAssertions, FieldPresence, FieldSchema, ValueSchema, Json, Result) {
    var checkErr = function (label, expectedPart, input, processor) {
      ValueSchema.asRaw(label, processor, input).fold(function (err) {
        var message = ValueSchema.formatError(err);
        RawAssertions.assertEq(label + '. Was looking to see if contained: ' + expectedPart + '.\nWas: ' + message, true, message.indexOf(expectedPart) > -1);
      }, function (val) {
        assert.fail(label + '\nExpected error: ' + expectedPart + '\nWas success(' + Json.stringify(val, null, 2) + ')');
      });
    };

    var check = function (label, input, processor) {
      var actual = ValueSchema.asRawOrDie(label, processor, input);
      RawAssertions.assertEq(label, input, actual);
    };

    check('test.1', 10, ValueSchema.anyValue());

    check('test.2', [ 10, 20, 50 ], ValueSchema.arrOfVal());

    check('test.3', {
      a: 'a',
      b: 'b'
    }, ValueSchema.objOf([
      FieldSchema.strict('a'),
      FieldSchema.strict('b')
    ]));

    check('test.4', {
      urls: [
        { url: 'hi', fresh: 'true' },
        { url: 'hi', fresh: 'true' }
      ]
    }, ValueSchema.objOf([
      FieldSchema.strictArrayOfObj('urls', [
        FieldSchema.strict('url'),
        FieldSchema.defaulted('fresh', '10')
      ])
    ]));

    check('test.5', {
      urls: [ 'dog', 'cat' ]
    }, ValueSchema.objOf([
      FieldSchema.strictArrayOf('urls', ValueSchema.anyValue())
    ]));

    checkErr('test.6 should fail because fields do not both start with f',
      'start-with-f',
      {
        'fieldA': { val: 'a' },
        'cieldB': { val: 'b' }
      }, ValueSchema.setOf(function (key) {
        return key.indexOf('f') > -1 ? Result.value(key) : Result.error('start-with-f error');
      }, ValueSchema.objOf([
        FieldSchema.strict('val')
      ]))
    );

    checkErr('test.6b should fail because values do not contain "val"',
      'val',
      {
        'fieldA': { val2: 'a' },
        'fieldB': { val2: 'b' }
      }, ValueSchema.setOf(Result.value, ValueSchema.objOf([
        FieldSchema.strict('val')
      ]))
    );

    check('test.7',
      {
        'fieldA': { val: 'a' },
        'fieldB': { val: 'b' }
      }, ValueSchema.setOf(Result.value, ValueSchema.objOf([
        FieldSchema.strict('val')
      ]))
    );

    check('test.8',
      {
        prop: {
          'merged': true,
          other: 'yes'
        }
      }, ValueSchema.objOf([
        FieldSchema.field('prop', 'prop', FieldPresence.mergeWith({ merged: true }), ValueSchema.anyValue())
      ])
    );

    var optionValue = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.option('alpha')
    ]), {});
    RawAssertions.assertEq('alpha should be none', true, optionValue.alpha.isNone());

    var optionValue2 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.option('alpha')
    ]), { alpha: 'beta' });
    RawAssertions.assertEq('alpha should be some', true, optionValue2.alpha.isSome());

    var optionValue3 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.field('alpha', 'alpha', FieldPresence.asDefaultedOption('fallback'), ValueSchema.anyValue())
    ]), { alpha: 'beta' });
    RawAssertions.assertEq('fallback.opt: alpha:beta should be some(beta)', 'beta', optionValue3.alpha.getOrDie());

    var optionValue4 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.field('alpha', 'alpha', FieldPresence.asDefaultedOption('fallback'), ValueSchema.anyValue())
    ]), { alpha: true });
    RawAssertions.assertEq('fallback.opt: alpha:true should be some(fallback)', 'fallback', optionValue4.alpha.getOrDie());

    var optionValue5 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.field('alpha', 'alpha', FieldPresence.asDefaultedOption('fallback'), ValueSchema.anyValue())
    ]), {  });
    RawAssertions.assertEq('fallback.opt: no alpha should be none', true, optionValue5.alpha.isNone());

    Logger.sync(
      'Checking choose',
      function () {
        var input = {
          type: 'other',
          houses: '10'
        };

        var output = ValueSchema.asRawOrDie('checking choose', ValueSchema.choose(
          'type',
          {
            'general': [
              FieldSchema.strict('cards')
            ],
            'other': [
              FieldSchema.strict('houses')
            ]
          }
        ), input);

        console.log('output', output);
      }
    );

  }
);<commit_msg>VAN-42: more tests for choose<commit_after>test(
  'ValueSchemaRawTest',

  [
    'ephox.agar.api.Logger',
    'ephox.agar.api.RawAssertions',
    'ephox.boulder.api.FieldPresence',
    'ephox.boulder.api.FieldSchema',
    'ephox.boulder.api.ValueSchema',
    'ephox.numerosity.api.JSON',
    'ephox.perhaps.Result'
  ],

  function (Logger, RawAssertions, FieldPresence, FieldSchema, ValueSchema, Json, Result) {
    var checkErr = function (label, expectedPart, input, processor) {
      ValueSchema.asRaw(label, processor, input).fold(function (err) {
        var message = ValueSchema.formatError(err);
        RawAssertions.assertEq(label + '. Was looking to see if contained: ' + expectedPart + '.\nWas: ' + message, true, message.indexOf(expectedPart) > -1);
      }, function (val) {
        assert.fail(label + '\nExpected error: ' + expectedPart + '\nWas success(' + Json.stringify(val, null, 2) + ')');
      });
    };

    var check = function (label, input, processor) {
      var actual = ValueSchema.asRawOrDie(label, processor, input);
      RawAssertions.assertEq(label, input, actual);
    };

    var checkIs = function (label, expected, input, processor) {
      var actual = ValueSchema.asRawOrDie(label, processor, input);
      RawAssertions.assertEq(label, expected, actual);
    };

    check('test.1', 10, ValueSchema.anyValue());

    check('test.2', [ 10, 20, 50 ], ValueSchema.arrOfVal());

    check('test.3', {
      a: 'a',
      b: 'b'
    }, ValueSchema.objOf([
      FieldSchema.strict('a'),
      FieldSchema.strict('b')
    ]));

    check('test.4', {
      urls: [
        { url: 'hi', fresh: 'true' },
        { url: 'hi', fresh: 'true' }
      ]
    }, ValueSchema.objOf([
      FieldSchema.strictArrayOfObj('urls', [
        FieldSchema.strict('url'),
        FieldSchema.defaulted('fresh', '10')
      ])
    ]));

    check('test.5', {
      urls: [ 'dog', 'cat' ]
    }, ValueSchema.objOf([
      FieldSchema.strictArrayOf('urls', ValueSchema.anyValue())
    ]));

    checkErr('test.6 should fail because fields do not both start with f',
      'start-with-f',
      {
        'fieldA': { val: 'a' },
        'cieldB': { val: 'b' }
      }, ValueSchema.setOf(function (key) {
        return key.indexOf('f') > -1 ? Result.value(key) : Result.error('start-with-f error');
      }, ValueSchema.objOf([
        FieldSchema.strict('val')
      ]))
    );

    checkErr('test.6b should fail because values do not contain "val"',
      'val',
      {
        'fieldA': { val2: 'a' },
        'fieldB': { val2: 'b' }
      }, ValueSchema.setOf(Result.value, ValueSchema.objOf([
        FieldSchema.strict('val')
      ]))
    );

    check('test.7',
      {
        'fieldA': { val: 'a' },
        'fieldB': { val: 'b' }
      }, ValueSchema.setOf(Result.value, ValueSchema.objOf([
        FieldSchema.strict('val')
      ]))
    );

    check('test.8',
      {
        prop: {
          'merged': true,
          other: 'yes'
        }
      }, ValueSchema.objOf([
        FieldSchema.field('prop', 'prop', FieldPresence.mergeWith({ merged: true }), ValueSchema.anyValue())
      ])
    );

    var optionValue = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.option('alpha')
    ]), {});
    RawAssertions.assertEq('alpha should be none', true, optionValue.alpha.isNone());

    var optionValue2 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.option('alpha')
    ]), { alpha: 'beta' });
    RawAssertions.assertEq('alpha should be some', true, optionValue2.alpha.isSome());

    var optionValue3 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.field('alpha', 'alpha', FieldPresence.asDefaultedOption('fallback'), ValueSchema.anyValue())
    ]), { alpha: 'beta' });
    RawAssertions.assertEq('fallback.opt: alpha:beta should be some(beta)', 'beta', optionValue3.alpha.getOrDie());

    var optionValue4 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.field('alpha', 'alpha', FieldPresence.asDefaultedOption('fallback'), ValueSchema.anyValue())
    ]), { alpha: true });
    RawAssertions.assertEq('fallback.opt: alpha:true should be some(fallback)', 'fallback', optionValue4.alpha.getOrDie());

    var optionValue5 = ValueSchema.asRawOrDie('test.option', ValueSchema.objOf([
      FieldSchema.field('alpha', 'alpha', FieldPresence.asDefaultedOption('fallback'), ValueSchema.anyValue())
    ]), {  });
    RawAssertions.assertEq('fallback.opt: no alpha should be none', true, optionValue5.alpha.isNone());

    Logger.sync(
      'Checking choose',
      function () {

        var processor = ValueSchema.choose(
          'type',
          {
            'general': [
              FieldSchema.strict('cards')
            ],
            'other': [
              FieldSchema.strict('houses')
            ]
          }
        );

        checkIs(
          'Checking choose(other)',
          { houses: '10' },
          {
            type: 'other',
            houses: '10'
          },
          processor
        );

        checkErr(
          'Checking choose(other) without everything',
          'Could not find valid *strict* value for "houses"',
          {
            type: 'other',
            house: '10'
          },
          processor
        );

        checkErr(
          'Checking choose(other) without wrong schema',
          'Could not find valid *strict* value for "houses"',
          {
            type: 'other',
            cards: '10'
          },
          processor
        );

        checkIs(
          'Checking choose(general)',
          { cards: '10' },
          {
            type: 'general',
            cards: '10'
          },
          processor
        );

        checkErr(
          'Checking choose(general) without everything',
          'Could not find valid *strict* value for "cards"',
          {
            type: 'general',
            mech: '10'
          },
          processor
        );

        checkErr(
          'Checking choose(general) with wrong schema',
          'Could not find valid *strict* value for "cards"',
          {
            type: 'general',
            houses: '10'
          },
          processor
        );

        checkErr(
          'Checking choose(dog)',
          'chosen schema: "dog" did not exist',
          {
            type: 'dog',
            houses: '10'
          },
          processor
        );

      }
    );

  }
);<|endoftext|>


<commit_before>const assert = require('assert');
const range = require('lodash/range');
const sample = require('lodash/sample');
const shuffle = require('lodash/shuffle');

module.exports.getPrecedingIndices = (cellIndex) => {
	const width = 5;
	const height = 5;
	assert(cellIndex >= 0);
	assert(cellIndex < width * height);

	const x = cellIndex % width;
	const y = Math.floor(cellIndex / width);

	const precedingCells = [];
	if (x - 1 >= 0) {
		precedingCells.push(y * width + (x - 1));
	}
	if (x + 1 < width) {
		precedingCells.push(y * width + (x + 1));
	}
	if (y - 1 >= 0) {
		precedingCells.push((y - 1) * width + x);
	}
	if (y + 1 < height) {
		precedingCells.push((y + 1) * width + x);
	}

	return precedingCells.filter(
		(cell) => ![0, 4, 5, 9, 10, 14, 15, 19, 20, 24].includes(cell),
	);
};

module.exports.generateInput = () => {
	const n = sample(range(1, 26));
	const numbers = shuffle(('1'.repeat(n) + '0'.repeat(26 - n)).split(''));
	const letters = numbers.map((n, i) => String.fromCharCode(i + (n === '1' ? 'A' : 'a').charCodeAt()));

	assert(letters.length === 26);

	return `${letters.join('')}\n`;
};

module.exports.isValidAnswer = (input, output) => {
	const chunks = input.split(/(?=[a-zA-Z])/);

	assert(chunks.length === 26);

	const correctOutput = chunks
		.map((chunk) => (chunk === chunk.toUpperCase() ? 1 : 0))
		.join('');
	const trimmedOutput = output.toString().replace(/\s/g, '');

	console.log('info:', {input, correctOutput, output, trimmedOutput});

	return trimmedOutput === correctOutput;
};
<commit_msg>Implemented generateInput() and isValidAnswer()<commit_after>const assert = require('assert');
const concat = require('lodash/concat');
const includes = require('lodash/includes');
const range = require('lodash/range');
const sample = require('lodash/sample');
const shuffle = require('lodash/shuffle');

module.exports.getPrecedingIndices = (cellIndex) => {
	const width = 5;
	const height = 5;
	assert(cellIndex >= 0);
	assert(cellIndex < width * height);

	const x = cellIndex % width;
	const y = Math.floor(cellIndex / width);

	const precedingCells = [];
	if (x - 1 >= 0) {
		precedingCells.push(y * width + (x - 1));
	}
	if (x + 1 < width) {
		precedingCells.push(y * width + (x + 1));
	}
	if (y - 1 >= 0) {
		precedingCells.push((y - 1) * width + x);
	}
	if (y + 1 < height) {
		precedingCells.push((y + 1) * width + x);
	}

	return precedingCells.filter(
		(cell) => ![0, 4, 5, 9, 10, 14, 15, 19, 20, 24].includes(cell),
	);
};

const ascendingCases = ["0123", "1230", "2301", "3012"];
const descendingCases = ["3210", "0321", "1032", "2103"];
const cases = concat(ascendingCases, descendingCases);

const lineNum = 32;

assert(lineNum > cases.length);

module.exports.generateInput = () => {
	const determinedNumbers = range(cases.length);
	const randomNumbers = range(lineNum - cases.length).map(() => sample(range(cases.length)));
    const numbers = shuffle(concat(determinedNumbers, randomNumbers));
	const lines = numbers.map(n => cases[n]);

	return `${lines.join('\n')}\n`;
};

module.exports.isValidAnswer = (input, output) => {
	const inputLines = input.split("\n");

	assert(inputLines.length === lineNum);

	const correctOutput = inputLines
		.map(line => (includes(ascendingCases, line) ? 1 : 0))
		.join('');
	const trimmedOutput = output.toString().replace(/\s/g, '');

	console.log('info:', {input, correctOutput, output, trimmedOutput});

	return trimmedOutput === correctOutput;
};
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';

import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import MenuItem from '@material-ui/core/MenuItem';
import Paper from '@material-ui/core/Paper';
import Switch from '@material-ui/core/Switch';
import Typography from '@material-ui/core/Typography';

import ChevronRightIcon from '@material-ui/icons/ChevronRight';

import connectComponent from '../../helpers/connect-component';

import StatedMenu from '../shared/stated-menu';

import { updateIsDefaultMailClient } from '../../state/general/actions';

import {
  requestOpenInBrowser,
  requestSetPreference,
  requestSetSystemPreference,
  requestResetPreferences,
  requestClearBrowsingData,
  requestShowRequireRestartDialog,
  requestShowCodeInjectionWindow,
} from '../../senders';

const { remote } = window.require('electron');

const styles = (theme) => ({
  root: {
    padding: theme.spacing.unit * 2,
    background: theme.palette.background.default,
  },
  sectionTitle: {
    paddingLeft: theme.spacing.unit * 2,
  },
  paper: {
    marginTop: theme.spacing.unit * 0.5,
    marginBottom: theme.spacing.unit * 3,
  },
  switchBase: {
    height: 'auto',
  },
});

const appJson = remote.getGlobal('appJson');

const getThemeString = (theme) => {
  if (theme === 'light') return 'Light';
  if (theme === 'dark') return 'Dark';
  return 'Automatic';
};

const getOpenAtLoginString = (openAtLogin) => {
  if (openAtLogin === 'yes-hidden') return 'Yes, but minimized';
  if (openAtLogin === 'yes') return 'Yes';
  return 'No';
};

const Preferences = ({
  askForDownloadPath,
  attachToMenubar,
  autoCheckForUpdates,
  classes,
  cssCodeInjection,
  downloadPath,
  isDefaultMailClient,
  jsCodeInjection,
  navigationBar,
  onUpdateIsDefaultMailClient,
  openAtLogin,
  rememberLastPageVisited,
  shareWorkspaceBrowsingData,
  sidebar,
  spellChecker,
  swipeToNavigate,
  theme,
  unreadCountBadge,
}) => (

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Appearance
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <StatedMenu
          id="theme"
          buttonElement={(
            <ListItem button>
              <ListItemText primary="Theme" secondary={getThemeString(theme)} />
              <ChevronRightIcon color="action" />
            </ListItem>
          )}
        >
          <MenuItem onClick={() => requestSetPreference('theme', 'automatic')}>Automatic</MenuItem>
          <MenuItem onClick={() => requestSetPreference('theme', 'light')}>Light</MenuItem>
          <MenuItem onClick={() => requestSetPreference('theme', 'dark')}>Dark</MenuItem>
        </StatedMenu>
        <Divider />
        <ListItem>
          <ListItemText
            primary="Show sidebar"
            secondary="Sidebar lets you switch easily between workspaces."
          />
          <Switch
            checked={sidebar}
            onChange={(e) => {
              requestSetPreference('sidebar', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText
            primary="Show navigation bar"
            secondary="Navigation bar lets you go back, forward, home and reload."
          />
          <Switch
            checked={navigationBar}
            onChange={(e) => {
              requestSetPreference('navigationBar', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Experience
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem>
          <ListItemText
            primary="Attach to menubar"
          />
          <Switch
            checked={attachToMenubar}
            onChange={(e) => {
              requestSetPreference('attachToMenubar', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        {window.process.platform === 'darwin' && (
          <>
            <ListItem>
              <ListItemText primary="Show unread count badge" />
              <Switch
                checked={unreadCountBadge}
                onChange={(e) => {
                  requestSetPreference('unreadCountBadge', e.target.checked);
                  requestShowRequireRestartDialog();
                }}
                classes={{
                  switchBase: classes.switchBase,
                }}
              />
            </ListItem>
            <Divider />
            <ListItem>
              <ListItemText
                primary="Swipe to navigate"
                secondary={(
                  <>
                    Navigate between pages with 3-finger gestures.

                    To enable it, you also need to change

                      macOS Preferences > Trackpad > More Gestures > Swipe between page

                     to
                    Swipe with three fingers
                     or
                    Swipe with two or three fingers.
                  </>
                )}
              />
              <Switch
                checked={swipeToNavigate}
                onChange={(e) => {
                  requestSetPreference('swipeToNavigate', e.target.checked);
                  requestShowRequireRestartDialog();
                }}
                classes={{
                  switchBase: classes.switchBase,
                }}
              />
            </ListItem>
            <Divider />
          </>
        )}
        <ListItem>
          <ListItemText primary="Use spell checker" />
          <Switch
            checked={spellChecker}
            onChange={(e) => {
              requestSetPreference('spellChecker', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText primary="Automatically check for updates" />
          <Switch
            checked={autoCheckForUpdates}
            onChange={(e) => {
              requestSetPreference('autoCheckForUpdates', e.target.checked);
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Downloads
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem
          button
          onClick={() => {
            remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
              properties: ['openDirectory'],
            }).then((result) => {
              if (!result.canceled && result.filePaths) {
                requestSetPreference('downloadPath', result.filePaths[0]);
              }
            }).catch((err) => {
              console.log(err);
            });
          }}
        >
          <ListItemText
            primary="Download Location"
            secondary={downloadPath}
          />
          <ChevronRightIcon color="action" />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText primary="Ask where to save each file before downloading" />
          <Switch
            checked={askForDownloadPath}
            onChange={(e) => {
              requestSetPreference('askForDownloadPath', e.target.checked);
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Privacy & Security
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem>
          <ListItemText primary="Remember last page visited" />
          <Switch
            checked={rememberLastPageVisited}
            onChange={(e) => {
              requestSetPreference('rememberLastPageVisited', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText primary="Share browsing data betwwen workspaces" />
          <Switch
            checked={shareWorkspaceBrowsingData}
            onChange={(e) => {
              requestSetPreference('shareWorkspaceBrowsingData', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem button onClick={requestClearBrowsingData}>
          <ListItemText primary="Clear browsing data" secondary="Clear cookies, cache, and more" />
          <ChevronRightIcon color="action" />
        </ListItem>
        <Divider />
        <ListItem button onClick={() => requestOpenInBrowser('https://getwebcatalog.com/privacy')}>
          <ListItemText primary="Privacy Policy" />
        </ListItem>
      </List>
    </Paper>

    {appJson.mailtoHandler && appJson.mailtoHandler.length > 0 && (
      <>
        <Typography variant="subtitle2" className={classes.sectionTitle}>
          Default Email Client
        </Typography>
        <Paper className={classes.paper}>
          <List dense>
            {isDefaultMailClient ? (
              <ListItem>
                <ListItemText secondary={`${appJson.name} is your default email client.`} />
              </ListItem>
            ) : (
              <ListItem>
                <ListItemText primary="Default email client" secondary={`Make ${appJson.name} the default email client.`} />
                <Button
                  variant="outlined"
                  size="small"
                  color="default"
                  className={classes.button}
                  onClick={() => {
                    remote.app.setAsDefaultProtocolClient('mailto');
                    onUpdateIsDefaultMailClient(remote.app.isDefaultProtocolClient('mailto'));
                  }}
                >
                  Make default
                </Button>
              </ListItem>
            )}
          </List>
        </Paper>
      </>
    )}

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      System
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <StatedMenu
          id="openAtLogin"
          buttonElement={(
            <ListItem button>
              <ListItemText primary="Open at login" secondary={getOpenAtLoginString(openAtLogin)} />
              <ChevronRightIcon color="action" />
            </ListItem>
          )}
        >
          <MenuItem onClick={() => requestSetSystemPreference('openAtLogin', 'yes')}>Yes</MenuItem>
          <MenuItem onClick={() => requestSetSystemPreference('openAtLogin', 'yes-hidden')}>Yes, but minimized</MenuItem>
          <MenuItem onClick={() => requestSetSystemPreference('openAtLogin', 'no')}>No</MenuItem>
        </StatedMenu>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Developers
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem button onClick={() => requestShowCodeInjectionWindow('js')}>
          <ListItemText primary="JS Code Injection" secondary={jsCodeInjection ? 'Set' : 'Not set'} />
          <ChevronRightIcon color="action" />
        </ListItem>
        <ListItem button onClick={() => requestShowCodeInjectionWindow('css')}>
          <ListItemText primary="CSS Code Injection" secondary={cssCodeInjection ? 'Set' : 'Not set'} />
          <ChevronRightIcon color="action" />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Reset
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem button onClick={requestResetPreferences}>
          <ListItemText primary="Restore preferences to their original defaults" />
          <ChevronRightIcon color="action" />
        </ListItem>
      </List>
    </Paper>

);

Preferences.defaultProps = {
  cssCodeInjection: null,
  jsCodeInjection: null,
};

Preferences.propTypes = {
  askForDownloadPath: PropTypes.bool.isRequired,
  attachToMenubar: PropTypes.bool.isRequired,
  autoCheckForUpdates: PropTypes.bool.isRequired,
  classes: PropTypes.object.isRequired,
  cssCodeInjection: PropTypes.string,
  downloadPath: PropTypes.string.isRequired,
  isDefaultMailClient: PropTypes.bool.isRequired,
  jsCodeInjection: PropTypes.string,
  navigationBar: PropTypes.bool.isRequired,
  onUpdateIsDefaultMailClient: PropTypes.func.isRequired,
  openAtLogin: PropTypes.oneOf(['yes', 'yes-hidden', 'no']).isRequired,
  rememberLastPageVisited: PropTypes.bool.isRequired,
  shareWorkspaceBrowsingData: PropTypes.bool.isRequired,
  sidebar: PropTypes.bool.isRequired,
  spellChecker: PropTypes.bool.isRequired,
  swipeToNavigate: PropTypes.bool.isRequired,
  theme: PropTypes.string.isRequired,
  unreadCountBadge: PropTypes.bool.isRequired,
};

const mapStateToProps = (state) => ({
  askForDownloadPath: state.preferences.askForDownloadPath,
  attachToMenubar: state.preferences.attachToMenubar,
  autoCheckForUpdates: state.preferences.autoCheckForUpdates,
  cssCodeInjection: state.preferences.cssCodeInjection,
  downloadPath: state.preferences.downloadPath,
  isDefaultMailClient: state.general.isDefaultMailClient,
  jsCodeInjection: state.preferences.jsCodeInjection,
  navigationBar: state.preferences.navigationBar,
  openAtLogin: state.systemPreferences.openAtLogin,
  rememberLastPageVisited: state.preferences.rememberLastPageVisited,
  shareWorkspaceBrowsingData: state.preferences.shareWorkspaceBrowsingData,
  sidebar: state.preferences.sidebar,
  spellChecker: state.preferences.spellChecker,
  swipeToNavigate: state.preferences.swipeToNavigate,
  theme: state.preferences.theme,
  unreadCountBadge: state.preferences.unreadCountBadge,
});

const actionCreators = {
  updateIsDefaultMailClient,
};

export default connectComponent(
  Preferences,
  mapStateToProps,
  actionCreators,
  styles,
);
<commit_msg>Fix minor spelling error (#331)<commit_after>import React from 'react';
import PropTypes from 'prop-types';

import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import MenuItem from '@material-ui/core/MenuItem';
import Paper from '@material-ui/core/Paper';
import Switch from '@material-ui/core/Switch';
import Typography from '@material-ui/core/Typography';

import ChevronRightIcon from '@material-ui/icons/ChevronRight';

import connectComponent from '../../helpers/connect-component';

import StatedMenu from '../shared/stated-menu';

import { updateIsDefaultMailClient } from '../../state/general/actions';

import {
  requestOpenInBrowser,
  requestSetPreference,
  requestSetSystemPreference,
  requestResetPreferences,
  requestClearBrowsingData,
  requestShowRequireRestartDialog,
  requestShowCodeInjectionWindow,
} from '../../senders';

const { remote } = window.require('electron');

const styles = (theme) => ({
  root: {
    padding: theme.spacing.unit * 2,
    background: theme.palette.background.default,
  },
  sectionTitle: {
    paddingLeft: theme.spacing.unit * 2,
  },
  paper: {
    marginTop: theme.spacing.unit * 0.5,
    marginBottom: theme.spacing.unit * 3,
  },
  switchBase: {
    height: 'auto',
  },
});

const appJson = remote.getGlobal('appJson');

const getThemeString = (theme) => {
  if (theme === 'light') return 'Light';
  if (theme === 'dark') return 'Dark';
  return 'Automatic';
};

const getOpenAtLoginString = (openAtLogin) => {
  if (openAtLogin === 'yes-hidden') return 'Yes, but minimized';
  if (openAtLogin === 'yes') return 'Yes';
  return 'No';
};

const Preferences = ({
  askForDownloadPath,
  attachToMenubar,
  autoCheckForUpdates,
  classes,
  cssCodeInjection,
  downloadPath,
  isDefaultMailClient,
  jsCodeInjection,
  navigationBar,
  onUpdateIsDefaultMailClient,
  openAtLogin,
  rememberLastPageVisited,
  shareWorkspaceBrowsingData,
  sidebar,
  spellChecker,
  swipeToNavigate,
  theme,
  unreadCountBadge,
}) => (

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Appearance
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <StatedMenu
          id="theme"
          buttonElement={(
            <ListItem button>
              <ListItemText primary="Theme" secondary={getThemeString(theme)} />
              <ChevronRightIcon color="action" />
            </ListItem>
          )}
        >
          <MenuItem onClick={() => requestSetPreference('theme', 'automatic')}>Automatic</MenuItem>
          <MenuItem onClick={() => requestSetPreference('theme', 'light')}>Light</MenuItem>
          <MenuItem onClick={() => requestSetPreference('theme', 'dark')}>Dark</MenuItem>
        </StatedMenu>
        <Divider />
        <ListItem>
          <ListItemText
            primary="Show sidebar"
            secondary="Sidebar lets you switch easily between workspaces."
          />
          <Switch
            checked={sidebar}
            onChange={(e) => {
              requestSetPreference('sidebar', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText
            primary="Show navigation bar"
            secondary="Navigation bar lets you go back, forward, home and reload."
          />
          <Switch
            checked={navigationBar}
            onChange={(e) => {
              requestSetPreference('navigationBar', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Experience
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem>
          <ListItemText
            primary="Attach to menubar"
          />
          <Switch
            checked={attachToMenubar}
            onChange={(e) => {
              requestSetPreference('attachToMenubar', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        {window.process.platform === 'darwin' && (
          <>
            <ListItem>
              <ListItemText primary="Show unread count badge" />
              <Switch
                checked={unreadCountBadge}
                onChange={(e) => {
                  requestSetPreference('unreadCountBadge', e.target.checked);
                  requestShowRequireRestartDialog();
                }}
                classes={{
                  switchBase: classes.switchBase,
                }}
              />
            </ListItem>
            <Divider />
            <ListItem>
              <ListItemText
                primary="Swipe to navigate"
                secondary={(
                  <>
                    Navigate between pages with 3-finger gestures.

                    To enable it, you also need to change

                      macOS Preferences > Trackpad > More Gestures > Swipe between page

                     to
                    Swipe with three fingers
                     or
                    Swipe with two or three fingers.
                  </>
                )}
              />
              <Switch
                checked={swipeToNavigate}
                onChange={(e) => {
                  requestSetPreference('swipeToNavigate', e.target.checked);
                  requestShowRequireRestartDialog();
                }}
                classes={{
                  switchBase: classes.switchBase,
                }}
              />
            </ListItem>
            <Divider />
          </>
        )}
        <ListItem>
          <ListItemText primary="Use spell checker" />
          <Switch
            checked={spellChecker}
            onChange={(e) => {
              requestSetPreference('spellChecker', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText primary="Automatically check for updates" />
          <Switch
            checked={autoCheckForUpdates}
            onChange={(e) => {
              requestSetPreference('autoCheckForUpdates', e.target.checked);
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Downloads
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem
          button
          onClick={() => {
            remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
              properties: ['openDirectory'],
            }).then((result) => {
              if (!result.canceled && result.filePaths) {
                requestSetPreference('downloadPath', result.filePaths[0]);
              }
            }).catch((err) => {
              console.log(err);
            });
          }}
        >
          <ListItemText
            primary="Download Location"
            secondary={downloadPath}
          />
          <ChevronRightIcon color="action" />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText primary="Ask where to save each file before downloading" />
          <Switch
            checked={askForDownloadPath}
            onChange={(e) => {
              requestSetPreference('askForDownloadPath', e.target.checked);
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Privacy & Security
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem>
          <ListItemText primary="Remember last page visited" />
          <Switch
            checked={rememberLastPageVisited}
            onChange={(e) => {
              requestSetPreference('rememberLastPageVisited', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem>
          <ListItemText primary="Share browsing data between workspaces" />
          <Switch
            checked={shareWorkspaceBrowsingData}
            onChange={(e) => {
              requestSetPreference('shareWorkspaceBrowsingData', e.target.checked);
              requestShowRequireRestartDialog();
            }}
            classes={{
              switchBase: classes.switchBase,
            }}
          />
        </ListItem>
        <Divider />
        <ListItem button onClick={requestClearBrowsingData}>
          <ListItemText primary="Clear browsing data" secondary="Clear cookies, cache, and more" />
          <ChevronRightIcon color="action" />
        </ListItem>
        <Divider />
        <ListItem button onClick={() => requestOpenInBrowser('https://getwebcatalog.com/privacy')}>
          <ListItemText primary="Privacy Policy" />
        </ListItem>
      </List>
    </Paper>

    {appJson.mailtoHandler && appJson.mailtoHandler.length > 0 && (
      <>
        <Typography variant="subtitle2" className={classes.sectionTitle}>
          Default Email Client
        </Typography>
        <Paper className={classes.paper}>
          <List dense>
            {isDefaultMailClient ? (
              <ListItem>
                <ListItemText secondary={`${appJson.name} is your default email client.`} />
              </ListItem>
            ) : (
              <ListItem>
                <ListItemText primary="Default email client" secondary={`Make ${appJson.name} the default email client.`} />
                <Button
                  variant="outlined"
                  size="small"
                  color="default"
                  className={classes.button}
                  onClick={() => {
                    remote.app.setAsDefaultProtocolClient('mailto');
                    onUpdateIsDefaultMailClient(remote.app.isDefaultProtocolClient('mailto'));
                  }}
                >
                  Make default
                </Button>
              </ListItem>
            )}
          </List>
        </Paper>
      </>
    )}

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      System
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <StatedMenu
          id="openAtLogin"
          buttonElement={(
            <ListItem button>
              <ListItemText primary="Open at login" secondary={getOpenAtLoginString(openAtLogin)} />
              <ChevronRightIcon color="action" />
            </ListItem>
          )}
        >
          <MenuItem onClick={() => requestSetSystemPreference('openAtLogin', 'yes')}>Yes</MenuItem>
          <MenuItem onClick={() => requestSetSystemPreference('openAtLogin', 'yes-hidden')}>Yes, but minimized</MenuItem>
          <MenuItem onClick={() => requestSetSystemPreference('openAtLogin', 'no')}>No</MenuItem>
        </StatedMenu>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Developers
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem button onClick={() => requestShowCodeInjectionWindow('js')}>
          <ListItemText primary="JS Code Injection" secondary={jsCodeInjection ? 'Set' : 'Not set'} />
          <ChevronRightIcon color="action" />
        </ListItem>
        <ListItem button onClick={() => requestShowCodeInjectionWindow('css')}>
          <ListItemText primary="CSS Code Injection" secondary={cssCodeInjection ? 'Set' : 'Not set'} />
          <ChevronRightIcon color="action" />
        </ListItem>
      </List>
    </Paper>

    <Typography variant="subtitle2" className={classes.sectionTitle}>
      Reset
    </Typography>
    <Paper className={classes.paper}>
      <List dense>
        <ListItem button onClick={requestResetPreferences}>
          <ListItemText primary="Restore preferences to their original defaults" />
          <ChevronRightIcon color="action" />
        </ListItem>
      </List>
    </Paper>

);

Preferences.defaultProps = {
  cssCodeInjection: null,
  jsCodeInjection: null,
};

Preferences.propTypes = {
  askForDownloadPath: PropTypes.bool.isRequired,
  attachToMenubar: PropTypes.bool.isRequired,
  autoCheckForUpdates: PropTypes.bool.isRequired,
  classes: PropTypes.object.isRequired,
  cssCodeInjection: PropTypes.string,
  downloadPath: PropTypes.string.isRequired,
  isDefaultMailClient: PropTypes.bool.isRequired,
  jsCodeInjection: PropTypes.string,
  navigationBar: PropTypes.bool.isRequired,
  onUpdateIsDefaultMailClient: PropTypes.func.isRequired,
  openAtLogin: PropTypes.oneOf(['yes', 'yes-hidden', 'no']).isRequired,
  rememberLastPageVisited: PropTypes.bool.isRequired,
  shareWorkspaceBrowsingData: PropTypes.bool.isRequired,
  sidebar: PropTypes.bool.isRequired,
  spellChecker: PropTypes.bool.isRequired,
  swipeToNavigate: PropTypes.bool.isRequired,
  theme: PropTypes.string.isRequired,
  unreadCountBadge: PropTypes.bool.isRequired,
};

const mapStateToProps = (state) => ({
  askForDownloadPath: state.preferences.askForDownloadPath,
  attachToMenubar: state.preferences.attachToMenubar,
  autoCheckForUpdates: state.preferences.autoCheckForUpdates,
  cssCodeInjection: state.preferences.cssCodeInjection,
  downloadPath: state.preferences.downloadPath,
  isDefaultMailClient: state.general.isDefaultMailClient,
  jsCodeInjection: state.preferences.jsCodeInjection,
  navigationBar: state.preferences.navigationBar,
  openAtLogin: state.systemPreferences.openAtLogin,
  rememberLastPageVisited: state.preferences.rememberLastPageVisited,
  shareWorkspaceBrowsingData: state.preferences.shareWorkspaceBrowsingData,
  sidebar: state.preferences.sidebar,
  spellChecker: state.preferences.spellChecker,
  swipeToNavigate: state.preferences.swipeToNavigate,
  theme: state.preferences.theme,
  unreadCountBadge: state.preferences.unreadCountBadge,
});

const actionCreators = {
  updateIsDefaultMailClient,
};

export default connectComponent(
  Preferences,
  mapStateToProps,
  actionCreators,
  styles,
);
<|endoftext|>