<commit_before>/*
---

name: Button

description: Provides a Button control.

license: MIT-style license.

authors:
	- Jean-Philippe Dery (jeanphilippe.dery@gmail.com)

requires:
	- Control

provides:
	- Button

...
*/

 /**
 * @name  Button
 * @class Provides a button control.
 *
 * @classdesc
 *
 * [TODO: Description]
 * [TODO: Events]
 * [TODO: Roles]
 * [TODO: Styles]
 * [TODO: Options]
 * [TODO: Element Structure]
 *
 * @extends Control
 *
 * @author  Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
 * @version 0.1
 */
Moobile.Button = new Class(/** @lends Button.prototype */ {

	Extends: Moobile.Control,

	/**
	 * @var    {Text} This button's label.
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	label: null,

	/**
	 * Sets the label.
	 *
	 * This method will set the label using either a string or an instance of a
	 * `Label`. When provided with a string, this methods creates a `Label`
	 * instance and assign the given string as its text.
	 *
	 * @param {Mixed} label The label as a string or a `Label` instance.
	 *
	 * @return {Button} This button.
	 *
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	setLabel: function(label) {

		if (this.label === label)
			return this;

		if (typeof label == 'string') {
			var text = label;
			label = new Moobile.Text();
			label.setText(text);
		}

		if (this.label == null) {
			this.label = label;
			this.addChild(label);
		} else {
			this.replaceChild(this.label, label);
			this.label.destroy();
			this.label = label;
		}

		label.addClass('label');

		return this;
	},

	/**
	 * Returns the label.
	 *
	 * @return {Text} The label.
	 *
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	getLabel: function() {
		return this.label;
	},

	willBuild: function() {

		this.parent();

		this.element.addClass('button');

		var label = this.getRoleElement('label');
		if (label == null) {
			label = new Element('div');
			label.ingest(this.element);
			label.inject(this.element);
		}

		this.attachRole(label, 'label');
	},

	destroy: function() {
		this.label = null;
		this.parent();
	},

	onTapStart: function(e) {
		this.parent(e);
		this.setHighlighted(true);
	},

	onTapEnd: function(e) {
		this.parent(e);
		this.setHighlighted(false);
	}

});

//------------------------------------------------------------------------------
// Roles
//------------------------------------------------------------------------------

Moobile.Entity.defineRole('button', null, function(element) {
	var instance = Moobile.Entity.fromElement(element, 'data-button',  Moobile.Button);
	this.addChild(instance);
});

Moobile.Entity.defineRole('label', Moobile.Button, function(element) {
	var instance = Moobile.Entity.fromElement(element, 'data-label', Moobile.Text);
	this.setLabel(instance);
});
<commit_msg>Added the required events that changes the button state<commit_after>/*
---

name: Button

description: Provides a Button control.

license: MIT-style license.

authors:
	- Jean-Philippe Dery (jeanphilippe.dery@gmail.com)

requires:
	- Control

provides:
	- Button

...
*/

 /**
 * @name  Button
 * @class Provides a button control.
 *
 * @classdesc
 *
 * [TODO: Description]
 * [TODO: Events]
 * [TODO: Roles]
 * [TODO: Styles]
 * [TODO: Options]
 * [TODO: Element Structure]
 *
 * @extends Control
 *
 * @author  Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
 * @version 0.1
 */
Moobile.Button = new Class(/** @lends Button.prototype */ {

	Extends: Moobile.Control,

	/**
	 * @var    {Text} This button's label.
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	label: null,

	/**
	 * Sets the label.
	 *
	 * This method will set the label using either a string or an instance of a
	 * `Label`. When provided with a string, this methods creates a `Label`
	 * instance and assign the given string as its text.
	 *
	 * @param {Mixed} label The label as a string or a `Label` instance.
	 *
	 * @return {Button} This button.
	 *
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	setLabel: function(label) {

		if (this.label === label)
			return this;

		if (typeof label == 'string') {
			var text = label;
			label = new Moobile.Text();
			label.setText(text);
		}

		if (this.label == null) {
			this.label = label;
			this.addChild(label);
		} else {
			this.replaceChild(this.label, label);
			this.label.destroy();
			this.label = label;
		}

		label.addClass('label');

		return this;
	},

	/**
	 * Returns the label.
	 *
	 * @return {Text} The label.
	 *
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	getLabel: function() {
		return this.label;
	},

	willBuild: function() {

		this.parent();

		this.element.addClass('button');

		var label = this.getRoleElement('label');
		if (label == null) {
			label = new Element('div');
			label.ingest(this.element);
			label.inject(this.element);
		}

		this.attachRole(label, 'label');

		this.addEvent('tapstart', this.bound('onTapStart'));
		this.addEvent('tapend', this.bound('onTapEnd'));
	},

	destroy: function() {
		this.removeEvent('tapstart', this.bound('onTapStart'));
		this.removeEvent('tapend', this.bound('onTapEnd'));
		this.label = null;
		this.parent();
	},

	onTapStart: function(e) {
		this.setHighlighted(true);
	},

	onTapEnd: function(e) {
		this.setHighlighted(false);
	}

});

//------------------------------------------------------------------------------
// Roles
//------------------------------------------------------------------------------

Moobile.Entity.defineRole('button', null, function(element) {
	var instance = Moobile.Entity.fromElement(element, 'data-button',  Moobile.Button);
	this.addChild(instance);
});

Moobile.Entity.defineRole('label', Moobile.Button, function(element) {
	var instance = Moobile.Entity.fromElement(element, 'data-label', Moobile.Text);
	this.setLabel(instance);
});
<|endoftext|>


<commit_before>//
// MDL style switch component.
//
// <image src="http://bit.ly/1OF6Z96" width="400"/>
//
// - @see [MDL Switch](http://bit.ly/1IcHMPo)
// - [Props](#props)
// - [Defaults](#defaults)
//
// Created by ywu on 15/7/28.
//

const React = require('react-native');
const MKColor = require('../MKColor');

const {
  Component,
  Animated,
  View,
  TouchableWithoutFeedback,
} = React;

// ##
Thumb

// `Thumb` component of the `Switch`.
// Which is displayed as a circle with shadow and ripple effect.
class Thumb extends Component {
  constructor(props) {
    super(props);
    this.state = {
      rippleScale: new Animated.Value(1),
      rippleAlpha: new Animated.Value(0),
    };
  }

  componentWillMount() {
    this.setState({checked: this.props.checked});
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.checked !== this.props.checked) {
      this.setState({checked: nextProps.checked});
    }
  }

  // When a toggle action started.
  startToggle() {
    this.showRipple();
  }

  // When a toggle action (from the given state) is confirmed.
  // - {`boolean`} `fromState` the previous state
  confirmToggle(fromState) {
    this.state.checked = !fromState;
  }

  // When a toggle action is finished (confirmed or canceled).
  endToggle() {
    this.hideRipple();
  }

  // Start the ripple effect
  showRipple() {
    this.state.rippleAlpha.setValue(1);
    this.state.rippleScale.setValue(0);

    // scaling up the ripple layer
    this._rippleAni = Animated.timing(this.state.rippleScale, {
      toValue: 1,
      duration: this.props.rippleAniDuration || 150,
    });

    this._rippleAni.start(() => {
      this._rippleAni = undefined;

      // if any pending animation, do it
      if (this._pendingRippleAni) {
        this._pendingRippleAni();
      }
    });
  }

  // Stop the ripple effect
  hideRipple() {
    this._pendingRippleAni = () => {
      Animated.timing(this.state.rippleAlpha, {
        toValue: 0,
        duration: 100,
      }).start();

      this._pendingRippleAni = undefined;
    };

    if (!this._rippleAni) {
      // previous ripple animation is done, good to go
      this._pendingRippleAni();
    }
  }

  _getBgColor() {
    return this.state.checked ? this.props.onColor : this.props.offColor;
  }

  // Rendering the `Thumb`
  render() {
    return (
      <View  // the circle
        style={[
          Thumb.defaultProps.style,
          this.props.style,
          {backgroundColor: this._getBgColor()},
        ]}
        >
        <Animated.View  // the ripple layer
          style={{
            opacity: this.state.rippleAlpha,
            backgroundColor: this.props.rippleColor,
            width: this.props.rippleRadius * 2,
            height: this.props.rippleRadius * 2,
            borderRadius: this.props.rippleRadius,
            top: -this.props.rippleRadius / 2,
            left: -this.props.rippleRadius / 2,
            transform: [
              {scale: this.state.rippleScale},
            ],
          }}
        />
      </View>
    );
  }
}

// Default props of `Thumb`
Thumb.defaultProps = {
  pointerEvents: 'none',
  onColor: MKColor.Indigo,
  offColor: MKColor.Silver,
  rippleColor: 'rgba(63,81,181,0.2)',  // Indigo + alpha
  style: {
    shadowColor: 'black',
    shadowRadius: 1,
    shadowOpacity: 0.7,
    shadowOffset: { width: 0, height: 1 },
  },
};

// Enable animations on `Thumb`
const AnimatedThumb = Animated.createAnimatedComponent(Thumb);

// ##
Switch

// The `Switch` component. Which is made up of a `Track` and a [`Thumb`](#thumb).
class Switch extends Component {
  constructor(props) {
    super(props);
    this.state = {
      trackWidth: 0,
      trackHeight: 0,
      trackRadii: 0,
      thumbLeft: new Animated.Value(0),
      thumbFrame: {x: 0, y: 0, r: 0, rippleRadii: 0},
    };
  }

  componentWillMount() {
    this.setState({checked: this.props.checked});
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.checked !== this.props.checked) {
      this.setState({checked: nextProps.checked});
    }
  }

  componentDidMount() {
    requestAnimationFrame(this._doMeasurement.bind(this));
  }

  _doMeasurement() {
    this.refs.track.measure(this._layoutThumb.bind(this));
  }

  // Layout the thumb according to the size of the track
  _layoutThumb(x, y, trackWidth, trackHeight) {
    const trackRadii = trackHeight / 2;
    const thumbRadii = this.props.thumbRadius;
    const thumbX0 = this.state.checked ? trackWidth - trackRadii : trackRadii;
    this.state.thumbLeft.setValue(thumbX0 - thumbRadii);
    this.setState({
      trackWidth,
      trackHeight,
      trackRadii,
      thumbFrame: {
        x: thumbX0 - thumbRadii,
        y: trackRadii - thumbRadii,
        r: thumbRadii,
        rippleRadii: trackWidth - trackRadii * 2,
      },
    });
  }

  // Move the thumb left or right according to the current state
  _translateThumb() {
    this.state.thumbLeft.setValue(this.state.thumbFrame.x);
    const newX = this._computeThumbX(this.state.checked);
    Animated.timing(this.state.thumbLeft, {
      toValue: newX,
      duration: this.props.thumbAniDuration || 300,
    }).start(() => {
      this.state.thumbFrame.x = newX;
    });
  }

  // Calc the next position (x-axis) of the thumb
  _computeThumbX(toChecked) {
    if (!this.state.thumbFrame.r) {
      return 0;
    }

    const {trackWidth, trackRadii} = this.state;
    const dx = (toChecked ? 1 : -1) * (trackWidth - trackRadii * 2);
    return this.state.thumbFrame.x + dx;
  }

  // When a toggle action started.
  startToggle() {
    this.getThumb().startToggle();
  }

  // When a toggle action is confirmed.
  confirmToggle() {
    const prevState = this.state.checked;
    this.setState({checked: !prevState}, () => {
      this.getThumb().confirmToggle(prevState);
      this._translateThumb();

      if (this.props.onCheckedChange) {
        this.props.onCheckedChange({checked: this.state.checked});
      }
    });
  }

  // When a toggle action is finished (confirmed or canceled).
  endToggle() {
    this.getThumb().endToggle();
  }

  // Un-boxing the `Thumb` node from `AnimatedComponent`,
  // in order to access the component functions defined in `Thumb`
  getThumb() {
    return this.refs.thumb.refs.node;
  }

  _getBgColor() {
    return this.state.checked ? this.props.onColor : this.props.offColor;
  }

  _onPress() {
    this.confirmToggle();
    if (this.props.onPress) {
      this.props.onPress();
    }
  }

  _onPressIn() {
    this.startToggle();
    if (this.props.onPressIn) {
      this.props.onPressIn();
    }
  }

  _onPressOut() {
    this.endToggle();
    if (this.props.onPressOut) {
      this.props.onPressOut();
    }
  }

  // Rendering the `Switch`
  render() {
    const touchProps = {
      delayPressIn: this.props.delayPressIn,
      delayPressOut: this.props.delayPressOut,
      delayLongPress: this.props.delayLongPress,
      onLongPress: this.props.onLongPress,
    };

    const thumbFrame = this.state.thumbFrame;
    const thumbProps = {
      checked: this.state.checked,
      onColor: this.props.thumbOnColor,
      offColor: this.props.thumbOffColor,
      rippleColor: this.props.rippleColor,
      rippleRadius: thumbFrame.rippleRadii,
      rippleAniDuration: this.props.rippleAniDuration,
      style: {
        left: this.state.thumbLeft,
        top: thumbFrame.y,
        width: this.props.thumbRadius * 2,
        height: this.props.thumbRadius * 2,
        borderRadius: this.props.thumbRadius,
      },
    };

    return (
      <TouchableWithoutFeedback
        {...touchProps}
        onPress={this._onPress.bind(this)}
        onPressIn={this._onPressIn.bind(this)}
        onPressOut={this._onPressOut.bind(this)}
        >
        <View ref="track"  // the 'track' part
              style={[Switch.defaultProps.style,
                      this.props.style,
                      {
                        backgroundColor: this._getBgColor(),
                        borderRadius: this.state.trackRadii,
                      },
                    ]}
          >
          <AnimatedThumb ref="thumb"  // the 'thumb' part
            {...thumbProps}
          />
        </View>
      </TouchableWithoutFeedback>
    );
  }
}

// ##
Props

Switch.propTypes = {
  // Touchable...
  ...TouchableWithoutFeedback.propTypes,

  // Toggle status of the `Switch`
  checked: React.PropTypes.bool,

  // Callback when the toggle state is changed.
  onCheckedChange: React.PropTypes.func,

  // Color of the track, when switch is checked
  onColor: React.PropTypes.string,

  // Color of the track, when switch is off
  offColor: React.PropTypes.string,

  // Radius of the thumb button
  thumbRadius: React.PropTypes.number,

  // Color of the thumb, when switch is checked
  thumbOnColor: React.PropTypes.string,

  // Color of the thumb, when switch is off
  thumbOffColor: React.PropTypes.string,

  // Duration of the thumb sliding animation, in milliseconds
  thumbAniDuration: React.PropTypes.number,

  // Color of the ripple layer
  rippleColor: React.PropTypes.string,

  // Duration of the ripple effect, in milliseconds
  rippleAniDuration: React.PropTypes.number,
};

// ##
Defaults

Switch.defaultProps = {
  checked: false,
  onColor: 'rgba(63,81,181,0.4)',  // Indigo + alpha
  offColor: 'rgba(0,0,0,0.25)',
  thumbRadius: 10,
  thumbOnColor: MKColor.Indigo,
  thumbOffColor: MKColor.Silver,
  rippleColor: 'rgba(63,81,181,0.2)',  // Indigo + alpha
  style: {
    width: 36,
    height: 14,
    borderRadius: 7,
  },
};

// ## Public interface
module.exports = Switch;
<commit_msg>fix Switch ripple positioning bug when it's radius not eq 2*thumb.radius<commit_after>//
// MDL style switch component.
//
// <image src="http://bit.ly/1OF6Z96" width="400"/>
//
// - @see [MDL Switch](http://bit.ly/1IcHMPo)
// - [Props](#props)
// - [Defaults](#defaults)
//
// Created by ywu on 15/7/28.
//

const React = require('react-native');
const MKColor = require('../MKColor');

const {
  Component,
  Animated,
  View,
  TouchableWithoutFeedback,
} = React;

// ##
Thumb

// `Thumb` component of the `Switch`.
// Which is displayed as a circle with shadow and ripple effect.
class Thumb extends Component {
  constructor(props) {
    super(props);
    this.state = {
      rippleScale: new Animated.Value(1),
      rippleAlpha: new Animated.Value(0),
    };
  }

  componentWillMount() {
    this.setState({checked: this.props.checked});
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.checked !== this.props.checked) {
      this.setState({checked: nextProps.checked});
    }
  }

  // When a toggle action started.
  startToggle() {
    this.showRipple();
  }

  // When a toggle action (from the given state) is confirmed.
  // - {`boolean`} `fromState` the previous state
  confirmToggle(fromState) {
    this.state.checked = !fromState;
  }

  // When a toggle action is finished (confirmed or canceled).
  endToggle() {
    this.hideRipple();
  }

  // Start the ripple effect
  showRipple() {
    this.state.rippleAlpha.setValue(1);
    this.state.rippleScale.setValue(0);

    // scaling up the ripple layer
    this._rippleAni = Animated.timing(this.state.rippleScale, {
      toValue: 1,
      duration: this.props.rippleAniDuration || 150,
    });

    this._rippleAni.start(() => {
      this._rippleAni = undefined;

      // if any pending animation, do it
      if (this._pendingRippleAni) {
        this._pendingRippleAni();
      }
    });
  }

  // Stop the ripple effect
  hideRipple() {
    this._pendingRippleAni = () => {
      Animated.timing(this.state.rippleAlpha, {
        toValue: 0,
        duration: 100,
      }).start();

      this._pendingRippleAni = undefined;
    };

    if (!this._rippleAni) {
      // previous ripple animation is done, good to go
      this._pendingRippleAni();
    }
  }

  _getBgColor() {
    return this.state.checked ? this.props.onColor : this.props.offColor;
  }

  // Rendering the `Thumb`
  render() {
    const rippleSize = this.props.rippleRadius * 2;
    const rippleOffset = this.props.radius - this.props.rippleRadius;

    return (
      <View  // the circle
        style={[
          Thumb.defaultProps.style,
          this.props.style,
          {backgroundColor: this._getBgColor()},
        ]}
        >
        <Animated.View  // the ripple layer
          style={{
            opacity: this.state.rippleAlpha,
            backgroundColor: this.props.rippleColor,
            width: rippleSize,
            height: rippleSize,
            borderRadius: this.props.rippleRadius,
            top: rippleOffset,
            left: rippleOffset,
            transform: [
              {scale: this.state.rippleScale},
            ],
          }}
        />
      </View>
    );
  }
}

// Default props of `Thumb`
Thumb.defaultProps = {
  pointerEvents: 'none',
  onColor: MKColor.Indigo,
  offColor: MKColor.Silver,
  rippleColor: 'rgba(63,81,181,0.2)',  // Indigo + alpha
  style: {
    shadowColor: 'black',
    shadowRadius: 1,
    shadowOpacity: 0.7,
    shadowOffset: { width: 0, height: 1 },
  },
};

// Enable animations on `Thumb`
const AnimatedThumb = Animated.createAnimatedComponent(Thumb);

// ##
Switch

// The `Switch` component. Which is made up of a `Track` and a [`Thumb`](#thumb).
class Switch extends Component {
  constructor(props) {
    super(props);
    this.state = {
      trackWidth: 0,
      trackHeight: 0,
      trackRadii: 0,
      thumbLeft: new Animated.Value(0),
      thumbFrame: {x: 0, y: 0, r: 0, rippleRadii: 0},
    };
  }

  componentWillMount() {
    this.setState({checked: this.props.checked});
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.checked !== this.props.checked) {
      this.setState({checked: nextProps.checked});
    }
  }

  componentDidMount() {
    requestAnimationFrame(this._doMeasurement.bind(this));
  }

  _doMeasurement() {
    this.refs.track.measure(this._layoutThumb.bind(this));
  }

  // Layout the thumb according to the size of the track
  _layoutThumb(x, y, trackWidth, trackHeight) {
    const trackRadii = trackHeight / 2;
    const thumbRadii = this.props.thumbRadius;
    const thumbX0 = this.state.checked ? trackWidth - trackRadii : trackRadii;
    this.state.thumbLeft.setValue(thumbX0 - thumbRadii);
    this.setState({
      trackWidth,
      trackHeight,
      trackRadii,
      thumbFrame: {
        x: thumbX0 - thumbRadii,
        y: trackRadii - thumbRadii,
        r: thumbRadii,
        rippleRadii: trackWidth - trackRadii * 2,
      },
    });
  }

  // Move the thumb left or right according to the current state
  _translateThumb() {
    this.state.thumbLeft.setValue(this.state.thumbFrame.x);
    const newX = this._computeThumbX(this.state.checked);
    Animated.timing(this.state.thumbLeft, {
      toValue: newX,
      duration: this.props.thumbAniDuration || 300,
    }).start(() => {
      this.state.thumbFrame.x = newX;
    });
  }

  // Calc the next position (x-axis) of the thumb
  _computeThumbX(toChecked) {
    if (!this.state.thumbFrame.r) {
      return 0;
    }

    const {trackWidth, trackRadii} = this.state;
    const dx = (toChecked ? 1 : -1) * (trackWidth - trackRadii * 2);
    return this.state.thumbFrame.x + dx;
  }

  // When a toggle action started.
  startToggle() {
    this.getThumb().startToggle();
  }

  // When a toggle action is confirmed.
  confirmToggle() {
    const prevState = this.state.checked;
    this.setState({checked: !prevState}, () => {
      this.getThumb().confirmToggle(prevState);
      this._translateThumb();

      if (this.props.onCheckedChange) {
        this.props.onCheckedChange({checked: this.state.checked});
      }
    });
  }

  // When a toggle action is finished (confirmed or canceled).
  endToggle() {
    this.getThumb().endToggle();
  }

  // Un-boxing the `Thumb` node from `AnimatedComponent`,
  // in order to access the component functions defined in `Thumb`
  getThumb() {
    return this.refs.thumb.refs.node;
  }

  _getBgColor() {
    return this.state.checked ? this.props.onColor : this.props.offColor;
  }

  _onPress() {
    this.confirmToggle();
    if (this.props.onPress) {
      this.props.onPress();
    }
  }

  _onPressIn() {
    this.startToggle();
    if (this.props.onPressIn) {
      this.props.onPressIn();
    }
  }

  _onPressOut() {
    this.endToggle();
    if (this.props.onPressOut) {
      this.props.onPressOut();
    }
  }

  // Rendering the `Switch`
  render() {
    const touchProps = {
      delayPressIn: this.props.delayPressIn,
      delayPressOut: this.props.delayPressOut,
      delayLongPress: this.props.delayLongPress,
      onLongPress: this.props.onLongPress,
    };

    const thumbFrame = this.state.thumbFrame;
    const thumbProps = {
      checked: this.state.checked,
      onColor: this.props.thumbOnColor,
      offColor: this.props.thumbOffColor,
      rippleColor: this.props.rippleColor,
      rippleRadius: thumbFrame.rippleRadii,
      rippleAniDuration: this.props.rippleAniDuration,
      radius: this.props.thumbRadius,
      style: {
        left: this.state.thumbLeft,
        top: thumbFrame.y,
        width: this.props.thumbRadius * 2,
        height: this.props.thumbRadius * 2,
        borderRadius: this.props.thumbRadius,
      },
    };

    return (
      <TouchableWithoutFeedback
        {...touchProps}
        onPress={this._onPress.bind(this)}
        onPressIn={this._onPressIn.bind(this)}
        onPressOut={this._onPressOut.bind(this)}
        >
        <View ref="track"  // the 'track' part
              style={[Switch.defaultProps.style,
                      this.props.style,
                      {
                        backgroundColor: this._getBgColor(),
                        borderRadius: this.state.trackRadii,
                      },
                    ]}
          >
          <AnimatedThumb ref="thumb"  // the 'thumb' part
            {...thumbProps}
          />
        </View>
      </TouchableWithoutFeedback>
    );
  }
}

// ##
Props

Switch.propTypes = {
  // Touchable...
  ...TouchableWithoutFeedback.propTypes,

  // Toggle status of the `Switch`
  checked: React.PropTypes.bool,

  // Callback when the toggle state is changed.
  onCheckedChange: React.PropTypes.func,

  // Color of the track, when switch is checked
  onColor: React.PropTypes.string,

  // Color of the track, when switch is off
  offColor: React.PropTypes.string,

  // Radius of the thumb button
  thumbRadius: React.PropTypes.number,

  // Color of the thumb, when switch is checked
  thumbOnColor: React.PropTypes.string,

  // Color of the thumb, when switch is off
  thumbOffColor: React.PropTypes.string,

  // Duration of the thumb sliding animation, in milliseconds
  thumbAniDuration: React.PropTypes.number,

  // Color of the ripple layer
  rippleColor: React.PropTypes.string,

  // Duration of the ripple effect, in milliseconds
  rippleAniDuration: React.PropTypes.number,
};

// ##
Defaults

Switch.defaultProps = {
  checked: false,
  onColor: 'rgba(63,81,181,0.4)',  // Indigo + alpha
  offColor: 'rgba(0,0,0,0.25)',
  thumbRadius: 10,
  thumbOnColor: MKColor.Indigo,
  thumbOffColor: MKColor.Silver,
  rippleColor: 'rgba(63,81,181,0.2)',  // Indigo + alpha
  style: {
    width: 36,
    height: 14,
    borderRadius: 7,
  },
};

// ## Public interface
module.exports = Switch;
<|endoftext|>


<commit_before>/*
---

script: Datalist.js

description: Add your widget have a real form value.

license: Public domain (http://unlicense.org).

requires:
  - LSD.Mixin

provides:
  - LSD.Mixin.Datalist

...
*/

LSD.Mixin.Datalist = new Class({
  options: {
    actions: {
      datalist: {
        enable: function() {
          if (this.hasRemoteList()) this.attributes.set('href', this.attributes.list);
          this.attributes.set('autocomplete', 'off');
        },
        disable: function() {
          if (this.hasRemoteList()) this.attributes.unset('href', this.attributes.list);
          this.attributes.unset('autocomplete', 'off');
        }
      }
    },
    events: {
      _datalist: {
        element: {
          'input:pause': 'findSuggestions',
          'mousedown': 'unsetSuggestion'
        },
        request: {
          success: 'onSuccess'
        }
      },
      self: {
        blur: 'unsuggest',
        focus: 'findSuggestions'
      }
    },
    shortcuts: {
      up: 'suggestPrevious',
      down: 'suggestNext',
      enter: 'suggestSelected',
      left: 'findSuggestions',
      right: 'findSuggestions'
    },
    request: {
      type: 'xhr'
    },
    datalist: {
      max: 10
    }
  },

  constructors: {
    datalist: function() {
      this.suggestionItems = []
      this.suggestionIndex = -1;
    }
  },

  suggestPrevious: function(e) {
    if (this.getSuggestionsList().parentNode == null) return;
    if (--this.suggestionIndex < 0) this.suggestionIndex = this.suggestionLength - 1;
    this.selectSuggestionItem(this.suggestionIndex);
    e.preventDefault();
  },

  suggestNext: function(e) {
    if (this.getSuggestionsList().parentNode == null) return;
    if (++this.suggestionIndex == this.suggestionLength) this.suggestionIndex = 0;
    this.selectSuggestionItem(this.suggestionIndex);
    e.preventDefault();
  },

  getCurrentValue: function(suggestion) {
    var position = this.element.getSelectedRange();
    var value = this.element.get('value');
    var start = value.lastIndexOf(',', position.start - 1) + 1;
    var end = value.indexOf(',', position.end);
    if (end === -1) end = value.length;
    while (value.charAt(start) == ' ') start++;
    if (suggestion === false && position.start != position.end)
      return value.substring(start, position.start) + value.substring(position.end + 1, end)
    else return value.substring(start, end);
  },

  setCurrentValue: function(text, suggestion) {
    var old = this.getCurrentValue();
    var position = this.element.getSelectedRange();
    var value = this.element.get('value');
    var start = value.lastIndexOf(',', position.start - 1) + 1;
    var end = value.indexOf(',', position.end);
    if (end === -1) end = value.length;
    if (start != 0) text = ' ' + text;
    this.setValue(value.substring(0, start) + text + value.substring(end, value.length));
    this.element.set('value', value.substring(0, start) + text + value.substring(end, value.length));
    if (suggestion != null)
      this.element.selectRange(suggestion === false ? position.start + text.length : position.start, start + text.length);
  },

  findSuggestions: function(event) {
    var input = this.getCurrentValue();
    if (!input || input === this.input && this.getSuggestionsList().parentNode) return;
    this.input = input;
    this.regexp = new RegExp(this.value.trim().replace(/\s+/, '\\s'), 'i')
    if (this.hasRemoteList()) this.send();
  },

  suggestSelected: function(e) {
    if (this.selectedSuggestion) {
      this.setCurrentValue(this.selectedSuggestion.get('text'), false);
      if (e) e.stop();
    }
    this.unsuggest();
  },

  unsetSuggestion: function() {
    this.setCurrentValue(this.getCurrentValue(false));
  },

  suggest: function(text) {
    var input = this.getCurrentValue();
    if (input != text) this.setCurrentValue(text, true);
  },

  unsuggest: function() {
    var range = this.element.getSelectedRange();
    if (range.start) this.unsetSuggestion();
    this.getSuggestionsList().dispose();
    if (this.selectedSuggestion) this.selectedSuggestion.removeClass('selected');
    this.selectedSuggestion = null;
    this.suggestionLength = 0;
    this.suggestionIndex = -1;
  },

  renderSuggestions: function(results) {
    var list = this.getSuggestionsList();
    var n = results && results.length || 0
    if (n) {
      this.suggestionLength = 0;
      for (var i = 0, j = this.options.datalist.max; i < j; i++) {
        if (i < n) {
          var item = this.getSuggestionItem(i).set('html', this.highlight(results[i]));
          if (!item.parentNode) item.inject(list);
        } else {
          var item = this.suggestionItems[i];
          if (item && item.parentNode) item.dispose();
          else break;
        }
        this.suggestionLength++;
      }
      if (!list.parentNode) list.inject(this.element, 'after');
    } else {
      if (list.parentNode) list.setStyle('display', 'none');
    }
  },

  highlight: function(result) {
    return result.replace(this.regexp, this.emphasize)
  },

  emphasize: function(value) {
    return '' + value + '';
  },

  selectSuggestionItem: function(i) {
    if (this.selectedSuggestion) this.selectedSuggestion.removeClass('selected');
    var item = this.getSuggestionItem(i);
    if (item) {
      this.selectedSuggestion = item.addClass('selected');
      this.setCurrentValue(item.get('text'), true);
    }
  },

  getSuggestionItem: function(i) {
    if (!this.suggestionItems[i]) this.suggestionItems[i] = new Element('li');
    return this.suggestionItems[i];
  },

  getSuggestionsList: function() {
    if (!this.suggestions) this.suggestions = new Element('ul', {'class': 'suggestions'}).addEvents({
      'mousedown:relay(li)': this.selectClickedSuggestion.bind(this),
      'mouseover:relay(li)': this.selectHoveredSuggestion.bind(this)
    });
    return this.suggestions;
  },

  getRequestData: function() {
    return {q: this.input}
  },

  selectClickedSuggestion: function(e, element) {
    if (this.selectedSuggestion) this.selectedSuggestion.removeClass('selected');
    this.setCurrentValue(element.get('text'), false);
  },

  selectHoveredSuggestion: function(e, element) {
    this.selectSuggestionItem(Array.prototype.slice.call(element.parentNode.childNodes).indexOf(element))
  },

  onSuccess: function(results) {
    if (!results.push) {
      for (var i in results) {
        results = results[i];
        break;
      }
    };
    var text = results && results[0];
    if (text && (results && results[1] != null || this.getCurrentValue() != text)) {
      this.renderSuggestions(results);
    } else if (this.suggestionIndex) this.unsuggest()
  },

  hasRemoteList: function() {
    return this.attributes.list && this.attributes.list.indexOf('/') > -1
  }
});

Element.implement({

  getSelectedRange: function() {
    if (!Browser.Engine.trident) return {start: this.selectionStart, end: this.selectionEnd};
    var pos = {start: 0, end: 0};
    var range = this.getDocument().selection.createRange();
    if (!range || range.parentElement() != this) return pos;
    var dup = range.duplicate();
    if (this.type == 'text') {
      pos.start = 0 - dup.moveStart('character', -100000);
      pos.end = pos.start + range.text.length;
    } else {
      var value = this.value;
      var offset = value.length - value.match(/[\n\r]*$/)[0].length;
      dup.moveToElementText(this);
      dup.setEndPoint('StartToEnd', range);
      pos.end = offset - dup.text.length;
      dup.setEndPoint('StartToStart', range);
      pos.start = offset - dup.text.length;
    }
    return pos;
  },

  selectRange: function(start, end) {
    if (Browser.Engine.trident) {
      var diff = this.value.substr(start, end - start).replace(/\r/g, '').length;
      start = this.value.substr(0, start).replace(/\r/g, '').length;
      var range = this.createTextRange();
      range.collapse(true);
      range.moveEnd('character', start + diff);
      range.moveStart('character', start);
      range.select();
    } else {
      this.focus();
      this.setSelectionRange(start, end);
    }
    return this;
  }

});

LSD.Behavior.define('[list]', LSD.Mixin.Datalist)
<commit_msg>Cache results for the same query string<commit_after>/*
---

script: Datalist.js

description: Add your widget have a real form value.

license: Public domain (http://unlicense.org).

requires:
  - LSD.Mixin

provides:
  - LSD.Mixin.Datalist

...
*/

LSD.Mixin.Datalist = new Class({
  options: {
    actions: {
      datalist: {
        enable: function() {
          if (this.hasRemoteList()) this.attributes.set('href', this.attributes.list);
          this.attributes.set('autocomplete', 'off');
        },
        disable: function() {
          if (this.hasRemoteList()) this.attributes.unset('href', this.attributes.list);
          this.attributes.unset('autocomplete', 'off');
        }
      }
    },
    events: {
      _datalist: {
        element: {
          'input:pause': 'findSuggestions',
          'mousedown': 'unsetSuggestion'
        },
        request: {
          success: 'onSuccess'
        }
      },
      self: {
        blur: 'unsuggest',
        focus: 'findSuggestions'
      }
    },
    shortcuts: {
      up: 'suggestPrevious',
      down: 'suggestNext',
      enter: 'suggestSelected',
      left: 'findSuggestions',
      right: 'findSuggestions'
    },
    request: {
      type: 'xhr'
    },
    datalist: {
      max: 10
    }
  },

  constructors: {
    datalist: function() {
      this.suggestionItems = []
      this.suggestionIndex = -1;
    }
  },

  suggestPrevious: function(e) {
    if (this.getSuggestionsList().parentNode == null) return;
    if (--this.suggestionIndex < 0) this.suggestionIndex = this.suggestionLength - 1;
    this.selectSuggestionItem(this.suggestionIndex);
    e.preventDefault();
  },

  suggestNext: function(e) {
    if (this.getSuggestionsList().parentNode == null) return;
    if (++this.suggestionIndex == this.suggestionLength) this.suggestionIndex = 0;
    this.selectSuggestionItem(this.suggestionIndex);
    e.preventDefault();
  },

  getCurrentValue: function(suggestion) {
    var position = this.element.getSelectedRange();
    var value = this.element.get('value');
    var start = value.lastIndexOf(',', position.start - 1) + 1;
    var end = value.indexOf(',', position.end);
    if (end === -1) end = value.length;
    while (value.charAt(start) == ' ') start++;
    if (suggestion === false && position.start != position.end)
      return value.substring(start, position.start) + value.substring(position.end + 1, end)
    else return value.substring(start, end);
  },

  setCurrentValue: function(text, suggestion) {
    var old = this.getCurrentValue();
    var position = this.element.getSelectedRange();
    var value = this.element.get('value');
    var start = value.lastIndexOf(',', position.start - 1) + 1;
    var end = value.indexOf(',', position.end);
    if (end === -1) end = value.length;
    if (start != 0) text = ' ' + text;
    this.setValue(value.substring(0, start) + text + value.substring(end, value.length));
    this.element.set('value', value.substring(0, start) + text + value.substring(end, value.length));
    if (suggestion != null)
      this.element.selectRange(suggestion === false ? position.start + text.length : position.start, start + text.length);
  },

  findSuggestions: function(event) {
    var input = this.getCurrentValue();
    var same = input === this.input;
    if (!input || same && this.getSuggestionsList().parentNode) return;
    this.input = input;
    this.regexp = new RegExp(this.value.trim().replace(/\s+/, '\\s'), 'i')
    if (this.hasRemoteList()) {
      if (this.suggestedResults && same) this.onSuccess(this.suggestedResults)
      else this.send();
    }
  },

  suggestSelected: function(e) {
    if (this.selectedSuggestion) {
      this.setCurrentValue(this.selectedSuggestion.get('text'), false);
      if (e) e.stop();
    }
    this.unsuggest();
  },

  unsetSuggestion: function() {
    this.setCurrentValue(this.getCurrentValue(false));
  },

  suggest: function(text) {
    var input = this.getCurrentValue();
    if (input != text) this.setCurrentValue(text, true);
  },

  unsuggest: function() {
    var range = this.element.getSelectedRange();
    if (range.start != range.end) this.unsetSuggestion();
    this.getSuggestionsList().dispose();
    if (this.selectedSuggestion) this.selectedSuggestion.removeClass('selected');
    this.selectedSuggestion = null;
    this.suggestionLength = 0;
    this.suggestionIndex = -1;
  },

  renderSuggestions: function(results) {
    var list = this.getSuggestionsList();
    var n = results && results.length || 0
    if (n) {
      this.suggestionLength = 0;
      for (var i = 0, j = this.options.datalist.max; i < j; i++) {
        if (i < n) {
          var item = this.getSuggestionItem(i).set('html', this.highlight(results[i]));
          if (!item.parentNode) item.inject(list);
        } else {
          var item = this.suggestionItems[i];
          if (item && item.parentNode) item.dispose();
          else break;
        }
        this.suggestionLength++;
      }
      if (!list.parentNode) list.inject(this.element, 'after');
    } else {
      if (list.parentNode) list.setStyle('display', 'none');
    }
  },

  highlight: function(result) {
    return result.replace(this.regexp, this.emphasize)
  },

  emphasize: function(value) {
    return '' + value + '';
  },

  selectSuggestionItem: function(i) {
    if (this.selectedSuggestion) this.selectedSuggestion.removeClass('selected');
    var item = this.getSuggestionItem(i);
    if (item) {
      this.selectedSuggestion = item.addClass('selected');
      this.setCurrentValue(item.get('text'), true);
    }
  },

  getSuggestionItem: function(i) {
    if (!this.suggestionItems[i]) this.suggestionItems[i] = new Element('li');
    return this.suggestionItems[i];
  },

  getSuggestionsList: function() {
    if (!this.suggestions) this.suggestions = new Element('ul', {'class': 'suggestions'}).addEvents({
      'mousedown:relay(li)': this.selectClickedSuggestion.bind(this),
      'mouseover:relay(li)': this.selectHoveredSuggestion.bind(this)
    });
    return this.suggestions;
  },

  getRequestData: function() {
    return {q: this.input}
  },

  selectClickedSuggestion: function(e, element) {
    if (this.selectedSuggestion) this.selectedSuggestion.removeClass('selected');
    this.setCurrentValue(element.get('text'), false);
  },

  selectHoveredSuggestion: function(e, element) {
    this.selectSuggestionItem(Array.prototype.slice.call(element.parentNode.childNodes).indexOf(element))
  },

  onSuccess: function(results) {
    this.suggestedResults = results;
    if (!results.push) {
      for (var i in results) {
        results = results[i];
        break;
      }
    };
    var text = results && results[0];
    if (text && (results && results[1] != null || this.getCurrentValue() != text)) {
      this.renderSuggestions(results);
    } else this.unsuggest()
  },

  hasRemoteList: function() {
    return this.attributes.list && this.attributes.list.indexOf('/') > -1
  }
});

Element.implement({

  getSelectedRange: function() {
    if (!Browser.Engine.trident) return {start: this.selectionStart, end: this.selectionEnd};
    var pos = {start: 0, end: 0};
    var range = this.getDocument().selection.createRange();
    if (!range || range.parentElement() != this) return pos;
    var dup = range.duplicate();
    if (this.type == 'text') {
      pos.start = 0 - dup.moveStart('character', -100000);
      pos.end = pos.start + range.text.length;
    } else {
      var value = this.value;
      var offset = value.length - value.match(/[\n\r]*$/)[0].length;
      dup.moveToElementText(this);
      dup.setEndPoint('StartToEnd', range);
      pos.end = offset - dup.text.length;
      dup.setEndPoint('StartToStart', range);
      pos.start = offset - dup.text.length;
    }
    return pos;
  },

  selectRange: function(start, end) {
    if (Browser.Engine.trident) {
      var diff = this.value.substr(start, end - start).replace(/\r/g, '').length;
      start = this.value.substr(0, start).replace(/\r/g, '').length;
      var range = this.createTextRange();
      range.collapse(true);
      range.moveEnd('character', start + diff);
      range.moveStart('character', start);
      range.select();
    } else {
      this.focus();
      this.setSelectionRange(start, end);
    }
    return this;
  }

});

LSD.Behavior.define('[list]', LSD.Mixin.Datalist)
<|endoftext|>


<commit_before>'use strict';

require([
	'wrapper/chrome',
	'storage/chrome-storage',
	'vendor/showdown.min'
], (chrome, ChromeStorage, showdown) => {
	/**
	 * Run initialization
	 */
	init();

	function init() {
		$('#opt-in').click(() => {
			updateGaState(false).then(closePage);
		});

		$('#opt-out').click(() => {
			updateGaState(true).then(closePage);
		});

		preparePrivacyPolicy();
	}

	function closePage() {
		window.close();
		// $('.controls').hide();
		// $('.finished').show();
	}

	async function updateGaState(value) {
		const options = ChromeStorage.getStorage(ChromeStorage.OPTIONS);

		let data = await options.get();
		data.disableGa = value;
		await options.set(data);
	}

	async function preparePrivacyPolicy() {
		const locale = chrome.i18n.getMessage('@@ui_locale');
		const defaultPrivacyDoc = 'PRIVACY.md';
		let privacyDocs = [defaultPrivacyDoc];

		if (!locale.startsWith('en')) {
			let localeSplit = locale.split('_');
			privacyDocs.unshift(`PRIVACY.${localeSplit[0]}.md`);
			privacyDocs.unshift(`PRIVACY.${locale}.md`);
		}

		for (let privacyDoc of privacyDocs) {
			console.log(`fetching ${privacyDoc}`);
			try {
				const response = await fetch(chrome.runtime.getURL(privacyDoc));
				const markdown = await response.text();
				let converter = new showdown.Converter();
				let content = converter.makeHtml(markdown);
				$('.privacy-policy').html(content);
				break;
			} catch (err) {
				console.log(`Failed to load ${privacyDoc}, reason: ${err.message}`);
			}
		}
	}
});
<commit_msg>Don't close startup page on button click<commit_after>'use strict';

require([
	'wrapper/chrome',
	'storage/chrome-storage',
	'vendor/showdown.min'
], (chrome, ChromeStorage, showdown) => {
	/**
	 * Run initialization
	 */
	init();

	function init() {
		$('#opt-in').click(() => {
			updateGaState(false).then(closePage);
		});

		$('#opt-out').click(() => {
			updateGaState(true).then(closePage);
		});

		preparePrivacyPolicy();
	}

	function closePage() {
		$('.controls').hide();
		$('.finished').show();
	}

	async function updateGaState(value) {
		const options = ChromeStorage.getStorage(ChromeStorage.OPTIONS);

		let data = await options.get();
		data.disableGa = value;
		await options.set(data);
	}

	async function preparePrivacyPolicy() {
		const locale = chrome.i18n.getMessage('@@ui_locale');
		const defaultPrivacyDoc = 'PRIVACY.md';
		let privacyDocs = [defaultPrivacyDoc];

		if (!locale.startsWith('en')) {
			let localeSplit = locale.split('_');
			privacyDocs.unshift(`PRIVACY.${localeSplit[0]}.md`);
			privacyDocs.unshift(`PRIVACY.${locale}.md`);
		}

		for (let privacyDoc of privacyDocs) {
			console.log(`fetching ${privacyDoc}`);
			try {
				const response = await fetch(chrome.runtime.getURL(privacyDoc));
				const markdown = await response.text();
				let converter = new showdown.Converter();
				let content = converter.makeHtml(markdown);
				$('.privacy-policy').html(content);
				break;
			} catch (err) {
				console.log(`Failed to load ${privacyDoc}, reason: ${err.message}`);
			}
		}
	}
});
<|endoftext|>


<commit_before>import React from 'react'
const el = React.createElement

export const selectInput = function ({ options, multiple = false, nullLabel="" }) {
  if (!multiple) options = [['$null', nullLabel]].concat(options)
  return function ({ value, onChange }) {
    return el('select', {
      multiple,
      value: normalizeValue(value, multiple),
      onChange: (ev) => onChange(normalizeEvent(ev, multiple)),
    }, options.map(op => el('option', {key: op[0], value: op[0]}, op[1])))
  }
}

export default function(arg) {
  let { options, multiple } = arg
  if (!options) {
    options = arg
  }
  return selectInput({
    options,
    multiple,
  })
}

function normalizeValue(val, multiple) {
  return multiple ? normalizeMultiValue(val) : normalizeMonoValue(val)
}

function normalizeMultiValue(value) {
  if (value === undefined) return []
  if (value === null) return []
  return Array.isArray(value) ? value: [value]
}
function normalizeMonoValue(value) {
  return (value === undefined) ? '$null' : value
}

function normalizeEvent(ev, multiple) {
  return multiple ?
    [...ev.target.selectedOptions].map(opt => opt.value) :
    ev.target.value === '$null' ? null : ev.target.value
}
<commit_msg>Select form part accepts any type of value as option<commit_after>import React from 'react'
const el = React.createElement
import findIndex from 'lodash/findIndex'

export const selectInput = function ({ options, multiple = false, nullLabel="" }) {
  if (!multiple) options = [[null, nullLabel]].concat(options)
  return function ({ value, onChange }) {
    return el('select', {
      multiple,
      value: normalizeValue(value, options, multiple),
      onChange: (ev) => onChange(normalizeEvent(ev, options, multiple)),
    }, options.map((op, i) => el('option', { key: i, value: i }, op[1])))
  }
}

export default function(arg) {
  let { options, multiple } = arg
  if (!options) {
    options = arg
  }
  return selectInput({
    options,
    multiple,
  })
}

function indexFromValue(value, options) {
  return findIndex(options, (option) => option[0] === value)
}

function valueFromIndex(index, options) {
  return options[index][0]
}

function normalizeValue(val, options, multiple) {
  return multiple ? normalizeMultiValue(val, options) : normalizeMonoValue(val, options)
}

function normalizeMultiValue(value, options) {
  if (value === undefined) return []
  if (value === null) return []
  return Array.isArray(value) ? value.map(v => indexFromValue(v, options)) : [value]
}
function normalizeMonoValue(value, options) {
  return indexFromValue((value === undefined) ? null : value, options)
}

function normalizeEvent(ev, options, multiple) {
  return multiple ?
    [...ev.target.selectedOptions].map(opt => valueFromIndex(opt.value, options)) :
    valueFromIndex(ev.target.value, options)
}
<|endoftext|>


<commit_before>/**
 *
 *    Copyright (c) 2020 Silicon Labs
 *
 *    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.
 */
const restApi = require('../../../src-shared/rest-api.js')

export default function () {
  return {
    studioProject: '',
    leftDrawerOpenState: true,
    miniState: false,
    informationText: '',
    clusters: [],
    domains: [],
    attributes: [],
    commands: [],
    zclDeviceTypes: {},
    endpoints: [],
    genericOptions: {},
    selectedGenericOptions: {},
    clusterManager: {
      openDomains: {},
      filter: {
        label: 'N/A',
        filterFn: (domain, currentOpenDomains, context) =>
          currentOpenDomains[domain],
      },
      filterOptions: [
        {
          label: 'All Clusters',
          filterFn: (domain, currentOpenDomains, context) => true,
        },
        {
          label: 'Only Enabled',
          filterFn: (domain, currentOpenDomains, context) => {
            return context.enabledClusters
              .map((a) => a.domainName)
              .includes(domain)
          },
        },
        {
          label: 'Close All',
          filterFn: (domain, currentOpenDomains, context) => false,
        },
      ],
      filterString: '',
    },
    endpointView: {
      selectedEndpoint: [],
      endpointId: {},
      endpointType: {},
      networkId: {},
      endpointIdValidationIssues: {},
      networkIdValidationIssues: {},
    },
    endpointTypeView: {
      selectedEndpointType: [],
      name: {},
      deviceTypeRef: {},
    },
    clustersView: {
      selected: [],
      selectedServers: [],
      selectedClients: [],
      // These are based off of the selected ZCL Endpoints Device Type
      recommendedClients: [],
      recommendedServers: [],
    },
    attributeView: {
      selectedAttributes: [],
      editableAttributes: {},
      selectedSingleton: [],
      selectedBounded: [],
      defaultValue: {},
      storageOption: {},
      // These are based off of the selected ZCL Endpoint Device Type
      recommendedAttributes: [],
      defaultValueValidationIssues: {},
      selectedReporting: [],
      reportingMin: {},
      reportingMax: {},
      reportableChange: {},
    },
    commandView: {
      selectedIn: [],
      selectedOut: [],
      // These are based off of the selected ZCL Endpoint Device Type
      requiredCommands: [],
    },
    calledArgs: {
      defaultUiMode: restApi.uiMode.ZIGBEE,
      embeddedMode: false,
    },
  }
}
<commit_msg>NOJIRA Change N/A Filter to No Filter<commit_after>/**
 *
 *    Copyright (c) 2020 Silicon Labs
 *
 *    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.
 */
const restApi = require('../../../src-shared/rest-api.js')

export default function () {
  return {
    studioProject: '',
    leftDrawerOpenState: true,
    miniState: false,
    informationText: '',
    clusters: [],
    domains: [],
    attributes: [],
    commands: [],
    zclDeviceTypes: {},
    endpoints: [],
    genericOptions: {},
    selectedGenericOptions: {},
    clusterManager: {
      openDomains: {},
      filter: {
        label: 'No Filter',
        filterFn: (domain, currentOpenDomains, context) =>
          currentOpenDomains[domain],
      },
      filterOptions: [
        {
          label: 'No Filter',
          filterFn: (domain, currentOpenDomains, context) =>
            currentOpenDomains[domain],
        },
        {
          label: 'All Clusters',
          filterFn: (domain, currentOpenDomains, context) => true,
        },
        {
          label: 'Only Enabled',
          filterFn: (domain, currentOpenDomains, context) => {
            return context.enabledClusters
              .map((a) => a.domainName)
              .includes(domain)
          },
        },
        {
          label: 'Close All',
          filterFn: (domain, currentOpenDomains, context) => false,
        },
      ],
      filterString: '',
    },
    endpointView: {
      selectedEndpoint: [],
      endpointId: {},
      endpointType: {},
      networkId: {},
      endpointIdValidationIssues: {},
      networkIdValidationIssues: {},
    },
    endpointTypeView: {
      selectedEndpointType: [],
      name: {},
      deviceTypeRef: {},
    },
    clustersView: {
      selected: [],
      selectedServers: [],
      selectedClients: [],
      // These are based off of the selected ZCL Endpoints Device Type
      recommendedClients: [],
      recommendedServers: [],
    },
    attributeView: {
      selectedAttributes: [],
      editableAttributes: {},
      selectedSingleton: [],
      selectedBounded: [],
      defaultValue: {},
      storageOption: {},
      // These are based off of the selected ZCL Endpoint Device Type
      recommendedAttributes: [],
      defaultValueValidationIssues: {},
      selectedReporting: [],
      reportingMin: {},
      reportingMax: {},
      reportableChange: {},
    },
    commandView: {
      selectedIn: [],
      selectedOut: [],
      // These are based off of the selected ZCL Endpoint Device Type
      requiredCommands: [],
    },
    calledArgs: {
      defaultUiMode: restApi.uiMode.ZIGBEE,
      embeddedMode: false,
    },
  }
}
<|endoftext|>


<commit_before>import BuyActions from '../actions/BuyActions';
import Dispatcher from '../dispatchers/AppDispatcher';
import events from 'events';
import fetch from '../core/fetch';
import { googleApiKey, iapiBase, apiKey } from '../config';
const EventEmitter = events.EventEmitter;

const KEY = apiKey;
const GOOGLE_API_KEY = googleApiKey;

class BuyStore extends EventEmitter {
	constructor() {
		super();
		this.state = {
			local: true,
			center: {
				lat: 44.8167,
				lng: -91.5000,
			},
			bounds: {
				ne: '44.853219616918416,-91.4007797241211',
				sw: '44.78015723973733,-91.59647369384766',
			},
			skip: 0,
			sort: 2,
			count: 100,
			brand: 3,
			markers: [],
			showModal: false,
			origin: '',
			destination: '',
			fetchDirections: false,
			directions: null,
			suggestions: [],
			showRegions: false,
			regions: [],
			zoom: 13,
			error: null,
			checked: {
				platinum: true,
				gold: true,
				silver: true,
			},
		};
		this.bindListeners({
			setLocal: BuyActions.setLocal,
			bounds: BuyActions.bounds,
			setMarkers: BuyActions.setMarkers,
			online: BuyActions.online,
			setModal: BuyActions.setModal,
			setOrigin: BuyActions.setOrigin,
			showDirections: BuyActions.showDirections,
			setDirections: BuyActions.setDirections,
			setSuggestions: BuyActions.setSuggestions,
			setCurrentLocation: BuyActions.setCurrentLocation,
			setCenter: BuyActions.setCenter,
			setShowRegions: BuyActions.setShowRegions,
			regions: BuyActions.regions,
			setCenterAndZoom: BuyActions.setCenterAndZoom,
			setError: BuyActions.setError,
			geocode: BuyActions.geocode,
			markerVisibility: BuyActions.markerVisibility,
		});
	}

	setLocal(local) {
		this.setState({
			local,
		});
		if (local === false) {
			this.online();
		}
	}

	setMarkers(markers) {
		this.setState({ markers });
	}

	setSuggestions(suggestions) {
		this.setState({ suggestions });
	}

	setCurrentLocation(currentLocation) {
		this.setState({ currentLocation });
	}

	setCenter(center) {
		this.setState({ center });
	}

	setError(error) {
		this.setState({ error });
	}

	setCenterAndZoom(args) {
		let showRegions = false;
		if (args[1] < 7) {
			showRegions = true;
		}
		this.setState({ center: args[0], zoom: args[1], showRegions, markers: [] });
	}

	setModal(args) {
		const showModal = args[0];
		this.setState({ showModal });
		if (args.length > 1) {
			const address = args[1].address + ' ' + args[1].city + ', ' + args[1].state.abbreviation + ' ' + args[1].postalCode;
			this.setState({
				destination: address,
			});
		}
	}

	setOrigin(origin) {
		this.setState({ origin });
	}

	showDirections(fetchDirections) {
		this.setState({ fetchDirections, showModal: null });
	}

	setDirections(directions) {
		this.setState({ directions, showModal: null });
	}

	setShowRegions(showRegions) {
		this.setState({ showRegions });
	}

	markerVisibility(args) {
		const selectedTier = args[0];
		const checked = args[1];

		if (this.state.markers.length < 1) {
			return;
		}
		this.state.markers.map((marker, i) => {
			if (marker.dealerTier && marker.dealerTier.tier && marker.dealerTier.tier.toLowerCase() === selectedTier) {
				if (!this.state.markers[i].hide) {
					this.state.markers[i].hide = true;
					return;
				}
				this.state.markers[i].hide = !this.state.markers[i].hide;
			}
		});
		this.state.checked[selectedTier] = checked;
		this.setState({ markers: this.state.markers, checked: this.state.checked });
	}
	async bounds(args) {
		this.setState({ center: args[0], zoom: args[2] });
		let showRegions = false;
		if (args[2] < 7) {
			showRegions = true;
		}
		const params = `&brand=${this.state.brand}&skip=${this.state.skip}&sort=${this.state.sort}&count=${this.state.count}&center=${args[0].lat},${args[0].lng}&ne=${args[1].ne}&sw=${args[1].sw}`;
		try {
			await fetch(iapiBase + '/dealers/local/bounds?key=' + KEY + params, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				let markers = [];
				if (data && data !== null && showRegions === false) {
					markers = data;
				}
				this.setState({ markers, showRegions, error: null });
			});
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}

	async online() {
		const params = `&brand=${this.state.brand}&skip=${this.state.skip}&count=${this.state.count}`;
		try {
			await fetch(iapiBase + '/dealers/online?key=' + KEY + params, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				if (data && data !== null) {
					this.setState({ markers: data, error: null });
				} else {
					this.setState({ error: 'Not enough results.' });
				}
			});
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}

	async regions() {
		try {
			await fetch(iapiBase + '/dealers/local/regions?key=' + KEY, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				if (data && data !== null) {
					this.setState({ regions: data, error: null });
				} else {
					this.setState({ error: 'Not enough results.' });
				}
			});
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}

	async geocode(address) {
		try {
			let center = {};
			let bounds = {};
			const zoom = 13;
			await fetch('https://maps.googleapis.com/maps/api/geocode/json?address=' + encodeURIComponent(address) + '&key=' + GOOGLE_API_KEY, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				if (data && data !== null && data.results.length > 0) {
					center = { lat: data.results[0].geometry.location.lat, lng: data.results[0].geometry.location.lng };
					bounds = {
						ne: data.results[0].geometry.viewport.northeast.lat + ',' + data.results[0].geometry.viewport.northeast.lng,
						sw: data.results[0].geometry.viewport.southwest.lat + ',' + data.results[0].geometry.viewport.southwest.lng,
					};
					this.setState({ center, zoom, error: null, bounds, suggestions: null });
				} else {
					this.setState({ error: 'Not enough results.' });
				}
			});
			await this.bounds([center, bounds, zoom]);
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}
}

export default Dispatcher.createStore(BuyStore, 'BuyStore');
<commit_msg>wtb - keeps hidden markers hidden after map resize event<commit_after>import BuyActions from '../actions/BuyActions';
import Dispatcher from '../dispatchers/AppDispatcher';
import events from 'events';
import fetch from '../core/fetch';
import { googleApiKey, iapiBase, apiKey } from '../config';
const EventEmitter = events.EventEmitter;

const KEY = apiKey;
const GOOGLE_API_KEY = googleApiKey;

class BuyStore extends EventEmitter {
	constructor() {
		super();
		this.state = {
			local: true,
			center: {
				lat: 44.8167,
				lng: -91.5000,
			},
			bounds: {
				ne: '44.853219616918416,-91.4007797241211',
				sw: '44.78015723973733,-91.59647369384766',
			},
			skip: 0,
			sort: 2,
			count: 100,
			brand: 3,
			markers: [],
			showModal: false,
			origin: '',
			destination: '',
			fetchDirections: false,
			directions: null,
			suggestions: [],
			showRegions: false,
			regions: [],
			zoom: 13,
			error: null,
			checked: {
				platinum: true,
				gold: true,
				silver: true,
			},
		};
		this.bindListeners({
			setLocal: BuyActions.setLocal,
			bounds: BuyActions.bounds,
			setMarkers: BuyActions.setMarkers,
			online: BuyActions.online,
			setModal: BuyActions.setModal,
			setOrigin: BuyActions.setOrigin,
			showDirections: BuyActions.showDirections,
			setDirections: BuyActions.setDirections,
			setSuggestions: BuyActions.setSuggestions,
			setCurrentLocation: BuyActions.setCurrentLocation,
			setCenter: BuyActions.setCenter,
			setShowRegions: BuyActions.setShowRegions,
			regions: BuyActions.regions,
			setCenterAndZoom: BuyActions.setCenterAndZoom,
			setError: BuyActions.setError,
			geocode: BuyActions.geocode,
			markerVisibility: BuyActions.markerVisibility,
		});
	}

	setLocal(local) {
		this.setState({
			local,
		});
		if (local === false) {
			this.online();
		}
	}

	setMarkers(markers) {
		this.setState({ markers });
	}

	setSuggestions(suggestions) {
		this.setState({ suggestions });
	}

	setCurrentLocation(currentLocation) {
		this.setState({ currentLocation });
	}

	setCenter(center) {
		this.setState({ center });
	}

	setError(error) {
		this.setState({ error });
	}

	setCenterAndZoom(args) {
		let showRegions = false;
		if (args[1] < 7) {
			showRegions = true;
		}
		this.setState({ center: args[0], zoom: args[1], showRegions, markers: [] });
	}

	setModal(args) {
		const showModal = args[0];
		this.setState({ showModal });
		if (args.length > 1) {
			const address = args[1].address + ' ' + args[1].city + ', ' + args[1].state.abbreviation + ' ' + args[1].postalCode;
			this.setState({
				destination: address,
			});
		}
	}

	setOrigin(origin) {
		this.setState({ origin });
	}

	showDirections(fetchDirections) {
		this.setState({ fetchDirections, showModal: null });
	}

	setDirections(directions) {
		this.setState({ directions, showModal: null });
	}

	setShowRegions(showRegions) {
		this.setState({ showRegions });
	}

	markerVisibility(args) {
		const selectedTier = args[0];
		const checked = args[1];

		if (this.state.markers.length < 1) {
			return;
		}
		this.state.markers.map((marker, i) => {
			if (marker.dealerTier && marker.dealerTier.tier && marker.dealerTier.tier.toLowerCase() === selectedTier) {
				if (!this.state.markers[i].hide) {
					this.state.markers[i].hide = true;
					return;
				}
				this.state.markers[i].hide = !this.state.markers[i].hide;
			}
		});
		this.state.checked[selectedTier] = checked;
		this.setState({ markers: this.state.markers, checked: this.state.checked });
	}
	async bounds(args) {
		this.setState({ center: args[0], zoom: args[2] });
		let showRegions = false;
		if (args[2] < 7) {
			showRegions = true;
		}
		const params = `&brand=${this.state.brand}&skip=${this.state.skip}&sort=${this.state.sort}&count=${this.state.count}&center=${args[0].lat},${args[0].lng}&ne=${args[1].ne}&sw=${args[1].sw}`;
		try {
			await fetch(iapiBase + '/dealers/local/bounds?key=' + KEY + params, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				let markers = [];
				if (data && data !== null && showRegions === false) {
					data.map((marker) => {
						this.state.markers.map((currentMarker) => {
							if (currentMarker.id === marker.id) {
								marker.hide = currentMarker.hide;
							}
						});
					});
					markers = data;
				}
				this.setState({ markers, showRegions, error: null });
			});
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}

	async online() {
		const params = `&brand=${this.state.brand}&skip=${this.state.skip}&count=${this.state.count}`;
		try {
			await fetch(iapiBase + '/dealers/online?key=' + KEY + params, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				if (data && data !== null) {
					this.setState({ markers: data, error: null });
				} else {
					this.setState({ error: 'Not enough results.' });
				}
			});
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}

	async regions() {
		try {
			await fetch(iapiBase + '/dealers/local/regions?key=' + KEY, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				if (data && data !== null) {
					this.setState({ regions: data, error: null });
				} else {
					this.setState({ error: 'Not enough results.' });
				}
			});
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}

	async geocode(address) {
		try {
			let center = {};
			let bounds = {};
			const zoom = 13;
			await fetch('https://maps.googleapis.com/maps/api/geocode/json?address=' + encodeURIComponent(address) + '&key=' + GOOGLE_API_KEY, {
				method: 'get',
				headers: {
					'Accept': 'application/json',
				},
			}).then((resp) => {
				return resp.json();
			}).then((data) => {
				if (data && data !== null && data.status === 'OK' && data.results.length > 0) {
					center = { lat: data.results[0].geometry.location.lat, lng: data.results[0].geometry.location.lng };
					bounds = {
						ne: data.results[0].geometry.viewport.northeast.lat + ',' + data.results[0].geometry.viewport.northeast.lng,
						sw: data.results[0].geometry.viewport.southwest.lat + ',' + data.results[0].geometry.viewport.southwest.lng,
					};
					this.setState({ center, zoom, error: null, bounds, suggestions: null });
				} else {
					this.setState({ error: 'Not enough results.' });
				}
			});
			await this.bounds([center, bounds, zoom]);
		} catch (err) {
			this.setState({
				error: err,
			});
		}
	}
}

export default Dispatcher.createStore(BuyStore, 'BuyStore');
<|endoftext|>


<commit_before>'use strict';

/**
 * Dependencies
 */
const chalk = require('chalk');

/**
 * Response error class
 */
class ResponseError extends Error {

  /**
   * Constructor
   */
  constructor(response) {

    //Super
    super();

    //Extract data from response
    const {headers, statusCode, statusMessage, body} = response;

    //Set data
    this.code = statusCode;
    this.message = statusMessage;
    this.response = {headers, body};

    //Capture stack trace
    if (!this.stack) {
      Error.captureStackTrace(this, this.constructor);
    }

    //Clean up stack trace
    const regex = new RegExp(process.cwd() + '/', 'gi');
    this.stack = this.stack.replace(regex, '');
  }

  /**
   * Convert to string
   */
  toString() {
    const {body} = this.response;
    let err = chalk.red(`${this.message} (${this.code})`);
    if (Array.isArray(body.errors)) {
      body.errors.forEach(error => {
        const message = chalk.yellow(error.message);
        const field = chalk.grey(error.field);
        const help = chalk.grey(error.help);
        err += `\n  ${message}\n    ${field}\n    ${help}`;
      });
    }
    return err;
  }

  /**
   * Convert to simple object for JSON responses
   */
  toJSON() {
    const {message, code, response} = this;
    return {message, code, response};
  }
}

//Export
module.exports = ResponseError;
<commit_msg>Check if "body" on "response" is actually defined before accessing it<commit_after>'use strict';

/**
 * Dependencies
 */
const chalk = require('chalk');

/**
 * Response error class
 */
class ResponseError extends Error {

  /**
   * Constructor
   */
  constructor(response) {

    //Super
    super();

    //Extract data from response
    const {headers, statusCode, statusMessage, body} = response;

    //Set data
    this.code = statusCode;
    this.message = statusMessage;
    this.response = {headers, body};

    //Capture stack trace
    if (!this.stack) {
      Error.captureStackTrace(this, this.constructor);
    }

    //Clean up stack trace
    const regex = new RegExp(process.cwd() + '/', 'gi');
    this.stack = this.stack.replace(regex, '');
  }

  /**
   * Convert to string
   */
  toString() {
    const {body} = this.response;
    let err = chalk.red(`${this.message} (${this.code})`);
    if (body && Array.isArray(body.errors)) {
      body.errors.forEach(error => {
        const message = chalk.yellow(error.message);
        const field = chalk.grey(error.field);
        const help = chalk.grey(error.help);
        err += `\n  ${message}\n    ${field}\n    ${help}`;
      });
    }
    return err;
  }

  /**
   * Convert to simple object for JSON responses
   */
  toJSON() {
    const {message, code, response} = this;
    return {message, code, response};
  }
}

//Export
module.exports = ResponseError;
<|endoftext|>


<commit_before>import { ParseError, EmptyMJMLError, NullElementError } from '../Error'
import compact from 'lodash/compact'
import dom from '../helpers/dom'
import each from 'lodash/each'
import filter from 'lodash/filter'
import MJMLElements, { endingTags } from '../MJMLElementsCollection'
import MJMLHeadElements from '../MJMLHead'
import warning from 'warning'

const regexTag = tag => new RegExp(`<${tag}([^>]*)>([^]*?)</${tag}>`, 'gmi')

/**
 * Avoid htmlparser to parse ending tags
 */
const safeEndingTags = content => {
  const regexpBody = regexTag('mj-body')
  let bodyContent = content.match(regexpBody)

  if (!bodyContent) {
    return content
  }

  bodyContent = bodyContent[0]

  endingTags.forEach(tag => {
    bodyContent = bodyContent.replace(regexTag(tag), dom.replaceContentByCdata(tag))
  })

  return content.replace(regexpBody, bodyContent)
}

/**
 * converts MJML body into a JSON representation
 */
const mjmlElementParser = elem => {
  if (!elem) {
    throw new NullElementError('Null element found in mjmlElementParser')
  }

  const tagName = elem.tagName.toLowerCase()
  const attributes = dom.getAttributes(elem)

  const element = { tagName, attributes }

  if (!MJMLElements[tagName]) {
    warning(false, `Unregistered element: ${tagName}, skipping it`)
    return
  }

  if (endingTags.indexOf(tagName) !== -1) {
    const $ = dom.parseXML(elem)
    element.content = $(tagName).html().trim()
  } else {
    const children = dom.getChildren(elem)
    element.children = children ? compact(filter(children, child => child.tagName).map(mjmlElementParser)) : []
  }

  return element
}

const parseHead = (head, attributes) => {
  const $container = dom.parseHTML(attributes.container)

  each(compact(filter(dom.getChildren(head), child => child.tagName)), element => {
    const handler = MJMLHeadElements[element.tagName.toLowerCase()]

    if (handler) {
      handler(element, { $container, ...attributes })
    } else {
      warning(false, `No handler found for: ${element.tagName}, in mj-head, skipping it`)
    }
  })

  attributes.container = dom.getHTML($container)
}

/**
 * Import an html document containing some mjml
 * returns JSON
 *   - container: the mjml container
 *   - mjml: a json representation of the mjml
 */
const documentParser = (content, attributes) => {
  let root
  let head

  try {
    const $ = dom.parseXML(safeEndingTags(content))
    root = $('mjml > mj-body')
    head = $('mjml > mj-head')

    if (root.length < 1) {
      root = $('mj-body').get(0)
      warning(false, 'Please upgrade your MJML markup to add a <mjml> root tag, <mj-body> as root will no longer be supported soon, see https://github.com/mjmlio/mjml/blob/master/UPGRADE.md')
    } else {
      root = root.children().get(0)
    }
  } catch (e) {
    throw new ParseError('Error while parsing the file')
  }

  if (!root || root.length < 1) {
    throw new EmptyMJMLError('No root "<mjml>" or "<mj-body>" found in the file')
  }

  if (head && head.length === 1) {
    parseHead(head.get(0), attributes)
  }

  return mjmlElementParser(root)
}

export default documentParser
<commit_msg>Fix #341<commit_after>import { ParseError, EmptyMJMLError, NullElementError } from '../Error'
import compact from 'lodash/compact'
import dom from '../helpers/dom'
import each from 'lodash/each'
import filter from 'lodash/filter'
import MJMLElements, { endingTags } from '../MJMLElementsCollection'
import MJMLHeadElements from '../MJMLHead'
import warning from 'warning'

const regexTag = tag => new RegExp(`<${tag}([^>]*)>([^]*?)</${tag}>`, 'gmi')

/**
 * Avoid htmlparser to parse ending tags
 */
const safeEndingTags = content => {
  const regexpBody = regexTag('mj-body')
  let bodyContent = content.match(regexpBody)

  if (!bodyContent) {
    return content
  }

  bodyContent = bodyContent[0].replace('$', '$') // $ is a protected chars for regexp... avoid issue with duplicate content

  endingTags.forEach(tag => {
    bodyContent = bodyContent.replace(regexTag(tag), dom.replaceContentByCdata(tag))
  })

  return content.replace(regexpBody, bodyContent)
}

/**
 * converts MJML body into a JSON representation
 */
const mjmlElementParser = elem => {
  if (!elem) {
    throw new NullElementError('Null element found in mjmlElementParser')
  }

  const tagName = elem.tagName.toLowerCase()
  const attributes = dom.getAttributes(elem)

  const element = { tagName, attributes }

  if (!MJMLElements[tagName]) {
    warning(false, `Unregistered element: ${tagName}, skipping it`)
    return
  }

  if (endingTags.indexOf(tagName) !== -1) {
    const $ = dom.parseXML(elem)
    element.content = $(tagName).html().trim()
  } else {
    const children = dom.getChildren(elem)
    element.children = children ? compact(filter(children, child => child.tagName).map(mjmlElementParser)) : []
  }

  return element
}

const parseHead = (head, attributes) => {
  const $container = dom.parseHTML(attributes.container)

  each(compact(filter(dom.getChildren(head), child => child.tagName)), element => {
    const handler = MJMLHeadElements[element.tagName.toLowerCase()]

    if (handler) {
      handler(element, { $container, ...attributes })
    } else {
      warning(false, `No handler found for: ${element.tagName}, in mj-head, skipping it`)
    }
  })

  attributes.container = dom.getHTML($container)
}

/**
 * Import an html document containing some mjml
 * returns JSON
 *   - container: the mjml container
 *   - mjml: a json representation of the mjml
 */
const documentParser = (content, attributes) => {
  let root
  let head

  try {
    const $ = dom.parseXML(safeEndingTags(content))
    root = $('mjml > mj-body')
    head = $('mjml > mj-head')

    if (root.length < 1) {
      root = $('mj-body').get(0)
      warning(false, 'Please upgrade your MJML markup to add a <mjml> root tag, <mj-body> as root will no longer be supported soon, see https://github.com/mjmlio/mjml/blob/master/UPGRADE.md')
    } else {
      root = root.children().get(0)
    }
  } catch (e) {
    throw new ParseError('Error while parsing the file')
  }

  if (!root || root.length < 1) {
    throw new EmptyMJMLError('No root "<mjml>" or "<mj-body>" found in the file')
  }

  if (head && head.length === 1) {
    parseHead(head.get(0), attributes)
  }

  return mjmlElementParser(root)
}

export default documentParser
<|endoftext|>


<commit_before>const mongoose = require('mongoose')
const config = require('../../config.js')
const fs = require('fs')
const fsPromises = fs.promises
const path = require('path')
const log = require('../../util/logger.js')

/**
 * @typedef {import('mongoose').Model<import('mongoose').Document, {}>} MongooseModel
 */

class Base {
  /**
   * Object data
   * @param {MongooseModel|Object<string, any>} data
   */
  constructor (data = {}, _saved = false) {
    /**
     * this.data must be serialized to maintain
     * equal function between database and databaseless
     * @type {Object<string, any>}
     */
    this.data = data instanceof mongoose.Model ? JSON.parse(JSON.stringify(data.toJSON())) : data

    /**
     * Internal ID, usually MongoDB's ObjectId purely used
     * to distinguish between documents and to grab from
     * database during testing.
     * @type {string}
     */
    this._id = this.data._id

    /**
     * Whether this has been saved to the database already
     * @type {boolean}
     */
    this._saved = _saved

    /**
     * Only used for database methods
     * @type {MongooseModel}
     */
    this.document = data instanceof mongoose.Model ? data : null

    /**
     * The bot version this data model was created on
     * @type {string}
     */
    this.version = this.getField('version')

    /**
     * Time of entry
     * @type {String}
     */
    this.addedAt = this.getField('addedAt')

    // Run the get method and throw its associated error if unimplemented
    void this.constructor.Model
  }

  /**
   * Remove the internal version key when finding from database
   */
  static FIND_PROJECTION () {
    return { __v: 0 }
  }

  /**
   * Returns the Mongoose model specific to the class
   * @returns {MongooseModel}
   */
  static get Model () {
    throw new Error('Model static get method must be implemented by subclasses')
  }

  /**
   * Checks whether the app is databaseless
   * @returns {boolean}
   */
  static get isMongoDatabase () {
    return config.database.uri.startsWith('mongo')
  }

  /**
   * Get the folder paths of the intended fs location the data will
   * be written to.
   * @returns {string[]}
   */
  static getFolderPaths () {
    const folderPath = config.database.uri
    const subfolderPath = path.join(folderPath, this.Model.collection.collectionName)
    return [ folderPath, subfolderPath ]
  }

  /**
   * Helper function to return undefined for empty objects
   * @param {any} field - The field name
   * @private
   */
  static resolveObject (value) {
    if (!value || Object.keys(value).length === 0) {
      return undefined
    } else {
      return value
    }
  }

  /**
   * A function that validates data before saving it.
   * Used by extended classes.
   */
  async validate () {}

  /**
   * Resolves data acquisition based on whether the app is databaseless
   * @param {string} field - The field to get
   * @param {*} def - The default value if there is no value
   * @returns {*} - The field value
   */
  getField (field, def) {
    const value = this.data[field]
    return value === undefined || value === null ? def : value
  }

  /**
   * Convert data into a plain object
   * @returns {Object<string, any>} - The plain object
   */
  toObject () {
    throw new Error('Method must be implemented by subclasses')
  }

  /**
   * Get a document
   * @param {string} id - Guild ID
   * @returns {Base|null}
   */
  static async get (id) {
    if (!id) {
      throw new Error('Undefined id')
    }
    if (typeof id !== 'string') {
      throw new Error('id must be a string')
    }

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const doc = await DatabaseModel.findById(id).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    const folderPath = folderPaths[folderPaths.length - 1]
    const filePath = path.join(folderPath, `${id}.json`)
    if (!fs.existsSync(filePath)) {
      return null
    }

    try {
      const readContent = fs.readFileSync(filePath)
      return new this(JSON.parse(readContent), true)
    } catch (err) {
      log.general.warning(`Could not parse ${DatabaseModel.collection.collectionName} JSON from file ${id}`, err)
      return null
    }
  }

  /**
   * Get by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base|null}
   */
  static async getBy (field, value) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const query = {
        [field]: value
      }
      const doc = await DatabaseModel.findOne(query, this.FIND_PROJECTION).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const many = await this.getManyBy(field, value)
    return many[0] ? new this(many[0], true) : null
  }

  /**
   * Get many with a custom query
   * @param {Object<string, any>} query - MongoDB-format query
   * @returns {Base[]}
   */
  static async getManyByQuery (query) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const docs = await DatabaseModel.find(query, this.FIND_PROJECTION).exec()
      return docs.map(doc => new this(doc, true))
    }

    // Databaseless - very slow
    const folderPaths = this.getFolderPaths()
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(folderPath)) {
      return []
    }

    const fileNames = await fsPromises.readdir(folderPath)
    const promises = fileNames.map(name => fsPromises.readFile(path.join(folderPath, name)))
    const resolved = await Promise.all(promises)
    const jsons = resolved.map((contents, index) => {
      try {
        return JSON.parse(contents)
      } catch (err) {
        log.general.error(`Failed to parse json at ${folderPath} ${fileNames[index]}`, err)
      }
    })
    return jsons
      .filter(item => {
        if (!item) {
          return false
        }
        let allTrue = true
        for (const key in query) {
          allTrue = allTrue && item[key] === query[key]
        }
        return allTrue
      })
      .map(item => new this(item, true))
  }

  /**
   * Get many by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base[]}
   */
  static async getManyBy (field, value) {
    return this.getManyByQuery({ [field]: value })
  }

  /**
   * Get multiple documents
   * @param {string[]} ids - Array of guild IDs
   * @returns {Base[]}
   */
  static async getMany (ids) {
    return Promise.all(ids.map(id => this.get(id)))
  }

  /**
   * Get all documents
   * @returns {Base[]}
   */
  static async getAll () {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const documents = await DatabaseModel.find({}, this.FIND_PROJECTION).exec()
      return documents.map(doc => new this(doc, true))
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    const fullPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(fullPath)) {
      return []
    }
    const fileReads = fs.readdirSync(fullPath)
      .filter(id => /\.json$/.test(id))
      .map(fileName => this.get(fileName.replace(/\.json/i, '')))

    return Promise.all(fileReads)
  }

  /**
   * Deletes all from the database
   */
  static async deleteAll () {
    // Mongo
    if (this.isMongoDatabase) {
      await this.Model.deleteMany({}).exec()
      return
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    if (fs.existsSync(folderPaths[1])) {
      const files = await fsPromises.readdir(folderPaths[1])
      await Promise.all(files.map(name => fsPromises.unlink(path.join(folderPaths[1], name))))
      await fsPromises.rmdir(folderPaths[1])
    }
  }

  /**
   * Deletes this from either the database from the file system
   * depending on whether the app is databaseless.
   */
  async delete () {
    if (!this._saved) {
      throw new Error('Data has not been saved')
    }

    // Mongo
    if (this.constructor.isMongoDatabase) {
      await this.document.remove()
      return
    }

    const Model = this.constructor.Model

    // Databaseless
    const paths = this.constructor.getFolderPaths()
    const folderPath = paths[paths.length - 1]
    const filePath = path.join(folderPath, `${this._id}.json`)
    if (!fs.existsSync(filePath)) {
      log.general.warning(`Unable to delete ${Model.collection.collectionName} ${this._id} at ${filePath} since its nonexistent`)
    } else {
      fs.unlinkSync(filePath)
    }
  }

  /**
   * Save the data to either the database or a file depending on whether the
   * app is databaseless.
   * @returns {Base} - This instance
   */
  async save () {
    await this.validate()
    if (this.constructor.isMongoDatabase) {
      return this.saveToDatabase()
    } else {
      return this.saveToFile()
    }
  }

  /**
   * Save the data to the database
   * @returns {Base}
   */
  async saveToDatabase () {
    const toSave = this.toObject()

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.constructor.Model

    if (!this._saved) {
      // Delete all undefined keys
      for (const key in toSave) {
        if (toSave[key] === undefined) {
          delete toSave[key]
        }
      }
      const model = new DatabaseModel(toSave)
      const document = await model.save()

      this._saved = true
      this._id = document.id
      this.document = document
      this.data = JSON.parse(JSON.stringify(document.toJSON()))
    } else {
      for (const key in toSave) {
        const value = toSave[key]
        // Map values must be individually set and deleted
        if (value instanceof Map) {
          if (!this.document[key]) {
            this.document.set(key, new Map())
          }
          const docMap = this.document[key]
          // First remove all unknown keys
          docMap.forEach((v, key) => {
            if (!value.has(key)) {
              docMap.delete(key)
            }
          })
          // Then set the new values
          value.forEach((value, mapKey) => docMap.set(mapKey, value))
        } else {
          this.document.set(key, toSave[key])
        }
      }
      const saved = await this.document.save()
      this.data = JSON.parse(JSON.stringify(saved.toJSON()))

      // Update class data
      for (const key in toSave) {
        this[key] = this.data[key]
      }
    }

    return this
  }

  /**
   * Saves the data to a file
   * @returns {Base}
   */
  async saveToFile () {
    const toSave = this.toObject()

    for (const key in toSave) {
      if (toSave[key] === undefined) {
        delete toSave[key]
      }
    }

    const folderPaths = this.constructor.getFolderPaths()
    for (const p of folderPaths) {
      if (!fs.existsSync(p)) {
        fs.mkdirSync(p)
      }
    }
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!this._saved) {
      let useId = toSave._id
      if (!useId) {
        useId = new mongoose.Types.ObjectId().toHexString()
        toSave._id = useId
      }
      const serialized = JSON.stringify(toSave, null, 2)
      await fs.writeFileSync(path.join(folderPath, `${useId}.json`), serialized)
      this._id = useId
      this._saved = true
    } else {
      const serialized = JSON.stringify(toSave, null, 2)
      await fs.writeFileSync(path.join(folderPath, `${this._id}.json`), serialized)
    }
    return this
  }
}

module.exports = Base
<commit_msg>Fix case<commit_after>const mongoose = require('mongoose')
const config = require('../../config.js')
const fs = require('fs')
const fsPromises = fs.promises
const path = require('path')
const log = require('../../util/logger.js')

/**
 * @typedef {import('mongoose').Model<import('mongoose').Document, {}>} MongooseModel
 */

class Base {
  /**
   * Object data
   * @param {MongooseModel|Object<string, any>} data
   */
  constructor (data = {}, _saved = false) {
    /**
     * this.data must be serialized to maintain
     * equal function between database and databaseless
     * @type {Object<string, any>}
     */
    this.data = data instanceof mongoose.Model ? JSON.parse(JSON.stringify(data.toJSON())) : data

    /**
     * Internal ID, usually MongoDB's ObjectId purely used
     * to distinguish between documents and to grab from
     * database during testing.
     * @type {string}
     */
    this._id = this.data._id

    /**
     * Whether this has been saved to the database already
     * @type {boolean}
     */
    this._saved = _saved

    /**
     * Only used for database methods
     * @type {MongooseModel}
     */
    this.document = data instanceof mongoose.Model ? data : null

    /**
     * The bot version this data model was created on
     * @type {string}
     */
    this.version = this.getField('version')

    /**
     * Time of entry
     * @type {string}
     */
    this.addedAt = this.getField('addedAt')

    // Run the get method and throw its associated error if unimplemented
    void this.constructor.Model
  }

  /**
   * Remove the internal version key when finding from database
   */
  static FIND_PROJECTION () {
    return { __v: 0 }
  }

  /**
   * Returns the Mongoose model specific to the class
   * @returns {MongooseModel}
   */
  static get Model () {
    throw new Error('Model static get method must be implemented by subclasses')
  }

  /**
   * Checks whether the app is databaseless
   * @returns {boolean}
   */
  static get isMongoDatabase () {
    return config.database.uri.startsWith('mongo')
  }

  /**
   * Get the folder paths of the intended fs location the data will
   * be written to.
   * @returns {string[]}
   */
  static getFolderPaths () {
    const folderPath = config.database.uri
    const subfolderPath = path.join(folderPath, this.Model.collection.collectionName)
    return [ folderPath, subfolderPath ]
  }

  /**
   * Helper function to return undefined for empty objects
   * @param {any} field - The field name
   * @private
   */
  static resolveObject (value) {
    if (!value || Object.keys(value).length === 0) {
      return undefined
    } else {
      return value
    }
  }

  /**
   * A function that validates data before saving it.
   * Used by extended classes.
   */
  async validate () {}

  /**
   * Resolves data acquisition based on whether the app is databaseless
   * @param {string} field - The field to get
   * @param {*} def - The default value if there is no value
   * @returns {*} - The field value
   */
  getField (field, def) {
    const value = this.data[field]
    return value === undefined || value === null ? def : value
  }

  /**
   * Convert data into a plain object
   * @returns {Object<string, any>} - The plain object
   */
  toObject () {
    throw new Error('Method must be implemented by subclasses')
  }

  /**
   * Get a document
   * @param {string} id - Guild ID
   * @returns {Base|null}
   */
  static async get (id) {
    if (!id) {
      throw new Error('Undefined id')
    }
    if (typeof id !== 'string') {
      throw new Error('id must be a string')
    }

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const doc = await DatabaseModel.findById(id).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    const folderPath = folderPaths[folderPaths.length - 1]
    const filePath = path.join(folderPath, `${id}.json`)
    if (!fs.existsSync(filePath)) {
      return null
    }

    try {
      const readContent = fs.readFileSync(filePath)
      return new this(JSON.parse(readContent), true)
    } catch (err) {
      log.general.warning(`Could not parse ${DatabaseModel.collection.collectionName} JSON from file ${id}`, err)
      return null
    }
  }

  /**
   * Get by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base|null}
   */
  static async getBy (field, value) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const query = {
        [field]: value
      }
      const doc = await DatabaseModel.findOne(query, this.FIND_PROJECTION).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const many = await this.getManyBy(field, value)
    return many[0] ? new this(many[0], true) : null
  }

  /**
   * Get many with a custom query
   * @param {Object<string, any>} query - MongoDB-format query
   * @returns {Base[]}
   */
  static async getManyByQuery (query) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const docs = await DatabaseModel.find(query, this.FIND_PROJECTION).exec()
      return docs.map(doc => new this(doc, true))
    }

    // Databaseless - very slow
    const folderPaths = this.getFolderPaths()
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(folderPath)) {
      return []
    }

    const fileNames = await fsPromises.readdir(folderPath)
    const promises = fileNames.map(name => fsPromises.readFile(path.join(folderPath, name)))
    const resolved = await Promise.all(promises)
    const jsons = resolved.map((contents, index) => {
      try {
        return JSON.parse(contents)
      } catch (err) {
        log.general.error(`Failed to parse json at ${folderPath} ${fileNames[index]}`, err)
      }
    })
    return jsons
      .filter(item => {
        if (!item) {
          return false
        }
        let allTrue = true
        for (const key in query) {
          allTrue = allTrue && item[key] === query[key]
        }
        return allTrue
      })
      .map(item => new this(item, true))
  }

  /**
   * Get many by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base[]}
   */
  static async getManyBy (field, value) {
    return this.getManyByQuery({ [field]: value })
  }

  /**
   * Get multiple documents
   * @param {string[]} ids - Array of guild IDs
   * @returns {Base[]}
   */
  static async getMany (ids) {
    return Promise.all(ids.map(id => this.get(id)))
  }

  /**
   * Get all documents
   * @returns {Base[]}
   */
  static async getAll () {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const documents = await DatabaseModel.find({}, this.FIND_PROJECTION).exec()
      return documents.map(doc => new this(doc, true))
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    const fullPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(fullPath)) {
      return []
    }
    const fileReads = fs.readdirSync(fullPath)
      .filter(id => /\.json$/.test(id))
      .map(fileName => this.get(fileName.replace(/\.json/i, '')))

    return Promise.all(fileReads)
  }

  /**
   * Deletes all from the database
   */
  static async deleteAll () {
    // Mongo
    if (this.isMongoDatabase) {
      await this.Model.deleteMany({}).exec()
      return
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    if (fs.existsSync(folderPaths[1])) {
      const files = await fsPromises.readdir(folderPaths[1])
      await Promise.all(files.map(name => fsPromises.unlink(path.join(folderPaths[1], name))))
      await fsPromises.rmdir(folderPaths[1])
    }
  }

  /**
   * Deletes this from either the database from the file system
   * depending on whether the app is databaseless.
   */
  async delete () {
    if (!this._saved) {
      throw new Error('Data has not been saved')
    }

    // Mongo
    if (this.constructor.isMongoDatabase) {
      await this.document.remove()
      return
    }

    const Model = this.constructor.Model

    // Databaseless
    const paths = this.constructor.getFolderPaths()
    const folderPath = paths[paths.length - 1]
    const filePath = path.join(folderPath, `${this._id}.json`)
    if (!fs.existsSync(filePath)) {
      log.general.warning(`Unable to delete ${Model.collection.collectionName} ${this._id} at ${filePath} since its nonexistent`)
    } else {
      fs.unlinkSync(filePath)
    }
  }

  /**
   * Save the data to either the database or a file depending on whether the
   * app is databaseless.
   * @returns {Base} - This instance
   */
  async save () {
    await this.validate()
    if (this.constructor.isMongoDatabase) {
      return this.saveToDatabase()
    } else {
      return this.saveToFile()
    }
  }

  /**
   * Save the data to the database
   * @returns {Base}
   */
  async saveToDatabase () {
    const toSave = this.toObject()

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.constructor.Model

    if (!this._saved) {
      // Delete all undefined keys
      for (const key in toSave) {
        if (toSave[key] === undefined) {
          delete toSave[key]
        }
      }
      const model = new DatabaseModel(toSave)
      const document = await model.save()

      this._saved = true
      this._id = document.id
      this.document = document
      this.data = JSON.parse(JSON.stringify(document.toJSON()))
    } else {
      for (const key in toSave) {
        const value = toSave[key]
        // Map values must be individually set and deleted
        if (value instanceof Map) {
          if (!this.document[key]) {
            this.document.set(key, new Map())
          }
          const docMap = this.document[key]
          // First remove all unknown keys
          docMap.forEach((v, key) => {
            if (!value.has(key)) {
              docMap.delete(key)
            }
          })
          // Then set the new values
          value.forEach((value, mapKey) => docMap.set(mapKey, value))
        } else {
          this.document.set(key, toSave[key])
        }
      }
      const saved = await this.document.save()
      this.data = JSON.parse(JSON.stringify(saved.toJSON()))

      // Update class data
      for (const key in toSave) {
        this[key] = this.data[key]
      }
    }

    return this
  }

  /**
   * Saves the data to a file
   * @returns {Base}
   */
  async saveToFile () {
    const toSave = this.toObject()

    for (const key in toSave) {
      if (toSave[key] === undefined) {
        delete toSave[key]
      }
    }

    const folderPaths = this.constructor.getFolderPaths()
    for (const p of folderPaths) {
      if (!fs.existsSync(p)) {
        fs.mkdirSync(p)
      }
    }
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!this._saved) {
      let useId = toSave._id
      if (!useId) {
        useId = new mongoose.Types.ObjectId().toHexString()
        toSave._id = useId
      }
      const serialized = JSON.stringify(toSave, null, 2)
      await fs.writeFileSync(path.join(folderPath, `${useId}.json`), serialized)
      this._id = useId
      this._saved = true
    } else {
      const serialized = JSON.stringify(toSave, null, 2)
      await fs.writeFileSync(path.join(folderPath, `${this._id}.json`), serialized)
    }
    return this
  }
}

module.exports = Base
<|endoftext|>