<commit_before>/*jslint browser: true, plusplus: true, indent: 2 */

// This will be removed by uglify, along with the DEBUG code
if (typeof DEBUG === 'undefined') {
  DEBUG = true;
}

// Logic and fallbacks based on the following SO answers:
// - Getting caret position cross browser: http://stackoverflow.com/a/9370239/147507
// - Selection API on non input-text fields: http://stackoverflow.com/a/24247942/147507
// - Set cursor position on input text: http://stackoverflow.com/q/5755826/147507
angular.module('ngPatternRestrict', [])
  .directive('ngPatternRestrict', ['$log', function ($log) {
    'use strict';

    function showDebugInfo() {
      $log.debug("[ngPatternRestrict] " + Array.prototype.join.call(arguments, ' '));
    }

    return {
      restrict: 'A',
      require: "?ngModel",
      compile: function uiPatternRestrictCompile() {
        DEBUG && showDebugInfo("Loaded");

        return function ngPatternRestrictLinking(scope, iElement, iAttrs, ngModelController) {
          var regex, // validation regex object
            oldValue, // keeping track of the previous value of the element
            caretPosition, // keeping track of where the caret is at to avoid jumpiness
            // housekeeping
            initialized = false, // have we initialized our directive yet?
            eventsBound = false, // have we bound our events yet?
            // functions
            getCaretPosition, // function to get the caret position, set in detectGetCaretPositionMethods
            setCaretPosition; // function to set the caret position, set in detectSetCaretPositionMethods

          //-------------------------------------------------------------------
          // caret position
          function getCaretPositionWithInputSelectionStart() {
            return iElement[0].selectionStart; // we need to go under jqlite
          }

          function getCaretPositionWithDocumentSelection() {
            // create a selection range from where we are to the beggining
            // and measure how much we moved
            var range = document.selection.createRange();
            range.moveStart('character', -iElement.val().length);
            return range.text.length;
          }

          function getCaretPositionWithWindowSelection() {
            var s = window.getSelection(),
              originalSelectionLength = String(s).length,
              selectionLength,
              didReachZero = false,
              detectedCaretPosition,
              restorePositionCounter;

            do {
              selectionLength = String(s).length;
              s.modify('extend', 'backward', 'character');
              // we're undoing a selection, and starting a new one towards the beggining of the string
              if (String(s).length === 0) {
                didReachZero = true;
              }
            } while (selectionLength !== String(s).length);

            detectedCaretPosition = didReachZero ? selectionLength : selectionLength - originalSelectionLength;
            s.collapseToStart();

            restorePositionCounter = detectedCaretPosition;
            while (restorePositionCounter-- > 0) {
              s.modify('move', 'forward', 'character');
            }
            while (originalSelectionLength-- > 0) {
              s.modify('extend', 'forward', 'character');
            }

            return detectedCaretPosition;
          }

          function setCaretPositionWithSetSelectionRange(position) {
            iElement[0].setSelectionRange(position, position);
          }

          function setCaretPositionWithCreateTextRange(position) {
            var textRange = iElement[0].createTextRange();
            textRange.collapse(true);
            textRange.moveEnd('character', position);
            textRange.moveStart('character', position);
            textRange.select();
          }

          function setCaretPositionWithWindowSelection(position) {
            var s = window.getSelection(),
              selectionLength;

            do {
              selectionLength = String(s).length;
              s.modify('extend', 'backward', 'line');
            } while (selectionLength !== String(s).length);
            s.collapseToStart();

            while (position--) {
              s.modify('move', 'forward', 'character');
            }
          }

          // HACK: Opera 12 won't give us a wrong validity status although the input is invalid
          // we can select the whole text and check the selection size
          // Congratulations to IE 11 for doing the same but not returning the selection.
          function getValueLengthThroughSelection(input) {
            // only do this on opera, since it'll mess up the caret position
            // and break Firefox functionality
            if (!/Opera/i.test(navigator.userAgent)) {
              return 0;
            }

            input.focus();
            document.execCommand("selectAll");
            var focusNode = window.getSelection().focusNode;
            return (focusNode || {}).selectionStart || 0;
          }

          //-------------------------------------------------------------------
          // event handlers
          function revertToPreviousValue() {
            if (ngModelController) {
              scope.$apply(function () {
                ngModelController.$setViewValue(oldValue);
              });
            }
            iElement.val(oldValue);

            if (!angular.isUndefined(caretPosition)) {
              setCaretPosition(caretPosition);
            }
          }

          function updateCurrentValue(newValue) {
            oldValue = newValue;
            caretPosition = getCaretPosition();
          }

          function genericEventHandler(evt) {
            DEBUG && showDebugInfo("Reacting to event:", evt.type);

            //HACK Chrome returns an empty string as value if user inputs a non-numeric string into a number type input
            // and this may happen with other non-text inputs soon enough. As such, if getting the string only gives us an
            // empty string, we don't have the chance of validating it against a regex. All we can do is assume it's wrong,
            // since the browser is rejecting it either way.
            var newValue = iElement.val(),
              inputValidity = iElement.prop("validity");
            if (newValue === "" && iElement.attr("type") !== "text" && inputValidity && inputValidity.badInput) {
              DEBUG && showDebugInfo("Value cannot be verified. Should be invalid. Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            } else if (newValue === "" && getValueLengthThroughSelection(iElement[0]) !== 0) {
              DEBUG && showDebugInfo("Invalid input. Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            } else if (regex.test(newValue)) {
              DEBUG && showDebugInfo("New value passed validation against", regex, newValue);
              updateCurrentValue(newValue);
            } else {
              DEBUG && showDebugInfo("New value did NOT pass validation against", regex, newValue, "Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            }
          }

          //-------------------------------------------------------------------
          // setup based on attributes
          function tryParseRegex(regexString) {
            try {
              regex = new RegExp(regexString);
            } catch (e) {
              throw "Invalid RegEx string parsed for ngPatternRestrict: " + regexString;
            }
          }

          //-------------------------------------------------------------------
          // setup events
          function bindListeners() {
            if (eventsBound) {
              return;
            }

            iElement.bind('input keyup click', genericEventHandler);

            DEBUG && showDebugInfo("Bound events: input, keyup, click");
          }

          function unbindListeners() {
            if (!eventsBound) {
              return;
            }

            iElement.unbind('input', genericEventHandler);
            //input: HTML5 spec, changes in content

            iElement.unbind('keyup', genericEventHandler);
            //keyup: DOM L3 spec, key released (possibly changing content)

            iElement.unbind('click', genericEventHandler);
            //click: DOM L3 spec, mouse clicked and released (possibly changing content)

            DEBUG && showDebugInfo("Unbound events: input, keyup, click");

            eventsBound = false;
          }

          //-------------------------------------------------------------------
          // initialization
          function readPattern() {
            var entryRegex = !!iAttrs.ngPatternRestrict ? iAttrs.ngPatternRestrict : iAttrs.pattern;
            DEBUG && showDebugInfo("RegEx to use:", entryRegex);
            tryParseRegex(entryRegex);
          }

          function notThrows(testFn, shouldReturnTruthy) {
          	try {
          		return testFn() || !shouldReturnTruthy;
          	} catch (e) {
          		return false;
          	}
          }

          function detectGetCaretPositionMethods() {
            var input = iElement[0];

            // Chrome will throw on input.selectionStart of input type=number
            // See http://stackoverflow.com/a/21959157/147507
            if (notThrows(function () { return input.selectionStart; })) {
              getCaretPosition = getCaretPositionWithInputSelectionStart;
            } else {
              // IE 9- will use document.selection
              // TODO support IE 11+ with document.getSelection()
              if (notThrows(function () { return document.selection; }, true)) {
                getCaretPosition = getCaretPositionWithDocumentSelection;
              } else {
                getCaretPosition = getCaretPositionWithWindowSelection;
              }
            }
          }

          function detectSetCaretPositionMethods() {
            var input = iElement[0];
            if (typeof input.setSelectionRange === 'function') {
              setCaretPosition = setCaretPositionWithSetSelectionRange;
            } else if (typeof input.createTextRange === 'function') {
              setCaretPosition = setCaretPositionWithCreateTextRange;
            } else {
              setCaretPosition = setCaretPositionWithWindowSelection;
            }
          }

          function initialize() {
            if (initialized) {
              return;
            }
            DEBUG && showDebugInfo("Initializing");

            readPattern();

            oldValue = iElement.val();
            if (!oldValue) {
              oldValue = "";
            }
            DEBUG && showDebugInfo("Original value:", oldValue);

            bindListeners();

            detectGetCaretPositionMethods();
            detectSetCaretPositionMethods();

            initialized = true;
          }

          function uninitialize() {
            DEBUG && showDebugInfo("Uninitializing");
            unbindListeners();
          }

          iAttrs.$observe("ngPatternRestrict", readPattern);
          iAttrs.$observe("pattern", readPattern);

          scope.$on("$destroy", uninitialize);

          initialize();
        };
      }
    };
  }]);
<commit_msg>:bug: Added ngModel update to genericEventHandler (fixed IME broken ng-model)<commit_after>/*jslint browser: true, plusplus: true, indent: 2 */

// This will be removed by uglify, along with the DEBUG code
if (typeof DEBUG === 'undefined') {
  DEBUG = true;
}

// Logic and fallbacks based on the following SO answers:
// - Getting caret position cross browser: http://stackoverflow.com/a/9370239/147507
// - Selection API on non input-text fields: http://stackoverflow.com/a/24247942/147507
// - Set cursor position on input text: http://stackoverflow.com/q/5755826/147507
angular.module('ngPatternRestrict', [])
  .directive('ngPatternRestrict', ['$log', function ($log) {
    'use strict';

    function showDebugInfo() {
      $log.debug("[ngPatternRestrict] " + Array.prototype.join.call(arguments, ' '));
    }

    return {
      restrict: 'A',
      require: "?ngModel",
      compile: function uiPatternRestrictCompile() {
        DEBUG && showDebugInfo("Loaded");

        return function ngPatternRestrictLinking(scope, iElement, iAttrs, ngModelController) {
          var regex, // validation regex object
            oldValue, // keeping track of the previous value of the element
            caretPosition, // keeping track of where the caret is at to avoid jumpiness
            // housekeeping
            initialized = false, // have we initialized our directive yet?
            eventsBound = false, // have we bound our events yet?
            // functions
            getCaretPosition, // function to get the caret position, set in detectGetCaretPositionMethods
            setCaretPosition; // function to set the caret position, set in detectSetCaretPositionMethods

          //-------------------------------------------------------------------
          // caret position
          function getCaretPositionWithInputSelectionStart() {
            return iElement[0].selectionStart; // we need to go under jqlite
          }

          function getCaretPositionWithDocumentSelection() {
            // create a selection range from where we are to the beggining
            // and measure how much we moved
            var range = document.selection.createRange();
            range.moveStart('character', -iElement.val().length);
            return range.text.length;
          }

          function getCaretPositionWithWindowSelection() {
            var s = window.getSelection(),
              originalSelectionLength = String(s).length,
              selectionLength,
              didReachZero = false,
              detectedCaretPosition,
              restorePositionCounter;

            do {
              selectionLength = String(s).length;
              s.modify('extend', 'backward', 'character');
              // we're undoing a selection, and starting a new one towards the beggining of the string
              if (String(s).length === 0) {
                didReachZero = true;
              }
            } while (selectionLength !== String(s).length);

            detectedCaretPosition = didReachZero ? selectionLength : selectionLength - originalSelectionLength;
            s.collapseToStart();

            restorePositionCounter = detectedCaretPosition;
            while (restorePositionCounter-- > 0) {
              s.modify('move', 'forward', 'character');
            }
            while (originalSelectionLength-- > 0) {
              s.modify('extend', 'forward', 'character');
            }

            return detectedCaretPosition;
          }

          function setCaretPositionWithSetSelectionRange(position) {
            iElement[0].setSelectionRange(position, position);
          }

          function setCaretPositionWithCreateTextRange(position) {
            var textRange = iElement[0].createTextRange();
            textRange.collapse(true);
            textRange.moveEnd('character', position);
            textRange.moveStart('character', position);
            textRange.select();
          }

          function setCaretPositionWithWindowSelection(position) {
            var s = window.getSelection(),
              selectionLength;

            do {
              selectionLength = String(s).length;
              s.modify('extend', 'backward', 'line');
            } while (selectionLength !== String(s).length);
            s.collapseToStart();

            while (position--) {
              s.modify('move', 'forward', 'character');
            }
          }

          // HACK: Opera 12 won't give us a wrong validity status although the input is invalid
          // we can select the whole text and check the selection size
          // Congratulations to IE 11 for doing the same but not returning the selection.
          function getValueLengthThroughSelection(input) {
            // only do this on opera, since it'll mess up the caret position
            // and break Firefox functionality
            if (!/Opera/i.test(navigator.userAgent)) {
              return 0;
            }

            input.focus();
            document.execCommand("selectAll");
            var focusNode = window.getSelection().focusNode;
            return (focusNode || {}).selectionStart || 0;
          }

          //-------------------------------------------------------------------
          // event handlers
          function revertToPreviousValue() {
            iElement.val(oldValue);

            if (!angular.isUndefined(caretPosition)) {
              setCaretPosition(caretPosition);
            }
          }

          function updateCurrentValue(newValue) {
            oldValue = newValue;
            caretPosition = getCaretPosition();
          }

          function genericEventHandler(evt) {
            DEBUG && showDebugInfo("Reacting to event:", evt.type);

            //HACK Chrome returns an empty string as value if user inputs a non-numeric string into a number type input
            // and this may happen with other non-text inputs soon enough. As such, if getting the string only gives us an
            // empty string, we don't have the chance of validating it against a regex. All we can do is assume it's wrong,
            // since the browser is rejecting it either way.
            var newValue = iElement.val(),
              inputValidity = iElement.prop("validity");
            if (newValue === "" && iElement.attr("type") !== "text" && inputValidity && inputValidity.badInput) {
              DEBUG && showDebugInfo("Value cannot be verified. Should be invalid. Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            } else if (newValue === "" && getValueLengthThroughSelection(iElement[0]) !== 0) {
              DEBUG && showDebugInfo("Invalid input. Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            } else if (regex.test(newValue)) {
              DEBUG && showDebugInfo("New value passed validation against", regex, newValue);
              updateCurrentValue(newValue);
            } else {
              DEBUG && showDebugInfo("New value did NOT pass validation against", regex, newValue, "Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            }

            // make sure the model is consistent with last approach
            // needed even when we don't change what has been input -- see https://github.com/AlphaGit/ng-pattern-restrict/pull/43
            if (ngModelController) {
              scope.$apply(function () {
                ngModelController.$setViewValue(oldValue);
              });
            }
          }

          //-------------------------------------------------------------------
          // setup based on attributes
          function tryParseRegex(regexString) {
            try {
              regex = new RegExp(regexString);
            } catch (e) {
              throw "Invalid RegEx string parsed for ngPatternRestrict: " + regexString;
            }
          }

          //-------------------------------------------------------------------
          // setup events
          function bindListeners() {
            if (eventsBound) {
              return;
            }

            iElement.bind('input keyup click', genericEventHandler);

            DEBUG && showDebugInfo("Bound events: input, keyup, click");
          }

          function unbindListeners() {
            if (!eventsBound) {
              return;
            }

            iElement.unbind('input', genericEventHandler);
            //input: HTML5 spec, changes in content

            iElement.unbind('keyup', genericEventHandler);
            //keyup: DOM L3 spec, key released (possibly changing content)

            iElement.unbind('click', genericEventHandler);
            //click: DOM L3 spec, mouse clicked and released (possibly changing content)

            DEBUG && showDebugInfo("Unbound events: input, keyup, click");

            eventsBound = false;
          }

          //-------------------------------------------------------------------
          // initialization
          function readPattern() {
            var entryRegex = !!iAttrs.ngPatternRestrict ? iAttrs.ngPatternRestrict : iAttrs.pattern;
            DEBUG && showDebugInfo("RegEx to use:", entryRegex);
            tryParseRegex(entryRegex);
          }

          function notThrows(testFn, shouldReturnTruthy) {
          	try {
          		return testFn() || !shouldReturnTruthy;
          	} catch (e) {
          		return false;
          	}
          }

          function detectGetCaretPositionMethods() {
            var input = iElement[0];

            // Chrome will throw on input.selectionStart of input type=number
            // See http://stackoverflow.com/a/21959157/147507
            if (notThrows(function () { return input.selectionStart; })) {
              getCaretPosition = getCaretPositionWithInputSelectionStart;
            } else {
              // IE 9- will use document.selection
              // TODO support IE 11+ with document.getSelection()
              if (notThrows(function () { return document.selection; }, true)) {
                getCaretPosition = getCaretPositionWithDocumentSelection;
              } else {
                getCaretPosition = getCaretPositionWithWindowSelection;
              }
            }
          }

          function detectSetCaretPositionMethods() {
            var input = iElement[0];
            if (typeof input.setSelectionRange === 'function') {
              setCaretPosition = setCaretPositionWithSetSelectionRange;
            } else if (typeof input.createTextRange === 'function') {
              setCaretPosition = setCaretPositionWithCreateTextRange;
            } else {
              setCaretPosition = setCaretPositionWithWindowSelection;
            }
          }

          function initialize() {
            if (initialized) {
              return;
            }
            DEBUG && showDebugInfo("Initializing");

            readPattern();

            oldValue = iElement.val();
            if (!oldValue) {
              oldValue = "";
            }
            DEBUG && showDebugInfo("Original value:", oldValue);

            bindListeners();

            detectGetCaretPositionMethods();
            detectSetCaretPositionMethods();

            initialized = true;
          }

          function uninitialize() {
            DEBUG && showDebugInfo("Uninitializing");
            unbindListeners();
          }

          iAttrs.$observe("ngPatternRestrict", readPattern);
          iAttrs.$observe("pattern", readPattern);

          scope.$on("$destroy", uninitialize);

          initialize();
        };
      }
    };
  }]);
<|endoftext|>


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

  /**
   *  Use a RegExp to check if the `md-colors="<expression>"` is static string
   *  or one that should be observed and dynamically interpolated.
   */
  var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/;
  var colorPalettes = null;

  /**
   * @ngdoc module
   * @name material.components.colors
   *
   * @description
   * Define $mdColors service and a `md-colors=""` attribute directive
   */
  angular
    .module('material.components.colors', ['material.core'])
    .directive('mdColors', MdColorsDirective)
    .service('$mdColors', MdColorsService);

  /**
   * @ngdoc service
   * @name $mdColors
   * @module material.components.colors
   *
   * @description
   * With only defining themes, one couldn't get non ngMaterial elements colored with Material colors,
   * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply
   * those values to element as CSS property values.
   *
   *  @usage
   *  <hljs lang="js">
   *    angular.controller('myCtrl', function ($mdColors) {
   *      var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
   *      ...
   *    });
   *  </hljs>
   *
   */
  function MdColorsService($mdTheming, $mdUtil, $log) {
    colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES);

    // Publish service instance
    return {
      applyThemeColors: applyThemeColors,
      getThemeColor: getThemeColor,
      hasTheme: hasTheme
    };

    // ********************************************
    // Internal Methods
    // ********************************************

    /**
     * @ngdoc method
     * @name $mdColors#applyThemeColors
     *
     * @description
     * Gets a color json object, keys are css properties and values are string of the wanted color
     * Then calculate the rgba() values based on the theme color parts
     *
     * @param {DOMElement} element the element to apply the styles on.
     * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color,
     * for example: `{color: 'red-A200-0.3'}`.
     *
     * @usage
     * <hljs lang="js">
     *   app.directive('myDirective', function($mdColors) {
     *     return {
     *       ...
     *       link: function (scope, elem) {
     *         $mdColors.applyThemeColors(elem, {color: 'red'});
     *       }
     *    }
     *   });
     * </hljs>
     */
    function applyThemeColors(element, colorExpression) {
      try {
        if (colorExpression) {
          // Assign the calculate RGBA color values directly as inline CSS
          element.css(interpolateColors(colorExpression));
        }
      } catch (e) {
        $log.error(e.message);
      }

    }

    /**
     * @ngdoc method
     * @name $mdColors#getThemeColor
     *
     * @description
     * Get parsed color from expression
     *
     * @param {string} expression string of a color expression (for instance `'red-700-0.8'`)
     *
     * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`)
     *
     * @usage
     *  <hljs lang="js">
     *    angular.controller('myCtrl', function ($mdColors) {
     *      var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
     *      ...
     *    });
     *  </hljs>
     */
    function getThemeColor(expression) {
      var color = extractColorOptions(expression);

      return parseColor(color);
    }

    /**
     * Return the parsed color
     * @param color hashmap of color definitions
     * @param contrast whether use contrast color for foreground
     * @returns rgba color string
     */
    function parseColor(color, contrast) {
      contrast = contrast || false;
      var rgbValues = $mdTheming.PALETTES[color.palette][color.hue];

      rgbValues = contrast ? rgbValues.contrast : rgbValues.value;

      return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})',
        [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity]
      );
    }

    /**
     * Convert the color expression into an object with scope-interpolated values
     * Then calculate the rgba() values based on the theme color parts
     *
     * @results Hashmap of CSS properties with associated `rgba( )` string vales
     *
     *
     */
    function interpolateColors(themeColors) {
      var rgbColors = {};

      var hasColorProperty = themeColors.hasOwnProperty('color');

      angular.forEach(themeColors, function (value, key) {
        var color = extractColorOptions(value);
        var hasBackground = key.indexOf('background') > -1;

        rgbColors[key] = parseColor(color);
        if (hasBackground && !hasColorProperty) {
          rgbColors.color = parseColor(color, true);
        }
      });

      return rgbColors;
    }

    /**
     * Check if expression has defined theme
     * e.g.
     * 'myTheme-primary' => true
     * 'red-800' => false
     */
    function hasTheme(expression) {
      return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]);
    }

    /**
     * For the evaluated expression, extract the color parts into a hash map
     */
    function extractColorOptions(expression) {
      var parts = expression.split('-');
      var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]);
      var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme();

      return {
        theme: theme,
        palette: extractPalette(parts, theme),
        hue: extractHue(parts, theme),
        opacity: parts[2] || 1
      };
    }

    /**
     * Calculate the theme palette name
     */
    function extractPalette(parts, theme) {
      // If the next section is one of the palettes we assume it's a two word palette
      // Two word palette can be also written in camelCase, forming camelCase to dash-case

      var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1;
      var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();

      if (isTwoWord)  palette = parts[0] + '-' + parts.splice(1, 1);

      if (colorPalettes.indexOf(palette) === -1) {
        // If the palette is not in the palette list it's one of primary/accent/warn/background
        var scheme = $mdTheming.THEMES[theme].colors[palette];
        if (!scheme) {
          throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette}));
        }
        palette = scheme.name;
      }

      return palette;
    }

    function extractHue(parts, theme) {
      var themeColors = $mdTheming.THEMES[theme].colors;

      if (parts[1] === 'hue') {
        var hueNumber = parseInt(parts.splice(2, 1)[0], 10);

        if (hueNumber < 1 || hueNumber > 3) {
          throw new Error($mdUtil.supplant('mdColors: \'hue-{hueNumber}\' is not a valid hue, can be only \'hue-1\', \'hue-2\' and \'hue-3\'', {hueNumber: hueNumber}));
        }
        parts[1] = 'hue-' + hueNumber;

        if (!(parts[0] in themeColors)) {
          throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', {
            availableThemes: Object.keys(themeColors).join(', '),
            usedTheme: parts[0]
          }));
        }

        return themeColors[parts[0]].hues[parts[1]];
      }

      return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default'];
    }
  }

  /**
   * @ngdoc directive
   * @name mdColors
   * @module material.components.colors
   *
   * @restrict A
   *
   * @description
   * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values.
   *
   *   The format will be similar to our color defining in the scss files:
   *
   *   ## `[?theme]-[palette]-[?hue]-[?opacity]`
   *   - [theme]    - default value is the default theme
   *   - [palette]  - can be either palette name or primary/accent/warn/background
   *   - [hue]      - default is 500 (hue-x can be used with primary/accent/warn/background)
   *   - [opacity]  - default is 1
   *
   *   > `?` indicates optional parameter
   *
   * @usage
   * <hljs lang="html">
   *

   *

   *       Color demo
   *

   *

   * </hljs>
   *
   * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation
   * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression
   * to indicate a one-time data binding.
   * <hljs lang="html">
   *   <md-card md-colors="::{background: '{{theme}}-primary-700'}">
   *   </md-card>
   * </hljs>
   *
   */
  function MdColorsDirective($mdColors, $mdUtil, $log, $parse) {
    return {
      restrict: 'A',
      require: ['^?mdTheme'],
      compile: function (tElem, tAttrs) {
        var shouldWatch = shouldColorsWatch();

        return function (scope, element, attrs, ctrl) {
          var mdThemeController = ctrl[0];

          var lastColors = {};

          var parseColors = function (theme) {
            if (typeof theme !== 'string') {
              theme = '';
            }

            if (!attrs.mdColors) {
              attrs.mdColors = '{}';
            }

            /**
             * Json.parse() does not work because the keys are not quoted;
             * use $parse to convert to a hash map
             */
            var colors = $parse(attrs.mdColors)(scope);

            /**
             * If mdTheme is defined up the DOM tree
             * we add mdTheme theme to colors who doesn't specified a theme
             *
             * # example
             * <hljs lang="html">
             *

             *

             *       Color demo
             *

             *

             * </hljs>
             *
             * 'primary-600' will be 'myTheme-primary-600',
             * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix
             */
            if (mdThemeController) {
              Object.keys(colors).forEach(function (prop) {
                var color = colors[prop];
                if (!$mdColors.hasTheme(color)) {
                  colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color;
                }
              });
            }

            cleanElement(colors);

            return colors;
          };

          var cleanElement = function (colors) {
            if (!angular.equals(colors, lastColors)) {
              var keys = Object.keys(lastColors);

              if (lastColors.background && !keys.color) {
                keys.push('color');
              }

              keys.forEach(function (key) {
                element.css(key, '');
              });
            }

            lastColors = colors;
          };

          /**
           * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes
           */
          var unregisterChanges = angular.noop;

          if (mdThemeController) {
            unregisterChanges = mdThemeController.registerChanges(function (theme) {
              $mdColors.applyThemeColors(element, parseColors(theme));
            });
          }

          scope.$on('$destroy', function () {
            unregisterChanges();
          });

          try {
            if (shouldWatch) {
              scope.$watch(parseColors, angular.bind(this,
                $mdColors.applyThemeColors, element
              ), true);
            }
            else {
              $mdColors.applyThemeColors(element, parseColors());
            }

          }
          catch (e) {
            $log.error(e.message);
          }

        };

        function shouldColorsWatch() {
          // Simulate 1x binding and mark mdColorsWatch == false
          var rawColorExpression = tAttrs.mdColors;
          var bindOnce = rawColorExpression.indexOf('::') > -1;
          var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors);

          // Remove it for the postLink...
          tAttrs.mdColors = rawColorExpression.replace('::', '');

          var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch);

          return (bindOnce || isStatic) ? false :
            hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true;
        }
      }
    };

  }

})();
<commit_msg>docs(md-colors): fix md-colors use of border-color key (#9337)<commit_after>(function () {
  "use strict";

  /**
   *  Use a RegExp to check if the `md-colors="<expression>"` is static string
   *  or one that should be observed and dynamically interpolated.
   */
  var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/;
  var colorPalettes = null;

  /**
   * @ngdoc module
   * @name material.components.colors
   *
   * @description
   * Define $mdColors service and a `md-colors=""` attribute directive
   */
  angular
    .module('material.components.colors', ['material.core'])
    .directive('mdColors', MdColorsDirective)
    .service('$mdColors', MdColorsService);

  /**
   * @ngdoc service
   * @name $mdColors
   * @module material.components.colors
   *
   * @description
   * With only defining themes, one couldn't get non ngMaterial elements colored with Material colors,
   * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply
   * those values to element as CSS property values.
   *
   *  @usage
   *  <hljs lang="js">
   *    angular.controller('myCtrl', function ($mdColors) {
   *      var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
   *      ...
   *    });
   *  </hljs>
   *
   */
  function MdColorsService($mdTheming, $mdUtil, $log) {
    colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES);

    // Publish service instance
    return {
      applyThemeColors: applyThemeColors,
      getThemeColor: getThemeColor,
      hasTheme: hasTheme
    };

    // ********************************************
    // Internal Methods
    // ********************************************

    /**
     * @ngdoc method
     * @name $mdColors#applyThemeColors
     *
     * @description
     * Gets a color json object, keys are css properties and values are string of the wanted color
     * Then calculate the rgba() values based on the theme color parts
     *
     * @param {DOMElement} element the element to apply the styles on.
     * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color,
     * for example: `{color: 'red-A200-0.3'}`.
     *
     * @usage
     * <hljs lang="js">
     *   app.directive('myDirective', function($mdColors) {
     *     return {
     *       ...
     *       link: function (scope, elem) {
     *         $mdColors.applyThemeColors(elem, {color: 'red'});
     *       }
     *    }
     *   });
     * </hljs>
     */
    function applyThemeColors(element, colorExpression) {
      try {
        if (colorExpression) {
          // Assign the calculate RGBA color values directly as inline CSS
          element.css(interpolateColors(colorExpression));
        }
      } catch (e) {
        $log.error(e.message);
      }

    }

    /**
     * @ngdoc method
     * @name $mdColors#getThemeColor
     *
     * @description
     * Get parsed color from expression
     *
     * @param {string} expression string of a color expression (for instance `'red-700-0.8'`)
     *
     * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`)
     *
     * @usage
     *  <hljs lang="js">
     *    angular.controller('myCtrl', function ($mdColors) {
     *      var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
     *      ...
     *    });
     *  </hljs>
     */
    function getThemeColor(expression) {
      var color = extractColorOptions(expression);

      return parseColor(color);
    }

    /**
     * Return the parsed color
     * @param color hashmap of color definitions
     * @param contrast whether use contrast color for foreground
     * @returns rgba color string
     */
    function parseColor(color, contrast) {
      contrast = contrast || false;
      var rgbValues = $mdTheming.PALETTES[color.palette][color.hue];

      rgbValues = contrast ? rgbValues.contrast : rgbValues.value;

      return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})',
        [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity]
      );
    }

    /**
     * Convert the color expression into an object with scope-interpolated values
     * Then calculate the rgba() values based on the theme color parts
     *
     * @results Hashmap of CSS properties with associated `rgba( )` string vales
     *
     *
     */
    function interpolateColors(themeColors) {
      var rgbColors = {};

      var hasColorProperty = themeColors.hasOwnProperty('color');

      angular.forEach(themeColors, function (value, key) {
        var color = extractColorOptions(value);
        var hasBackground = key.indexOf('background') > -1;

        rgbColors[key] = parseColor(color);
        if (hasBackground && !hasColorProperty) {
          rgbColors.color = parseColor(color, true);
        }
      });

      return rgbColors;
    }

    /**
     * Check if expression has defined theme
     * e.g.
     * 'myTheme-primary' => true
     * 'red-800' => false
     */
    function hasTheme(expression) {
      return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]);
    }

    /**
     * For the evaluated expression, extract the color parts into a hash map
     */
    function extractColorOptions(expression) {
      var parts = expression.split('-');
      var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]);
      var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme();

      return {
        theme: theme,
        palette: extractPalette(parts, theme),
        hue: extractHue(parts, theme),
        opacity: parts[2] || 1
      };
    }

    /**
     * Calculate the theme palette name
     */
    function extractPalette(parts, theme) {
      // If the next section is one of the palettes we assume it's a two word palette
      // Two word palette can be also written in camelCase, forming camelCase to dash-case

      var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1;
      var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();

      if (isTwoWord)  palette = parts[0] + '-' + parts.splice(1, 1);

      if (colorPalettes.indexOf(palette) === -1) {
        // If the palette is not in the palette list it's one of primary/accent/warn/background
        var scheme = $mdTheming.THEMES[theme].colors[palette];
        if (!scheme) {
          throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette}));
        }
        palette = scheme.name;
      }

      return palette;
    }

    function extractHue(parts, theme) {
      var themeColors = $mdTheming.THEMES[theme].colors;

      if (parts[1] === 'hue') {
        var hueNumber = parseInt(parts.splice(2, 1)[0], 10);

        if (hueNumber < 1 || hueNumber > 3) {
          throw new Error($mdUtil.supplant('mdColors: \'hue-{hueNumber}\' is not a valid hue, can be only \'hue-1\', \'hue-2\' and \'hue-3\'', {hueNumber: hueNumber}));
        }
        parts[1] = 'hue-' + hueNumber;

        if (!(parts[0] in themeColors)) {
          throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', {
            availableThemes: Object.keys(themeColors).join(', '),
            usedTheme: parts[0]
          }));
        }

        return themeColors[parts[0]].hues[parts[1]];
      }

      return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default'];
    }
  }

  /**
   * @ngdoc directive
   * @name mdColors
   * @module material.components.colors
   *
   * @restrict A
   *
   * @description
   * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values.
   *
   *   The format will be similar to our color defining in the scss files:
   *
   *   ## `[?theme]-[palette]-[?hue]-[?opacity]`
   *   - [theme]    - default value is the default theme
   *   - [palette]  - can be either palette name or primary/accent/warn/background
   *   - [hue]      - default is 500 (hue-x can be used with primary/accent/warn/background)
   *   - [opacity]  - default is 1
   *
   *   > `?` indicates optional parameter
   *
   * @usage
   * <hljs lang="html">
   *

   *

   *       Color demo
   *

   *

   * </hljs>
   *
   * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation
   * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression
   * to indicate a one-time data binding.
   * <hljs lang="html">
   *   <md-card md-colors="::{background: '{{theme}}-primary-700'}">
   *   </md-card>
   * </hljs>
   *
   */
  function MdColorsDirective($mdColors, $mdUtil, $log, $parse) {
    return {
      restrict: 'A',
      require: ['^?mdTheme'],
      compile: function (tElem, tAttrs) {
        var shouldWatch = shouldColorsWatch();

        return function (scope, element, attrs, ctrl) {
          var mdThemeController = ctrl[0];

          var lastColors = {};

          var parseColors = function (theme) {
            if (typeof theme !== 'string') {
              theme = '';
            }

            if (!attrs.mdColors) {
              attrs.mdColors = '{}';
            }

            /**
             * Json.parse() does not work because the keys are not quoted;
             * use $parse to convert to a hash map
             */
            var colors = $parse(attrs.mdColors)(scope);

            /**
             * If mdTheme is defined up the DOM tree
             * we add mdTheme theme to colors who doesn't specified a theme
             *
             * # example
             * <hljs lang="html">
             *

             *

             *       Color demo
             *

             *

             * </hljs>
             *
             * 'primary-600' will be 'myTheme-primary-600',
             * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix
             */
            if (mdThemeController) {
              Object.keys(colors).forEach(function (prop) {
                var color = colors[prop];
                if (!$mdColors.hasTheme(color)) {
                  colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color;
                }
              });
            }

            cleanElement(colors);

            return colors;
          };

          var cleanElement = function (colors) {
            if (!angular.equals(colors, lastColors)) {
              var keys = Object.keys(lastColors);

              if (lastColors.background && !keys.color) {
                keys.push('color');
              }

              keys.forEach(function (key) {
                element.css(key, '');
              });
            }

            lastColors = colors;
          };

          /**
           * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes
           */
          var unregisterChanges = angular.noop;

          if (mdThemeController) {
            unregisterChanges = mdThemeController.registerChanges(function (theme) {
              $mdColors.applyThemeColors(element, parseColors(theme));
            });
          }

          scope.$on('$destroy', function () {
            unregisterChanges();
          });

          try {
            if (shouldWatch) {
              scope.$watch(parseColors, angular.bind(this,
                $mdColors.applyThemeColors, element
              ), true);
            }
            else {
              $mdColors.applyThemeColors(element, parseColors());
            }

          }
          catch (e) {
            $log.error(e.message);
          }

        };

        function shouldColorsWatch() {
          // Simulate 1x binding and mark mdColorsWatch == false
          var rawColorExpression = tAttrs.mdColors;
          var bindOnce = rawColorExpression.indexOf('::') > -1;
          var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors);

          // Remove it for the postLink...
          tAttrs.mdColors = rawColorExpression.replace('::', '');

          var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch);

          return (bindOnce || isStatic) ? false :
            hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true;
        }
      }
    };

  }

})();
<|endoftext|>


<commit_before>/* -*- mode: javascript; c-basic-offset: 4; indent-tabs-mode: nil -*- */

//
// Dalliance Genome Explorer
// (c) Thomas Down 2006-2010
//
// domui.js: SVG UI components
//

"use strict";

if (typeof(require) !== 'undefined') {
    var browser = require('./cbrowser');
    var Browser = browser.Browser;

    var utils = require('./utils');
    var makeElement = utils.makeElement;
    var removeChildren = utils.removeChildren;
}

Browser.prototype.removeAllPopups = function() {
    for (var pi = 0; pi < this.popups.length; ++pi) {
        var popup = this.popups[pi];
        if (popup.onClose) {
            try {
                popup.onClose();
            } catch (ex) {
                console.log(ex);
            }
        }
        this.hPopupHolder.removeChild(popup.component);
    }
    this.popups = [];
    removeChildren(this.hPopupHolder);
    removeChildren(this.popupHolder);
}

Browser.prototype.makeTooltip = function(ele, text)
{
    ele.title = text;
}

Browser.prototype.popit = function(ev, name, ele, opts)
{
    var thisB = this;
    if (!opts)
        opts = {};
    if (!ev)
        ev = {};

    var width = opts.width || 200;

    var mx, my;

    if (ev.clientX) {
        var mx =  ev.clientX, my = ev.clientY;
    } else {
        mx = 500; my= 50;
    }
    mx +=  document.documentElement.scrollLeft || document.body.scrollLeft;
    my +=  document.documentElement.scrollTop || document.body.scrollTop;
    var winWidth = window.innerWidth;

    var top = my;
    var left = Math.min(mx - (width/2) - 4, (winWidth - width - 30));

    var popup = makeElement('div');
    popup.className = 'popover fade ' + (ev.clientX ? 'bottom ' : '') + 'in';
    popup.style.display = 'block';
    popup.style.position = 'absolute';
    popup.style.top = '' + top + 'px';
    popup.style.left = '' + left + 'px';
    popup.style.width = width + 'px';
    if (width > 276) {
        // HACK Bootstrappification...
        popup.style.maxWidth = width + 'px';
    }

    popup.appendChild(makeElement('div', null, {className: 'arrow'}));

    if (name) {
        var closeButton = makeElement('button', '', {className: 'close'});
        closeButton.innerHTML = '×'

        closeButton.addEventListener('mouseover', function(ev) {
            closeButton.style.color = 'red';
        }, false);
        closeButton.addEventListener('mouseout', function(ev) {
            closeButton.style.color = 'black';
        }, false);
        closeButton.addEventListener('click', function(ev) {
            ev.preventDefault(); ev.stopPropagation();
            thisB.removeAllPopups();
        }, false);
        var tbar = makeElement('h4', [makeElement('span', name, null, {maxWidth: '200px'}), closeButton], {/*className: 'popover-title' */}, {paddingLeft: '10px', paddingRight: '10px'});

        var dragOX, dragOY;
        var moveHandler, upHandler;
        moveHandler = function(ev) {
            ev.stopPropagation(); ev.preventDefault();
            left = left + (ev.clientX - dragOX);
            if (left < 8) {
                left = 8;
            } if (left > (winWidth - width - 32)) {
                left = (winWidth - width - 26);
            }
            top = top + (ev.clientY - dragOY);
            top = Math.max(10, top);
            popup.style.top = '' + top + 'px';
            popup.style.left = '' + Math.min(left, (winWidth - width - 10)) + 'px';
            dragOX = ev.clientX; dragOY = ev.clientY;
        }
        upHandler = function(ev) {
            ev.stopPropagation(); ev.preventDefault();
            window.removeEventListener('mousemove', moveHandler, false);
            window.removeEventListener('mouseup', upHandler, false);
        }
        tbar.addEventListener('mousedown', function(ev) {
            ev.preventDefault(); ev.stopPropagation();
            dragOX = ev.clientX; dragOY = ev.clientY;
            window.addEventListener('mousemove', moveHandler, false);
            window.addEventListener('mouseup', upHandler, false);
        }, false);

        popup.appendChild(tbar);
    }

    popup.appendChild(makeElement('div', ele, {className: 'popover-content'}, {
        padding: '0px'
    }));
    this.hPopupHolder.appendChild(popup);

    this.popups.push({
        component: popup,
        onClose: opts.onClose
    });
}

function makeTreeTableSection(title, content, visible) {
    var ttButton = makeElement('i');
    function update() {
        if (visible) {
            ttButton.className = 'fa fa-caret-down';
            content.style.display = 'table';
        } else {
            ttButton.className = 'fa fa-caret-right';
            content.style.display = 'none';
        }
    }
    update();

    ttButton.addEventListener('click', function(ev) {
        ev.preventDefault(); ev.stopPropagation();
        visible = !visible;
        update();
    }, false);

    var heading = makeElement('h6', [ttButton, ' ', title], {}, {display: 'block', background: 'gray', color: 'white', width: '100%', padding: '5px 2px', margin: '0px'});
    return makeElement('div', [heading, content], {});
}

if (typeof(module) !== 'undefined') {
    module.exports = {
        makeTreeTableSection: makeTreeTableSection
    };
}
<commit_msg>Cleanup.<commit_after>/* -*- mode: javascript; c-basic-offset: 4; indent-tabs-mode: nil -*- */

//
// Dalliance Genome Explorer
// (c) Thomas Down 2006-2010
//
// domui.js: SVG UI components
//

"use strict";

if (typeof(require) !== 'undefined') {
    var browser = require('./cbrowser');
    var Browser = browser.Browser;

    var utils = require('./utils');
    var makeElement = utils.makeElement;
    var removeChildren = utils.removeChildren;
}

Browser.prototype.removeAllPopups = function() {
    for (var pi = 0; pi < this.popups.length; ++pi) {
        var popup = this.popups[pi];
        if (popup.onClose) {
            try {
                popup.onClose();
            } catch (ex) {
                console.log(ex);
            }
        }
        this.hPopupHolder.removeChild(popup.component);
    }
    this.popups = [];
}

Browser.prototype.makeTooltip = function(ele, text)
{
    ele.title = text;
}

Browser.prototype.popit = function(ev, name, ele, opts)
{
    var thisB = this;
    if (!opts)
        opts = {};
    if (!ev)
        ev = {};

    var width = opts.width || 200;

    var mx, my;

    if (ev.clientX) {
        var mx =  ev.clientX, my = ev.clientY;
    } else {
        mx = 500; my= 50;
    }
    mx +=  document.documentElement.scrollLeft || document.body.scrollLeft;
    my +=  document.documentElement.scrollTop || document.body.scrollTop;
    var winWidth = window.innerWidth;

    var top = my;
    var left = Math.min(mx - (width/2) - 4, (winWidth - width - 30));

    var popup = makeElement('div');
    popup.className = 'popover fade ' + (ev.clientX ? 'bottom ' : '') + 'in';
    popup.style.display = 'block';
    popup.style.position = 'absolute';
    popup.style.top = '' + top + 'px';
    popup.style.left = '' + left + 'px';
    popup.style.width = width + 'px';
    if (width > 276) {
        // HACK Bootstrappification...
        popup.style.maxWidth = width + 'px';
    }

    popup.appendChild(makeElement('div', null, {className: 'arrow'}));

    if (name) {
        var closeButton = makeElement('button', '', {className: 'close'});
        closeButton.innerHTML = '×'

        closeButton.addEventListener('mouseover', function(ev) {
            closeButton.style.color = 'red';
        }, false);
        closeButton.addEventListener('mouseout', function(ev) {
            closeButton.style.color = 'black';
        }, false);
        closeButton.addEventListener('click', function(ev) {
            ev.preventDefault(); ev.stopPropagation();
            thisB.removeAllPopups();
        }, false);
        var tbar = makeElement('h4', [makeElement('span', name, null, {maxWidth: '200px'}), closeButton], {/*className: 'popover-title' */}, {paddingLeft: '10px', paddingRight: '10px'});

        var dragOX, dragOY;
        var moveHandler, upHandler;
        moveHandler = function(ev) {
            ev.stopPropagation(); ev.preventDefault();
            left = left + (ev.clientX - dragOX);
            if (left < 8) {
                left = 8;
            } if (left > (winWidth - width - 32)) {
                left = (winWidth - width - 26);
            }
            top = top + (ev.clientY - dragOY);
            top = Math.max(10, top);
            popup.style.top = '' + top + 'px';
            popup.style.left = '' + Math.min(left, (winWidth - width - 10)) + 'px';
            dragOX = ev.clientX; dragOY = ev.clientY;
        }
        upHandler = function(ev) {
            ev.stopPropagation(); ev.preventDefault();
            window.removeEventListener('mousemove', moveHandler, false);
            window.removeEventListener('mouseup', upHandler, false);
        }
        tbar.addEventListener('mousedown', function(ev) {
            ev.preventDefault(); ev.stopPropagation();
            dragOX = ev.clientX; dragOY = ev.clientY;
            window.addEventListener('mousemove', moveHandler, false);
            window.addEventListener('mouseup', upHandler, false);
        }, false);

        popup.appendChild(tbar);
    }

    popup.appendChild(makeElement('div', ele, {className: 'popover-content'}, {
        padding: '0px'
    }));
    this.hPopupHolder.appendChild(popup);

    this.popups.push({
        component: popup,
        onClose: opts.onClose
    });
}

function makeTreeTableSection(title, content, visible) {
    var ttButton = makeElement('i');
    function update() {
        if (visible) {
            ttButton.className = 'fa fa-caret-down';
            content.style.display = 'table';
        } else {
            ttButton.className = 'fa fa-caret-right';
            content.style.display = 'none';
        }
    }
    update();

    ttButton.addEventListener('click', function(ev) {
        ev.preventDefault(); ev.stopPropagation();
        visible = !visible;
        update();
    }, false);

    var heading = makeElement('h6', [ttButton, ' ', title], {}, {display: 'block', background: 'gray', color: 'white', width: '100%', padding: '5px 2px', margin: '0px'});
    return makeElement('div', [heading, content], {});
}

if (typeof(module) !== 'undefined') {
    module.exports = {
        makeTreeTableSection: makeTreeTableSection
    };
}
<|endoftext|>


<commit_before>const events = {
    documentMouseup: [],
    windowResize: [],
};

window.onload = function () {
    document.body.addEventListener("mouseup", function (e) {
        events.documentMouseup = events.documentMouseup.filter(function (event) {
            // go through automatic cleanup when running events.
            if (!document.body.contains(event.canvas)) {
                return false;
            }

            event.callback.call(this, e);
            return true;
        });
    });

    window.addEventListener("resize", function (e) {
        events.windowResize = events.windowResize.filter(function (event) {
            if (!document.body.contains(event.canvas)) {
                return false;
            }

            event.callback.call(this, e);

            return true;
        }.bind(this));
    });
};

const funcs = {
    setZoom: function (meta, zoom) {
        // condition to handle zooming event by zoom hotkeys
        if (zoom === '+') {
            zoom = meta.zoom * 1.2;
        } else if (zoom === '-') {
            zoom = meta.zoom / 1.2;
        }
        // make sure the zoom is above 1 and below the maxZoom.
        meta.zoom = Math.min(Math.max(zoom, 1), meta.maxZoom);
    },

    // this is a function given a canvas that attaches all of the events
    // required to pan and zoom.
    attachEvents: function (canvas, context, meta) {
        let mousedown = false;

        // wheelEvent.deltaMode is a value that describes what the unit is
        // for the `deltaX`, `deltaY`, and `deltaZ` properties.
        const DELTA_MODE = {
            PIXEL: 0,
            LINE: 1,
            PAGE: 2,
        };

        // give object structure in `mousedown`, because its props are only
        // ever set once `mousedown` + `mousemove` is triggered.
        let lastPosition = {};

        // in browsers such as Safari, the `e.movementX` and `e.movementY`
        // props don't exist, so we need to create them as a difference of
        // where the last `layerX` and `layerY` movements since the last
        // `mousemove` event in this `mousedown` event were registered.
        const polyfillMouseMovement = function (e) {
            e.movementX = e.layerX - lastPosition.x || 0;
            e.movementY = e.layerY - lastPosition.y || 0;

            lastPosition = {
                x: e.layerX,
                y: e.layerY,
            };
        };

        // use the wheel event rather than scroll because this isn't
        // actually an element that can scroll. The wheel event will
        // detect the *gesture* of scrolling over an element, without actually
        // worrying about scrollable content.
        canvas.addEventListener("wheel", function (e) {
            e.preventDefault();

            // this is to reverse scrolling directions for the image.
            let delta = meta.direction * e.deltaY;

            if (e.deltaMode === DELTA_MODE.LINE) {
                // the vertical height in pixels of an approximate line.
                delta *= 15;
            }

            if (e.deltaMode === DELTA_MODE.PAGE) {
                // the vertical height in pixels of an approximate page.
                delta *= 300;
            }

            // this is calculated as the user defined speed times the normalizer
            // (which just is what it takes to take the raw delta and transform
            // it to a normal speed), multiply it against the current zoom.
            // Example:
            // delta = 8
            // normalizedDelta = delta * (1 / 20) * 1 = 0.4
            // zoom = zoom * (0.4 / 100) + 1
            const zoom = meta.zoom * (
                meta.speed * meta.internalSpeedMultiplier * delta / 100 + 1
            );

            funcs.setZoom(meta, zoom);
            funcs.displayImage(canvas, context, meta);

            return false;
        });

        // the only valid mousedown events should originate inside of the
        // canvas.
        canvas.addEventListener("mousedown", function () {
            mousedown = true;
        });

        // on mousemove, actually run the pan events.
        canvas.addEventListener("mousemove", function (e) {
            // to pan, there must be mousedown and mousemove, check if valid.
            if (mousedown === true) {
                polyfillMouseMovement(e);
                // find the percent of movement relative to the canvas width
                // since e.movementX, e.movementY are in px.
                const percentMovement = {
                    x: e.movementX / canvas.width,
                    y: e.movementY / canvas.height,
                };

                // add the percentMovement to the meta coordinates but divide
                // out by the zoom ratio because when zoomed in 10x for example
                // moving the photo by 1% will appear like 10% on the <canvas>.
                meta.coords.x += percentMovement.x * 2 / meta.zoom;
                meta.coords.y += percentMovement.y * 2 / meta.zoom;

                // redraw the image.
                funcs.displayImage(canvas, context, meta);
            }
        });

        // event listener to handle zoom in and out from using keyboard keys z/Z and +/-
        // in the canvas
        // these hotkeys are not implemented in static/js/hotkey.js as the code in
        // static/js/lightbox_canvas.js and static/js/lightbox.js isn't written a way
        // that the LightboxCanvas instance created in lightbox.js can be
        // accessed from hotkey.js. Major code refactoring is required in lightbox.js
        // to implement these keyboard shortcuts in hotkey.js
        document.addEventListener('keydown', function (e) {
            if (!overlays.lightbox_open()) {
                return;
            }
            if (e.key === "Z" || e.key === '+') {
                funcs.setZoom(meta, '+');
                funcs.displayImage(canvas, context, meta);
            } else if (e.key === "z" || e.key === '-') {
                funcs.setZoom(meta, '-');
                funcs.displayImage(canvas, context, meta);
            } else if (e.key === 'v') {
                overlays.close_overlay('lightbox');
            }
            e.preventDefault();
            e.stopPropagation();
        });

        // make sure that when the mousedown is lifted on <canvas>to prevent
        // panning events.
        canvas.addEventListener("mouseup", function () {
            mousedown = false;
            // reset this to be empty so that the values will `NaN` on first
            // mousemove and default to a change of (0, 0).
            lastPosition = {};
        });

        // do so on the document.body as well, though depending on the infra,
        // these are less reliable as preventDefault may prevent these events
        // from propagating all the way to the <body>.
        events.documentMouseup.push({
            canvas: canvas,
            meta: meta,
            callback: function () {
                mousedown = false;
            },
        });

        events.windowResize.push({
            canvas: canvas,
            meta: meta,
            callback: function () {
                funcs.sizeCanvas(canvas, meta);
                funcs.displayImage(canvas, context, meta);
            },
        });
    },

    imageRatio: function (image) {
        return image.naturalWidth / image.naturalHeight;
    },

    displayImage: function (canvas, context, meta) {
        meta.coords.x = Math.max(1 / (meta.zoom * 2), meta.coords.x);
        meta.coords.x = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.x);

        meta.coords.y = Math.max(1 / (meta.zoom * 2), meta.coords.y);
        meta.coords.y = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.y);

        const c = {
            x: meta.coords.x - 1,
            y: meta.coords.y - 1,
        };

        const x = meta.zoom * c.x * canvas.width + canvas.width / 2;
        const y = meta.zoom * c.y * canvas.height + canvas.height / 2;
        const w = canvas.width * meta.zoom;
        const h = canvas.height * meta.zoom;

        canvas.width = canvas.width;
        context.imageSmoothingEnabled = false;

        context.drawImage(meta.image, x, y, w, h);
    },

    // the `sizeCanvas` method figures out the appropriate bounding box for
    // the canvas given a parent that has constraints.
    // for example, if a photo has a ration of 1.5:1 (w:h), and the parent
    // box is 1:1 respectively, we want to stretch the photo to be as large
    // as we can, which means that we check if having the photo width = 100%
    // means that the height is less than 100% of the parent height. If so,
    // then we size the photo as w = 100%, h = 100% / 1.5.
    sizeCanvas: function (canvas, meta) {
        if (canvas.parentNode === null) {
            return;
        }

        if (typeof meta.onresize === "function") {
            meta.onresize(canvas);
        }

        const parent = {
            width: canvas.parentNode.clientWidth,
            height: canvas.parentNode.clientHeight,
        };

        if (parent.height * meta.ratio > parent.width) {
            canvas.width = parent.width * 2;
            canvas.style.width = parent.width + "px";

            canvas.height = parent.width / meta.ratio * 2;
            canvas.style.height = parent.width / meta.ratio + "px";
        } else {
            canvas.height = parent.height * 2;
            canvas.style.height = parent.height + "px";

            canvas.width = parent.height * meta.ratio * 2;
            canvas.style.width = parent.height * meta.ratio + "px";
        }

        blueslip.warn("Please specify a 'data-width' or 'data-height' argument for canvas.");
    },
};

// a class w/ prototype to create a new `LightboxCanvas` instance.
const LightboxCanvas = function (el) {
    const self = this;

    this.meta = {
        direction: -1,
        zoom: 1,
        image: null,
        coords: {
            x: 0.5,
            y: 0.5,
        },
        speed: 1,
        // this is to normalize the speed to what I would consider to be
        // "standard" zoom speed.
        internalSpeedMultiplier: 0.05,
        maxZoom: 10,
    };

    if (el instanceof Node) {
        this.canvas = el;
    } else if (typeof el === "string") {
        this.canvas = document.querySelector(el);
    } else {
        blueslip.warn("Error. 'LightboxCanvas' accepts either string selector or node.");
        return;
    }

    this.context = this.canvas.getContext("2d");

    this.meta.image = new Image();
    this.meta.image.src = this.canvas.getAttribute("data-src");
    this.meta.image.onload = function () {
        self.meta.ratio = funcs.imageRatio(this);

        funcs.sizeCanvas(self.canvas, self.meta);
        funcs.displayImage(self.canvas, self.context, self.meta);
    };

    this.canvas.image = this.meta.image;

    funcs.attachEvents(this.canvas, this.context, self.meta);
};

LightboxCanvas.prototype = {
    // set the speed at which scrolling zooms in on a photo.
    speed: function (speed) {
        this.meta.speed = speed;
    },

    // set the max zoom of the `LightboxCanvas` canvas as a mult of the total width.
    maxZoom: function (maxZoom) {
        this.meta.maxZoom = maxZoom;
    },

    reverseScrollDirection: function () {
        this.meta.direction = 1;
    },

    setZoom: function (zoom) {
        funcs.setZoom(this.meta, zoom);
        funcs.displayImage(this.canvas, this.context, this.meta);
    },

    resize: function (callback) {
        this.meta.onresize = callback;
    },
};

module.exports = LightboxCanvas;
window.LightboxCanvas = LightboxCanvas;
<commit_msg>lightbox_canvas: Clear canvas with less obfuscation.<commit_after>const events = {
    documentMouseup: [],
    windowResize: [],
};

window.onload = function () {
    document.body.addEventListener("mouseup", function (e) {
        events.documentMouseup = events.documentMouseup.filter(function (event) {
            // go through automatic cleanup when running events.
            if (!document.body.contains(event.canvas)) {
                return false;
            }

            event.callback.call(this, e);
            return true;
        });
    });

    window.addEventListener("resize", function (e) {
        events.windowResize = events.windowResize.filter(function (event) {
            if (!document.body.contains(event.canvas)) {
                return false;
            }

            event.callback.call(this, e);

            return true;
        }.bind(this));
    });
};

const funcs = {
    setZoom: function (meta, zoom) {
        // condition to handle zooming event by zoom hotkeys
        if (zoom === '+') {
            zoom = meta.zoom * 1.2;
        } else if (zoom === '-') {
            zoom = meta.zoom / 1.2;
        }
        // make sure the zoom is above 1 and below the maxZoom.
        meta.zoom = Math.min(Math.max(zoom, 1), meta.maxZoom);
    },

    // this is a function given a canvas that attaches all of the events
    // required to pan and zoom.
    attachEvents: function (canvas, context, meta) {
        let mousedown = false;

        // wheelEvent.deltaMode is a value that describes what the unit is
        // for the `deltaX`, `deltaY`, and `deltaZ` properties.
        const DELTA_MODE = {
            PIXEL: 0,
            LINE: 1,
            PAGE: 2,
        };

        // give object structure in `mousedown`, because its props are only
        // ever set once `mousedown` + `mousemove` is triggered.
        let lastPosition = {};

        // in browsers such as Safari, the `e.movementX` and `e.movementY`
        // props don't exist, so we need to create them as a difference of
        // where the last `layerX` and `layerY` movements since the last
        // `mousemove` event in this `mousedown` event were registered.
        const polyfillMouseMovement = function (e) {
            e.movementX = e.layerX - lastPosition.x || 0;
            e.movementY = e.layerY - lastPosition.y || 0;

            lastPosition = {
                x: e.layerX,
                y: e.layerY,
            };
        };

        // use the wheel event rather than scroll because this isn't
        // actually an element that can scroll. The wheel event will
        // detect the *gesture* of scrolling over an element, without actually
        // worrying about scrollable content.
        canvas.addEventListener("wheel", function (e) {
            e.preventDefault();

            // this is to reverse scrolling directions for the image.
            let delta = meta.direction * e.deltaY;

            if (e.deltaMode === DELTA_MODE.LINE) {
                // the vertical height in pixels of an approximate line.
                delta *= 15;
            }

            if (e.deltaMode === DELTA_MODE.PAGE) {
                // the vertical height in pixels of an approximate page.
                delta *= 300;
            }

            // this is calculated as the user defined speed times the normalizer
            // (which just is what it takes to take the raw delta and transform
            // it to a normal speed), multiply it against the current zoom.
            // Example:
            // delta = 8
            // normalizedDelta = delta * (1 / 20) * 1 = 0.4
            // zoom = zoom * (0.4 / 100) + 1
            const zoom = meta.zoom * (
                meta.speed * meta.internalSpeedMultiplier * delta / 100 + 1
            );

            funcs.setZoom(meta, zoom);
            funcs.displayImage(canvas, context, meta);

            return false;
        });

        // the only valid mousedown events should originate inside of the
        // canvas.
        canvas.addEventListener("mousedown", function () {
            mousedown = true;
        });

        // on mousemove, actually run the pan events.
        canvas.addEventListener("mousemove", function (e) {
            // to pan, there must be mousedown and mousemove, check if valid.
            if (mousedown === true) {
                polyfillMouseMovement(e);
                // find the percent of movement relative to the canvas width
                // since e.movementX, e.movementY are in px.
                const percentMovement = {
                    x: e.movementX / canvas.width,
                    y: e.movementY / canvas.height,
                };

                // add the percentMovement to the meta coordinates but divide
                // out by the zoom ratio because when zoomed in 10x for example
                // moving the photo by 1% will appear like 10% on the <canvas>.
                meta.coords.x += percentMovement.x * 2 / meta.zoom;
                meta.coords.y += percentMovement.y * 2 / meta.zoom;

                // redraw the image.
                funcs.displayImage(canvas, context, meta);
            }
        });

        // event listener to handle zoom in and out from using keyboard keys z/Z and +/-
        // in the canvas
        // these hotkeys are not implemented in static/js/hotkey.js as the code in
        // static/js/lightbox_canvas.js and static/js/lightbox.js isn't written a way
        // that the LightboxCanvas instance created in lightbox.js can be
        // accessed from hotkey.js. Major code refactoring is required in lightbox.js
        // to implement these keyboard shortcuts in hotkey.js
        document.addEventListener('keydown', function (e) {
            if (!overlays.lightbox_open()) {
                return;
            }
            if (e.key === "Z" || e.key === '+') {
                funcs.setZoom(meta, '+');
                funcs.displayImage(canvas, context, meta);
            } else if (e.key === "z" || e.key === '-') {
                funcs.setZoom(meta, '-');
                funcs.displayImage(canvas, context, meta);
            } else if (e.key === 'v') {
                overlays.close_overlay('lightbox');
            }
            e.preventDefault();
            e.stopPropagation();
        });

        // make sure that when the mousedown is lifted on <canvas>to prevent
        // panning events.
        canvas.addEventListener("mouseup", function () {
            mousedown = false;
            // reset this to be empty so that the values will `NaN` on first
            // mousemove and default to a change of (0, 0).
            lastPosition = {};
        });

        // do so on the document.body as well, though depending on the infra,
        // these are less reliable as preventDefault may prevent these events
        // from propagating all the way to the <body>.
        events.documentMouseup.push({
            canvas: canvas,
            meta: meta,
            callback: function () {
                mousedown = false;
            },
        });

        events.windowResize.push({
            canvas: canvas,
            meta: meta,
            callback: function () {
                funcs.sizeCanvas(canvas, meta);
                funcs.displayImage(canvas, context, meta);
            },
        });
    },

    imageRatio: function (image) {
        return image.naturalWidth / image.naturalHeight;
    },

    displayImage: function (canvas, context, meta) {
        meta.coords.x = Math.max(1 / (meta.zoom * 2), meta.coords.x);
        meta.coords.x = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.x);

        meta.coords.y = Math.max(1 / (meta.zoom * 2), meta.coords.y);
        meta.coords.y = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.y);

        const c = {
            x: meta.coords.x - 1,
            y: meta.coords.y - 1,
        };

        const x = meta.zoom * c.x * canvas.width + canvas.width / 2;
        const y = meta.zoom * c.y * canvas.height + canvas.height / 2;
        const w = canvas.width * meta.zoom;
        const h = canvas.height * meta.zoom;

        context.clearRect(0, 0, canvas.width, canvas.height);
        context.imageSmoothingEnabled = false;

        context.drawImage(meta.image, x, y, w, h);
    },

    // the `sizeCanvas` method figures out the appropriate bounding box for
    // the canvas given a parent that has constraints.
    // for example, if a photo has a ration of 1.5:1 (w:h), and the parent
    // box is 1:1 respectively, we want to stretch the photo to be as large
    // as we can, which means that we check if having the photo width = 100%
    // means that the height is less than 100% of the parent height. If so,
    // then we size the photo as w = 100%, h = 100% / 1.5.
    sizeCanvas: function (canvas, meta) {
        if (canvas.parentNode === null) {
            return;
        }

        if (typeof meta.onresize === "function") {
            meta.onresize(canvas);
        }

        const parent = {
            width: canvas.parentNode.clientWidth,
            height: canvas.parentNode.clientHeight,
        };

        if (parent.height * meta.ratio > parent.width) {
            canvas.width = parent.width * 2;
            canvas.style.width = parent.width + "px";

            canvas.height = parent.width / meta.ratio * 2;
            canvas.style.height = parent.width / meta.ratio + "px";
        } else {
            canvas.height = parent.height * 2;
            canvas.style.height = parent.height + "px";

            canvas.width = parent.height * meta.ratio * 2;
            canvas.style.width = parent.height * meta.ratio + "px";
        }

        blueslip.warn("Please specify a 'data-width' or 'data-height' argument for canvas.");
    },
};

// a class w/ prototype to create a new `LightboxCanvas` instance.
const LightboxCanvas = function (el) {
    const self = this;

    this.meta = {
        direction: -1,
        zoom: 1,
        image: null,
        coords: {
            x: 0.5,
            y: 0.5,
        },
        speed: 1,
        // this is to normalize the speed to what I would consider to be
        // "standard" zoom speed.
        internalSpeedMultiplier: 0.05,
        maxZoom: 10,
    };

    if (el instanceof Node) {
        this.canvas = el;
    } else if (typeof el === "string") {
        this.canvas = document.querySelector(el);
    } else {
        blueslip.warn("Error. 'LightboxCanvas' accepts either string selector or node.");
        return;
    }

    this.context = this.canvas.getContext("2d");

    this.meta.image = new Image();
    this.meta.image.src = this.canvas.getAttribute("data-src");
    this.meta.image.onload = function () {
        self.meta.ratio = funcs.imageRatio(this);

        funcs.sizeCanvas(self.canvas, self.meta);
        funcs.displayImage(self.canvas, self.context, self.meta);
    };

    this.canvas.image = this.meta.image;

    funcs.attachEvents(this.canvas, this.context, self.meta);
};

LightboxCanvas.prototype = {
    // set the speed at which scrolling zooms in on a photo.
    speed: function (speed) {
        this.meta.speed = speed;
    },

    // set the max zoom of the `LightboxCanvas` canvas as a mult of the total width.
    maxZoom: function (maxZoom) {
        this.meta.maxZoom = maxZoom;
    },

    reverseScrollDirection: function () {
        this.meta.direction = 1;
    },

    setZoom: function (zoom) {
        funcs.setZoom(this.meta, zoom);
        funcs.displayImage(this.canvas, this.context, this.meta);
    },

    resize: function (callback) {
        this.meta.onresize = callback;
    },
};

module.exports = LightboxCanvas;
window.LightboxCanvas = LightboxCanvas;
<|endoftext|>


<commit_before>var RSVP = require('rsvp');
var _ = require('lodash');
var crypto = require('crypto');

exports.hooksAll = {
  _before:{
    read: [],
    write: []
  },
  _after: {
    read: [],
    write: []
  }
};

//Required for testing :/
exports._clearGlobalHooks = function(){
  exports.hooksAll = {
    _before:{
      read: [],
      write: []
    },
    _after: {
      read: [],
      write: []
    }
  }
};

/**
 * For internal use by fortune in beforeAll/afterAll
 * @param when
 * @param type
 * @param provider - Array of hooks. name and init props are required
 * @param config - Array of additional configuration for global hook
 */
exports.registerGlobalHook = function(when, type, provider, config){
  provider.forEach(function(hook){
    var inlineConfig = {};
    inlineConfig[hook.name] = matchConfig(hook, config);
    hook.priority = hook.priority || 0;
    exports.hooksAll[when][type].push({
      constructor: hook,
      inlineConfig: inlineConfig
    });
  });
};

/**
 * Applies all registered ALL hooks to provided resource.
 * All hooks enabled by default.
 * You can disable specific hook in resource definition.
 * @param resource - resource configuration object
 * @param fortuneConfig - fortune configuration object
 */
exports.initGlobalHooks = function(resource, fortuneConfig){
  resource.hooks = resource.hooks || {};
  //Iterates before and after hooks
  _.each(exports.hooksAll, function(timeHooks, when){
    var stageHook = resource.hooks[when] = resource.hooks[when] || {};
    //Iterates read and write hooks
    _.each(timeHooks, function(typeHooks, type){
      var typeHook = stageHook[type] = stageHook[type] || [];
      //Iterates over registered hooks scoped to before/after, read/write
      _.each(_.sortBy(typeHooks, function(i){return i.constructor.priority}), function(hook){
        var hookConfig = getHookConfig(hook.constructor, resource, hook.inlineConfig);
        if (!hookConfig.disable) {
          var fn = hook.constructor.init(hookConfig, fortuneConfig);
          //fn._priority = hook.constructor.priority || 0;
          typeHook.unshift({
            name: hook.constructor.name,
            _priority: hook.constructor.priority || 0,
            fn: fn,
            options: hookConfig
          });
        }
      });
    });
  });
};

exports.addHook = function(name, hooks, stage, type, inlineConfig){
  var _this = this;

  if (typeof name === 'function') {
    hooks = name;
    name = this._resource;
  }

  name.split(' ').forEach(function(resourceName) {
    hooks = normalize(hooks);

    var resource;
    if (!_this._resources || !_this._resources[resourceName]) {
      if(!_this._customTypes || !_this._customTypes[resourceName]) {
        return console.warn('You are trying to attach a hook to %s, ' +
          'that is not defined in this instance of fortune', resourceName);
      }
      else resource = _this._customTypes[resourceName];
    }
    else resource = _this._resources[resourceName];

    resource.hooks = resource.hooks || {};
    resource.hooks[stage] = resource.hooks[stage] || {};
    resource.hooks[stage][type] = resource.hooks[stage][type] || [];
    _.each(hooks, function(hook){
      var hookOptions = getHookConfig(hook, resource, inlineConfig);
      var fn = hook.init(hookOptions, _this);
      resource.hooks[stage][type].push(_.extend({
          _priority: hook.priority || 0,
          name: hook.name,
          options: hookOptions
        },
        {fn: fn}
      ));
    });
    resource.hooks[stage][type] = _.sortBy(resource.hooks[stage][type], function(h){return -h._priority});
  });
};

/**
 * Merge multiple hookset collected from resources, customTypes, etc
 * @param hookSet -
 */
exports.merge = function(hookset, rec) {
  var isArray = _.any(hookset, function(arg) { return _.isArray(arg); });
  if(isArray) {
    return _.reduce(hookset, function(result, value, key) {
      return result.concat(value || []);
    }, []);
  }

  var keys = _.unique(_.flatten(_.map(hookset, function(arg) {
    return _.keys(arg);
  })));

  return _.object(_.map(keys, function(key) {
    return [key, exports.merge(
      _.compact(_.map(hookset, function(arg) {
        return arg[key];
      })), 1
    )];
  }));
}

/* Collect hooks provided with custom types
 * and convert them to a resource-wide hooks
 * @param customTypes - a map with field names => custom types for particular resource
 */
exports.fromCustomTypesMap = function(customTypes) {
  return _.flatten(_.map(_.keys(customTypes), function(key) {
    // Modify a names of the hooks to include field name they applied to
    var result = _.cloneDeep(customTypes[key].hooks);
    _.each(result, function(whenHooks, when) {
      _.each(whenHooks, function(actionHooks, action) {
        _.each(actionHooks, function(ahook) {
          if(ahook.name) {
            ahook.name = [key, ahook.name].join('-');
          }

          // Bind the handler to particular data type
          var handler = ahook.fn

          if(handler) {
            ahook.fn = function(req, res) {
              var doc = this;
              if(doc[key]) {
                var ret =  handler.call(doc[key], req, res);
                return (_.isFunction(ret.then) ? ret : RSVP.resolve(ret)).then(function(result) {
                  doc[key] = result;
                  return doc;
                });
              }
              return doc;
            }
          }
        });
      });
    })
    return result;
  }));
}

/**
 * Backward compatibility method.
 * Accepts array or function and return array of constructor objects.
 * @param hookFunction
 * @returns {Array}
 */
function normalize(hookFunction){
  if (!_.isArray(hookFunction)){
    var tmp = {};
    if (_.isFunction(hookFunction)){
      tmp.init = function(){
        return hookFunction;
      };
      //This name should be unique somehow O_o
      tmp.name = 'generated_' + crypto.createHash('md5').update(hookFunction.toString()).digest('hex');
      tmp.config = {};
    }
    return [tmp];
  }else{
    return hookFunction;
  }
}

/**
 *
 * @param hook - normalized hook constructor
 * @param resource - resource object
 * @param inlineConfig - object that is passed along with hook
 */

function getHookConfig(hook, resource, inlineConfig){
  var config = {};
  var inline = (inlineConfig || {})[hook.name] || {};
  var hookConfig = _.cloneDeep(hook.config) || {};
  if (resource.hooksOptions){
    if (resource.hooksOptions[hook.name]){
      config = _.extend(hookConfig, inline, resource.hooksOptions[hook.name]);
    }else{
      config = _.extend(hookConfig, inline);
    }
  }else{
   config = _.extend(hookConfig, inline);
  }
  return config;
}

/**
 *
 * @param hook
 * @param config
 */
function matchConfig(hook, config){
  if (!hook.name || !config) return hook.config || {};
  var hookName = hook.name;
  var defaultConfig = _.extend({}, hook.config);
  var newConfig = config[hookName] || {};
  return _.extend(defaultConfig, newConfig);
}
<commit_msg>More fail safety<commit_after>var RSVP = require('rsvp');
var _ = require('lodash');
var crypto = require('crypto');

exports.hooksAll = {
  _before:{
    read: [],
    write: []
  },
  _after: {
    read: [],
    write: []
  }
};

//Required for testing :/
exports._clearGlobalHooks = function(){
  exports.hooksAll = {
    _before:{
      read: [],
      write: []
    },
    _after: {
      read: [],
      write: []
    }
  }
};

/**
 * For internal use by fortune in beforeAll/afterAll
 * @param when
 * @param type
 * @param provider - Array of hooks. name and init props are required
 * @param config - Array of additional configuration for global hook
 */
exports.registerGlobalHook = function(when, type, provider, config){
  provider.forEach(function(hook){
    var inlineConfig = {};
    inlineConfig[hook.name] = matchConfig(hook, config);
    hook.priority = hook.priority || 0;
    exports.hooksAll[when][type].push({
      constructor: hook,
      inlineConfig: inlineConfig
    });
  });
};

/**
 * Applies all registered ALL hooks to provided resource.
 * All hooks enabled by default.
 * You can disable specific hook in resource definition.
 * @param resource - resource configuration object
 * @param fortuneConfig - fortune configuration object
 */
exports.initGlobalHooks = function(resource, fortuneConfig){
  resource.hooks = resource.hooks || {};
  //Iterates before and after hooks
  _.each(exports.hooksAll, function(timeHooks, when){
    var stageHook = resource.hooks[when] = resource.hooks[when] || {};
    //Iterates read and write hooks
    _.each(timeHooks, function(typeHooks, type){
      var typeHook = stageHook[type] = stageHook[type] || [];
      //Iterates over registered hooks scoped to before/after, read/write
      _.each(_.sortBy(typeHooks, function(i){return i.constructor.priority}), function(hook){
        var hookConfig = getHookConfig(hook.constructor, resource, hook.inlineConfig);
        if (!hookConfig.disable) {
          var fn = hook.constructor.init(hookConfig, fortuneConfig);
          //fn._priority = hook.constructor.priority || 0;
          typeHook.unshift({
            name: hook.constructor.name,
            _priority: hook.constructor.priority || 0,
            fn: fn,
            options: hookConfig
          });
        }
      });
    });
  });
};

exports.addHook = function(name, hooks, stage, type, inlineConfig){
  var _this = this;

  if (typeof name === 'function') {
    hooks = name;
    name = this._resource;
  }

  name.split(' ').forEach(function(resourceName) {
    hooks = normalize(hooks);

    var resource;
    if (!_this._resources || !_this._resources[resourceName]) {
      if(!_this._customTypes || !_this._customTypes[resourceName]) {
        return console.warn('You are trying to attach a hook to %s, ' +
          'that is not defined in this instance of fortune', resourceName);
      }
      else resource = _this._customTypes[resourceName];
    }
    else resource = _this._resources[resourceName];

    resource.hooks = resource.hooks || {};
    resource.hooks[stage] = resource.hooks[stage] || {};
    resource.hooks[stage][type] = resource.hooks[stage][type] || [];
    _.each(hooks, function(hook){
      var hookOptions = getHookConfig(hook, resource, inlineConfig);
      var fn = hook.init(hookOptions, _this);
      resource.hooks[stage][type].push(_.extend({
          _priority: hook.priority || 0,
          name: hook.name,
          options: hookOptions
        },
        {fn: fn}
      ));
    });
    resource.hooks[stage][type] = _.sortBy(resource.hooks[stage][type], function(h){return -h._priority});
  });
};

/**
 * Merge multiple hookset collected from resources, customTypes, etc
 * @param hookSet -
 */
exports.merge = function(hookset, rec) {
  var isArray = _.any(hookset, function(arg) { return _.isArray(arg); });
  if(isArray) {
    return _.reduce(hookset, function(result, value, key) {
      return result.concat(value || []);
    }, []);
  }

  var keys = _.unique(_.flatten(_.map(hookset, function(arg) {
    return _.keys(arg);
  })));

  return _.object(_.map(keys, function(key) {
    return [key, exports.merge(
      _.compact(_.map(hookset, function(arg) {
        return arg[key];
      })), 1
    )];
  }));
}

/* Collect hooks provided with custom types
 * and convert them to a resource-wide hooks
 * @param customTypes - a map with field names => custom types for particular resource
 */
exports.fromCustomTypesMap = function(customTypes) {
  return _.flatten(_.map(_.keys(customTypes), function(key) {
    // Modify a names of the hooks to include field name they applied to
    var result = _.cloneDeep(customTypes[key].hooks);
    _.each(result, function(whenHooks, when) {
      _.each(whenHooks, function(actionHooks, action) {
        _.each(actionHooks, function(ahook) {
          if(ahook.name) {
            ahook.name = [key, ahook.name].join('-');
          }

          // Bind the handler to particular data type
          var handler = ahook.fn

          if(handler) {
            ahook.fn = function(req, res) {
              var doc = this;
              if(doc && doc[key]) {
                var ret =  handler.call(doc[key], req, res);
                return (_.isFunction(ret.then) ? ret : RSVP.resolve(ret)).then(function(result) {
                  doc[key] = result;
                  return doc;
                });
              }
              return doc;
            }
          }
        });
      });
    })
    return result;
  }));
}

/**
 * Backward compatibility method.
 * Accepts array or function and return array of constructor objects.
 * @param hookFunction
 * @returns {Array}
 */
function normalize(hookFunction){
  if (!_.isArray(hookFunction)){
    var tmp = {};
    if (_.isFunction(hookFunction)){
      tmp.init = function(){
        return hookFunction;
      };
      //This name should be unique somehow O_o
      tmp.name = 'generated_' + crypto.createHash('md5').update(hookFunction.toString()).digest('hex');
      tmp.config = {};
    }
    return [tmp];
  }else{
    return hookFunction;
  }
}

/**
 *
 * @param hook - normalized hook constructor
 * @param resource - resource object
 * @param inlineConfig - object that is passed along with hook
 */

function getHookConfig(hook, resource, inlineConfig){
  var config = {};
  var inline = (inlineConfig || {})[hook.name] || {};
  var hookConfig = _.cloneDeep(hook.config) || {};
  if (resource.hooksOptions){
    if (resource.hooksOptions[hook.name]){
      config = _.extend(hookConfig, inline, resource.hooksOptions[hook.name]);
    }else{
      config = _.extend(hookConfig, inline);
    }
  }else{
   config = _.extend(hookConfig, inline);
  }
  return config;
}

/**
 *
 * @param hook
 * @param config
 */
function matchConfig(hook, config){
  if (!hook.name || !config) return hook.config || {};
  var hookName = hook.name;
  var defaultConfig = _.extend({}, hook.config);
  var newConfig = config[hookName] || {};
  return _.extend(defaultConfig, newConfig);
}
<|endoftext|>


<commit_before>/**
 * Copyright 2018 The Subscribe with Google 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.
 */
'use strict';

module.exports = function(context) {
  return {
    ExportNamedDeclaration: function(node) {
      if (node.declaration) {
        var declaration = node.declaration;
        if (declaration.type === 'VariableDeclaration') {
          declaration.declarations
              .map(function(declarator) {
                return declarator.init
              }).filter(function(init) {
                return init && /(?:Call|New)Expression/.test(init.type)
                    // Special case creating a map object from a literal.
                    && init.callee.name != 'map';
              }).forEach(function(init) {
                context.report(init, 'Cannot export side-effect');
              });
        }
      } else if (node.specifiers) {
        context.report(node, 'Side-effect linting not implemented');
      }
    },
  };
};
<commit_msg>Sync with amphtml version<commit_after>/**
 * Copyright 2018 The Subscribe with Google 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.
 */
'use strict';

module.exports = function(context) {
  return {
    ExportNamedDeclaration: function(node) {
      if (node.declaration) {
        const {declaration} = node;
        if (declaration.type === 'VariableDeclaration') {
          declaration.declarations
            .map(function(declarator) {
              return declarator.init;
            })
            .filter(function(init) {
              return init && /(?:Call|New)Expression/.test(init.type);
            })
            .forEach(function(init) {
              context.report({
                node: init,
                message: 'Cannot export side-effect',
              });
            });
        }
      }
    },
  };
};<|endoftext|>