<commit_before>/**
 * Copyright (c) 2015, Facebook, Inc.  All rights reserved.
 *
 * Facebook, Inc. ("Facebook") owns all right, title and interest, including
 * all intellectual property and other proprietary rights, in and to the React
 * Native CustomComponents software (the "Software").  Subject to your
 * compliance with these terms, you are hereby granted a non-exclusive,
 * worldwide, royalty-free copyright license to (1) use and copy the Software;
 * and (2) reproduce and distribute the Software as part of your own software
 * ("Your Software").  Facebook reserves all rights not expressly granted to
 * you in this license agreement.
 *
 * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
 * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
 * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
 * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
import React, {
  PropTypes,
} from 'react';
import {
  Platform,
  Animated,
  Image,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import Actions from './Actions';
import _drawerImage from './menu_burger.png';
import _backButtonImage from './back_chevron.png';

const styles = StyleSheet.create({
  title: {
    textAlign: 'center',
    color: '#0A0A0A',
    fontSize: 18,
    width: 180,
    alignSelf: 'center',
  },
  titleImage: {
    width: 180,
    alignSelf: 'center',
  },
  titleWrapper: {
    marginTop: 10,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 20,
      },
      android: {
        top: 5,
      },
    }),
    left: 0,
    right: 0,
  },
  header: {
    backgroundColor: '#EFEFF2',
    paddingTop: 0,
    top: 0,
    ...Platform.select({
      ios: {
        height: 64,
      },
      android: {
        height: 54,
      },
    }),
    right: 0,
    left: 0,
    borderBottomWidth: 0.5,
    borderBottomColor: '#828287',
    position: 'absolute',
  },
  backButton: {
    height: 37,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 22,
      },
      android: {
        top: 10,
      },
    }),
    left: 2,
    padding: 8,
    flexDirection: 'row',
  },
  rightButton: {
    height: 37,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 22,
      },
      android: {
        top: 10,
      },
    }),
    right: 2,
    padding: 8,
  },
  leftButton: {
    height: 37,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 20,
      },
      android: {
        top: 8,
      },
    }),
    left: 2,
    padding: 8,
  },
  barRightButtonText: {
    color: 'rgb(0, 122, 255)',
    textAlign: 'right',
    fontSize: 17,
  },
  barBackButtonText: {
    color: 'rgb(0, 122, 255)',
    textAlign: 'left',
    fontSize: 17,
    paddingLeft: 6,
  },
  barLeftButtonText: {
    color: 'rgb(0, 122, 255)',
    textAlign: 'left',
    fontSize: 17,
  },
  backButtonImage: {
    width: 13,
    height: 21,
  },
  defaultImageStyle: {
    height: 24,
    resizeMode: 'contain',
  },
});

const propTypes = {
  navigationState: PropTypes.object,
  backButtonImage: Image.propTypes.source,
  wrapBy: PropTypes.any,
  component: PropTypes.any,
  backButtonTextStyle: Text.propTypes.style,
  leftButtonStyle: View.propTypes.style,
  leftButtonIconStyle: Image.propTypes.style,
  getTitle: PropTypes.func,
  titleWrapperStyle: Text.propTypes.style,
  titleStyle: Text.propTypes.style,
  titleOpacity: PropTypes.number,
  titleProps: PropTypes.any,
  position: PropTypes.object,
  navigationBarStyle: View.propTypes.style,
  navigationBarBackgroundImage: Image.propTypes.source,
  navigationBarBackgroundImageStyle: Image.propTypes.style,
  navigationBarTitleImage: Image.propTypes.source,
  navigationBarTitleImageStyle: Image.propTypes.style,
  renderTitle: PropTypes.any,
};

const contextTypes = {
  drawer: PropTypes.object,
};

const defaultProps = {
  drawerImage: _drawerImage,
  backButtonImage: _backButtonImage,
  titleOpacity: 1,
};

class NavBar extends React.Component {

  constructor(props) {
    super(props);

    this.renderRightButton = this.renderRightButton.bind(this);
    this.renderBackButton = this.renderBackButton.bind(this);
    this.renderLeftButton = this.renderLeftButton.bind(this);
    this.renderTitle = this.renderTitle.bind(this);
    this.renderImageTitle = this.renderImageTitle.bind(this);
  }

  renderBackButton() {
    const state = this.props.navigationState;
    const childState = state.children[state.index];
    const BackButton = (childState.component && childState.component.backButton) || state.backButton
      || childState.backButton;
    const textButtonStyle = [
      styles.barBackButtonText,
      this.props.backButtonTextStyle,
      state.backButtonTextStyle,
      childState.backButtonTextStyle,
    ];
    const style = [
      styles.backButton,
      this.props.leftButtonStyle,
      state.leftButtonStyle,
      childState.leftButtonStyle,
    ];

    if (state.index === 0 && (!state.parentIndex || state.parentIndex === 0)) {
      return null;
    }

    if (BackButton) {
      return (
        <BackButton
          testID="backNavButton"
          textButtonStyle={textButtonStyle}
          {...childState}
          style={style}
        />
      );
    }
    const buttonImage = childState.backButtonImage ||
      state.backButtonImage || this.props.backButtonImage;
    let onPress = childState.onBack || childState.component.onBack;
    if (onPress) {
      onPress = onPress.bind(null, state);
    } else {
      onPress = Actions.pop;
    }

    const text = childState.backTitle ?
      (<Text style={textButtonStyle}>
        {childState.backTitle}
      </Text>)
      : null;

    return (
      <TouchableOpacity
        testID="backNavButton"
        style={style}
        onPress={onPress}
      >
        {buttonImage && !childState.hideBackImage &&
          <Image
            source={buttonImage}
            style={[
              styles.backButtonImage,
              this.props.leftButtonIconStyle,
              state.barButtonIconStyle,
              state.leftButtonIconStyle,
              childState.leftButtonIconStyle,
            ]}
          />
        }
        {text}
      </TouchableOpacity>
    );
  }

  renderRightButton(navProps) {
    const self = this;
    function tryRender(state, wrapBy) {
      if (!state) {
        return null;
      }
      const rightTitle = state.getRightTitle ? state.getRightTitle(navProps) : state.rightTitle;

      const textStyle = [styles.barRightButtonText, self.props.rightButtonTextStyle,
        state.rightButtonTextStyle];
      const style = [styles.rightButton, self.props.rightButtonStyle, state.rightButtonStyle];
      if (state.rightButton) {
        let Button = state.rightButton;
        if (wrapBy) {
          Button = wrapBy(Button);
        }
        return (
          <Button
            {...self.props}
            {...state}
            key={'rightNavBarBtn'}
            testID="rightNavButton"
            style={style}
            textButtonStyle={textStyle}
          />
        );
      }
      if (state.onRight && (rightTitle || state.rightButtonImage)) {
        const onPress = state.onRight.bind(null, state);
        return (
          <TouchableOpacity
            key={'rightNavBarBtn'}
            testID="rightNavButton"
            style={style}
            onPress={onPress}
          >
            {rightTitle &&
              <Text style={textStyle}>
                {rightTitle}
              </Text>
            }
            {state.rightButtonImage &&
              <View style={{ flex: 1, justifyContent: 'center', alignItems: 'flex-end' }}>
                <Image
                  source={state.rightButtonImage}
                  style={state.rightButtonIconStyle}
                />
              </View>
            }
          </TouchableOpacity>
        );
      }
      if ((!!state.onRight ^ !!(typeof (rightTitle) !== 'undefined'
        || typeof (state.rightButtonImage) !== 'undefined'))) {
        console.warn(
          `Both onRight and rightTitle/rightButtonImage
            must be specified for the scene: ${state.name}`,
        );
      }
      return null;
    }
    return tryRender(this.props.component, this.props.wrapBy) || tryRender(this.props);
  }

  renderLeftButton(navProps) {
    const self = this;
    const drawer = this.context.drawer;
    function tryRender(state, wrapBy) {
      let onPress = state.onLeft;
      let buttonImage = state.leftButtonImage;
      let menuIcon = state.drawerIcon;
      const style = [styles.leftButton, self.props.leftButtonStyle, state.leftButtonStyle];
      const textStyle = [styles.barLeftButtonText, self.props.leftButtonTextStyle,
        state.leftButtonTextStyle];
      const leftButtonStyle = [styles.defaultImageStyle, state.leftButtonIconStyle];
      const leftTitle = state.getLeftTitle ? state.getLeftTitle(navProps) : state.leftTitle;

      if (state.leftButton) {
        let Button = state.leftButton;
        if (wrapBy) {
          Button = wrapBy(Button);
        }
        return (
          <Button
            {...self.props}
            {...state}
            key={'leftNavBarBtn'}
            testID="leftNavButton"
            style={style}
            textStyle={textStyle}
          />
        );
      }

      if (!onPress && !!drawer && typeof drawer.toggle === 'function') {
        buttonImage = state.drawerImage;
        if (buttonImage || menuIcon) {
          onPress = drawer.toggle;
        }
        if (!menuIcon) {
          menuIcon = (
            <Image
              source={buttonImage}
              style={leftButtonStyle}
            />
          );
        }
      }

      if (onPress && (leftTitle || buttonImage)) {
        onPress = onPress.bind(null, state);
        return (
          <TouchableOpacity
            key={'leftNavBarBtn'}
            testID="leftNavButton"
            style={style}
            onPress={onPress}
          >
            {leftTitle &&
              <Text style={textStyle}>
                {leftTitle}
              </Text>
            }
            {buttonImage &&
              <View style={{ flex: 1, justifyContent: 'center', alignItems: 'flex-start' }}>
                {menuIcon || <Image
                  source={buttonImage}
                  style={state.leftButtonIconStyle || styles.defaultImageStyle}
                />
                }
              </View>
            }
          </TouchableOpacity>
        );
      }
      if ((!!state.onLeft ^ !!(leftTitle || buttonImage))) {
        console.warn(
          `Both onLeft and leftTitle/leftButtonImage
            must be specified for the scene: ${state.name}`,
        );
      }
      return null;
    }
    return tryRender(this.props.component, this.props.wrapBy) || tryRender(this.props);
  }

  renderTitle(childState, index:number) {
    let title = this.props.getTitle ? this.props.getTitle(childState) : childState.title;
    if (title === undefined && childState.component && childState.component.title) {
      title = childState.component.title;
    }
    if (typeof (title) === 'function') {
      title = title(childState);
    }
    return (
      <Animated.View
        key={childState.key}
        style={[
          styles.titleWrapper,
          this.props.titleWrapperStyle,
        ]}
      >
        <Animated.Text
          lineBreakMode="tail"
          numberOfLines={1}
          {...this.props.titleProps}
          style={[
            styles.title,
            this.props.titleStyle,
            this.props.navigationState.titleStyle,
            childState.titleStyle,
            {
              opacity: this.props.position.interpolate({
                inputRange: [index - 1, index, index + 1],
                outputRange: [0, this.props.titleOpacity, 0],
              }),
              left: this.props.position.interpolate({
                inputRange: [index - 1, index + 1],
                outputRange: [200, -200],
              }),
              right: this.props.position.interpolate({
                inputRange: [index - 1, index + 1],
                outputRange: [-200, 200],
              }),
            },
          ]}
        >
          {title}
        </Animated.Text>
      </Animated.View>
    );
  }

  renderImageTitle() {
    const navigationBarTitleImage = this.props.navigationBarTitleImage ||
      this.state.navigationBarTitleImage;
    const navigationBarTitleImageStyle = this.props.navigationBarTitleImageStyle ||
        this.state.navigationBarTitleImageStyle;
    return (
      <Animated.View
        style={[
          styles.titleWrapper,
          this.props.titleWrapperStyle,
        ]}
      >
        <Animated.Image
          style={[styles.titleImage, navigationBarTitleImageStyle]}
          source={navigationBarTitleImage}
        />
      </Animated.View>
    );
  }

  render() {
    let state = this.props.navigationState;
    let selected = state.children[state.index];
    while ({}.hasOwnProperty.call(selected, 'children')) {
      state = selected;
      selected = selected.children[selected.index];
    }
    const navProps = { ...this.props, ...selected };

    const wrapByStyle = (component, wrapStyle) => {
      if (!component) { return null; }
      return props => <View style={wrapStyle}>{component(props)}</View>;
    };

    const leftButtonStyle = [styles.leftButton, { alignItems: 'flex-start' }];
    const rightButtonStyle = [styles.rightButton, { alignItems: 'flex-end' }];

    const renderLeftButton = wrapByStyle(selected.renderLeftButton, leftButtonStyle) ||
      wrapByStyle(selected.component.renderLeftButton, leftButtonStyle) ||
      this.renderLeftButton;
    const renderRightButton = wrapByStyle(selected.renderRightButton, rightButtonStyle) ||
      wrapByStyle(selected.component.renderRightButton, rightButtonStyle) ||
      this.renderRightButton;
    const renderBackButton = wrapByStyle(selected.renderBackButton, leftButtonStyle) ||
      wrapByStyle(selected.component.renderBackButton, leftButtonStyle) ||
      this.renderBackButton;
    const renderTitle = selected.renderTitle ||
      selected.component.renderTitle ||
      this.props.renderTitle;
    const navigationBarBackgroundImage = this.props.navigationBarBackgroundImage ||
      state.navigationBarBackgroundImage;
    const navigationBarBackgroundImageStyle = this.props.navigationBarBackgroundImageStyle ||
      state.navigationBarBackgroundImageStyle;
    const navigationBarTitleImage = this.props.navigationBarTitleImage ||
      state.navigationBarTitleImage;
    let imageOrTitle = null;
    if (navigationBarTitleImage) {
      imageOrTitle = this.renderImageTitle();
    } else {
      imageOrTitle = renderTitle ? renderTitle(navProps)
      : state.children.map(this.renderTitle, this);
    }
    const contents = (
      <View>
        {imageOrTitle}
        {renderBackButton(navProps) || renderLeftButton(navProps)}
        {renderRightButton(navProps)}
      </View>
    );
    return (
      <Animated.View
        style={[
          styles.header,
          this.props.navigationBarStyle,
          state.navigationBarStyle,
          selected.navigationBarStyle,
        ]}
      >
        {navigationBarBackgroundImage ? (
          <Image style={navigationBarBackgroundImageStyle} source={navigationBarBackgroundImage}>
            {contents}
          </Image>
        ) : contents}
      </Animated.View>
    );
  }
}

NavBar.propTypes = propTypes;
NavBar.contextTypes = contextTypes;
NavBar.defaultProps = defaultProps;

export default NavBar;
<commit_msg>Change back button direction in RTL mode (#1661)<commit_after>/**
 * Copyright (c) 2015, Facebook, Inc.  All rights reserved.
 *
 * Facebook, Inc. ("Facebook") owns all right, title and interest, including
 * all intellectual property and other proprietary rights, in and to the React
 * Native CustomComponents software (the "Software").  Subject to your
 * compliance with these terms, you are hereby granted a non-exclusive,
 * worldwide, royalty-free copyright license to (1) use and copy the Software;
 * and (2) reproduce and distribute the Software as part of your own software
 * ("Your Software").  Facebook reserves all rights not expressly granted to
 * you in this license agreement.
 *
 * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
 * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
 * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
 * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
import React, {
  PropTypes,
} from 'react';
import {
  Platform,
  Animated,
  I18nManager,
  Image,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import Actions from './Actions';
import _drawerImage from './menu_burger.png';
import _backButtonImage from './back_chevron.png';

const styles = StyleSheet.create({
  title: {
    textAlign: 'center',
    color: '#0A0A0A',
    fontSize: 18,
    width: 180,
    alignSelf: 'center',
  },
  titleImage: {
    width: 180,
    alignSelf: 'center',
  },
  titleWrapper: {
    marginTop: 10,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 20,
      },
      android: {
        top: 5,
      },
    }),
    left: 0,
    right: 0,
  },
  header: {
    backgroundColor: '#EFEFF2',
    paddingTop: 0,
    top: 0,
    ...Platform.select({
      ios: {
        height: 64,
      },
      android: {
        height: 54,
      },
    }),
    right: 0,
    left: 0,
    borderBottomWidth: 0.5,
    borderBottomColor: '#828287',
    position: 'absolute',
  },
  backButton: {
    height: 37,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 22,
      },
      android: {
        top: 10,
      },
    }),
    left: 2,
    padding: 8,
    flexDirection: 'row',
    transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
  },
  rightButton: {
    height: 37,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 22,
      },
      android: {
        top: 10,
      },
    }),
    right: 2,
    padding: 8,
  },
  leftButton: {
    height: 37,
    position: 'absolute',
    ...Platform.select({
      ios: {
        top: 20,
      },
      android: {
        top: 8,
      },
    }),
    left: 2,
    padding: 8,
  },
  barRightButtonText: {
    color: 'rgb(0, 122, 255)',
    textAlign: 'right',
    fontSize: 17,
  },
  barBackButtonText: {
    color: 'rgb(0, 122, 255)',
    textAlign: 'left',
    fontSize: 17,
    paddingLeft: 6,
  },
  barLeftButtonText: {
    color: 'rgb(0, 122, 255)',
    textAlign: 'left',
    fontSize: 17,
  },
  backButtonImage: {
    width: 13,
    height: 21,
  },
  defaultImageStyle: {
    height: 24,
    resizeMode: 'contain',
  },
});

const propTypes = {
  navigationState: PropTypes.object,
  backButtonImage: Image.propTypes.source,
  wrapBy: PropTypes.any,
  component: PropTypes.any,
  backButtonTextStyle: Text.propTypes.style,
  leftButtonStyle: View.propTypes.style,
  leftButtonIconStyle: Image.propTypes.style,
  getTitle: PropTypes.func,
  titleWrapperStyle: Text.propTypes.style,
  titleStyle: Text.propTypes.style,
  titleOpacity: PropTypes.number,
  titleProps: PropTypes.any,
  position: PropTypes.object,
  navigationBarStyle: View.propTypes.style,
  navigationBarBackgroundImage: Image.propTypes.source,
  navigationBarBackgroundImageStyle: Image.propTypes.style,
  navigationBarTitleImage: Image.propTypes.source,
  navigationBarTitleImageStyle: Image.propTypes.style,
  renderTitle: PropTypes.any,
};

const contextTypes = {
  drawer: PropTypes.object,
};

const defaultProps = {
  drawerImage: _drawerImage,
  backButtonImage: _backButtonImage,
  titleOpacity: 1,
};

class NavBar extends React.Component {

  constructor(props) {
    super(props);

    this.renderRightButton = this.renderRightButton.bind(this);
    this.renderBackButton = this.renderBackButton.bind(this);
    this.renderLeftButton = this.renderLeftButton.bind(this);
    this.renderTitle = this.renderTitle.bind(this);
    this.renderImageTitle = this.renderImageTitle.bind(this);
  }

  renderBackButton() {
    const state = this.props.navigationState;
    const childState = state.children[state.index];
    const BackButton = (childState.component && childState.component.backButton) || state.backButton
      || childState.backButton;
    const textButtonStyle = [
      styles.barBackButtonText,
      this.props.backButtonTextStyle,
      state.backButtonTextStyle,
      childState.backButtonTextStyle,
    ];
    const style = [
      styles.backButton,
      this.props.leftButtonStyle,
      state.leftButtonStyle,
      childState.leftButtonStyle,
    ];

    if (state.index === 0 && (!state.parentIndex || state.parentIndex === 0)) {
      return null;
    }

    if (BackButton) {
      return (
        <BackButton
          testID="backNavButton"
          textButtonStyle={textButtonStyle}
          {...childState}
          style={style}
        />
      );
    }
    const buttonImage = childState.backButtonImage ||
      state.backButtonImage || this.props.backButtonImage;
    let onPress = childState.onBack || childState.component.onBack;
    if (onPress) {
      onPress = onPress.bind(null, state);
    } else {
      onPress = Actions.pop;
    }

    const text = childState.backTitle ?
      (<Text style={textButtonStyle}>
        {childState.backTitle}
      </Text>)
      : null;

    return (
      <TouchableOpacity
        testID="backNavButton"
        style={style}
        onPress={onPress}
      >
        {buttonImage && !childState.hideBackImage &&
          <Image
            source={buttonImage}
            style={[
              styles.backButtonImage,
              this.props.leftButtonIconStyle,
              state.barButtonIconStyle,
              state.leftButtonIconStyle,
              childState.leftButtonIconStyle,
            ]}
          />
        }
        {text}
      </TouchableOpacity>
    );
  }

  renderRightButton(navProps) {
    const self = this;
    function tryRender(state, wrapBy) {
      if (!state) {
        return null;
      }
      const rightTitle = state.getRightTitle ? state.getRightTitle(navProps) : state.rightTitle;

      const textStyle = [styles.barRightButtonText, self.props.rightButtonTextStyle,
        state.rightButtonTextStyle];
      const style = [styles.rightButton, self.props.rightButtonStyle, state.rightButtonStyle];
      if (state.rightButton) {
        let Button = state.rightButton;
        if (wrapBy) {
          Button = wrapBy(Button);
        }
        return (
          <Button
            {...self.props}
            {...state}
            key={'rightNavBarBtn'}
            testID="rightNavButton"
            style={style}
            textButtonStyle={textStyle}
          />
        );
      }
      if (state.onRight && (rightTitle || state.rightButtonImage)) {
        const onPress = state.onRight.bind(null, state);
        return (
          <TouchableOpacity
            key={'rightNavBarBtn'}
            testID="rightNavButton"
            style={style}
            onPress={onPress}
          >
            {rightTitle &&
              <Text style={textStyle}>
                {rightTitle}
              </Text>
            }
            {state.rightButtonImage &&
              <View style={{ flex: 1, justifyContent: 'center', alignItems: 'flex-end' }}>
                <Image
                  source={state.rightButtonImage}
                  style={state.rightButtonIconStyle}
                />
              </View>
            }
          </TouchableOpacity>
        );
      }
      if ((!!state.onRight ^ !!(typeof (rightTitle) !== 'undefined'
        || typeof (state.rightButtonImage) !== 'undefined'))) {
        console.warn(
          `Both onRight and rightTitle/rightButtonImage
            must be specified for the scene: ${state.name}`,
        );
      }
      return null;
    }
    return tryRender(this.props.component, this.props.wrapBy) || tryRender(this.props);
  }

  renderLeftButton(navProps) {
    const self = this;
    const drawer = this.context.drawer;
    function tryRender(state, wrapBy) {
      let onPress = state.onLeft;
      let buttonImage = state.leftButtonImage;
      let menuIcon = state.drawerIcon;
      const style = [styles.leftButton, self.props.leftButtonStyle, state.leftButtonStyle];
      const textStyle = [styles.barLeftButtonText, self.props.leftButtonTextStyle,
        state.leftButtonTextStyle];
      const leftButtonStyle = [styles.defaultImageStyle, state.leftButtonIconStyle];
      const leftTitle = state.getLeftTitle ? state.getLeftTitle(navProps) : state.leftTitle;

      if (state.leftButton) {
        let Button = state.leftButton;
        if (wrapBy) {
          Button = wrapBy(Button);
        }
        return (
          <Button
            {...self.props}
            {...state}
            key={'leftNavBarBtn'}
            testID="leftNavButton"
            style={style}
            textStyle={textStyle}
          />
        );
      }

      if (!onPress && !!drawer && typeof drawer.toggle === 'function') {
        buttonImage = state.drawerImage;
        if (buttonImage || menuIcon) {
          onPress = drawer.toggle;
        }
        if (!menuIcon) {
          menuIcon = (
            <Image
              source={buttonImage}
              style={leftButtonStyle}
            />
          );
        }
      }

      if (onPress && (leftTitle || buttonImage)) {
        onPress = onPress.bind(null, state);
        return (
          <TouchableOpacity
            key={'leftNavBarBtn'}
            testID="leftNavButton"
            style={style}
            onPress={onPress}
          >
            {leftTitle &&
              <Text style={textStyle}>
                {leftTitle}
              </Text>
            }
            {buttonImage &&
              <View style={{ flex: 1, justifyContent: 'center', alignItems: 'flex-start' }}>
                {menuIcon || <Image
                  source={buttonImage}
                  style={state.leftButtonIconStyle || styles.defaultImageStyle}
                />
                }
              </View>
            }
          </TouchableOpacity>
        );
      }
      if ((!!state.onLeft ^ !!(leftTitle || buttonImage))) {
        console.warn(
          `Both onLeft and leftTitle/leftButtonImage
            must be specified for the scene: ${state.name}`,
        );
      }
      return null;
    }
    return tryRender(this.props.component, this.props.wrapBy) || tryRender(this.props);
  }

  renderTitle(childState, index:number) {
    let title = this.props.getTitle ? this.props.getTitle(childState) : childState.title;
    if (title === undefined && childState.component && childState.component.title) {
      title = childState.component.title;
    }
    if (typeof (title) === 'function') {
      title = title(childState);
    }
    return (
      <Animated.View
        key={childState.key}
        style={[
          styles.titleWrapper,
          this.props.titleWrapperStyle,
        ]}
      >
        <Animated.Text
          lineBreakMode="tail"
          numberOfLines={1}
          {...this.props.titleProps}
          style={[
            styles.title,
            this.props.titleStyle,
            this.props.navigationState.titleStyle,
            childState.titleStyle,
            {
              opacity: this.props.position.interpolate({
                inputRange: [index - 1, index, index + 1],
                outputRange: [0, this.props.titleOpacity, 0],
              }),
              left: this.props.position.interpolate({
                inputRange: [index - 1, index + 1],
                outputRange: [200, -200],
              }),
              right: this.props.position.interpolate({
                inputRange: [index - 1, index + 1],
                outputRange: [-200, 200],
              }),
            },
          ]}
        >
          {title}
        </Animated.Text>
      </Animated.View>
    );
  }

  renderImageTitle() {
    const navigationBarTitleImage = this.props.navigationBarTitleImage ||
      this.state.navigationBarTitleImage;
    const navigationBarTitleImageStyle = this.props.navigationBarTitleImageStyle ||
        this.state.navigationBarTitleImageStyle;
    return (
      <Animated.View
        style={[
          styles.titleWrapper,
          this.props.titleWrapperStyle,
        ]}
      >
        <Animated.Image
          style={[styles.titleImage, navigationBarTitleImageStyle]}
          source={navigationBarTitleImage}
        />
      </Animated.View>
    );
  }

  render() {
    let state = this.props.navigationState;
    let selected = state.children[state.index];
    while ({}.hasOwnProperty.call(selected, 'children')) {
      state = selected;
      selected = selected.children[selected.index];
    }
    const navProps = { ...this.props, ...selected };

    const wrapByStyle = (component, wrapStyle) => {
      if (!component) { return null; }
      return props => <View style={wrapStyle}>{component(props)}</View>;
    };

    const leftButtonStyle = [styles.leftButton, { alignItems: 'flex-start' }];
    const rightButtonStyle = [styles.rightButton, { alignItems: 'flex-end' }];

    const renderLeftButton = wrapByStyle(selected.renderLeftButton, leftButtonStyle) ||
      wrapByStyle(selected.component.renderLeftButton, leftButtonStyle) ||
      this.renderLeftButton;
    const renderRightButton = wrapByStyle(selected.renderRightButton, rightButtonStyle) ||
      wrapByStyle(selected.component.renderRightButton, rightButtonStyle) ||
      this.renderRightButton;
    const renderBackButton = wrapByStyle(selected.renderBackButton, leftButtonStyle) ||
      wrapByStyle(selected.component.renderBackButton, leftButtonStyle) ||
      this.renderBackButton;
    const renderTitle = selected.renderTitle ||
      selected.component.renderTitle ||
      this.props.renderTitle;
    const navigationBarBackgroundImage = this.props.navigationBarBackgroundImage ||
      state.navigationBarBackgroundImage;
    const navigationBarBackgroundImageStyle = this.props.navigationBarBackgroundImageStyle ||
      state.navigationBarBackgroundImageStyle;
    const navigationBarTitleImage = this.props.navigationBarTitleImage ||
      state.navigationBarTitleImage;
    let imageOrTitle = null;
    if (navigationBarTitleImage) {
      imageOrTitle = this.renderImageTitle();
    } else {
      imageOrTitle = renderTitle ? renderTitle(navProps)
      : state.children.map(this.renderTitle, this);
    }
    const contents = (
      <View>
        {imageOrTitle}
        {renderBackButton(navProps) || renderLeftButton(navProps)}
        {renderRightButton(navProps)}
      </View>
    );
    return (
      <Animated.View
        style={[
          styles.header,
          this.props.navigationBarStyle,
          state.navigationBarStyle,
          selected.navigationBarStyle,
        ]}
      >
        {navigationBarBackgroundImage ? (
          <Image style={navigationBarBackgroundImageStyle} source={navigationBarBackgroundImage}>
            {contents}
          </Image>
        ) : contents}
      </Animated.View>
    );
  }
}

NavBar.propTypes = propTypes;
NavBar.contextTypes = contextTypes;
NavBar.defaultProps = defaultProps;

export default NavBar;
<|endoftext|>


<commit_before>// # Local File System Image Storage module
// The (default) module for storing images, using the local file system

var _       = require('lodash'),
    express = require('express'),
    fs      = require('fs-extra'),
    nodefn  = require('when/node/function'),
    path    = require('path'),
    when    = require('when'),
    errors  = require('../errorHandling'),
    config  = require('../config'),
    baseStore   = require('./base'),
    dbname = 'ghostimages',
    imagestore,
    cloudantFileStore;

if (process.env.VCAP_SERVICES) {
    var services = JSON.parse(process.env.VCAP_SERVICES);
    // look for a service starting with 'User Provided'
    //
    for (var svcName in services) {
        if (svcName.match(/^user-provided/)) {
            cloudantCreds = services[svcName][0]['credentials'];
        }
    }
} else {
    console.log('No Cloudant CouchDB service attached');
}

var nano = require('nano')('https://' + cloudantCreds.username + ':' + cloudantCreds.password + '@' + cloudantCreds.url);

nano.db.get(dbname, function(err, body) {
    if (err) {
        nano.db.create(dbname, function(err, body) {
            if (!err) {
                console.log(dbname + ' database was successfully created');
                imagestore = nano.use(dbname);
            } else {
                console.log(dbname + ' database has failed to be created');
                console.log('Reason: ' + err.error);
            }
        });
    } else {
        console.log(dbname + ' database was successfully located');
        imagestore = nano.use(dbname);
    }
});

cloudantFileStore = _.extend(baseStore, {
    // ### Save
    // Saves the image to storage (the file system)
    // - image is the express image object
    // - returns a promise which ultimately returns the full url to the uploaded image
    'save': function (image) {
        var saved = when.defer(),
            targetDir = this.getTargetDir(config().paths.imagesPath),
            targetFilename;

        this.getUniqueFileName(this, image, targetDir).then(function (filename) {
            targetFilename = filename;
            return nodefn.call(fs.mkdirs, targetDir);
        }).then(function () {
            return nodefn.call(fs.copy, image.path, targetFilename);
        }).then(function () {
            return nodefn.call(fs.unlink, image.path).otherwise(errors.logError);
        }).then(function () {
            // The src for the image must be in URI format, not a file system path, which in Windows uses \
            // For local file system storage can use relative path so add a slash
            var fullUrl = (config().paths.subdir + '/' + path.relative(config().paths.appRoot, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/');

            // Let's assume PNG images are the norm and toggle the content-type from there
            var contentType = 'image/png';
            var extension = path.extname(targetFilename);
            switch (extension) {
                case '.jpg | .jpeg':
                    contentType = 'image/jpeg';
                    break;
                case '.png':
                    contentType = 'image/png';
                    break;
                case '.gif':
                    contentType = 'image/gif';
                    break;
                case '.svg | .svgz':
                    contentType = 'image/svg+xml';
                    break;
                default:
                    contentType = 'image';
                    break;
            }

            var base = path.basename(targetFilename, extension);
            //console.log('https://' + cloudantCreds.url + '/' + dbname + '/' + base + fullUrl);
            var cloudantImageUrl = 'https://' + cloudantCreds.url + '/' + dbname + '/' + base + fullUrl;
            var data = fs.readFileSync('/home/vcap/app' + fullUrl);

            //Let's try to find a document with the same label as the image and then drill down to find the image attachment
            imagestore.get(base, {revs_info: true}, function(err, getBody) {
                if (err) {
                    console.log('{SAVE} Could not find the doc: ' + base);
                    console.log('Reason: ' + err.error);
                    // Error recorded should be missing to indicate this is in fact a brand new image
                    //Create new document with name of filename
                    imagestore.insert({ type: 'ghost.js' }, base, function(err, insertBody) {
                        if (err) {
                            // Log any errors encountered for troubleshooting
                            console.log('{SAVE} Could not insert the doc: ' + base);
                            console.log('Reason: ' + err.error);
                        } else {
                            console.log('{SAVE} Successfully inserted doc: ' + base)
                            // Attach image to the newly created document
                             imagestore.attachment.insert(insertBody.id, fullUrl, new Buffer(data, 'binary'), contentType, {rev: insertBody.rev}, function(err, attachBody) {
                                if (err) {
                                    // Log any errors encountered for troubleshooting
                                    console.log('{SAVE} Could not insert the image' + fullUrl);
                                    console.log('Reason: ' + err.error);
                                } else {
                                    console.log('{SAVE} Successfully inserted image: ' + fullUrl);
                                }
                             });
                        }
                    });
                } else {
                    // Looks like a document already exists with this image name.  Let's update it with the new attachment
                    console.log('{SAVE} An image attachment already exists with this name');

                    imagestore.attachment.get(base, fullUrl,  function(err, getAttachBody) {
                        if (!err) {
                            console.log('{SAVE} Duplicate attachment image located');

                            imagestore.attachment.insert(base, fullUrl, new Buffer(data, 'binary'), contentType, {rev: getBody._rev}, function(err, attachBody) {
                                if (err) {
                                    // Log any errors encountered for troubleshooting
                                    console.log('{SAVE} Could not insert the updated image');
                                    console.log('Reason: ' + JSON.stringify(err));
                                } else {
                                    console.log('{SAVE} Successfully updated image: ' + fullUrl);
                                }
                            });

                        } else {
                            console.log('{SAVE} Did not find duplicate attachment');
                            console.log('Reason: ' + err.error);
                        }
                    });

                }

            });

            // Let's give Cloudant a wee bit of time to get the data and create the doc + attachment.  Say 250 ms seems like a good first approx.
            setTimeout(function() {return saved.resolve(cloudantImageUrl);}, 250);

        }).otherwise(function (e) {
            errors.logError(e);
            return saved.reject(e);
        });

        return saved.promise;
    },

    'exists': function (filename) {

        // fs.exists does not play nicely with nodefn because the callback doesn't have an error argument
        var done = when.defer();
        var extension = path.extname(filename);
        var base = path.basename(filename, extension);
        var fullname = base + extension;
        var fullUrl = (config().paths.subdir + '/' + path.relative(config().paths.appRoot, filename)).replace(new RegExp('\\' + path.sep, 'g'), '/');

        // Let's try to get this image from Cloudant by checking for the doc first
        imagestore.get(base, {revs_info: true}, function(err, getBody) {
            if (err) {
                // Couldn't find the doc.  Record the reason and notify as false.
                console.log('{EXISTS} Could not find the doc: ' + base);
                console.log('Reason: ' + err.error);
                done.resolve(false);
            } else {
                console.log('{EXISTS} Found image document in Cloudant cache: ' + base);
                // Let's try to get the image attachment associated with this doc
                imagestore.attachment.get(base, fullUrl, function (err, attachBody) {
                    if (!err) {
                        // We found it.  Notify as true.
                        console.log('{EXISTS} Found image attachment: ' + fullUrl)
                        done.resolve(false);    // Let's fall through the same code as not being there.
                    } else {
                        // Not there.  Record the reason and notify as false
                        console.log('{EXISTS} Could not find the image ' + fullUrl);
                        console.log('Reason: ' + err.error);
                        done.resolve(false);
                    }
                });
            }
        });

        return done.promise;
    },

    // middleware for serving the files
    'serve': function () {
        var ONE_HOUR_MS = 60 * 60 * 1000,
            ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS;

        // For some reason send divides the max age number by 1000
        return express['static'](config().paths.imagesPath, {maxAge: ONE_YEAR_MS});
    }
});

module.exports = cloudantFileStore;
<commit_msg>Dynamic Cloudant DB creation, permissions and handling<commit_after>// # Local File System Image Storage module
// The (default) module for storing images, using the local file system

var _       = require('lodash'),
    express = require('express'),
    fs      = require('fs-extra'),
    nodefn  = require('when/node/function'),
    path    = require('path'),
    when    = require('when'),
    errors  = require('../errorHandling'),
    config  = require('../config'),
    baseStore   = require('./base'),
    https =  require('https'),
    querystring = require('querystring'),
    dbname = 'ghost-images',
    imagestore,
    cloudantFileStore;

if (process.env.VCAP_APPLICATION) {
    var appdetails = JSON.parse(process.env.VCAP_APPLICATION);
    dbname = appdetails.name + '-ghost-images';
}

if (process.env.VCAP_SERVICES) {
    var services = JSON.parse(process.env.VCAP_SERVICES);
    // look for a service starting with 'User Provided'
    //
    for (var svcName in services) {
        if (svcName.match(/^user-provided/)) {
            cloudantCreds = services[svcName][0]['credentials'];
        }
    }
} else {
    console.log('No Cloudant CouchDB service attached');
}

var nano = require('nano')('https://' + cloudantCreds.username + ':' + cloudantCreds.password + '@' + cloudantCreds.url);

nano.db.get(dbname, function(err, body) {
    if (err) {
        nano.db.create(dbname, function(err, body) {
            if (!err) {
                console.log(dbname.toUpperCase() + ' database was successfully created');
                // Using the format  https://<username>:<password>@cloudant.com/api/set_permissions
                // Content-Type:  application/x-www-form-urlencoded
                // Post Body:  username=nobody&database=<username>/<dbname>&roles=_reader
                // The username "nobody" represents the "Everone Else" unauthenticated user group (aka Public)

                var data = querystring.stringify({
                    username: 'nobody',
                    database: cloudantCreds.username + '/' + dbname,
                    roles: '_reader'
                });

                var auth = 'Basic ' + new Buffer( cloudantCreds.username + ':' + cloudantCreds.password).toString('base64');

                var options = {
                    hostname: 'cloudant.com',
                    port: 443,
                    path: '/api/set_permissions',
                    method: 'POST',
                    headers: {
                        'Authorization' : auth,
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'Content-Length': Buffer.byteLength(data)
                    }
                };

                console.log('Setting permissions to reader on ' + dbname.toUpperCase() + ' for unauthenticated users ...');
                var request=https.request(options, function(response) {
                    response.setEncoding('utf8');
                    response.on('data', function (chunk) {
                        if (JSON.parse(chunk).ok === true) {
                        //if (chunk.indexOf('ok') >= 0) {
                            console.log("Permissions successfully set.");
                        } else {
                            console.log("Permissions fail to set");
                            console.log(JSON.parse.ok);
                            console.log('Body: ' + chunk);
                        }
                    });
                });
                request.write(data);
                request.end();
                imagestore = nano.use(dbname);
            } else {
                console.log(dbname.toUpperCase() + ' database has failed to be created');
                console.log('Reason: ' + err.error);
            }
        });
    } else {
        console.log(dbname.toUpperCase() + ' database already exists and will be used.');
        imagestore = nano.use(dbname);
    }
});

cloudantFileStore = _.extend(baseStore, {
    // ### Save
    // Saves the image to storage (the file system)
    // - image is the express image object
    // - returns a promise which ultimately returns the full url to the uploaded image
    'save': function (image) {
        var saved = when.defer(),
            targetDir = this.getTargetDir(config().paths.imagesPath),
            targetFilename;

        this.getUniqueFileName(this, image, targetDir).then(function (filename) {
            targetFilename = filename;
            return nodefn.call(fs.mkdirs, targetDir);
        }).then(function () {
            return nodefn.call(fs.copy, image.path, targetFilename);
        }).then(function () {
            return nodefn.call(fs.unlink, image.path).otherwise(errors.logError);
        }).then(function () {
            // The src for the image must be in URI format, not a file system path, which in Windows uses \
            // For local file system storage can use relative path so add a slash
            var fullUrl = (config().paths.subdir + '/' + path.relative(config().paths.appRoot, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/');

            // Let's assume PNG images are the norm and toggle the content-type from there
            var contentType = 'image/png';
            var extension = path.extname(targetFilename);
            switch (extension) {
                case '.jpg | .jpeg':
                    contentType = 'image/jpeg';
                    break;
                case '.png':
                    contentType = 'image/png';
                    break;
                case '.gif':
                    contentType = 'image/gif';
                    break;
                case '.svg | .svgz':
                    contentType = 'image/svg+xml';
                    break;
                default:
                    contentType = 'image';
                    break;
            }

            var base = path.basename(targetFilename, extension);
            //console.log('https://' + cloudantCreds.url + '/' + dbname + '/' + base + fullUrl);
            var cloudantImageUrl = 'https://' + cloudantCreds.url + '/' + dbname + '/' + base + fullUrl;
            var data = fs.readFileSync('/home/vcap/app' + fullUrl);

            //Let's try to find a document with the same label as the image and then drill down to find the image attachment
            imagestore.get(base, {revs_info: true}, function(err, getBody) {
                if (err) {
                    console.log('{SAVE} Could not find the doc: ' + base);
                    console.log('Reason: ' + err.error);
                    // Error recorded should be missing to indicate this is in fact a brand new image
                    //Create new document with name of filename
                    imagestore.insert({ type: 'ghost.js' }, base, function(err, insertBody) {
                        if (err) {
                            // Log any errors encountered for troubleshooting
                            console.log('{SAVE} Could not insert the doc: ' + base);
                            console.log('Reason: ' + err.error);
                        } else {
                            console.log('{SAVE} Successfully inserted doc: ' + base)
                            // Attach image to the newly created document
                             imagestore.attachment.insert(insertBody.id, fullUrl, new Buffer(data, 'binary'), contentType, {rev: insertBody.rev}, function(err, attachBody) {
                                if (err) {
                                    // Log any errors encountered for troubleshooting
                                    console.log('{SAVE} Could not insert the image' + fullUrl);
                                    console.log('Reason: ' + err.error);
                                } else {
                                    console.log('{SAVE} Successfully inserted image: ' + fullUrl);
                                }
                             });
                        }
                    });
                } else {
                    // Looks like a document already exists with this image name.  Let's update it with the new attachment
                    console.log('{SAVE} An image attachment already exists with this name');

                    imagestore.attachment.get(base, fullUrl,  function(err, getAttachBody) {
                        if (!err) {
                            console.log('{SAVE} Duplicate attachment image located');

                            imagestore.attachment.insert(base, fullUrl, new Buffer(data, 'binary'), contentType, {rev: getBody._rev}, function(err, attachBody) {
                                if (err) {
                                    // Log any errors encountered for troubleshooting
                                    console.log('{SAVE} Could not insert the updated image');
                                    console.log('Reason: ' + JSON.stringify(err));
                                } else {
                                    console.log('{SAVE} Successfully updated image: ' + fullUrl);
                                }
                            });

                        } else {
                            console.log('{SAVE} Did not find duplicate attachment');
                            console.log('Reason: ' + err.error);
                        }
                    });

                }

            });

            // Let's give Cloudant a wee bit of time to get the data and create the doc + attachment.  Say 1000 ms seems like a good first approx.
            setTimeout(function() {return saved.resolve(cloudantImageUrl);}, 1000);

        }).otherwise(function (e) {
            errors.logError(e);
            return saved.reject(e);
        });

        return saved.promise;
    },

    'exists': function (filename) {

        // fs.exists does not play nicely with nodefn because the callback doesn't have an error argument
        var done = when.defer();
        var extension = path.extname(filename);
        var base = path.basename(filename, extension);
        var fullname = base + extension;
        var fullUrl = (config().paths.subdir + '/' + path.relative(config().paths.appRoot, filename)).replace(new RegExp('\\' + path.sep, 'g'), '/');

        // Let's try to get this image from Cloudant by checking for the doc first
        imagestore.get(base, {revs_info: true}, function(err, getBody) {
            if (err) {
                // Couldn't find the doc.  Record the reason and notify as false.
                console.log('{EXISTS} Could not find the doc: ' + base);
                console.log('Reason: ' + err.error);
                done.resolve(false);
            } else {
                console.log('{EXISTS} Found image document in Cloudant cache: ' + base);
                // Let's try to get the image attachment associated with this doc
                imagestore.attachment.get(base, fullUrl, function (err, attachBody) {
                    if (!err) {
                        // We found it.  Notify as true.
                        console.log('{EXISTS} Found image attachment: ' + fullUrl)
                        done.resolve(false);    // Let's fall through the same code as not being there.
                    } else {
                        // Not there.  Record the reason and notify as false
                        console.log('{EXISTS} Could not find the image ' + fullUrl);
                        console.log('Reason: ' + err.error);
                        done.resolve(false);
                    }
                });
            }
        });

        return done.promise;
    },

    // middleware for serving the files
    'serve': function () {
        var ONE_HOUR_MS = 60 * 60 * 1000,
            ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS;

        // For some reason send divides the max age number by 1000
        return express['static'](config().paths.imagesPath, {maxAge: ONE_YEAR_MS});
    }
});

module.exports = cloudantFileStore;
<|endoftext|>


<commit_before>/*
---
name: Behavior
description: Auto-instantiates widgets/classes based on parsed, declarative HTML.
requires: [Core/Class.Extras, Core/Element, Core/Selectors, /ART.Window, /Element.Data, Table/Table, /DashSelectors]
provides: [Behavior]
...
*/

(function(){

	this.Behavior = new Class({

		Implements: [Options, Events],

		options: {
			//apply behaviors on instantiation?
			applyNow: true,
			//default error behavior when a filter cannot be applied
			onError: function(){
				if (window.console && console.warn) console.warn.apply(console, arguments);
			}
			//Components that have a Behavior instance (like an ART.Window)
			//call these events to tell filters that need to know that the state
			//has changed:
			// onResize: $empty, //the element's dimensions have changed
			// onShow: $empty, //the element is displayed
			// onHide: $empty //the element is hidden
		},

		initialize: function(element, options){
			this.element = document.id(element);
			this.setOptions(options);
			if (this.options.applyNow) this.apply();
		},

		toElement: function(){
			return this.element;
		},

		//These methods don't change the element's state but rather are used
		//to tell filters that need to adapt to the new element state that
		//it has changed.

		//the element is now visible
		show: function(){
			this.element.fireEvent('show');
			this.fireEvent('show');
		},

		//the element is hidden
		hide: function(){
			this.element.fireEvent('hide');
			this.fireEvent('hide');
		},

		//the element's dimensions are now the specified width and height
		resize: function(w, h){
			this.element.fireEvent('resize', [w, h]);
			this.fireEvent('resize', [w, h]);
		},

		//Applies all the behavior filters for an element.
		//container - (element; optional) an optional element to apply the filters registered with this Behavior instance to.
		//force - (boolean; optional) passed through to applyBehavior (see it for docs)
		apply: function(container, force){
			document.id(container || this.element).getElements('[data-filters]').each(function(element){
				element.get('data', 'filters').split(',').each(function(name){
					var behavior = this.getBehavior(name.trim());
					if (!behavior) this.fireEvent('error', ['There is no behavior registered with this name: ', name, element]);
					else this.applyBehavior(element, behavior, force);
				}, this);
			}, this);
			return this;
		},

		//Applies a specific behavior to a specific element.
		//element - the element to which to apply the behavior
		//behavior - (object) a specific behavior filter, typically one registered with this instance or registered globally.
		//force - (boolean; optional) apply the behavior to each element it matches, even if it was previously applied. Defaults to *false*.
		applyBehavior: function(element, behavior, force){
			//get the filters already applied to this element
			var applied = getApplied(element);
			//if this filter is not yet applied to the element, or we are forcing the filter
			if (!applied[behavior.name] || force) {
				//if it was previously applied, garbage collect it
				if (applied[behavior.name]) applied[behavior.name].cleanup(element);
				//apply the filter
				behavior.attach(element, this.element);
				//and mark it as having been previously applied
				applied[behavior.name] = behavior;
			}
		},

		//given a name, returns a registered behavior
		getBehavior: function(name){
			return this._registered[name] || Behavior._registered[name];
		},

		//Garbage collects all applied filters for an element and its children.
		//element - (*element*; optional) container to cleanup; if not specified, defaults to this.element
		//ignoreChildren - (*boolean*; optional) if *true* only the element will be cleaned, otherwise the element and all the
		//          children with filters applied will be cleaned. Defaults to *false*.
		cleanup: function(element, ignoreChildren){
			element = document.id(element || this.element);
			var applied = getApplied(element);
			for (behavior in applied) {
				applied[behavior].cleanup(element);
				delete applied[behavior];
			}
			if (!ignoreChildren) element.getElements(':hasBehaviors').each(this.cleanup, this);
		}

	});

	//Returns the applied behaviors for an element.
	var getApplied = function(el){
		return el.retrieve('_appliedBehaviors', {});
	};

	//Registers a behavior filter.
	//name - the name of the filter
	//behavior - an instance of Behavior.Filter
	//overwrite - (boolean) if true, will overwrite existing filter if one exists; defaults to false
	var register = function(name, behavior, overwrite){
		if (!this._registered[name] || overwrite) this._registered[name] = behavior;
		return this;
	};
	//Overwrites a filter.
	var overwrite = function(name, behavior){
		return register.apply(this, [name, behavior, true]);
	};

	var addFilter = function(name, fn, overwrite){
		if (overwrite) overwrite.apply(this, [name, new Behavior.Filter(name, fn)]);
		else register.apply(this, [name, new Behavior.Filter(name, fn)]);
	};

	var addFilters = function(obj, overwrite) {
		for (var name in obj) {
			addFilter.apply(this, [name, new Behavior.Filter(name, fn), overwrite]);
		}
	};

	//Add methods to the Behavior namespace for global registration.
	$extend(Behavior, {
		_registered: {},
		registerGlobal: register,
		overwriteGlobal: overwrite,
		addGlobalFilter: addFilter,
		addGlobalFilters: addFilters
	});
	//Add methods to the Behavior class for instance registration.
	Behavior.implement({
		_registered: {},
		register: register,
		overwrite: overwrite,
		addFilter: addFilter,
		addFilters: addFilters
	});

	//This class is an actual filter that, given an element, alters it with specific behaviors.
	Behavior.Filter = new Class({

		//Filter implements window tools for filters that integrate with ART.Window.
		Implements: [ART.WindowTools],

		//Pass in an object with the following properties:
		//name - the name of this filter
		//attacher - a function that applies the filter to the given element
		initialize: function(name, attacher){
			this.name = name;
			this.attach = attacher;
			this._marks = new Table();
		},

		//Stores a garbage collection pointer for a specific element.
		//Example: if your filter enhances all the inputs in the container
		//you might have a function that removes that enhancement for garbage collection.
		//You would mark each input matched with its own cleanup function.
		//NOTE: this MUST be the element passed to the filter - the element with this filters
		//      name in its data-filter property. I.E.:
		//<form data-filter="FormValidator">
		//  <input type="text" name="email"/>
		//</form>
		//If this filter is FormValidator, you can mark the form for cleanup, but not, for example
		//the input. Only elements that match this filter can be marked.
		markForCleanup: function(element, fn){
			var marks = this._marks.get(element);
			if (!marks) marks = [];
			marks.include(fn);
			this._marks.set(element, marks);
			return this;
		},

		//Garbage collect a specific element.
		//NOTE: this should be an element that has a data-filter property that matches this filter.
		cleanup: function(element){
			var marks = this._marks.get(element);
			if (marks) {
				marks.each(function(fn){ fn(); });
				this._marks.erase(element);
			}
			return this;
		}

	});

})();

//a selector to find all elements that have behaviors applied to them.
Selectors.Pseudo.hasBehaviors = function(){
	return !!this.retrieve('_appliedBehaviors');
};

Element.implement({

	addDataFilter: function(name){
		this.set('data', 'filters', this.getDataFilters().include(name).join(','));
	},

	removeDataFilter: function(name){
		this.set('data', 'filters', this.getDataFilters().erase(name).join(','));
	},

	getDataFilters: function(){
		var filters = this.get('data', 'filters');
		if (!filters) return [];
		return filters.split(',').map(String.trim);
	},

	hasDataFilter: function(name){
		return this.getDataFilters().contains(name);
	}

});<commit_msg>removing container reference; passing in an object with event methods instead<commit_after>/*
---
name: Behavior
description: Auto-instantiates widgets/classes based on parsed, declarative HTML.
requires: [Core/Class.Extras, Core/Element, Core/Selectors, /ART.Window, /Element.Data, Table/Table, /DashSelectors]
provides: [Behavior]
...
*/

(function(){

	this.Behavior = new Class({

		Implements: [Options, Events],

		options: {
			//apply behaviors on instantiation?
			applyNow: true,
			//default error behavior when a filter cannot be applied
			onError: function(){
				if (window.console && console.warn) console.warn.apply(console, arguments);
			}
			//Components that have a Behavior instance (like an ART.Window)
			//call these events to tell filters that need to know that the state
			//has changed:
			// onResize: $empty, //the element's dimensions have changed
			// onShow: $empty, //the element is displayed
			// onHide: $empty //the element is hidden
		},

		initialize: function(options){
			this.setOptions(options);
			this._eventMethods = {};
			var self = this;
			var passEvent = function(method) {
				self.eventMethods[method] = function(name, fn) {
					self[method](name, fn);
					return self.eventMethods;
				};
			};
			['addEvent', 'removeEvent', 'addEvents', 'removeEvents'].each(passEvent);
			if (this.options.applyNow) this.apply();
		},

		//These methods don't change the element's state but rather are used
		//to tell filters that need to adapt to the new element state that
		//it has changed.
		show: function(){
			this.fireEvent('show');
		},

		//the element is hidden
		hide: function(){
			this.fireEvent('hide');
		},

		//the element's dimensions are now the specified width and height
		resize: function(w, h){
			this.fireEvent('resize', [w, h]);
		},

		//Applies all the behavior filters for an element.
		//container - (element) an element to apply the filters registered with this Behavior instance to.
		//force - (boolean; optional) passed through to applyBehavior (see it for docs)
		apply: function(container, force){
			document.id(container).getElements('[data-filters]').each(function(element){
				element.get('data', 'filters').split(',').each(function(name){
					var behavior = this.getBehavior(name.trim());
					if (!behavior) this.fireEvent('error', ['There is no behavior registered with this name: ', name, element]);
					else this.applyBehavior(element, behavior, force);
				}, this);
			}, this);
			return this;
		},

		//Applies a specific behavior to a specific element.
		//element - the element to which to apply the behavior
		//behavior - (object) a specific behavior filter, typically one registered with this instance or registered globally.
		//force - (boolean; optional) apply the behavior to each element it matches, even if it was previously applied. Defaults to *false*.
		applyBehavior: function(element, behavior, force){
			element = document.id(element);
			//get the filters already applied to this element
			var applied = getApplied(element);
			//if this filter is not yet applied to the element, or we are forcing the filter
			if (!applied[behavior.name] || force) {
				//if it was previously applied, garbage collect it
				if (applied[behavior.name]) applied[behavior.name].cleanup(element);
				//apply the filter
				behavior.attach(element, this._eventMethods);
				//and mark it as having been previously applied
				applied[behavior.name] = behavior;
			}
		},

		//given a name, returns a registered behavior
		getBehavior: function(name){
			return this._registered[name] || Behavior._registered[name];
		},

		//Garbage collects all applied filters for an element and its children.
		//element - (*element*) container to cleanup
		//ignoreChildren - (*boolean*; optional) if *true* only the element will be cleaned, otherwise the element and all the
		//          children with filters applied will be cleaned. Defaults to *false*.
		cleanup: function(element, ignoreChildren){
			element = document.id(element);
			var applied = getApplied(element);
			for (behavior in applied) {
				applied[behavior].cleanup(element);
				delete applied[behavior];
			}
			if (!ignoreChildren) element.getElements(':hasBehaviors').each(this.cleanup, this);
		}

	});

	//Returns the applied behaviors for an element.
	var getApplied = function(el){
		return el.retrieve('_appliedBehaviors', {});
	};

	//Registers a behavior filter.
	//name - the name of the filter
	//behavior - an instance of Behavior.Filter
	//overwrite - (boolean) if true, will overwrite existing filter if one exists; defaults to false.
	var addFilter = function(name, fn, overwrite){
		if (!this._registered[name] || overwrite) this._registered[name] = behavior;
	};

	var addFilters = function(obj, overwrite) {
		for (var name in obj) {
			addFilter.apply(this, [name, new Behavior.Filter(name, fn), overwrite]);
		}
	};

	//Add methods to the Behavior namespace for global registration.
	$extend(Behavior, {
		_registered: {},
		addGlobalFilter: addFilter,
		addGlobalFilters: addFilters
	});
	//Add methods to the Behavior class for instance registration.
	Behavior.implement({
		_registered: {},
		addFilter: addFilter,
		addFilters: addFilters
	});

	//This class is an actual filter that, given an element, alters it with specific behaviors.
	Behavior.Filter = new Class({

		//Filter implements window tools for filters that integrate with ART.Window.
		Implements: [ART.WindowTools],

		//Pass in an object with the following properties:
		//name - the name of this filter
		//attacher - a function that applies the filter to the given element
		initialize: function(name, attacher){
			this.name = name;
			this.attach = attacher;
			this._marks = new Table();
		},

		//Stores a garbage collection pointer for a specific element.
		//Example: if your filter enhances all the inputs in the container
		//you might have a function that removes that enhancement for garbage collection.
		//You would mark each input matched with its own cleanup function.
		//NOTE: this MUST be the element passed to the filter - the element with this filters
		//      name in its data-filter property. I.E.:
		//<form data-filter="FormValidator">
		//  <input type="text" name="email"/>
		//</form>
		//If this filter is FormValidator, you can mark the form for cleanup, but not, for example
		//the input. Only elements that match this filter can be marked.
		markForCleanup: function(element, fn){
			var marks = this._marks.get(element);
			if (!marks) marks = [];
			marks.include(fn);
			this._marks.set(element, marks);
			return this;
		},

		//Garbage collect a specific element.
		//NOTE: this should be an element that has a data-filter property that matches this filter.
		cleanup: function(element){
			var marks = this._marks.get(element);
			if (marks) {
				marks.each(function(fn){ fn(); });
				this._marks.erase(element);
			}
			return this;
		}

	});

})();

//a selector to find all elements that have behaviors applied to them.
Selectors.Pseudo.hasBehaviors = function(){
	return !!this.retrieve('_appliedBehaviors');
};

Element.implement({

	addDataFilter: function(name){
		this.set('data', 'filters', this.getDataFilters().include(name).join(','));
	},

	removeDataFilter: function(name){
		this.set('data', 'filters', this.getDataFilters().erase(name).join(','));
	},

	getDataFilters: function(){
		var filters = this.get('data', 'filters');
		if (!filters) return [];
		return filters.split(',').map(String.trim);
	},

	hasDataFilter: function(name){
		return this.getDataFilters().contains(name);
	}

});<|endoftext|>


<commit_before>// Helper class for rendering a button
export var Button = function(action, label, icon, opts) {
  this.action = action;
  this.label = label;

  if (typeof icon === "object") {
    this.opts = icon;
  } else {
    this.icon = icon;
  }
  this.opts = this.opts || opts || {};
};

Button.prototype.render = function(buffer) {
  var opts = this.opts;

  buffer.push("<button title=\"" + I18n.t(this.label) + "\"");
  if (opts.disabled) { buffer.push(" disabled"); }
  if (opts.className) { buffer.push(" class=\"" + opts.className + "\""); }
  if (opts.shareUrl) { buffer.push(" data-share-url=\"" + opts.shareUrl + "\""); }
  if (opts.postNumber) { buffer.push(" data-post-number=\"" + opts.postNumber + "\""); }
  buffer.push(" data-action=\"" + this.action + "\">");
  if (this.icon) { buffer.push(""); }
  if (opts.textLabel) { buffer.push(I18n.t(opts.textLabel)); }
  if (opts.innerHTML) { buffer.push(opts.innerHTML); }
  buffer.push("</button>");
};

var hiddenButtons;

export default Discourse.View.extend({
  tagName: 'section',
  classNames: ['post-menu-area', 'clearfix'],

  shouldRerender: Discourse.View.renderIfChanged(
    'post.deleted_at',
    'post.flagsAvailable.@each',
    'post.reply_count',
    'post.showRepliesBelow',
    'post.can_delete',
    'post.bookmarked',
    'post.shareUrl',
    'post.topic.deleted_at',
    'post.replies.length',
    'post.wiki',
    'collapsed'),

  _collapsedByDefault: function() {
    this.set('collapsed', true);
  }.on('init'),

  render: function(buffer) {
    var post = this.get('post');

    buffer.push("");
    this.renderReplies(post, buffer);
    this.renderButtons(post, buffer);
    this.renderAdminPopup(post, buffer);
    buffer.push("");
  },

  // Delegate click actions
  click: function(e) {
    var $target = $(e.target),
        action = $target.data('action') || $target.parent().data('action');

    if (!action) return;
    var handler = this["click" + action.capitalize()];
    if (!handler) return;

    handler.call(this, this.get('post'));
  },

  // Replies Button
  renderReplies: function(post, buffer) {
    if (!post.get('showRepliesBelow')) return;

    var reply_count = post.get('reply_count');
    buffer.push("<button class='show-replies' data-action='replies'>");
    buffer.push("" + reply_count + "");
    buffer.push(I18n.t("post.has_replies", { count: reply_count }));

    var icon = (this.get('post.replies.length') > 0) ? 'fa-chevron-up' : 'fa-chevron-down';
    return buffer.push("</button>");
  },

  renderButtons: function(post, buffer) {
    var self = this,
        allButtons = [],
        visibleButtons = [];

    if (typeof hiddenButtons === "undefined") {
      if (!Em.isEmpty(Discourse.SiteSettings.post_menu_hidden_items)) {
        hiddenButtons = Discourse.SiteSettings.post_menu_hidden_items.split('|');
      } else {
        hiddenButtons = [];
      }
    }

    var yours = post.get('yours');
    Discourse.SiteSettings.post_menu.split("|").forEach(function(i) {
      var creator = self["buttonFor" + i.replace(/\+/, '').capitalize()];
      if (creator) {
        var button = creator.call(self, post);
        if (button) {
          allButtons.push(button);
          if ((yours && button.opts.alwaysShowYours) ||
              (post.get('wiki') && button.opts.alwaysShowWiki) ||
              (hiddenButtons.indexOf(i) === -1)) {
            visibleButtons.push(button);
          }
        }
      }
    });

    // Only show ellipsis if there is more than one button hidden
    if (!this.get('collapsed') || (allButtons.length <= visibleButtons.length + 1)) {
      visibleButtons = allButtons;
    } else {
      visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post));
    }

    buffer.push('
');
    visibleButtons.forEach(function (b) {
      b.render(buffer);
    });
    buffer.push("
");
  },

  clickReplies: function() {
    if (this.get('post.replies.length') > 0) {
      this.set('post.replies', []);
    } else {
      this.get('post').loadReplies();
    }
  },

  // Delete button
  buttonForDelete: function(post) {
    var label, icon;

    if (post.get('post_number') === 1) {
      // If it's the first post, the delete/undo actions are related to the topic
      var topic = post.get('topic');
      if (topic.get('deleted_at')) {
        if (!topic.get('details.can_recover')) { return; }
        label = "topic.actions.recover";
        icon = "undo";
      } else {
        if (!topic.get('details.can_delete')) { return; }
        label = "topic.actions.delete";
        icon = "trash-o";
      }

    } else {
      // The delete actions target the post iteself
      if (post.get('deleted_at') || post.get('user_deleted')) {
        if (!post.get('can_recover')) { return; }
        label = "post.controls.undelete";
        icon = "undo";
      } else {
        if (!post.get('can_delete')) { return; }
        label = "post.controls.delete";
        icon = "trash-o";
      }
    }
    var action = (icon === 'trash-o') ? 'delete' : 'recover';
    var opts;
    if (icon === "trash-o"){
      opts = {className: 'delete'};
    }
    return new Button(action, label, icon, opts);
  },

  clickRecover: function(post) {
    this.get('controller').send('recoverPost', post);
  },

  clickDelete: function(post) {
    this.get('controller').send('deletePost', post);
  },

  // Like button
  buttonForLike: function(post) {
    var likeAction = post.get('actionByName.like');
    if (!likeAction) { return; }

    var className = likeAction.get('acted') ? 'has-like' : 'like';
    if (likeAction.get('canToggle')) {
      var descKey = likeAction.get('acted') ? 'post.controls.undo_like' : 'post.controls.like';
      return new Button('like', descKey, 'heart', {className: className});
    } else if (likeAction.get('acted')) {
      return new Button('like', 'post.controls.has_liked', 'heart', {className: className, disabled: true});
    }
  },

  clickLike: function(post) {
    this.get('controller').send('toggleLike', post);
  },

  // Flag button
  buttonForFlag: function(post) {
    if (Em.isEmpty(post.get('flagsAvailable'))) return;
    return new Button('flag', 'post.controls.flag', 'flag');
  },

  clickFlag: function(post) {
    this.get('controller').send('showFlags', post);
  },

  // Edit button
  buttonForEdit: function(post) {
    if (!post.get('can_edit')) return;
    return new Button('edit', 'post.controls.edit', 'pencil', {
      alwaysShowYours: true,
      alwaysShowWiki: true
    });
  },

  clickEdit: function(post) {
    this.get('controller').send('editPost', post);
  },

  // Share button
  buttonForShare: function(post) {
    if (!Discourse.User.current()) return;
    var options = {
      shareUrl: post.get('shareUrl'),
      postNumber: post.get('post_number')
    };
    return new Button('share', 'post.controls.share', 'link', options);
  },

  // Reply button
  buttonForReply: function() {
    if (!this.get('controller.model.details.can_create_post')) return;
    var options = {className: 'create'};

    if(!Discourse.Mobile.mobileView) {
      options.textLabel = 'topic.reply.title';
    }

    return new Button('reply', 'post.controls.reply', 'reply', options);
  },

  clickReply: function(post) {
    this.get('controller').send('replyToPost', post);
  },

  // Bookmark button
  buttonForBookmark: function(post) {
    if (!Discourse.User.current()) return;

    var iconClass = 'read-icon',
        buttonClass = 'bookmark',
        tooltip = 'bookmarks.not_bookmarked';

    if (post.get('bookmarked')) {
      iconClass += ' bookmarked';
      buttonClass += ' bookmarked';
      tooltip = 'bookmarks.created';
    }

    return new Button('bookmark', tooltip, {className: buttonClass, innerHTML: "
"});
  },

  clickBookmark: function(post) {
    this.get('controller').send('toggleBookmark', post);
  },

  buttonForAdmin: function() {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }
    return new Button('admin', 'post.controls.admin', 'wrench');
  },

  renderAdminPopup: function(post, buffer) {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }

    var isWiki = post.get('wiki'),
        wikiIcon = '',
        wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki');

    var isModerator = post.get('post_type') === Discourse.Site.currentProp('post_types.moderator_action'),
        postTypeIcon = '',
        postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator');

    var rebakePostIcon = '',
        rebakePostText = I18n.t('post.controls.rebake');

    var unhidePostIcon = '',
        unhidePostText = I18n.t('post.controls.unhide');

    var html = '
' +
                 '
' + I18n.t('admin_title') + '
' +
                 '
' +
                   '
' + wikiIcon + wikiText + '
' +
                   '
' + postTypeIcon + postTypeText + '
' +
                   '
' + rebakePostIcon + rebakePostText + '
' +
                   (post.hidden ? '
' + unhidePostIcon + unhidePostText + '
' : '') +
                 '
' +
               '
';

    buffer.push(html);
  },

  clickAdmin: function() {
    var $adminMenu = this.$('.post-admin-menu');
    this.set('adminMenu', $adminMenu);
    $adminMenu.show();
  },

  clickToggleWiki: function() {
    this.get('controller').send('toggleWiki', this.get('post'));
  },

  clickTogglePostType: function () {
    this.get("controller").send("togglePostType", this.get("post"));
  },

  clickRebakePost: function () {
    this.get("controller").send("rebakePost", this.get("post"));
  },

  clickUnhidePost: function () {
    this.get("controller").send("unhidePost", this.get("post"));
  },

  buttonForShowMoreActions: function() {
    return new Button('showMoreActions', 'show_more', 'ellipsis-h');
  },

  clickShowMoreActions: function() {
    this.set('collapsed', false);
  }

});
<commit_msg>FIX: reply after like does nothing<commit_after>// Helper class for rendering a button
export var Button = function(action, label, icon, opts) {
  this.action = action;
  this.label = label;

  if (typeof icon === "object") {
    this.opts = icon;
  } else {
    this.icon = icon;
  }
  this.opts = this.opts || opts || {};
};

Button.prototype.render = function(buffer) {
  var opts = this.opts;

  buffer.push("<button title=\"" + I18n.t(this.label) + "\"");
  if (opts.disabled) { buffer.push(" disabled"); }
  if (opts.className) { buffer.push(" class=\"" + opts.className + "\""); }
  if (opts.shareUrl) { buffer.push(" data-share-url=\"" + opts.shareUrl + "\""); }
  if (opts.postNumber) { buffer.push(" data-post-number=\"" + opts.postNumber + "\""); }
  buffer.push(" data-action=\"" + this.action + "\">");
  if (this.icon) { buffer.push(""); }
  if (opts.textLabel) { buffer.push(I18n.t(opts.textLabel)); }
  if (opts.innerHTML) { buffer.push(opts.innerHTML); }
  buffer.push("</button>");
};

var hiddenButtons;

export default Discourse.View.extend({
  tagName: 'section',
  classNames: ['post-menu-area', 'clearfix'],

  shouldRerender: Discourse.View.renderIfChanged(
    'post.deleted_at',
    'post.flagsAvailable.@each',
    'post.reply_count',
    'post.showRepliesBelow',
    'post.can_delete',
    'post.bookmarked',
    'post.shareUrl',
    'post.topic.deleted_at',
    'post.replies.length',
    'post.wiki',
    'collapsed'),

  _collapsedByDefault: function() {
    this.set('collapsed', true);
  }.on('init'),

  render: function(buffer) {
    var post = this.get('post');

    buffer.push("");
    this.renderReplies(post, buffer);
    this.renderButtons(post, buffer);
    this.renderAdminPopup(post, buffer);
    buffer.push("");
  },

  didInsertElement: function(){
    // @Robin, why is this being called twice on initial render?
    // this is a workaround to try to fix https://meta.discourse.org/t/click-on-reply-sometimes-without-effect/19860
    var self = this;
    var $self = this.$();

    $self.off('click.post').on('click.post', function(e){
      var $target = $(e.target),
          action = $target.data('action') || $target.parent().data('action');

      if (!action) return;
      var handler = self["click" + action.capitalize()];
      if (!handler) return;

      handler.call(self, self.get('post'));
    });
  },

  willDestroyElement: function(){
    this.$().off('click.post');
  },

  // Replies Button
  renderReplies: function(post, buffer) {
    if (!post.get('showRepliesBelow')) return;

    var reply_count = post.get('reply_count');
    buffer.push("<button class='show-replies' data-action='replies'>");
    buffer.push("" + reply_count + "");
    buffer.push(I18n.t("post.has_replies", { count: reply_count }));

    var icon = (this.get('post.replies.length') > 0) ? 'fa-chevron-up' : 'fa-chevron-down';
    return buffer.push("</button>");
  },

  renderButtons: function(post, buffer) {
    var self = this,
        allButtons = [],
        visibleButtons = [];

    if (typeof hiddenButtons === "undefined") {
      if (!Em.isEmpty(Discourse.SiteSettings.post_menu_hidden_items)) {
        hiddenButtons = Discourse.SiteSettings.post_menu_hidden_items.split('|');
      } else {
        hiddenButtons = [];
      }
    }

    var yours = post.get('yours');
    Discourse.SiteSettings.post_menu.split("|").forEach(function(i) {
      var creator = self["buttonFor" + i.replace(/\+/, '').capitalize()];
      if (creator) {
        var button = creator.call(self, post);
        if (button) {
          allButtons.push(button);
          if ((yours && button.opts.alwaysShowYours) ||
              (post.get('wiki') && button.opts.alwaysShowWiki) ||
              (hiddenButtons.indexOf(i) === -1)) {
            visibleButtons.push(button);
          }
        }
      }
    });

    // Only show ellipsis if there is more than one button hidden
    if (!this.get('collapsed') || (allButtons.length <= visibleButtons.length + 1)) {
      visibleButtons = allButtons;
    } else {
      visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post));
    }

    buffer.push('
');
    visibleButtons.forEach(function (b) {
      b.render(buffer);
    });
    buffer.push("
");
  },

  clickReplies: function() {
    if (this.get('post.replies.length') > 0) {
      this.set('post.replies', []);
    } else {
      this.get('post').loadReplies();
    }
  },

  // Delete button
  buttonForDelete: function(post) {
    var label, icon;

    if (post.get('post_number') === 1) {
      // If it's the first post, the delete/undo actions are related to the topic
      var topic = post.get('topic');
      if (topic.get('deleted_at')) {
        if (!topic.get('details.can_recover')) { return; }
        label = "topic.actions.recover";
        icon = "undo";
      } else {
        if (!topic.get('details.can_delete')) { return; }
        label = "topic.actions.delete";
        icon = "trash-o";
      }

    } else {
      // The delete actions target the post iteself
      if (post.get('deleted_at') || post.get('user_deleted')) {
        if (!post.get('can_recover')) { return; }
        label = "post.controls.undelete";
        icon = "undo";
      } else {
        if (!post.get('can_delete')) { return; }
        label = "post.controls.delete";
        icon = "trash-o";
      }
    }
    var action = (icon === 'trash-o') ? 'delete' : 'recover';
    var opts;
    if (icon === "trash-o"){
      opts = {className: 'delete'};
    }
    return new Button(action, label, icon, opts);
  },

  clickRecover: function(post) {
    this.get('controller').send('recoverPost', post);
  },

  clickDelete: function(post) {
    this.get('controller').send('deletePost', post);
  },

  // Like button
  buttonForLike: function(post) {
    var likeAction = post.get('actionByName.like');
    if (!likeAction) { return; }

    var className = likeAction.get('acted') ? 'has-like' : 'like';
    if (likeAction.get('canToggle')) {
      var descKey = likeAction.get('acted') ? 'post.controls.undo_like' : 'post.controls.like';
      return new Button('like', descKey, 'heart', {className: className});
    } else if (likeAction.get('acted')) {
      return new Button('like', 'post.controls.has_liked', 'heart', {className: className, disabled: true});
    }
  },

  clickLike: function(post) {
    this.get('controller').send('toggleLike', post);
  },

  // Flag button
  buttonForFlag: function(post) {
    if (Em.isEmpty(post.get('flagsAvailable'))) return;
    return new Button('flag', 'post.controls.flag', 'flag');
  },

  clickFlag: function(post) {
    this.get('controller').send('showFlags', post);
  },

  // Edit button
  buttonForEdit: function(post) {
    if (!post.get('can_edit')) return;
    return new Button('edit', 'post.controls.edit', 'pencil', {
      alwaysShowYours: true,
      alwaysShowWiki: true
    });
  },

  clickEdit: function(post) {
    this.get('controller').send('editPost', post);
  },

  // Share button
  buttonForShare: function(post) {
    if (!Discourse.User.current()) return;
    var options = {
      shareUrl: post.get('shareUrl'),
      postNumber: post.get('post_number')
    };
    return new Button('share', 'post.controls.share', 'link', options);
  },

  // Reply button
  buttonForReply: function() {
    if (!this.get('controller.model.details.can_create_post')) return;
    var options = {className: 'create'};

    if(!Discourse.Mobile.mobileView) {
      options.textLabel = 'topic.reply.title';
    }

    return new Button('reply', 'post.controls.reply', 'reply', options);
  },

  clickReply: function(post) {
    this.get('controller').send('replyToPost', post);
  },

  // Bookmark button
  buttonForBookmark: function(post) {
    if (!Discourse.User.current()) return;

    var iconClass = 'read-icon',
        buttonClass = 'bookmark',
        tooltip = 'bookmarks.not_bookmarked';

    if (post.get('bookmarked')) {
      iconClass += ' bookmarked';
      buttonClass += ' bookmarked';
      tooltip = 'bookmarks.created';
    }

    return new Button('bookmark', tooltip, {className: buttonClass, innerHTML: "
"});
  },

  clickBookmark: function(post) {
    this.get('controller').send('toggleBookmark', post);
  },

  buttonForAdmin: function() {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }
    return new Button('admin', 'post.controls.admin', 'wrench');
  },

  renderAdminPopup: function(post, buffer) {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }

    var isWiki = post.get('wiki'),
        wikiIcon = '',
        wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki');

    var isModerator = post.get('post_type') === Discourse.Site.currentProp('post_types.moderator_action'),
        postTypeIcon = '',
        postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator');

    var rebakePostIcon = '',
        rebakePostText = I18n.t('post.controls.rebake');

    var unhidePostIcon = '',
        unhidePostText = I18n.t('post.controls.unhide');

    var html = '
' +
                 '
' + I18n.t('admin_title') + '
' +
                 '
' +
                   '
' + wikiIcon + wikiText + '
' +
                   '
' + postTypeIcon + postTypeText + '
' +
                   '
' + rebakePostIcon + rebakePostText + '
' +
                   (post.hidden ? '
' + unhidePostIcon + unhidePostText + '
' : '') +
                 '
' +
               '
';

    buffer.push(html);
  },

  clickAdmin: function() {
    var $adminMenu = this.$('.post-admin-menu');
    this.set('adminMenu', $adminMenu);
    $adminMenu.show();
  },

  clickToggleWiki: function() {
    this.get('controller').send('toggleWiki', this.get('post'));
  },

  clickTogglePostType: function () {
    this.get("controller").send("togglePostType", this.get("post"));
  },

  clickRebakePost: function () {
    this.get("controller").send("rebakePost", this.get("post"));
  },

  clickUnhidePost: function () {
    this.get("controller").send("unhidePost", this.get("post"));
  },

  buttonForShowMoreActions: function() {
    return new Button('showMoreActions', 'show_more', 'ellipsis-h');
  },

  clickShowMoreActions: function() {
    this.set('collapsed', false);
  }

});
<|endoftext|>


<commit_before><commit_msg>change column asin to _id of book<commit_after><|endoftext|>