<commit_before>// @inheritedComponent IconButton

import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { refType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import { alpha } from '../styles/colorManipulator';
import capitalize from '../utils/capitalize';
import SwitchBase from '../internal/SwitchBase';

export const styles = (theme) => ({
  /* Styles applied to the root element. */
  root: {
    display: 'inline-flex',
    width: 34 + 12 * 2,
    height: 14 + 12 * 2,
    overflow: 'hidden',
    padding: 12,
    boxSizing: 'border-box',
    position: 'relative',
    flexShrink: 0,
    zIndex: 0, // Reset the stacking context.
    verticalAlign: 'middle', // For correct alignment with the text.
    '@media print': {
      colorAdjust: 'exact',
    },
  },
  /* Styles applied to the root element if `edge="start"`. */
  edgeStart: {
    marginLeft: -8,
  },
  /* Styles applied to the root element if `edge="end"`. */
  edgeEnd: {
    marginRight: -8,
  },
  /* Styles applied to the internal `SwitchBase` component's `root` class. */
  switchBase: {
    position: 'absolute',
    top: 0,
    left: 0,
    zIndex: 1, // Render above the focus ripple.
    color: theme.palette.mode === 'light' ? theme.palette.grey[50] : theme.palette.grey[400],
    transition: theme.transitions.create(['left', 'transform'], {
      duration: theme.transitions.duration.shortest,
    }),
    '&$checked': {
      transform: 'translateX(20px)',
    },
    '&$disabled': {
      color: theme.palette.mode === 'light' ? theme.palette.grey[400] : theme.palette.grey[800],
    },
    '&$checked + $track': {
      opacity: 0.5,
    },
    '&$disabled + $track': {
      opacity: theme.palette.mode === 'light' ? 0.12 : 0.1,
    },
  },
  /* Styles applied to the internal SwitchBase component's root element if `color="primary"`. */
  colorPrimary: {
    '&$checked': {
      color: theme.palette.primary.main,
      '&:hover': {
        backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity),
        '@media (hover: none)': {
          backgroundColor: 'transparent',
        },
      },
    },
    '&$disabled': {
      color: theme.palette.mode === 'light' ? theme.palette.grey[400] : theme.palette.grey[800],
    },
    '&$checked + $track': {
      backgroundColor: theme.palette.primary.main,
    },
    '&$disabled + $track': {
      backgroundColor:
        theme.palette.mode === 'light' ? theme.palette.common.black : theme.palette.common.white,
    },
  },
  /* Styles applied to the internal SwitchBase component's root element if `color="secondary"`. */
  colorSecondary: {
    '&$checked': {
      color: theme.palette.secondary.main,
      '&:hover': {
        backgroundColor: alpha(theme.palette.secondary.main, theme.palette.action.hoverOpacity),
        '@media (hover: none)': {
          backgroundColor: 'transparent',
        },
      },
    },
    '&$disabled': {
      color: theme.palette.mode === 'light' ? theme.palette.grey[400] : theme.palette.grey[800],
    },
    '&$checked + $track': {
      backgroundColor: theme.palette.secondary.main,
    },
    '&$disabled + $track': {
      backgroundColor:
        theme.palette.mode === 'light' ? theme.palette.common.black : theme.palette.common.white,
    },
  },
  /* Styles applied to the root element if `size="small"`. */
  sizeSmall: {
    width: 40,
    height: 24,
    padding: 7,
    '& $thumb': {
      width: 16,
      height: 16,
    },
    '& $switchBase': {
      padding: 4,
      '&$checked': {
        transform: 'translateX(16px)',
      },
    },
  },
  /* Pseudo-class applied to the internal `SwitchBase` component's `checked` class. */
  checked: {},
  /* Pseudo-class applied to the internal SwitchBase component's disabled class. */
  disabled: {},
  /* Styles applied to the internal SwitchBase component's input element. */
  input: {
    left: '-100%',
    width: '300%',
  },
  /* Styles used to create the thumb passed to the internal `SwitchBase` component `icon` prop. */
  thumb: {
    boxShadow: theme.shadows[1],
    backgroundColor: 'currentColor',
    width: 20,
    height: 20,
    borderRadius: '50%',
  },
  /* Styles applied to the track element. */
  track: {
    height: '100%',
    width: '100%',
    borderRadius: 14 / 2,
    zIndex: -1,
    transition: theme.transitions.create(['opacity', 'background-color'], {
      duration: theme.transitions.duration.shortest,
    }),
    backgroundColor:
      theme.palette.mode === 'light' ? theme.palette.common.black : theme.palette.common.white,
    opacity: theme.palette.mode === 'light' ? 0.38 : 0.3,
  },
});

const Switch = React.forwardRef(function Switch(props, ref) {
  const {
    classes,
    className,
    color = 'secondary',
    edge = false,
    size = 'medium',
    ...other
  } = props;

  const icon = ;

  return (
    <span
      className={clsx(
        classes.root,
        {
          [classes.edgeStart]: edge === 'start',
          [classes.edgeEnd]: edge === 'end',
          [classes[`size${capitalize(size)}`]]: size !== 'medium',
        },
        className,
      )}
    >
      <SwitchBase
        type="checkbox"
        icon={icon}
        checkedIcon={icon}
        classes={{
          root: clsx(classes.switchBase, classes[`color${capitalize(color)}`]),
          input: classes.input,
          checked: classes.checked,
          disabled: classes.disabled,
        }}
        ref={ref}
        {...other}
      />

  );
});

Switch.propTypes = {
  // ----------------------------- Warning --------------------------------
  // | These PropTypes are generated from the TypeScript type definitions |
  // |     To update them edit the d.ts file and run "yarn proptypes"     |
  // ----------------------------------------------------------------------
  /**
   * If `true`, the component is checked.
   */
  checked: PropTypes.bool,
  /**
   * The icon to display when the component is checked.
   */
  checkedIcon: PropTypes.node,
  /**
   * Override or extend the styles applied to the component.
   */
  classes: PropTypes.object,
  /**
   * @ignore
   */
  className: PropTypes.string,
  /**
   * The color of the component. It supports those theme colors that make sense for this component.
   * @default 'secondary'
   */
  color: PropTypes.oneOf(['default', 'primary', 'secondary']),
  /**
   * The default checked state. Use when the component is not controlled.
   */
  defaultChecked: PropTypes.bool,
  /**
   * If `true`, the component is disabled.
   */
  disabled: PropTypes.bool,
  /**
   * If `true`, the ripple effect is disabled.
   */
  disableRipple: PropTypes.bool,
  /**
   * If given, uses a negative margin to counteract the padding on one
   * side (this is often helpful for aligning the left or right
   * side of the icon with content above or below, without ruining the border
   * size and shape).
   * @default false
   */
  edge: PropTypes.oneOf(['end', 'start', false]),
  /**
   * The icon to display when the component is unchecked.
   */
  icon: PropTypes.node,
  /**
   * The id of the `input` element.
   */
  id: PropTypes.string,
  /**
   * [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) applied to the `input` element.
   */
  inputProps: PropTypes.object,
  /**
   * Pass a ref to the `input` element.
   */
  inputRef: refType,
  /**
   * Callback fired when the state is changed.
   *
   * @param {object} event The event source of the callback.
   * You can pull out the new value by accessing `event.target.value` (string).
   * You can pull out the new checked state by accessing `event.target.checked` (boolean).
   */
  onChange: PropTypes.func,
  /**
   * If `true`, the `input` element is required.
   */
  required: PropTypes.bool,
  /**
   * The size of the component.
   * `small` is equivalent to the dense switch styling.
   * @default 'medium'
   */
  size: PropTypes.oneOf(['medium', 'small']),
  /**
   * The value of the component. The DOM API casts this to a string.
   * The browser uses "on" as the default value.
   */
  value: PropTypes.any,
};

export default withStyles(styles, { name: 'MuiSwitch' })(Switch);
<commit_msg>[Switch] Update to follow current MD guidelines (#24954)<commit_after>// @inheritedComponent IconButton

import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { refType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import { alpha, darken, lighten } from '../styles/colorManipulator';
import capitalize from '../utils/capitalize';
import SwitchBase from '../internal/SwitchBase';

export const styles = (theme) => ({
  /* Styles applied to the root element. */
  root: {
    display: 'inline-flex',
    width: 34 + 12 * 2,
    height: 14 + 12 * 2,
    overflow: 'hidden',
    padding: 12,
    boxSizing: 'border-box',
    position: 'relative',
    flexShrink: 0,
    zIndex: 0, // Reset the stacking context.
    verticalAlign: 'middle', // For correct alignment with the text.
    '@media print': {
      colorAdjust: 'exact',
    },
  },
  /* Styles applied to the root element if `edge="start"`. */
  edgeStart: {
    marginLeft: -8,
  },
  /* Styles applied to the root element if `edge="end"`. */
  edgeEnd: {
    marginRight: -8,
  },
  /* Styles applied to the internal `SwitchBase` component's `root` class. */
  switchBase: {
    position: 'absolute',
    top: 0,
    left: 0,
    zIndex: 1, // Render above the focus ripple.
    color: theme.palette.mode === 'light' ? theme.palette.common.white : theme.palette.grey[300],
    transition: theme.transitions.create(['left', 'transform'], {
      duration: theme.transitions.duration.shortest,
    }),
    '&$checked': {
      transform: 'translateX(20px)',
    },
    '&$disabled': {
      color: theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[600],
    },
    '&$checked + $track': {
      opacity: 0.5,
    },
    '&$disabled + $track': {
      opacity: theme.palette.mode === 'light' ? 0.12 : 0.2,
    },
  },
  /* Styles applied to the internal SwitchBase component's root element if `color="primary"`. */
  colorPrimary: {
    '&$checked': {
      color: theme.palette.primary.main,
      '&:hover': {
        backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity),
        '@media (hover: none)': {
          backgroundColor: 'transparent',
        },
      },
      '&$disabled': {
        color:
          theme.palette.mode === 'light'
            ? lighten(theme.palette.primary.main, 0.62)
            : darken(theme.palette.primary.main, 0.55),
      },
    },
    '&$checked + $track': {
      backgroundColor: theme.palette.primary.main,
    },
  },
  /* Styles applied to the internal SwitchBase component's root element if `color="secondary"`. */
  colorSecondary: {
    '&$checked': {
      color: theme.palette.secondary.main,
      '&:hover': {
        backgroundColor: alpha(theme.palette.secondary.main, theme.palette.action.hoverOpacity),
        '@media (hover: none)': {
          backgroundColor: 'transparent',
        },
      },
      '&$disabled': {
        color:
          theme.palette.mode === 'light'
            ? lighten(theme.palette.secondary.main, 0.62)
            : darken(theme.palette.secondary.main, 0.55),
      },
    },
    '&$checked + $track': {
      backgroundColor: theme.palette.secondary.main,
    },
  },
  /* Styles applied to the root element if `size="small"`. */
  sizeSmall: {
    width: 40,
    height: 24,
    padding: 7,
    '& $thumb': {
      width: 16,
      height: 16,
    },
    '& $switchBase': {
      padding: 4,
      '&$checked': {
        transform: 'translateX(16px)',
      },
    },
  },
  /* Pseudo-class applied to the internal `SwitchBase` component's `checked` class. */
  checked: {},
  /* Pseudo-class applied to the internal SwitchBase component's disabled class. */
  disabled: {},
  /* Styles applied to the internal SwitchBase component's input element. */
  input: {
    left: '-100%',
    width: '300%',
  },
  /* Styles used to create the thumb passed to the internal `SwitchBase` component `icon` prop. */
  thumb: {
    boxShadow: theme.shadows[1],
    backgroundColor: 'currentColor',
    width: 20,
    height: 20,
    borderRadius: '50%',
  },
  /* Styles applied to the track element. */
  track: {
    height: '100%',
    width: '100%',
    borderRadius: 14 / 2,
    zIndex: -1,
    transition: theme.transitions.create(['opacity', 'background-color'], {
      duration: theme.transitions.duration.shortest,
    }),
    backgroundColor:
      theme.palette.mode === 'light' ? theme.palette.common.black : theme.palette.common.white,
    opacity: theme.palette.mode === 'light' ? 0.38 : 0.3,
  },
});

const Switch = React.forwardRef(function Switch(props, ref) {
  const {
    classes,
    className,
    color = 'secondary',
    edge = false,
    size = 'medium',
    ...other
  } = props;

  const icon = ;

  return (
    <span
      className={clsx(
        classes.root,
        {
          [classes.edgeStart]: edge === 'start',
          [classes.edgeEnd]: edge === 'end',
          [classes[`size${capitalize(size)}`]]: size !== 'medium',
        },
        className,
      )}
    >
      <SwitchBase
        type="checkbox"
        icon={icon}
        checkedIcon={icon}
        classes={{
          root: clsx(classes.switchBase, classes[`color${capitalize(color)}`]),
          input: classes.input,
          checked: classes.checked,
          disabled: classes.disabled,
        }}
        ref={ref}
        {...other}
      />

  );
});

Switch.propTypes = {
  // ----------------------------- Warning --------------------------------
  // | These PropTypes are generated from the TypeScript type definitions |
  // |     To update them edit the d.ts file and run "yarn proptypes"     |
  // ----------------------------------------------------------------------
  /**
   * If `true`, the component is checked.
   */
  checked: PropTypes.bool,
  /**
   * The icon to display when the component is checked.
   */
  checkedIcon: PropTypes.node,
  /**
   * Override or extend the styles applied to the component.
   */
  classes: PropTypes.object,
  /**
   * @ignore
   */
  className: PropTypes.string,
  /**
   * The color of the component. It supports those theme colors that make sense for this component.
   * @default 'secondary'
   */
  color: PropTypes.oneOf(['default', 'primary', 'secondary']),
  /**
   * The default checked state. Use when the component is not controlled.
   */
  defaultChecked: PropTypes.bool,
  /**
   * If `true`, the component is disabled.
   */
  disabled: PropTypes.bool,
  /**
   * If `true`, the ripple effect is disabled.
   */
  disableRipple: PropTypes.bool,
  /**
   * If given, uses a negative margin to counteract the padding on one
   * side (this is often helpful for aligning the left or right
   * side of the icon with content above or below, without ruining the border
   * size and shape).
   * @default false
   */
  edge: PropTypes.oneOf(['end', 'start', false]),
  /**
   * The icon to display when the component is unchecked.
   */
  icon: PropTypes.node,
  /**
   * The id of the `input` element.
   */
  id: PropTypes.string,
  /**
   * [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) applied to the `input` element.
   */
  inputProps: PropTypes.object,
  /**
   * Pass a ref to the `input` element.
   */
  inputRef: refType,
  /**
   * Callback fired when the state is changed.
   *
   * @param {object} event The event source of the callback.
   * You can pull out the new value by accessing `event.target.value` (string).
   * You can pull out the new checked state by accessing `event.target.checked` (boolean).
   */
  onChange: PropTypes.func,
  /**
   * If `true`, the `input` element is required.
   */
  required: PropTypes.bool,
  /**
   * The size of the component.
   * `small` is equivalent to the dense switch styling.
   * @default 'medium'
   */
  size: PropTypes.oneOf(['medium', 'small']),
  /**
   * The value of the component. The DOM API casts this to a string.
   * The browser uses "on" as the default value.
   */
  value: PropTypes.any,
};

export default withStyles(styles, { name: 'MuiSwitch' })(Switch);
<|endoftext|>


<commit_before>/**
* @license Apache-2.0
*
* Copyright (c) 2022 The Stdlib Authors.
*
* 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';

// MODULES //

var config = require( 'config' );

// MAIN //

/**
* Returns a cookie string for a provided name-value pair.
*
* @private
* @param {string} name - cookie name
* @param {string} value - cookie value
* @param {Object} [options] - cookie options
* @param {string} [options.path] - cookie path
* @param {string} [options.domain] - cookie domain
* @param {number} [options.maxAge] - maximum age (in seconds; takes precedence over `expire`)
* @param {(string|Date)} [options.expires] - cookie expiration date
* @param {boolean} [options.secure] - boolean indicating whether the cookie may only be transmitted over a secure protocol (e.g., HTTPS)
* @param {string} [options.samesite] - specifies whether the browser should send this cookie along with cross-site requests
* @returns {(string|null)} cookie value (or null)
*/
function cookieString( name, value, options ) {
	var opts;
	var keys;
	var tmp;
	var k;
	var v;
	var i;

	opts = {
		'path': config.mount,
		'max-age': 31536000, // 60*60*24*365 (1 year)
		'secure': true,
		'samesite': 'strict'
	};
	if ( arguments.length > 2 ) {
		if ( options.path ) {
			opts.path = options.path;
		}
		if ( options.domain ) {
			opts.domain = options.domain;
		}
		if ( typeof options.maxAge === 'number' ) {
			opts[ 'max-age' ] = options.maxAge;
		}
		if ( options.expires ) {
			if ( options.expires instanceof Date ) {
				opts.expires = options.expires.toUTCString();
			} else {
				opts.expires = options.expires;
			}
		}
		if ( typeof options.secure === 'boolean' ) {
			opts.secure = options.secure;
		}
		if ( options.samesite ) {
			opts.samesite = options.samesite;
		}
	}
	tmp = [ name + '=' + encodeURIComponent( value ) ];
	keys = Object.keys( opts );
	for ( i = 0; i < keys.length; i++ ) {
		k = keys[ i ];
		v = opts[ k ];
		if ( k === 'secure' && v ) {
			tmp.push( k );
		} else {
			tmp.push( k + '=' + v );
		}
	}
	return tmp.join( '; ' );
}

// EXPORTS //

module.exports = cookieString;
<commit_msg>Extend the cookie age to make permanent<commit_after>/**
* @license Apache-2.0
*
* Copyright (c) 2022 The Stdlib Authors.
*
* 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';

// MODULES //

var config = require( 'config' );

// MAIN //

/**
* Returns a cookie string for a provided name-value pair.
*
* @private
* @param {string} name - cookie name
* @param {string} value - cookie value
* @param {Object} [options] - cookie options
* @param {string} [options.path] - cookie path
* @param {string} [options.domain] - cookie domain
* @param {number} [options.maxAge] - maximum age (in seconds; takes precedence over `expire`)
* @param {(string|Date)} [options.expires] - cookie expiration date
* @param {boolean} [options.secure] - boolean indicating whether the cookie may only be transmitted over a secure protocol (e.g., HTTPS)
* @param {string} [options.samesite] - specifies whether the browser should send this cookie along with cross-site requests
* @returns {string} cookie string
*/
function cookieString( name, value, options ) {
	var opts;
	var keys;
	var tmp;
	var k;
	var v;
	var i;

	opts = {
		'path': config.mount,
		'max-age': 315360000, // 60*60*24*365*10 (10 years)
		'secure': true,
		'samesite': 'strict'
	};
	if ( arguments.length > 2 ) {
		if ( options.path ) {
			opts.path = options.path;
		}
		if ( options.domain ) {
			opts.domain = options.domain;
		}
		if ( typeof options.maxAge === 'number' ) {
			opts[ 'max-age' ] = options.maxAge;
		}
		if ( options.expires ) {
			if ( options.expires instanceof Date ) {
				opts.expires = options.expires.toUTCString();
			} else {
				opts.expires = options.expires;
			}
		}
		if ( typeof options.secure === 'boolean' ) {
			opts.secure = options.secure;
		}
		if ( options.samesite ) {
			opts.samesite = options.samesite;
		}
	}
	tmp = [ name + '=' + encodeURIComponent( value ) ];
	keys = Object.keys( opts );
	for ( i = 0; i < keys.length; i++ ) {
		k = keys[ i ];
		v = opts[ k ];
		if ( k === 'secure' && v ) {
			tmp.push( k );
		} else {
			tmp.push( k + '=' + v );
		}
	}
	return tmp.join( '; ' );
}

// EXPORTS //

module.exports = cookieString;
<|endoftext|>


<commit_before>// eslint-disable-next-line max-classes-per-file
import {
  get,
  forEach,
  identity,
  reduce,
  kebabCase,
  find,
  filter,
  isNil,
} from 'lodash'

import MJMLParser from 'mjml-parser-xml'

import shorthandParser, { borderParser } from './helpers/shorthandParser'
import formatAttributes from './helpers/formatAttributes'
import jsonToXML from './helpers/jsonToXML'

import components, { initComponent } from './components'

class Component {
  static getTagName() {
    return kebabCase(this.name)
  }

  static isRawElement() {
    return !!this.rawElement
  }

  static defaultAttributes = {}

  constructor(initialDatas = {}) {
    const {
      attributes = {},
      children = [],
      content = '',
      context = {},
      props = {},
      globalAttributes = {},
    } = initialDatas

    this.props = {
      ...props,
      children,
      content,
    }

    this.attributes = formatAttributes(
      {
        ...this.constructor.defaultAttributes,
        ...globalAttributes,
        ...attributes,
      },
      this.constructor.allowedAttributes,
    )
    this.context = context

    return this
  }

  getChildContext() {
    return this.context
  }

  getAttribute(name) {
    return this.attributes[name]
  }

  getContent() {
    return this.props.content.trim()
  }

  renderMJML(mjml, options = {}) {
    if (typeof mjml === 'string') {
      if (mjml.indexOf('<mjml>') === -1) {
        // supports returning siblings elements from a custom component
        const partialMjml = MJMLParser(`<mjml><mj-body>${mjml}</mj-body></mjml>`, {
          ...options,
          components,
          ignoreIncludes: true,
        })
        const body = find(partialMjml.children, { tagName: 'mj-body' })

        return body.children
          .map(child => this.context.processing(child, this.context))
          .join('')
      }

      mjml = MJMLParser(mjml, {
        ...options,
        components,
        ignoreIncludes: true,
      })
    }

    return this.context.processing(mjml, this.context)
  }
}

export class BodyComponent extends Component {
  // eslint-disable-next-line class-methods-use-this
  getStyles() {
    return {}
  }

  getShorthandAttrValue(attribute, direction) {
    const mjAttributeDirection = this.getAttribute(`${attribute}-${direction}`)
    const mjAttribute = this.getAttribute(attribute)

    if (mjAttributeDirection) {
      return parseInt(mjAttributeDirection, 10)
    }

    if (!mjAttribute) {
      return 0
    }

    return shorthandParser(mjAttribute, direction)
  }

  getShorthandBorderValue(direction) {
    const borderDirection =
      direction && this.getAttribute(`border-${direction}`)
    const border = this.getAttribute('border')

    return borderParser(borderDirection || border || '0')
  }

  getBoxWidths() {
    const { containerWidth } = this.context
    const parsedWidth = parseInt(containerWidth, 10)

    const paddings =
      this.getShorthandAttrValue('padding', 'right') +
      this.getShorthandAttrValue('padding', 'left')

    const borders =
      this.getShorthandBorderValue('right') +
      this.getShorthandBorderValue('left')

    return {
      totalWidth: parsedWidth,
      borders,
      paddings,
      box: parsedWidth - paddings - borders,
    }
  }

  htmlAttributes(attributes) {
    const specialAttributes = {
      style: (v) => this.styles(v),
      default: identity,
    }

    return reduce(
      attributes,
      (output, v, name) => {
        const value = (specialAttributes[name] || specialAttributes.default)(v)

        if (!isNil(value)) {
          return `${output} ${name}="${value}"`
        }

        return output
      },
      '',
    )
  }

  styles(styles) {
    let stylesObject

    if (styles) {
      if (typeof styles === 'string') {
        stylesObject = get(this.getStyles(), styles)
      } else {
        stylesObject = styles
      }
    }

    return reduce(
      stylesObject,
      (output, value, name) => {
        if (!isNil(value)) {
          return `${output}${name}:${value};`
        }
        return output
      },
      '',
    )
  }

  renderChildren(childrens, options = {}) {
    const {
      props = {},
      renderer = (component) => component.render(),
      attributes = {},
      rawXML = false,
    } = options

    childrens = childrens || this.props.children

    if (rawXML) {
      return childrens.map((child) => jsonToXML(child)).join('\n')
    }

    const sibling = childrens.length

    const rawComponents = filter(components, (c) => c.isRawElement())
    const nonRawSiblings = childrens.filter(
      (child) => !find(rawComponents, (c) => c.getTagName() === child.tagName),
    ).length

    let output = ''
    let index = 0

    forEach(childrens, (children) => {
      const component = initComponent({
        name: children.tagName,
        initialDatas: {
          ...children,
          attributes: {
            ...attributes,
            ...children.attributes,
          },
          context: this.getChildContext(),
          props: {
            ...props,
            first: index === 0,
            index,
            last: index + 1 === sibling,
            sibling,
            nonRawSiblings,
          },
        },
      })

      if (component !== null) {
        output += renderer(component)
      }

      index++ // eslint-disable-line no-plusplus
    })

    return output
  }
}

export class HeadComponent extends Component {
  static getTagName() {
    return kebabCase(this.name)
  }

  handlerChildren() {
    const childrens = this.props.children

    return childrens.map((children) => {
      const component = initComponent({
        name: children.tagName,
        initialDatas: {
          ...children,
          context: this.getChildContext(),
        },
      })

      if (!component) {
        // eslint-disable-next-line no-console
        console.error(`No matching component for tag : ${children.tagName}`)
        return null
      }

      if (component.handler) {
        component.handler()
      }

      if (component.render) {
        return component.render()
      }
      return null
    })
  }
}
<commit_msg>Implement fragment suggestion<commit_after>// eslint-disable-next-line max-classes-per-file
import {
  get,
  forEach,
  identity,
  reduce,
  kebabCase,
  find,
  filter,
  isNil,
} from 'lodash'

import MJMLParser from 'mjml-parser-xml'

import shorthandParser, { borderParser } from './helpers/shorthandParser'
import formatAttributes from './helpers/formatAttributes'
import jsonToXML from './helpers/jsonToXML'

import components, { initComponent } from './components'

class Component {
  static getTagName() {
    return kebabCase(this.name)
  }

  static isRawElement() {
    return !!this.rawElement
  }

  static defaultAttributes = {}

  constructor(initialDatas = {}) {
    const {
      attributes = {},
      children = [],
      content = '',
      context = {},
      props = {},
      globalAttributes = {},
    } = initialDatas

    this.props = {
      ...props,
      children,
      content,
    }

    this.attributes = formatAttributes(
      {
        ...this.constructor.defaultAttributes,
        ...globalAttributes,
        ...attributes,
      },
      this.constructor.allowedAttributes,
    )
    this.context = context

    return this
  }

  getChildContext() {
    return this.context
  }

  getAttribute(name) {
    return this.attributes[name]
  }

  getContent() {
    return this.props.content.trim()
  }

  renderMJML(mjml, options = {}) {
    if (typeof mjml === 'string') {
      // supports returning siblings elements from a custom component
      const partialMjml = MJMLParser(`<fragment>${mjml}</fragment>`, {
        ...options,
        components,
        ignoreIncludes: true,
      })
      return partialMjml.children
        .map(child => this.context.processing(child, this.context))
        .join('')
    }

    return this.context.processing(mjml, this.context)
  }
}

export class BodyComponent extends Component {
  // eslint-disable-next-line class-methods-use-this
  getStyles() {
    return {}
  }

  getShorthandAttrValue(attribute, direction) {
    const mjAttributeDirection = this.getAttribute(`${attribute}-${direction}`)
    const mjAttribute = this.getAttribute(attribute)

    if (mjAttributeDirection) {
      return parseInt(mjAttributeDirection, 10)
    }

    if (!mjAttribute) {
      return 0
    }

    return shorthandParser(mjAttribute, direction)
  }

  getShorthandBorderValue(direction) {
    const borderDirection =
      direction && this.getAttribute(`border-${direction}`)
    const border = this.getAttribute('border')

    return borderParser(borderDirection || border || '0')
  }

  getBoxWidths() {
    const { containerWidth } = this.context
    const parsedWidth = parseInt(containerWidth, 10)

    const paddings =
      this.getShorthandAttrValue('padding', 'right') +
      this.getShorthandAttrValue('padding', 'left')

    const borders =
      this.getShorthandBorderValue('right') +
      this.getShorthandBorderValue('left')

    return {
      totalWidth: parsedWidth,
      borders,
      paddings,
      box: parsedWidth - paddings - borders,
    }
  }

  htmlAttributes(attributes) {
    const specialAttributes = {
      style: (v) => this.styles(v),
      default: identity,
    }

    return reduce(
      attributes,
      (output, v, name) => {
        const value = (specialAttributes[name] || specialAttributes.default)(v)

        if (!isNil(value)) {
          return `${output} ${name}="${value}"`
        }

        return output
      },
      '',
    )
  }

  styles(styles) {
    let stylesObject

    if (styles) {
      if (typeof styles === 'string') {
        stylesObject = get(this.getStyles(), styles)
      } else {
        stylesObject = styles
      }
    }

    return reduce(
      stylesObject,
      (output, value, name) => {
        if (!isNil(value)) {
          return `${output}${name}:${value};`
        }
        return output
      },
      '',
    )
  }

  renderChildren(childrens, options = {}) {
    const {
      props = {},
      renderer = (component) => component.render(),
      attributes = {},
      rawXML = false,
    } = options

    childrens = childrens || this.props.children

    if (rawXML) {
      return childrens.map((child) => jsonToXML(child)).join('\n')
    }

    const sibling = childrens.length

    const rawComponents = filter(components, (c) => c.isRawElement())
    const nonRawSiblings = childrens.filter(
      (child) => !find(rawComponents, (c) => c.getTagName() === child.tagName),
    ).length

    let output = ''
    let index = 0

    forEach(childrens, (children) => {
      const component = initComponent({
        name: children.tagName,
        initialDatas: {
          ...children,
          attributes: {
            ...attributes,
            ...children.attributes,
          },
          context: this.getChildContext(),
          props: {
            ...props,
            first: index === 0,
            index,
            last: index + 1 === sibling,
            sibling,
            nonRawSiblings,
          },
        },
      })

      if (component !== null) {
        output += renderer(component)
      }

      index++ // eslint-disable-line no-plusplus
    })

    return output
  }
}

export class HeadComponent extends Component {
  static getTagName() {
    return kebabCase(this.name)
  }

  handlerChildren() {
    const childrens = this.props.children

    return childrens.map((children) => {
      const component = initComponent({
        name: children.tagName,
        initialDatas: {
          ...children,
          context: this.getChildContext(),
        },
      })

      if (!component) {
        // eslint-disable-next-line no-console
        console.error(`No matching component for tag : ${children.tagName}`)
        return null
      }

      if (component.handler) {
        component.handler()
      }

      if (component.render) {
        return component.render()
      }
      return null
    })
  }
}
<|endoftext|>


<commit_before>
import fs from 'fs'

const includes = /<mj-include\s+path='(.*\.mjml)'\s*(\/>|>\s*<\/mj-include>)/g

const getContent = input =>
  input
    .replace(/<mjml>[\n\s\t]+<mj-body>[\n\s\t]+<mj-container>/, '')
    .replace(/<\/mj-container>[\n\s\t]+<\/mj-body>[\n\s\t]+<\/mjml>/, '')

export default mjml => mjml.replace(includes, (_, path) => {
  const template = fs.readFileSync(path, 'utf8')
  const content = getContent(template)

  if (!content) { throw new Error(`Error while parsing file: ${path}`) }

  return content
})

/*
const ret = translate(`
  <mjml>
    <mj-body>
      <mj-container>
        <mj-include path='hello.mjml' />
        <mj-include path='test.mjml' />
      </mj-container>
    </mj-body>
  </mjml>
`)

console.log(ret)
*/
<commit_msg>mjml extensions<commit_after>
import fs from 'fs'

const includes = /<mj-include\s+path='(.*\.mjml)'\s*(\/>|>\s*<\/mj-include>)/g

const getContent = input =>
  input
    .replace(/<mjml>[\n\s\t]+<mj-body>[\n\s\t]+<mj-container>/, '')
    .replace(/<\/mj-container>[\n\s\t]+<\/mj-body>[\n\s\t]+<\/mjml>/, '')

export default mjml => mjml.replace(includes, (_, path) => {

  const mjmlExtension = file => file.trim().match(/.mjml$/) && file || `${file}.mjml`

  const template = fs.readFileSync(mjmlExtension(path), 'utf8')
  const content = getContent(template)

  if (!content) { throw new Error(`Error while parsing file: ${path}`) }

  return content
})

/*
const ret = translate(`
  <mjml>
    <mj-body>
      <mj-container>
        <mj-include path='hello.mjml' />
        <mj-include path='test.mjml' />
      </mj-container>
    </mj-body>
  </mjml>
`)

console.log(ret)
*/
<|endoftext|>


<commit_before>import joi from 'joi'
import pify from 'pify'
import joinList from 'join-component'

const parser = joi.extend([
  {
    base: joi.string().replace(/^@/, ''),
    name: 'user',
    rules: []
  }
])

const validate = pify(joi.validate)

parser.parse = (input, schema) => {
  return validate(
    input,
    parser.array()
      .required()
      .ordered(schema)
      .error((errors) => {
        const missing = errors.find((err) => err.type === 'array.includesRequiredKnowns')

        if (missing) {
          return new Error(`Missing parameters: ${joinList(missing.context.knownMisses)}`)
        }

        return errors
      })
  )
}

export default parser
<commit_msg>core: Accept excess arguments.<commit_after>import joi from 'joi'
import pify from 'pify'
import joinList from 'join-component'

const parser = joi.extend([
  {
    base: joi.string().replace(/^@/, ''),
    name: 'user',
    rules: []
  }
])

const validate = pify(joi.validate)

parser.parse = (input, schema) => {
  return validate(
    input,
    parser.array()
      .required()
      .ordered(schema)
      .items(parser.any()) // Accept excess arguments.
      .error((errors) => {
        const missing = errors.find((err) => err.type === 'array.includesRequiredKnowns')

        if (missing) {
          return new Error(`Missing parameters: ${joinList(missing.context.knownMisses)}`)
        }

        return errors
      })
  )
}

export default parser
<|endoftext|>


<commit_before>'use strict';

var request = require('sync-request');

//de modificat
//const API_HOST = process.env.UI_API_URL;
const API_HOST = 'https://lapachet-app.herokuapp.com/api';
const PRODUCTS_URL = '/category_products?date={date}&categoryId={categoryId}';
const MENU_URL = '/menus/{date}';
const CATEGORIES_URL = '/categories/daily?date={date}';
const INTERVALS_URL = '/categories/intervals';

const HTTP_REQUEST_GET = 'GET';
const HTTP_REQUEST_POST = 'POST';

function ApiService() {
}

ApiService.prototype = {
    getSellingProducts: function (categoryId) {
        var date = new Date().toISOString().replace(/T.+/, ' ');
        var url = PRODUCTS_URL.replace('{date}', date);
        url = url.replace('{categoryId}', categoryId);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getProduct: function (id) {
        return {
            id: 1,
            name: "Produs 1",
            title: "Titlu produs 1"
        };
    },
    getSellingMenus: function (date) {
        var url = MENU_URL.replace('{data}', date);

        return this.request(url, HTTP_REQUEST_GET);
    },
    getSellingProductCategories: function () {
        var date = new Date().toISOString().replace(/T.+/, ' ');
        var url = CATEGORIES_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getIntervals: function () {

        return [
            {
                id: 1,
                start: 19,
                end: 20
            },
            {
                id: 2,
                start: 20,
                end: 21
            },
            {
                id: 3,
                start: 21,
                end: 22
            },
            {
                id: 4,
                start: 22,
                end: 23
            }
        ];
        return this.request(INTERVALS_URL, HTTP_REQUEST_GET);
    },
    getUrl: function (url) {
        return API_HOST + url;
    },
    request: function (method, url) {

        var requestUrl = this.getUrl(url);
        var res = request(HTTP_REQUEST_GET, requestUrl);
        console.log(requestUrl);
        return JSON.parse(res.body.toString('utf-8'));
    }
};

module.exports.getInstance = function () {
    return new ApiService();
};<commit_msg>debug<commit_after>'use strict';

var request = require('sync-request');

//de modificat
//const API_HOST = process.env.UI_API_URL;
const API_HOST = 'https://lapachet-app.herokuapp.com/api';
const PRODUCTS_URL = '/category_products?date={date}&categoryId={categoryId}';
const MENU_URL = '/menus/{date}';
const CATEGORIES_URL = '/categories/daily?date={date}';
const INTERVALS_URL = '/categories/intervals';

const HTTP_REQUEST_GET = 'GET';
const HTTP_REQUEST_POST = 'POST';

function ApiService() {
}

ApiService.prototype = {
    getSellingProducts: function (categoryId) {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = PRODUCTS_URL.replace('{date}', date);
        url = url.replace('{categoryId}', categoryId);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getProduct: function (id) {
        return {
            id: 1,
            name: "Produs 1",
            title: "Titlu produs 1"
        };
    },
    getSellingMenus: function (date) {
        var url = MENU_URL.replace('{data}', date);

        return this.request(url, HTTP_REQUEST_GET);
    },
    getSellingProductCategories: function () {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = CATEGORIES_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getIntervals: function () {

        return [
            {
                id: 1,
                start: 19,
                end: 20
            },
            {
                id: 2,
                start: 20,
                end: 21
            },
            {
                id: 3,
                start: 21,
                end: 22
            },
            {
                id: 4,
                start: 22,
                end: 23
            }
        ];
        return this.request(INTERVALS_URL, HTTP_REQUEST_GET);
    },
    getUrl: function (url) {
        return API_HOST + url;
    },
    request: function (method, url) {

        var requestUrl = this.getUrl(url);
        var res = request(HTTP_REQUEST_GET, requestUrl);
        console.log(requestUrl);
        return JSON.parse(res.body.toString('utf-8'));
    }
};

module.exports.getInstance = function () {
    return new ApiService();
};<|endoftext|>


<commit_before>import canUseDOM from 'dom-helpers/util/inDOM'
import activeElement from 'dom-helpers/activeElement'
import PropTypes from 'prop-types'
import React from 'react'
import { findDOMNode } from 'react-dom'
import { polyfill as polyfillLifecycles } from 'react-lifecycles-compat'

import Input from './Input'
import * as Props from './util/Props'
import * as CustomPropTypes from './util/PropTypes'
import { number as numberLocalizer } from './util/localizers'

let getFormat = props => numberLocalizer.getFormat('default', props.format)

let isSign = val => (val || '').trim() === '-'

function isPaddedZeros(str, culture) {
  let localeChar = numberLocalizer.decimalChar(null, culture)
  let [_, decimals] = str.split(localeChar)

  return !!(decimals && decimals.match(/0+$/))
}

function isAtDelimiter(num, str, culture) {
  let localeChar = numberLocalizer.decimalChar(null, culture),
    lastIndex = str.length - 1,
    char

  if (str.length < 1) return false

  char = str[lastIndex]

  return !!(char === localeChar && str.indexOf(char) === lastIndex)
}

@polyfillLifecycles
class NumberPickerInput extends React.Component {
  static defaultProps = {
    value: null,
    editing: false,
  }

  static propTypes = {
    value: PropTypes.number,
    editing: PropTypes.bool,
    placeholder: PropTypes.string,

    format: CustomPropTypes.numberFormat,

    parse: PropTypes.func,
    culture: PropTypes.string,

    min: PropTypes.number,
    max: PropTypes.number,

    disabled: CustomPropTypes.disabled,
    readOnly: CustomPropTypes.disabled,

    onChange: PropTypes.func.isRequired,
  }

  state = {}

  getSnapshotBeforeUpdate({ editing }) {
    return {
      reselectText: !editing && this.props.editing && this.isSelectingAllText(),
    }
  }

  static getDerivedStateFromProps(nextProps) {
    let { value, culture, editing } = nextProps

    let decimal = numberLocalizer.decimalChar(null, culture)
    let format = getFormat(nextProps)

    if (value == null || isNaN(value)) value = ''
    else
      value = editing
        ? ('' + value).replace('.', decimal)
        : numberLocalizer.format(value, format, culture)

    return {
      stringValue: '' + value,
    }
  }

  componentDidUpdate(_, __, { reselectText }) {
    if (reselectText) findDOMNode(this).select()
  }

  // this intermediate state is for when one runs into
  // the decimal or are typing the number
  setStringValue(stringValue) {
    this.setState({ stringValue })
  }

  handleBlur = event => {
    var str = this.state.stringValue,
      number = this.parseNumber(str)

    // if number is below the min
    // we need to flush low values and decimal stops, onBlur means i'm done inputing
    if (this.isIntermediateValue(number, str)) {
      if (isNaN(number)) {
        number = null
      }
      this.props.onChange(number, event)
    }
  }

  handleChange = event => {
    let { value, onChange } = this.props

    let stringValue = event.target.value,
      numberValue = this.parseNumber(stringValue)

    let isIntermediate = this.isIntermediateValue(numberValue, stringValue)

    if (stringValue == null || stringValue.trim() === '') {
      this.setStringValue('')
      onChange(null, event)

      return
    }
    // order here matters a lot
    if (isIntermediate) {
      this.setStringValue(stringValue)
    } else if (numberValue !== value) {
      onChange(numberValue, event)
    } else if (stringValue != this.state.stringValue) {
      this.setStringValue(stringValue)
    }
  }

  isIntermediateValue(num, str) {
    let { culture, min } = this.props

    return !!(
      num < min ||
      isSign(str) ||
      isAtDelimiter(num, str, culture) ||
      isPaddedZeros(str, culture)
    )
  }

  isSelectingAllText() {
    const node = canUseDOM && findDOMNode(this)
    return (
      canUseDOM &&
      activeElement() === node &&
      node.selectionStart === 0 &&
      node.selectionEnd === node.value.length
    )
  }

  parseNumber(strVal) {
    let { culture, parse: userParse } = this.props

    let delimChar = numberLocalizer.decimalChar(null, culture)

    if (userParse) return userParse(strVal, culture)

    strVal = strVal.replace(delimChar, '.')
    strVal = parseFloat(strVal)

    return strVal
  }

  render() {
    let { disabled, readOnly, placeholder, min, max } = this.props

    let value = this.state.stringValue
    let props = Props.omitOwn(this)

    return (
      <Input
        {...props}
        className="rw-widget-input"
        onChange={this.handleChange}
        onBlur={this.handleBlur}
        aria-valuenow={value}
        aria-valuemin={isFinite(min) ? min : null}
        aria-valuemax={isFinite(max) ? max : null}
        disabled={disabled}
        readOnly={readOnly}
        placeholder={placeholder}
        value={value}
      />
    )
  }
}

export default NumberPickerInput
<commit_msg>fix: problem typing decimals in NumberPicker (#811). (#880)<commit_after>import canUseDOM from 'dom-helpers/util/inDOM'
import activeElement from 'dom-helpers/activeElement'
import PropTypes from 'prop-types'
import React from 'react'
import { findDOMNode } from 'react-dom'
import { polyfill as polyfillLifecycles } from 'react-lifecycles-compat'

import Input from './Input'
import * as Props from './util/Props'
import * as CustomPropTypes from './util/PropTypes'
import { number as numberLocalizer } from './util/localizers'

let getFormat = props => numberLocalizer.getFormat('default', props.format)

let isSign = val => (val || '').trim() === '-'

function isPaddedZeros(str, culture) {
  let localeChar = numberLocalizer.decimalChar(null, culture)
  let [_, decimals] = str.split(localeChar)

  return !!(decimals && decimals.match(/0+$/))
}

function isAtDelimiter(num, str, culture) {
  let localeChar = numberLocalizer.decimalChar(null, culture),
    lastIndex = str.length - 1,
    char

  if (str.length < 1) return false

  char = str[lastIndex]

  return !!(char === localeChar && str.indexOf(char) === lastIndex)
}

@polyfillLifecycles
class NumberPickerInput extends React.Component {
  static defaultProps = {
    value: null,
    editing: false,
  }

  static propTypes = {
    value: PropTypes.number,
    editing: PropTypes.bool,
    placeholder: PropTypes.string,

    format: CustomPropTypes.numberFormat,

    parse: PropTypes.func,
    culture: PropTypes.string,

    min: PropTypes.number,
    max: PropTypes.number,

    disabled: CustomPropTypes.disabled,
    readOnly: CustomPropTypes.disabled,

    onChange: PropTypes.func.isRequired,
  }

  state = {}

  getSnapshotBeforeUpdate({ editing }) {
    return {
      reselectText: !editing && this.props.editing && this.isSelectingAllText(),
    }
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    let { value, culture, editing } = nextProps

    let decimal = numberLocalizer.decimalChar(null, culture)
    let format = getFormat(nextProps)

    if (value == null || isNaN(value)) value = ''
    else
      value = editing
        ? ('' + value).replace('.', decimal)
        : numberLocalizer.format(value, format, culture)

    let stringValue = '' + value

    if (prevState.lastValueFromProps !== stringValue)
      return {
        stringValue,
        lastValueFromProps: stringValue,
      }

    return null
  }

  componentDidUpdate(_, __, { reselectText }) {
    if (reselectText) findDOMNode(this).select()
  }

  // this intermediate state is for when one runs into
  // the decimal or are typing the number
  setStringValue(stringValue) {
    this.setState({ stringValue })
  }

  handleBlur = event => {
    var str = this.state.stringValue,
      number = this.parseNumber(str)

    // if number is below the min
    // we need to flush low values and decimal stops, onBlur means i'm done inputing
    if (this.isIntermediateValue(number, str)) {
      if (isNaN(number)) {
        number = null
      }
      this.props.onChange(number, event)
    }
  }

  handleChange = event => {
    let { value, onChange } = this.props

    let stringValue = event.target.value,
      numberValue = this.parseNumber(stringValue)

    let isIntermediate = this.isIntermediateValue(numberValue, stringValue)

    if (stringValue == null || stringValue.trim() === '') {
      this.setStringValue('')
      onChange(null, event)

      return
    }
    // order here matters a lot
    if (isIntermediate) {
      this.setStringValue(stringValue)
    } else if (numberValue !== value) {
      onChange(numberValue, event)
    } else if (stringValue != this.state.stringValue) {
      this.setStringValue(stringValue)
    }
  }

  isIntermediateValue(num, str) {
    let { culture, min } = this.props

    return !!(
      num < min ||
      isSign(str) ||
      isAtDelimiter(num, str, culture) ||
      isPaddedZeros(str, culture)
    )
  }

  isSelectingAllText() {
    const node = canUseDOM && findDOMNode(this)
    return (
      canUseDOM &&
      activeElement() === node &&
      node.selectionStart === 0 &&
      node.selectionEnd === node.value.length
    )
  }

  parseNumber(strVal) {
    let { culture, parse: userParse } = this.props

    let delimChar = numberLocalizer.decimalChar(null, culture)

    if (userParse) return userParse(strVal, culture)

    strVal = strVal.replace(delimChar, '.')
    strVal = parseFloat(strVal)

    return strVal
  }

  render() {
    let { disabled, readOnly, placeholder, min, max } = this.props

    let value = this.state.stringValue
    let props = Props.omitOwn(this)

    return (
      <Input
        {...props}
        className="rw-widget-input"
        onChange={this.handleChange}
        onBlur={this.handleBlur}
        aria-valuenow={value}
        aria-valuemin={isFinite(min) ? min : null}
        aria-valuemax={isFinite(max) ? max : null}
        disabled={disabled}
        readOnly={readOnly}
        placeholder={placeholder}
        value={value}
      />
    )
  }
}

export default NumberPickerInput
<|endoftext|>


<commit_before>import { isFunction, uniqueId, remove, every, find, isNumber, isString, keys, assign, noop, isEqual, pick } from 'lodash'
import { getNodes } from '../utils/context'
import { contains } from '../utils/dom'

let handlerBindings = []
const bindingKey = '__veui_outside__'

document.addEventListener('click', e => {
  handlerBindings.forEach(item => {
    item[bindingKey] && item[bindingKey].handler(e)
  })
}, true)

function getElementsByRefs (refs, context) {
  const elements = []
  refs.forEach((ref) => {
    elements.push(...getNodes(ref, context))
  })
  return elements
}

function parseParams (el, arg, modifiers, value, context) {
  let includeTargets
  let handler
  let trigger
  // delay 表示如果鼠标移动到 includeTargets 元素之外多少秒之后，才会触发 handler
  let delay

  // 如果 value 是 Function 的话，其余参数就尽量从 modifier、arg 里面去解析
  // 否则从value里面去解析
  if (isFunction(value)) {
    handler = value

    const refs = arg ? arg.split(',') : []
    includeTargets = [el, ...getElementsByRefs(refs, context)]

    trigger = modifiers.hover ? 'hover' : 'click'

    delay = find(keys(modifiers), key => isNumber(parseInt(key, 10)) && modifiers[key])
    delay = delay ? parseInt(delay, 10) : 0
  } else {
    const normalizedValue = value || {}
    handler = isFunction(normalizedValue.handler) ? normalizedValue.handler : noop

    const refs = Array.isArray(normalizedValue.refs) ? normalizedValue.refs
      : (isString(normalizedValue.refs) ? normalizedValue.refs.split(',') : [normalizedValue.refs])
    includeTargets = [el, ...getElementsByRefs(refs, context)]

    trigger = normalizedValue.trigger === 'hover' ? 'hover' : 'click'

    delay = parseInt(normalizedValue.delay, 10)
    if (isNaN(delay)) {
      delay = 0
    }
  }

  return {
    includeTargets,
    handler,
    trigger,
    delay
  }
}

function generate (el, { includeTargets, handler, trigger, delay }) {
  return function (e) {
    // click 模式，直接判断元素包含情况
    if (e.type === trigger && every(includeTargets, element => !contains(element, e.target))) {
      handler(e)
    }
  }
}

function bindHover (el, { includeTargets, handler, delay }) {
  unbindHover(el)

  const bindingData = assign(
    {},
    el[bindingKey] || {},
    {
      includeTargets,
      handler,
      delay,
      trigger: 'hover',
      hoverData: {
        state: 'ready',
        prevEvent: null,
        timer: null
      },
      mouseenterHandler: event => {
        bindingData.hoverData.state = 'in'
        bindingData.hoverData.prevEvent = event
      },
      mouseleaveHandler: event => {
        if (every(includeTargets, target => !contains(target, event.target))) {
          return
        }

        bindingData.hoverData.state = 'out'
        bindingData.hoverData.prevEvent = event

        clearTimeout(bindingData.hoverData.timer)
        let check = () => {
          // 超时没移回，就要触发handler了
          if (bindingData.hoverData.state === 'out') {
            // 此处用最后一次记录的event对象
            handler(bindingData.hoverData.prevEvent)
            // 重置状态
            bindingData.hoverData.state = 'ready'
          }
        }
        bindingData.hoverData.timer = bindingData.delay ? setTimeout(check, bindingData.delay) : check()
      }
    }
  )

  // 所有目标元素都绑定一遍事件
  bindHoverEvents(bindingData)

  el[bindingKey] = bindingData
}

function bindHoverEvents (bindingData) {
  bindingData.includeTargets.forEach((target) => {
    target.addEventListener('mouseenter', bindingData.mouseenterHandler)
    target.addEventListener('mouseleave', bindingData.mouseleaveHandler)
  })
}

function unbindHoverEvents (bindingData) {
  bindingData.includeTargets.forEach((target) => {
    target.removeEventListener('mouseenter', bindingData.mouseenterHandler)
    target.removeEventListener('mouseleave', bindingData.mouseleaveHandler)
  })
}

function unbindHover (el) {
  const bindingData = el[bindingKey]
  if (bindingData && bindingData.trigger === 'hover') {
    unbindHoverEvents(bindingData)
    el[bindingKey] = null
    clearTimeout(bindingData.hoverData.timer)
  }
}

function clear (el) {
  remove(handlerBindings, item => el[bindingKey] && item[bindingKey].id === el[bindingKey].id)
  unbindHover(el)
}

function refresh (el, { value, arg, modifiers, oldValue }, vnode) {
  const params = parseParams(el, arg, modifiers, value, vnode.context)

  // 真正发生了变化，才重刷
  let fields = params.trigger === 'click'
    ? ['includeTargets', 'trigger']
    : ['includeTargets', 'trigger', 'delay']
  if (isEqual(pick(el[bindingKey], fields), pick(params, fields))) {
    return
  }

  clear(el)
  if (params.trigger === 'click') {
    el[bindingKey] = {
      id: uniqueId('veui-outside-'),
      handler: generate(el, params),
      trigger: 'click'
    }
    handlerBindings.push(el)
  } else if (params.trigger === 'hover') {
    bindHover(el, params)
  }
}

export default {
  bind: refresh,
  componentUpdated: refresh,
  unbind: clear
}
<commit_msg>使用 mouseout<commit_after>import { isFunction, uniqueId, remove, find, isNumber, isString, keys, noop, isEqual, pick } from 'lodash'
import { getNodes } from '../utils/context'
import { contains } from '../utils/dom'

let clickHandlerBindings = []
const clickBindingKey = '__veui_click_outside__'
document.addEventListener('click', e => {
  clickHandlerBindings.forEach(item => {
    item[clickBindingKey] && item[clickBindingKey].handler(e)
  })
}, true)

let hoverHandlerBindings = []
const hoverBindingKey = '__veui_hover_outside__'
document.addEventListener('mouseout', e => {
  hoverHandlerBindings.forEach(item => {
    item[hoverBindingKey] && item[hoverBindingKey].handler(e)
  })
})

function getElementsByRefs (refs, context) {
  const elements = []
  refs.forEach((ref) => {
    elements.push(...getNodes(ref, context))
  })
  return elements
}

function parseParams (el, arg, modifiers, value, context) {
  let refs
  let handler
  let trigger
  // delay 表示如果鼠标移动到 includeTargets 元素之外多少秒之后，才会触发 handler
  let delay

  // 如果 value 是 Function 的话，其余参数就尽量从 modifier、arg 里面去解析
  // 否则从value里面去解析
  if (isFunction(value)) {
    handler = value

    refs = arg ? arg.split(',') : []

    trigger = modifiers.hover ? 'hover' : 'click'

    delay = find(keys(modifiers), key => isNumber(parseInt(key, 10)) && modifiers[key])
    delay = delay ? parseInt(delay, 10) : 0
  } else {
    const normalizedValue = value || {}
    handler = isFunction(normalizedValue.handler) ? normalizedValue.handler : noop

    refs = Array.isArray(normalizedValue.refs) ? normalizedValue.refs
      : (isString(normalizedValue.refs) ? normalizedValue.refs.split(',') : [normalizedValue.refs])

    trigger = normalizedValue.trigger === 'hover' ? 'hover' : 'click'

    delay = parseInt(normalizedValue.delay, 10)
    if (isNaN(delay)) {
      delay = 0
    }
  }

  return {
    refs,
    handler,
    trigger,
    delay
  }
}

function generate (el, { handler, trigger, delay, refs }, context) {
  if (trigger === 'click') {
    return function (e) {
      // click 模式，直接判断元素包含情况
      let includeTargets = [el, ...getElementsByRefs(refs, context)]
      if (e.type === trigger && !includeTargets.some(element => contains(element, e.target))) {
        handler(e)
      }
    }
  }

  if (trigger === 'hover') {
    return function (e) {
      let includeTargets = [el, ...getElementsByRefs(refs, context)]
      if (includeTargets.some(target => contains(target, e.target)) &&
        !includeTargets.some(target => contains(target, e.relatedTarget))
      ) {
        if (delay) {
          el[hoverBindingKey].timer = setTimeout(() => handler(e), delay)
        } else {
          handler(e)
        }
      }
    }
  }
}

function clear (el) {
  if (el[clickBindingKey]) {
    remove(clickHandlerBindings, item => item[clickBindingKey].id === el[clickBindingKey].id)
    el[clickBindingKey] = null
  }

  if (el[hoverBindingKey]) {
    remove(hoverHandlerBindings, item => item[hoverBindingKey].id === el[hoverBindingKey].id)
    clearTimeout(el[hoverBindingKey].timer)
    el[hoverBindingKey] = null
  }
}

function refresh (el, { value, arg, modifiers, oldValue }, vnode) {
  const params = parseParams(el, arg, modifiers, value, vnode.context)

  // 真正发生了变化，才重刷
  let fields = params.trigger === 'click'
    ? ['refs', 'trigger']
    : ['refs', 'trigger', 'delay']
  if (isEqual(pick(el[params.trigger === 'click' ? clickBindingKey : hoverBindingKey], fields), pick(params, fields))) {
    return
  }

  clear(el)
  if (params.trigger === 'click') {
    el[clickBindingKey] = {
      id: uniqueId('veui-outside-'),
      handler: generate(el, params, vnode.context),
      trigger: 'click',
      refs: params.refs
    }
    clickHandlerBindings.push(el)
  } else if (params.trigger === 'hover') {
    el[hoverBindingKey] = {
      id: uniqueId('veui-outside-'),
      handler: generate(el, params, vnode.context),
      trigger: 'hover',
      delay: params.delay,
      refs: params.refs
    }
    hoverHandlerBindings.push(el)
  }
}

export default {
  bind: refresh,
  update: refresh,
  unbind: clear
}
<|endoftext|>


<commit_before>import Vue from '../instance/index'
import config from '../config'
import { warn } from './debug'
import { set } from '../observer/index'
import {
  extend,
  isObject,
  isArray,
  isPlainObject,
  hasOwn,
  camelize,
  isBuiltInTag
} from 'shared/util'

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 *
 * All strategy functions follow the same signature:
 *
 * @param {*} parentVal
 * @param {*} childVal
 * @param {Vue} [vm]
 */

const strats = config.optionMergeStrategies = Object.create(null)

/**
 * Helper that recursively merges two data objects together.
 */

function mergeData (to, from) {
  let key, toVal, fromVal
  for (key in from) {
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isObject(toVal) && isObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 */

strats.data = function (parentVal, childVal, vm) {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        childVal.call(this),
        parentVal.call(this)
      )
    }
  } else if (parentVal || childVal) {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm)
        : undefined
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

/**
 * El
 */

strats.el = function (parentVal, childVal, vm) {
  if (!vm && childVal && typeof childVal !== 'function') {
    process.env.NODE_ENV !== 'production' && warn(
      'The "el" option should be a function ' +
      'that returns a per-instance value in component ' +
      'definitions.',
      vm
    )
    return
  }
  const ret = childVal || parentVal
  // invoke the element factory if this is instance merge
  return vm && typeof ret === 'function'
    ? ret.call(vm)
    : ret
}

/**
 * Hooks and param attributes are merged as arrays.
 */

function mergeHook (parentVal, childVal) {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

config._lifecycleHooks.forEach(hook => {
  strats[hook] = mergeHook
})

/**
 * Assets
 *
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 */

function mergeAssets (parentVal, childVal) {
  const res = Object.create(parentVal || null)
  return childVal
    ? extend(res, childVal)
    : res
}

config._assetTypes.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */

strats.watch = function (parentVal, childVal) {
  if (!childVal) return parentVal
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : [child]
  }
  return ret
}

/**
 * Other object hashes.
 */

strats.props =
strats.methods =
strats.computed = function (parentVal, childVal) {
  if (!childVal) return parentVal
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  extend(ret, childVal)
  return ret
}

/**
 * Default strategy.
 */

const defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
}

/**
 * Make sure component options get converted to actual
 * constructors.
 *
 * @param {Object} options
 */

function guardComponents (options) {
  if (options.components) {
    const components = options.components
    let def
    for (const key in components) {
      if (isBuiltInTag(key) || config.isReservedTag(key)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Do not use built-in or reserved HTML elements as component ' +
          'id: ' + key
        )
        continue
      }
      def = components[key]
      if (isPlainObject(def)) {
        components[key] = Vue.extend(def)
      }
    }
  }
}

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 *
 * @param {Object} options
 */

function guardProps (options) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  }
  options.props = res
}

function guardDirectives (options) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      if (typeof dirs[key] === 'function') {
        dirs[key] = { update: dirs[key] }
      }
    }
  }
}

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 *
 * @param {Object} parent
 * @param {Object} child
 * @param {Vue} [vm] - if vm is present, indicates this is
 *                     an instantiation merge.
 */

export function mergeOptions (parent, child, vm) {
  guardComponents(child)
  guardProps(child)
  guardDirectives(child)
  if (process.env.NODE_ENV !== 'production') {
    if (child.propsData && !vm) {
      warn('propsData can only be used as an instantiation option.')
    }
  }
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = typeof extendsFrom === 'function'
      ? mergeOptions(parent, extendsFrom.options, vm)
      : mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 *
 * @param {Object} options
 * @param {String} type
 * @param {String} id
 * @param {Boolean} warnMissing
 * @return {Object|Function}
 */

export function resolveAsset (options, type, id, warnMissing) {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  let camelizedId
  const res = assets[id] ||
    // camelCase ID
    assets[camelizedId = camelize(id)] ||
    // Pascal Case ID
    assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)]
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    )
  }
  return res
}
<commit_msg>add warnings for options with restrictions<commit_after>import Vue from '../instance/index'
import config from '../config'
import { warn } from './debug'
import { set } from '../observer/index'
import {
  extend,
  isObject,
  isArray,
  isPlainObject,
  hasOwn,
  camelize,
  isBuiltInTag
} from 'shared/util'

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 *
 * All strategy functions follow the same signature:
 *
 * @param {*} parentVal
 * @param {*} childVal
 * @param {Vue} [vm]
 */

const strats = config.optionMergeStrategies = Object.create(null)

/**
 * Options with restrictions
 */

if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }

  strats.name = function (parent, child, vm) {
    if (vm) {
      warn(
        'options "name" can only be used as a component definition option, ' +
        'not during instance creation.'
      )
    }
    return defaultStrat(parent, child)
  }
}

/**
 * Helper that recursively merges two data objects together.
 */

function mergeData (to, from) {
  let key, toVal, fromVal
  for (key in from) {
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isObject(toVal) && isObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 */

strats.data = function (parentVal, childVal, vm) {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        childVal.call(this),
        parentVal.call(this)
      )
    }
  } else if (parentVal || childVal) {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm)
        : undefined
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

/**
 * Hooks and param attributes are merged as arrays.
 */

function mergeHook (parentVal, childVal) {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

config._lifecycleHooks.forEach(hook => {
  strats[hook] = mergeHook
})

/**
 * Assets
 *
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 */

function mergeAssets (parentVal, childVal) {
  const res = Object.create(parentVal || null)
  return childVal
    ? extend(res, childVal)
    : res
}

config._assetTypes.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */

strats.watch = function (parentVal, childVal) {
  if (!childVal) return parentVal
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : [child]
  }
  return ret
}

/**
 * Other object hashes.
 */

strats.props =
strats.methods =
strats.computed = function (parentVal, childVal) {
  if (!childVal) return parentVal
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  extend(ret, childVal)
  return ret
}

/**
 * Default strategy.
 */

const defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
}

/**
 * Make sure component options get converted to actual
 * constructors.
 *
 * @param {Object} options
 */

function guardComponents (options) {
  if (options.components) {
    const components = options.components
    let def
    for (const key in components) {
      if (isBuiltInTag(key) || config.isReservedTag(key)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Do not use built-in or reserved HTML elements as component ' +
          'id: ' + key
        )
        continue
      }
      def = components[key]
      if (isPlainObject(def)) {
        components[key] = Vue.extend(def)
      }
    }
  }
}

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 *
 * @param {Object} options
 */

function guardProps (options) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  }
  options.props = res
}

function guardDirectives (options) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      if (typeof dirs[key] === 'function') {
        dirs[key] = { update: dirs[key] }
      }
    }
  }
}

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 *
 * @param {Object} parent
 * @param {Object} child
 * @param {Vue} [vm] - if vm is present, indicates this is
 *                     an instantiation merge.
 */

export function mergeOptions (parent, child, vm) {
  guardComponents(child)
  guardProps(child)
  guardDirectives(child)
  if (process.env.NODE_ENV !== 'production') {
    if (child.propsData && !vm) {
      warn('propsData can only be used as an instantiation option.')
    }
  }
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = typeof extendsFrom === 'function'
      ? mergeOptions(parent, extendsFrom.options, vm)
      : mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 *
 * @param {Object} options
 * @param {String} type
 * @param {String} id
 * @param {Boolean} warnMissing
 * @return {Object|Function}
 */

export function resolveAsset (options, type, id, warnMissing) {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  let camelizedId
  const res = assets[id] ||
    // camelCase ID
    assets[camelizedId = camelize(id)] ||
    // Pascal Case ID
    assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)]
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    )
  }
  return res
}
<|endoftext|>


<commit_before>/**
 * @file Tool definition for the PC super-crazy visual flood fill tool.
 **/
"use strict";

module.exports = function(paper) {
  var tool = new paper.Tool();

  // Paper global extenders
  var Path = paper.Path;
  var Point = paper.Point;
  var Rectangle = paper.Rectangle;
  var project = paper.project;
  var CompoundPath = paper.CompoundPath;

  // Tool identification (for building out tool palette)
  tool.name = 'tools.fill';
  tool.key = 'fill';
  tool.cursorColors = true; // Different icons/cursor for each color?
  tool.cursorOffset = '1 9'; // Position for cursor point
  tool.pathSimplifyAmt = 0.7; // How much to simplify pixel boundary paths
  tool.islandThreshold = 10; // Max distance between fill boundary nodes
  tool.alphaBitmapThreshold = 50; // 0-255, value that's considered visible

  // Tool vars
  var ndarray = require('ndarray');
  var flood = require('n-dimensional-flood-fill');

  tool.onMouseDown = function(event) {
    var fillPath = floodFill(event.point);
    if (fillPath !== false) {
      fillPath.fillColor = paper.pancakeShades[paper.pancakeCurrentShade]
      fillPath.data.color = paper.pancakeCurrentShade;
      paper.fileChanged();
    }
  };

  // Attempt to flood fill at the given point, on the current layer.
  // Will return false if clicked out of bounds or floods to boundary.
  // Otherwise will return either the fill path underneath the point, or the
  // new fill path created.
  function floodFill(point) {
    var cLayer = project.getActiveLayer();

    // Check for Paths
    if (cLayer.children.length === 0) {
      // No paths? Can't do a darn thing.
      toastr.warning(i18n.t("tools.warnings.fill.nopaths"));
      return;
    }

    // Fist check to see if the user clicked ON a stroke or existing fill
    var ht = cLayer.hitTest(point, {
      tolerance: 3,
      fill: true,
      stroke: true
    });

    if (ht) {
      if (ht.item.data.fill) {
        return ht.item;
      } else {
        toastr.warning(i18n.t("tools.warnings.fill.path"));
        return false;
      }
    }

    var rast = cLayer.rasterize(50);
    var w = rast.width; var h = rast.height;
    var pix = rast.getImageData(new Rectangle(0, 0, w, h)).data;
    var grid = ndarray(new Int8Array(pix.length/4), [w-1, h-1]);
    var boundaryPoints = [];

    // Move through all RGBA pixel data to generate a 1bit map of visible pix.
    for (var p = 0; p < pix.length; p+= 4) {
      // If the alpha is greater than threshold, it's visible! Map a 1.
      if (pix[p+3] > tool.alphaBitmapThreshold) {
        grid.set((p / 4) % w, Math.floor((p / 4) / w), 1);
      }
    }

    var rastPt = rast.globalToLocal(point);
    // Offset for centered matrix position and trunc floats.
    rastPt.x = parseInt(rastPt.x + w/2);
    rastPt.y = parseInt(rastPt.y + h/2);

    // Outside the bounds of the layer data!
    if (rastPt.x > w || rastPt.x < 0 || rastPt.y > h || rastPt.y < 0) {
      toastr.warning(i18n.t("tools.warnings.fill.bounds"));
      rast.remove();
      return false;
    }

    // n-dimensional-flood-fill
    var hitLimit = false;
    flood({
      seed: [rastPt.x, rastPt.y],
      getter: function(x, y){
        // Apparently ndarray will take insane values and return real values :/
        if (x < 0  || y < 0 || x > w || y > h || hitLimit) return undefined;
        return grid.get(x, y);
      },
      onBoundary: function(x, y) {
        if (x === w || x === 0 || y === h || y === 0) {
          hitLimit = true;
        }

        if (!hitLimit) {
          boundaryPoints.push(rast.localToGlobal(new Point(x - w/2, y - h/2)));
        }
      }
    });

    // If our flood fill touched the edge of the layer, fill wasn't closed.
    if (hitLimit) {
      toastr.warning(i18n.t("tools.warnings.fill.notclosed"));
      rast.remove();
      return false;
    }

    // We're done with the raster.
    rast.remove();

    // Create the compound fillpath from the boundary points.
    var fillPath = new CompoundPath({
      children: getCompoundBoundaryPaths(boundaryPoints),
      data: {fill: true}
    });
    fillPath.remove();
    cLayer.insertChild(0, fillPath); // Put all fills at the bottom

    return fillPath;
  }

  // Convert the list of fill boundary points into an array of paths.
  // If you're filling a face, everything within it will be a new path,
  function getCompoundBoundaryPaths(bPoints) {
    var pathGroups = distanceSort(bPoints);
    var out = [];
    _.each(pathGroups, function(segments){
      out.push(new Path({
        closed: true,
        segments: segments
      }));
      out[out.length-1].simplify(tool.pathSimplifyAmt);
    });
    return out;
  }

  // Sort an array of Points by distance, grouped by distance threshold.
  function distanceSort(points) {
    // Use an external function to find the most appropriate starting point.
    var out = [[points.splice(getFillStartID(points), 1)[0]]];
    var cGroup = 0;

    // Loop through every point, adding the next closest point,
    // removing the previous points.
    while (points.length) {
      var lastPoint = out[cGroup][out[cGroup].length-1];
      var nextPoint = closestPoint(lastPoint, points);

      // If the distance is further away than "normal", we must be jumping to a
      // sub path. Increment the group.
      if (nextPoint.dist > tool.islandThreshold) {
        cGroup++;
        out[cGroup] = [];
      }

      out[cGroup].push(points.splice(nextPoint.id, 1)[0]);
    }

    return out;
  }

  // Given a list of points, find the one closest to the given point.
  function closestPoint(point, list) {
    var closestID = 0;
    var closest = point.getDistance(list[0]);
    _.each(list, function(p, index){
      var dist = point.getDistance(p);
      if (dist < closest) {
        closest = dist;
        closestID = index;
      }
    });

    return {id: closestID, dist: closest};
  }

  // Find the most appropriate fill starting point given an array of points.
  function getFillStartID(points) {
    var bestID = 0;
    var lowestY = points[0].y;
    _.each(points, function(p, index){
      if (p.y < lowestY) {
        lowestY = p.y;
        bestID = index;
      }
    });

    return bestID;
  }

  return tool;
};
<commit_msg>Increase sensitivity for small shape simplification & edge detection for #33<commit_after>/**
 * @file Tool definition for the PC super-crazy visual flood fill tool.
 **/
"use strict";

module.exports = function(paper) {
  var tool = new paper.Tool();

  // Paper global extenders
  var Path = paper.Path;
  var Point = paper.Point;
  var Rectangle = paper.Rectangle;
  var project = paper.project;
  var CompoundPath = paper.CompoundPath;

  // Tool identification (for building out tool palette)
  tool.name = 'tools.fill';
  tool.key = 'fill';
  tool.cursorColors = true; // Different icons/cursor for each color?
  tool.cursorOffset = '1 9'; // Position for cursor point
  tool.pathSimplifyAmt = 0.7; // How much to simplify pixel boundary paths
  tool.smallShapeArea = 200; // Minimum size to be considered a "small" shape
  tool.smallShapeSimplify = 0.2; // Amount to simplify "small" fill shapes
  tool.islandThreshold = 10; // Max distance between fill boundary nodes
  tool.alphaBitmapThreshold = 150; // 0-255, value that's considered visible

  // Tool vars
  var ndarray = require('ndarray');
  var flood = require('n-dimensional-flood-fill');

  tool.onMouseDown = function(event) {
    var fillPath = floodFill(event.point);
    if (fillPath !== false) {
      fillPath.fillColor = paper.pancakeShades[paper.pancakeCurrentShade]
      fillPath.data.color = paper.pancakeCurrentShade;
      paper.fileChanged();
    }
  };

  // Attempt to flood fill at the given point, on the current layer.
  // Will return false if clicked out of bounds or floods to boundary.
  // Otherwise will return either the fill path underneath the point, or the
  // new fill path created.
  function floodFill(point) {
    var cLayer = project.getActiveLayer();

    // Check for Paths
    if (cLayer.children.length === 0) {
      // No paths? Can't do a darn thing.
      toastr.warning(i18n.t("tools.warnings.fill.nopaths"));
      return;
    }

    // Fist check to see if the user clicked ON a stroke or existing fill
    var ht = cLayer.hitTest(point, {
      tolerance: 3,
      fill: true,
      stroke: true
    });

    if (ht) {
      if (ht.item.data.fill) {
        return ht.item;
      } else {
        toastr.warning(i18n.t("tools.warnings.fill.path"));
        return false;
      }
    }

    var rast = cLayer.rasterize(50);
    var w = rast.width; var h = rast.height;
    var pix = rast.getImageData(new Rectangle(0, 0, w, h)).data;
    var grid = ndarray(new Int8Array(pix.length/4), [w-1, h-1]);
    var boundaryPoints = [];

    // Move through all RGBA pixel data to generate a 1bit map of visible pix.
    for (var p = 0; p < pix.length; p+= 4) {
      // If the alpha is greater than threshold, it's visible! Map a 1.
      if (pix[p+3] > tool.alphaBitmapThreshold) {
        grid.set((p / 4) % w, Math.floor((p / 4) / w), 1);
      }
    }

    var rastPt = rast.globalToLocal(point);
    // Offset for centered matrix position and trunc floats.
    rastPt.x = parseInt(rastPt.x + w/2);
    rastPt.y = parseInt(rastPt.y + h/2);

    // Outside the bounds of the layer data!
    if (rastPt.x > w || rastPt.x < 0 || rastPt.y > h || rastPt.y < 0) {
      toastr.warning(i18n.t("tools.warnings.fill.bounds"));
      rast.remove();
      return false;
    }

    // n-dimensional-flood-fill
    var hitLimit = false;
    flood({
      seed: [rastPt.x, rastPt.y],
      getter: function(x, y){
        // Apparently ndarray will take insane values and return real values :/
        if (x < 0  || y < 0 || x > w || y > h || hitLimit) return undefined;
        return grid.get(x, y);
      },
      onBoundary: function(x, y) {
        if (x === w || x === 0 || y === h || y === 0) {
          hitLimit = true;
        }

        if (!hitLimit) {
          boundaryPoints.push(rast.localToGlobal(new Point(x - w/2, y - h/2)));
        }
      }
    });

    // If our flood fill touched the edge of the layer, fill wasn't closed.
    if (hitLimit) {
      toastr.warning(i18n.t("tools.warnings.fill.notclosed"));
      rast.remove();
      return false;
    }

    // We're done with the raster.
    rast.remove();

    // Create the compound fillpath from the boundary points.
    var fillPath = new CompoundPath({
      children: getCompoundBoundaryPaths(boundaryPoints),
      data: {fill: true}
    });
    fillPath.remove();
    cLayer.insertChild(0, fillPath); // Put all fills at the bottom

    return fillPath;
  }

  // Convert the list of fill boundary points into an array of paths.
  // If you're filling a face, everything within it will be a new path,
  function getCompoundBoundaryPaths(bPoints) {
    var pathGroups = distanceSort(bPoints);
    var out = [];
    _.each(pathGroups, function(segments){
      out.push(new Path({
        closed: true,
        segments: segments
      }));
      var idx = out.length-1;
      var area = out[idx].bounds.width * out[idx].bounds.height;

      // Cleanup based on area size
      if (area === 0) {
        out.pop().remove();
      } else if (area <= tool.smallShapeArea) {
        out[idx].simplify(tool.smallShapeSimplify);
      } else {
        out[idx].simplify(tool.pathSimplifyAmt);
      }
    });
    return out;
  }

  // Sort an array of Points by distance, grouped by distance threshold.
  function distanceSort(points) {
    // Use an external function to find the most appropriate starting point.
    var out = [[points.splice(getFillStartID(points), 1)[0]]];
    var cGroup = 0;

    // Loop through every point, adding the next closest point,
    // removing the previous points.
    while (points.length) {
      var lastPoint = out[cGroup][out[cGroup].length-1];
      var nextPoint = closestPoint(lastPoint, points);

      // If the distance is further away than "normal", we must be jumping to a
      // sub path. Increment the group.
      if (nextPoint.dist > tool.islandThreshold) {
        cGroup++;
        out[cGroup] = [];
      }

      out[cGroup].push(points.splice(nextPoint.id, 1)[0]);
    }

    return out;
  }

  // Given a list of points, find the one closest to the given point.
  function closestPoint(point, list) {
    var closestID = 0;
    var closest = point.getDistance(list[0]);
    _.each(list, function(p, index){
      var dist = point.getDistance(p);
      if (dist < closest) {
        closest = dist;
        closestID = index;
      }
    });

    return {id: closestID, dist: closest};
  }

  // Find the most appropriate fill starting point given an array of points.
  function getFillStartID(points) {
    var bestID = 0;
    var lowestY = points[0].y;
    _.each(points, function(p, index){
      if (p.y < lowestY) {
        lowestY = p.y;
        bestID = index;
      }
    });

    return bestID;
  }

  return tool;
};
<|endoftext|>