<commit_before>/**
 * Patterns autosuggest - suggestion/completion support
 *
 * Copyright 2012-2013 Florian Friesdorf
 * Copyright 2012 JC Brand
 * Copyright 2013 Marko Durkovic
 */
define([
    "jquery",
    "../core/parser",
    "../registry",
    "jquery.select2"
], function($, Parser, registry) {
    var parser = new Parser("autosuggest");
    parser.add_argument("words", "");
    parser.add_argument("pre-fill");
    parser.add_argument("placeholder", function($el) {
        return $el.attr("placeholder") || "Enter text";
    });

    var autosuggestTextToHidden = {
        name: "autosuggestTextToHidden",
        transform: function($content) {
            $content.filter('input[type=text].pat-autosuggest').each(function() {
                $(this).clone().attr('type','hidden').insertAfter($(this)).prev().remove();
            });
            $content.find('input[type=text].pat-autosuggest').each(function() {
                $(this).clone().attr('type','hidden').insertAfter($(this)).prev().remove();
            });
        }
    };
    registry.register(autosuggestTextToHidden);

    var _ = {
        name: "autosuggest",
        trigger: "input.pat-autosuggest",
        init: function($el, opts) {
            if ($el.length > 1)
                return $el.each(function() { _.init($(this), opts); });

            var cfg = parser.parse($el, opts);
            cfg.startText =  $el.attr("readonly") ? "" : cfg.placeholder;

            if (cfg.preFill) {
                $el.val(cfg.preFill);
            }
            $el.select2({tags: cfg.words.split(/\s*,\s*/)});

            // suppress propagation for second input field
            $el.prev().on("input-change input-defocus input-change-delayed",
                function(e) { e.stopPropagation(); }
            );
            return $el;
        },
        destroy: function($el) {
            $el.off(".pat-autosuggest");
            $el.select2("destroy");
        }
    };
    registry.register(_);
    return _;
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<commit_msg>Improve prefill default handling.<commit_after>/**
 * Patterns autosuggest - suggestion/completion support
 *
 * Copyright 2012-2013 Florian Friesdorf
 * Copyright 2012 JC Brand
 * Copyright 2013 Marko Durkovic
 */
define([
    "jquery",
    "../core/parser",
    "../registry",
    "jquery.select2"
], function($, Parser, registry) {
    var parser = new Parser("autosuggest");
    parser.add_argument("words", "");
    parser.add_argument("pre-fill", function($el) { return $el.val(); });
    parser.add_argument("placeholder", function($el) {
        return $el.attr("placeholder") || "Enter text";
    });

    var autosuggestTextToHidden = {
        name: "autosuggestTextToHidden",
        transform: function($content) {
            $content.filter('input[type=text].pat-autosuggest').each(function() {
                $(this).clone().attr('type','hidden').insertAfter($(this)).prev().remove();
            });
            $content.find('input[type=text].pat-autosuggest').each(function() {
                $(this).clone().attr('type','hidden').insertAfter($(this)).prev().remove();
            });
        }
    };
    registry.register(autosuggestTextToHidden);

    var _ = {
        name: "autosuggest",
        trigger: "input.pat-autosuggest",
        init: function($el, opts) {
            if ($el.length > 1)
                return $el.each(function() { _.init($(this), opts); });

            var cfg = parser.parse($el, opts);
            cfg.startText =  $el.attr("readonly") ? "" : cfg.placeholder;

            $el.val(cfg.preFill.split(','));
            $el.select2({tags: cfg.words.split(/\s*,\s*/)});

            // suppress propagation for second input field
            $el.prev().on("input-change input-defocus input-change-delayed",
                function(e) { e.stopPropagation(); }
            );
            return $el;
        },
        destroy: function($el) {
            $el.off(".pat-autosuggest");
            $el.select2("destroy");
        }
    };
    registry.register(_);
    return _;
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<|endoftext|>


<commit_before>define(function(require, module, exports) {
    main.consumes = ["Dialog", "util"];
    main.provides = ["dialog.confirm"];
    return main;

    function main(options, imports, register) {
        var Dialog = imports.Dialog;
        var util = imports.util;

        /***** Initialization *****/

        var plugin = new Dialog("Ajax.org", main.consumes, {
            name: "dialog.confirm",
            allowClose: false,
            modal: true,
            elements: [
                { type: "button", id: "cancel", caption: "Cancel", hotkey: "ESC", onclick: function(){ plugin.hide() } },
                { type: "button", id: "ok", caption: "OK", color: "green", "default": true, onclick: function(){ plugin.hide() } }
            ]
        });

        /***** Methods *****/

        function show(title, header, msg, onconfirm, oncancel, options) {
            return plugin.queue(function(){
                plugin.title = title;
                plugin.heading = options && options.isHTML ? header : util.escapeXml(header);
                plugin.body = options && options.isHTML ? msg : util.escapeXml(msg).replace(/\n/g, "
");

                plugin.getElement("ok").setCaption(options.yes || options.ok || "OK");
                plugin.getElement("cancel").setCaption(options.no || options.cancel || "Cancel");

                plugin.update([
                    { id: "ok", onclick: function(){ plugin.hide(); onconfirm(); } },
                    { id: "cancel", onclick: function(){ plugin.hide(); oncancel && oncancel(); } },
                ]);
            });
        }

        /***** Register *****/

        plugin.freezePublicAPI({
            show: show
        });

        register("", {
            "dialog.confirm": plugin
        });
    }
});<commit_msg>fix error when dialog confirm is called without options<commit_after>define(function(require, module, exports) {
    main.consumes = ["Dialog", "util"];
    main.provides = ["dialog.confirm"];
    return main;

    function main(options, imports, register) {
        var Dialog = imports.Dialog;
        var util = imports.util;

        /***** Initialization *****/

        var plugin = new Dialog("Ajax.org", main.consumes, {
            name: "dialog.confirm",
            allowClose: false,
            modal: true,
            elements: [
                { type: "button", id: "cancel", caption: "Cancel", hotkey: "ESC", onclick: function(){ plugin.hide() } },
                { type: "button", id: "ok", caption: "OK", color: "green", "default": true, onclick: function(){ plugin.hide() } }
            ]
        });

        /***** Methods *****/

        function show(title, header, msg, onconfirm, oncancel, options) {
            return plugin.queue(function(){
                if (!options)
                    options = {};

                plugin.title = title;
                plugin.heading = options.isHTML ? header : util.escapeXml(header);
                plugin.body = options.isHTML ? msg : util.escapeXml(msg).replace(/\n/g, "
");

                plugin.getElement("ok").setCaption(options.yes || options.ok || "OK");
                plugin.getElement("cancel").setCaption(options.no || options.cancel || "Cancel");

                plugin.update([
                    { id: "ok", onclick: function(){ plugin.hide(); onconfirm(); } },
                    { id: "cancel", onclick: function(){ plugin.hide(); oncancel && oncancel(); } },
                ]);
            });
        }

        /***** Register *****/

        plugin.freezePublicAPI({
            show: show
        });

        register("", {
            "dialog.confirm": plugin
        });
    }
});<|endoftext|>


<commit_before>$(document).ready(function() {
        $("form.validate").validate();

        $("div.provisioning:lt(50)").accordion();
        $("div.progressbar").progressbar();
});
<commit_msg>prevent jquery running amok on large lists<commit_after>$(document).ready(function() {
        $("form.validate").validate();

        $("div.provisioning").slice(0,50).accordion();
        $("div.progressbar").progressbar();
});
<|endoftext|>


<commit_before>import React from 'react';
import ReactDOM from 'react-dom';
import cx from 'classnames';
import { props, t } from '../utils';

const NO_SIZE_WRAPPER = 'no-size-wrapper';

export const Props = {
  children: t.ReactChildren,
  popover: t.struct({
    content: t.ReactChildren,
    attachToBody: t.maybe(t.Boolean),
    position: t.maybe(t.enums.of(['top', 'bottom', 'left', 'right'])),
    anchor: t.maybe(t.enums.of(['start', 'center', 'end'])),
    event: t.maybe(t.enums.of(['click', 'hover'])),
    onShow: t.maybe(t.Function),
    onHide: t.maybe(t.Function),
    onToggle: t.maybe(t.Function),
    dismissOnScroll: t.maybe(t.Boolean),
    dismissOnClickOutside: t.maybe(t.Boolean),
    className: t.maybe(t.String),
    style: t.maybe(t.Object),
    id: t.maybe(t.String),
    maxWidth: t.maybe(t.union([ t.Number, t.String ])),
    distance: t.maybe(t.Number),
    offsetX: t.maybe(t.Number),
    offsetY: t.maybe(t.Number),
    isOpen: t.maybe(t.Boolean)
  }),
  id: t.maybe(t.String),
  className: t.maybe(t.String),
  style: t.maybe(t.Object)
};

/**
 * Composed of two children: trigger (children) and popover. After a particular event on the trigger (usually "hover" or "click") it renders the popover and positions it relative to it.
 * @param children - the trigger node. It's always visible
 * @param popover - popover settings. The popover is **not** always visible
 */
@props(Props)
export default class Popover extends React.Component {

  // LIFECYCLE

  constructor(props) {
    super(props);
    this.state = { isOpen: false };
  }

  componentDidMount() {
    this.saveValuesFromNodeTree();
    this.initialized = true;
    if (this.isOpen()) {
      this.onPopoverOpenChange(this.props);
    }
  }

  componentWillReceiveProps(nextProps) {
    this.saveValuesFromNodeTree();
    if (!this.isStateful() && this.getPopoverProps().isOpen !== this.getPopoverProps(nextProps).isOpen) {
      this.onPopoverOpenChange(nextProps);
    }
  }

  componentDidUpdate() {
    if (this.containerNode) {
      const popover = this.getVisiblePopover();
      ReactDOM.render(popover, this.containerNode);
    }
  }

  componentWillUnmount() {
    this.removePopover();
    this.removeListeners();
  }

  // LISTENERS

  addOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.addEventListener('click', this.onClickOutside, false);
    }
  };

  removeOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.removeEventListener('click', this.onClickOutside, false);
    }
  };

  onClickOutside = (e) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.isAbsolute() ? this.containerNode : childrenNode.childNodes[1];
    const el = e.target || e.srcElement;
    if (!this.isEventInsideTarget(el, childrenNode) && (!popoverNode || !this.isEventInsideTarget(el, popoverNode))) {
      this.hidePopover();
    }
  };

  addOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.addEventListener('mousewheel', this.onScroll, false);
    }
  };

  removeOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.removeEventListener('mousewheel', this.onScroll, false);
    }
  };

  onScroll = () => this.hidePopover();

  addListeners = () => {
    this.addOnScrollListener();
    this.addOnClickListener();
  };

  removeListeners = () => {
    this.removeOnScrollListener();
    this.removeOnClickListener();
  };

  // UTILS

  // extend with default values
  getPopoverProps = (_props) => {
    const props = _props || this.props;
    return {
      type: 'relative',
      position: 'top',
      anchor: 'center',
      event: 'hover',
      onShow: () => {},
      onHide: () => {},
      onToggle: () => {},
      dismissOnClickOutside: true,
      dismissOnScroll: true,
      className: '',
      distance: 5,
      offsetX: 0,
      offsetY: 0,
      ...props.popover
    };
  };

  getPopoverNode = () => {
    let popover;
    if (this.isAbsolute()) {
      popover = this.popoverNode;
    } else {
      const childrenNode = ReactDOM.findDOMNode(this.refs.children);
      popover = childrenNode.childNodes[1];
    }
    return (popover && popover.id === NO_SIZE_WRAPPER) ? popover.childNodes[0] : popover;
  };

  getOffsetRect = (target) => {
    const box = target.getBoundingClientRect();

    const body = document.body;
    const docElem = document.documentElement;

    const scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;

    const clientTop = docElem.clientTop || body.clientTop || 0;
    const clientLeft = docElem.clientLeft || body.clientLeft || 0;

    const top = Math.round(box.top + scrollTop - clientTop);
    const left = Math.round(box.left + scrollLeft - clientLeft);

    return { top, left };
  };

  saveValuesFromNodeTree = (cb) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.getPopoverNode();

    if (popoverNode) {
      const { clientWidth: childWidth, clientHeight: childHeight } = childrenNode;
      const { clientHeight: popoverHeight, clientWidth: popoverWidth } = popoverNode;
      const { top: childY, left: childX } = this.getOffsetRect(childrenNode);
      const { top: popoverY, left: popoverX } = this.getOffsetRect(popoverNode);

      this.setState({
        child: {
          width: childWidth,
          height: childHeight,
          x: childX,
          y: childY
        },
        popover: {
          width: popoverWidth,
          height: popoverHeight,
          x: popoverX,
          y: popoverY
        }
      }, cb);
    }
  };

  isStateful = (props) => typeof this.getPopoverProps(props).isOpen === 'undefined';

  isOpen = (props) => this.isStateful() ? this.state.isOpen : this.getPopoverProps(props).isOpen;

  isEventInsideTarget = (el, target) => {
    if (!el) {
      return false;
    } else if (el === target) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideTarget(el.parentNode, target);
    } else {
      return false;
    }
  };

  // VISIBILITY CHANGE

  appendPopover = () => {
    // create container node
    this.containerNode = document.createElement('div');
    document.body.appendChild(this.containerNode);

    // render invisible popover
    const hiddenPopover = this.getHiddenPopover();
    ReactDOM.render(hiddenPopover, this.containerNode);

    // add pointer to popover node
    this.popoverNode = this.containerNode.childNodes[0];

    // save popover size (visible popover will be rendered in componentDidUpdate)
    this.saveValuesFromNodeTree();
  };

  removePopover = () => {
    if (this.containerNode) {
      document.body.removeChild(this.containerNode);
      this.containerNode = null;
    }
  };

  onPopoverOpenChange = (props) => {
    if (this.isOpen(props)) {
      if (this.isAbsolute()) {
        this.appendPopover();
      }
      this.addListeners();
    } else {
      if (this.isAbsolute()) {
        this.removePopover();
      }
      this.removeListeners();
    }
  };

  onPopoverStateChange = () => {
    const { onShow, onHide, onToggle } = this.getPopoverProps();
    if (this.state.isOpen) {
      onShow();
    } else {
      onHide();
    }
    onToggle();
    this.onPopoverOpenChange();
  };

  eventWrapper = (cb) => {
    return (e) => {
      const { event } = this.getPopoverProps();
      const childrenNode = ReactDOM.findDOMNode(this.refs.children).childNodes[0];
      const el = e.target || e.srcElement;
      if (this.isAbsolute() || (event === 'hover') || this.isEventInsideTarget(el, childrenNode)) {
        cb();
      }
    };
  };

  showPopover = () => this.setIsOpen(true);

  hidePopover = () => this.setIsOpen(false);

  togglePopover = () => this.setIsOpen(!this.isOpen());

  setIsOpen = (isOpen) => {
    if (this.isStateful()) {
      this.setState({ isOpen }, this.onPopoverStateChange);
    } else {
      const { onShow, onHide, onToggle } = this.getPopoverProps();
      const cb = isOpen ? onShow : onHide;
      cb();
      onToggle();
    }
  };

  // LOCALES

  popoverTemplate = (style) => {
    const { position, anchor, className, content, id, event } = this.getPopoverProps();
    const { eventWrapper, hidePopover, isAbsolute } = this;
    const positionClass = `position-${position}`;
    const anchorClass = `anchor-${anchor}`;
    const _className = `popover-content ${positionClass} ${anchorClass} ${className}`;
    const events = !isAbsolute() && event === 'hover' ? { onMouseEnter: eventWrapper(hidePopover) } : undefined;
    return (

        {content}

    );
  };

  getVisiblePopover = () => this.popoverTemplate(this.computePopoverStyle());

  getHiddenPopover = () => {
    const style = { width: '100%', height: '100%', top: 0, left: 0, position: 'absolute', overflow: 'hidden', pointerEvents: 'none' };
    return (

        {this.popoverTemplate({ position: 'absolute', visibility: 'hidden' })}

    );
  };

  isAbsolute = () => this.getPopoverProps().attachToBody === true;

  getEventCallbacks = () => {
    const { event } = this.getPopoverProps();
    const onHover = event === 'hover';
    const onClick = event === 'click';
    const { eventWrapper, showPopover, hidePopover, togglePopover } = this;
    return {
      onMouseEnter: onHover ? eventWrapper(showPopover) : undefined,
      onMouseLeave: onHover ? eventWrapper(hidePopover) : undefined,
      onClick: onClick ? eventWrapper(togglePopover) : undefined
    };
  };

  computePopoverStyle = () => {
    const { child, popover } = this.state;
    const { anchor, position, maxWidth, offsetX, offsetY, distance } = this.getPopoverProps();

    const isAbsolute = this.isAbsolute();
    const isHorizontal = position === 'top' || position === 'bottom';
    const isVertical = position === 'right' || position === 'left';

    const anchorOffset = { top: 0, left: 0 };
    const positionOffset = { top: 0, left: 0 };

    switch (anchor) {
      case 'start':
        // default -> { top: 0, left: 0 }
        break;
      case 'center':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) / 2 : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) / 2 : 0;
        break;
      case 'end':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) : 0;
        break;
    }

    switch (position) {
      case 'top':
        positionOffset.top = -(popover.height + distance);
        break;
      case 'bottom':
        positionOffset.top = child.height + distance;
        break;
      case 'left':
        positionOffset.left = -(popover.width + distance);
        break;
      case 'right':
        positionOffset.left = child.width + distance;
        break;
    }

    return {
      position: 'absolute',
      top: (isAbsolute ? child.y : 0) + (positionOffset.top + anchorOffset.top + offsetY),
      left: (isAbsolute ? child.x : 0) + (positionOffset.left + anchorOffset.left + offsetX),
      maxWidth
    };
  };

  getLocals() {
    const isRelative = !this.isAbsolute();
    const isOpen = this.isOpen();
    const popover = isRelative && (this.initialized && isOpen ? this.getVisiblePopover() : this.getHiddenPopover());
    return {
      ...this.props,
      style: {
        display: 'inline-block',
        position: isRelative ? 'relative' : undefined,
        ...this.props.style
      },
      className: cx('react-popover', this.props.className, { 'is-open': isOpen, 'is-closed': !isOpen }),
      eventCallbacks: this.getEventCallbacks(),
      popover
    };
  }

  // RENDER

  render() {
    const { children, style, className, id, eventCallbacks, popover } = this.getLocals();
    return (

        {children}
        {popover}

    );
  }

}
<commit_msg>pass style to popover template<commit_after>import React from 'react';
import ReactDOM from 'react-dom';
import cx from 'classnames';
import { props, t } from '../utils';

const NO_SIZE_WRAPPER = 'no-size-wrapper';

export const Props = {
  children: t.ReactChildren,
  popover: t.struct({
    content: t.ReactChildren,
    attachToBody: t.maybe(t.Boolean),
    position: t.maybe(t.enums.of(['top', 'bottom', 'left', 'right'])),
    anchor: t.maybe(t.enums.of(['start', 'center', 'end'])),
    event: t.maybe(t.enums.of(['click', 'hover'])),
    onShow: t.maybe(t.Function),
    onHide: t.maybe(t.Function),
    onToggle: t.maybe(t.Function),
    dismissOnScroll: t.maybe(t.Boolean),
    dismissOnClickOutside: t.maybe(t.Boolean),
    className: t.maybe(t.String),
    style: t.maybe(t.Object),
    id: t.maybe(t.String),
    maxWidth: t.maybe(t.union([ t.Number, t.String ])),
    distance: t.maybe(t.Number),
    offsetX: t.maybe(t.Number),
    offsetY: t.maybe(t.Number),
    isOpen: t.maybe(t.Boolean)
  }),
  id: t.maybe(t.String),
  className: t.maybe(t.String),
  style: t.maybe(t.Object)
};

/**
 * Composed of two children: trigger (children) and popover. After a particular event on the trigger (usually "hover" or "click") it renders the popover and positions it relative to it.
 * @param children - the trigger node. It's always visible
 * @param popover - popover settings. The popover is **not** always visible
 */
@props(Props)
export default class Popover extends React.Component {

  // LIFECYCLE

  constructor(props) {
    super(props);
    this.state = { isOpen: false };
  }

  componentDidMount() {
    this.saveValuesFromNodeTree();
    this.initialized = true;
    if (this.isOpen()) {
      this.onPopoverOpenChange(this.props);
    }
  }

  componentWillReceiveProps(nextProps) {
    this.saveValuesFromNodeTree();
    if (!this.isStateful() && this.getPopoverProps().isOpen !== this.getPopoverProps(nextProps).isOpen) {
      this.onPopoverOpenChange(nextProps);
    }
  }

  componentDidUpdate() {
    if (this.containerNode) {
      const popover = this.getVisiblePopover();
      ReactDOM.render(popover, this.containerNode);
    }
  }

  componentWillUnmount() {
    this.removePopover();
    this.removeListeners();
  }

  // LISTENERS

  addOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.addEventListener('click', this.onClickOutside, false);
    }
  };

  removeOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.removeEventListener('click', this.onClickOutside, false);
    }
  };

  onClickOutside = (e) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.isAbsolute() ? this.containerNode : childrenNode.childNodes[1];
    const el = e.target || e.srcElement;
    if (!this.isEventInsideTarget(el, childrenNode) && (!popoverNode || !this.isEventInsideTarget(el, popoverNode))) {
      this.hidePopover();
    }
  };

  addOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.addEventListener('mousewheel', this.onScroll, false);
    }
  };

  removeOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.removeEventListener('mousewheel', this.onScroll, false);
    }
  };

  onScroll = () => this.hidePopover();

  addListeners = () => {
    this.addOnScrollListener();
    this.addOnClickListener();
  };

  removeListeners = () => {
    this.removeOnScrollListener();
    this.removeOnClickListener();
  };

  // UTILS

  // extend with default values
  getPopoverProps = (_props) => {
    const props = _props || this.props;
    return {
      type: 'relative',
      position: 'top',
      anchor: 'center',
      event: 'hover',
      onShow: () => {},
      onHide: () => {},
      onToggle: () => {},
      dismissOnClickOutside: true,
      dismissOnScroll: true,
      className: '',
      distance: 5,
      offsetX: 0,
      offsetY: 0,
      ...props.popover
    };
  };

  getPopoverNode = () => {
    let popover;
    if (this.isAbsolute()) {
      popover = this.popoverNode;
    } else {
      const childrenNode = ReactDOM.findDOMNode(this.refs.children);
      popover = childrenNode.childNodes[1];
    }
    return (popover && popover.id === NO_SIZE_WRAPPER) ? popover.childNodes[0] : popover;
  };

  getOffsetRect = (target) => {
    const box = target.getBoundingClientRect();

    const body = document.body;
    const docElem = document.documentElement;

    const scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;

    const clientTop = docElem.clientTop || body.clientTop || 0;
    const clientLeft = docElem.clientLeft || body.clientLeft || 0;

    const top = Math.round(box.top + scrollTop - clientTop);
    const left = Math.round(box.left + scrollLeft - clientLeft);

    return { top, left };
  };

  saveValuesFromNodeTree = (cb) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.getPopoverNode();

    if (popoverNode) {
      const { clientWidth: childWidth, clientHeight: childHeight } = childrenNode;
      const { clientHeight: popoverHeight, clientWidth: popoverWidth } = popoverNode;
      const { top: childY, left: childX } = this.getOffsetRect(childrenNode);
      const { top: popoverY, left: popoverX } = this.getOffsetRect(popoverNode);

      this.setState({
        child: {
          width: childWidth,
          height: childHeight,
          x: childX,
          y: childY
        },
        popover: {
          width: popoverWidth,
          height: popoverHeight,
          x: popoverX,
          y: popoverY
        }
      }, cb);
    }
  };

  isStateful = (props) => typeof this.getPopoverProps(props).isOpen === 'undefined';

  isOpen = (props) => this.isStateful() ? this.state.isOpen : this.getPopoverProps(props).isOpen;

  isEventInsideTarget = (el, target) => {
    if (!el) {
      return false;
    } else if (el === target) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideTarget(el.parentNode, target);
    } else {
      return false;
    }
  };

  // VISIBILITY CHANGE

  appendPopover = () => {
    // create container node
    this.containerNode = document.createElement('div');
    document.body.appendChild(this.containerNode);

    // render invisible popover
    const hiddenPopover = this.getHiddenPopover();
    ReactDOM.render(hiddenPopover, this.containerNode);

    // add pointer to popover node
    this.popoverNode = this.containerNode.childNodes[0];

    // save popover size (visible popover will be rendered in componentDidUpdate)
    this.saveValuesFromNodeTree();
  };

  removePopover = () => {
    if (this.containerNode) {
      document.body.removeChild(this.containerNode);
      this.containerNode = null;
    }
  };

  onPopoverOpenChange = (props) => {
    if (this.isOpen(props)) {
      if (this.isAbsolute()) {
        this.appendPopover();
      }
      this.addListeners();
    } else {
      if (this.isAbsolute()) {
        this.removePopover();
      }
      this.removeListeners();
    }
  };

  onPopoverStateChange = () => {
    const { onShow, onHide, onToggle } = this.getPopoverProps();
    if (this.state.isOpen) {
      onShow();
    } else {
      onHide();
    }
    onToggle();
    this.onPopoverOpenChange();
  };

  eventWrapper = (cb) => {
    return (e) => {
      const { event } = this.getPopoverProps();
      const childrenNode = ReactDOM.findDOMNode(this.refs.children).childNodes[0];
      const el = e.target || e.srcElement;
      if (this.isAbsolute() || (event === 'hover') || this.isEventInsideTarget(el, childrenNode)) {
        cb();
      }
    };
  };

  showPopover = () => this.setIsOpen(true);

  hidePopover = () => this.setIsOpen(false);

  togglePopover = () => this.setIsOpen(!this.isOpen());

  setIsOpen = (isOpen) => {
    if (this.isStateful()) {
      this.setState({ isOpen }, this.onPopoverStateChange);
    } else {
      const { onShow, onHide, onToggle } = this.getPopoverProps();
      const cb = isOpen ? onShow : onHide;
      cb();
      onToggle();
    }
  };

  // LOCALES

  popoverTemplate = (_style) => {
    const { position, anchor, className, content, id, event, style } = this.getPopoverProps();
    const { eventWrapper, hidePopover, isAbsolute } = this;
    const positionClass = `position-${position}`;
    const anchorClass = `anchor-${anchor}`;
    const _className = `popover-content ${positionClass} ${anchorClass} ${className}`;
    const events = !isAbsolute() && event === 'hover' ? { onMouseEnter: eventWrapper(hidePopover) } : undefined;
    return (

        {content}

    );
  };

  getVisiblePopover = () => this.popoverTemplate(this.computePopoverStyle());

  getHiddenPopover = () => {
    const style = { width: '100%', height: '100%', top: 0, left: 0, position: 'absolute', overflow: 'hidden', pointerEvents: 'none' };
    return (

        {this.popoverTemplate({ position: 'absolute', visibility: 'hidden' })}

    );
  };

  isAbsolute = () => this.getPopoverProps().attachToBody === true;

  getEventCallbacks = () => {
    const { event } = this.getPopoverProps();
    const onHover = event === 'hover';
    const onClick = event === 'click';
    const { eventWrapper, showPopover, hidePopover, togglePopover } = this;
    return {
      onMouseEnter: onHover ? eventWrapper(showPopover) : undefined,
      onMouseLeave: onHover ? eventWrapper(hidePopover) : undefined,
      onClick: onClick ? eventWrapper(togglePopover) : undefined
    };
  };

  computePopoverStyle = () => {
    const { child, popover } = this.state;
    const { anchor, position, maxWidth, offsetX, offsetY, distance } = this.getPopoverProps();

    const isAbsolute = this.isAbsolute();
    const isHorizontal = position === 'top' || position === 'bottom';
    const isVertical = position === 'right' || position === 'left';

    const anchorOffset = { top: 0, left: 0 };
    const positionOffset = { top: 0, left: 0 };

    switch (anchor) {
      case 'start':
        // default -> { top: 0, left: 0 }
        break;
      case 'center':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) / 2 : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) / 2 : 0;
        break;
      case 'end':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) : 0;
        break;
    }

    switch (position) {
      case 'top':
        positionOffset.top = -(popover.height + distance);
        break;
      case 'bottom':
        positionOffset.top = child.height + distance;
        break;
      case 'left':
        positionOffset.left = -(popover.width + distance);
        break;
      case 'right':
        positionOffset.left = child.width + distance;
        break;
    }

    return {
      position: 'absolute',
      top: (isAbsolute ? child.y : 0) + (positionOffset.top + anchorOffset.top + offsetY),
      left: (isAbsolute ? child.x : 0) + (positionOffset.left + anchorOffset.left + offsetX),
      maxWidth
    };
  };

  getLocals() {
    const isRelative = !this.isAbsolute();
    const isOpen = this.isOpen();
    const popover = isRelative && (this.initialized && isOpen ? this.getVisiblePopover() : this.getHiddenPopover());
    return {
      ...this.props,
      style: {
        display: 'inline-block',
        position: isRelative ? 'relative' : undefined,
        ...this.props.style
      },
      className: cx('react-popover', this.props.className, { 'is-open': isOpen, 'is-closed': !isOpen }),
      eventCallbacks: this.getEventCallbacks(),
      popover
    };
  }

  // RENDER

  render() {
    const { children, style, className, id, eventCallbacks, popover } = this.getLocals();
    return (

        {children}
        {popover}

    );
  }

}
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import qs from 'query-string'

import extractTimeFiltersFromQuery from 'src/util/nlp-time-filter'
import { remoteFunction } from 'src/util/webextensionRPC'
import { getPauseState } from 'src/activity-logger'
import * as blacklistI from 'src/blacklist'
import { getPageDocId, updateArchiveFlag } from './archive-button'
import Popup from './components/Popup'
import Button from './components/Button'
import HistoryPauser from './components/HistoryPauser'
import LinkButton from './components/LinkButton'
import SplitButton from './components/SplitButton'
import * as constants from './constants'

import { itemBtnBlacklisted } from './components/Button.css'

// Transforms URL checking results to state types
const getBlacklistButtonState = ({ loggable, blacklist }) => {
    if (!blacklist) return constants.BLACKLIST_BTN_STATE.BLACKLISTED
    if (!loggable) return constants.BLACKLIST_BTN_STATE.DISABLED
    return constants.BLACKLIST_BTN_STATE.UNLISTED
}

class PopupContainer extends Component {
    constructor(props) {
        super(props)

        this.state = {
            url: '',
            searchValue: '',
            pauseValue: 20,
            currentTabPageDocId: '',
            blacklistBtn: constants.BLACKLIST_BTN_STATE.DISABLED,
            isPaused: false,
            archiveBtnDisabled: true,
            blacklistChoice: false,
        }

        this.toggleLoggingPause = remoteFunction('toggleLoggingPause')

        this.onArchiveBtnClick = this.onArchiveBtnClick.bind(this)
        this.onSearchChange = this.onSearchChange.bind(this)
        this.onPauseChange = this.onPauseChange.bind(this)
        this.onSearchEnter = this.onSearchEnter.bind(this)
        this.onPauseConfirm = this.onPauseConfirm.bind(this)
    }

    async componentDidMount() {
        const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })

        // If we can't get the tab data, then can't init action button states
        if (!currentTab || !currentTab.url) { return }

        this.blacklistConfirm = remoteFunction('quickBlacklistConfirm', { tabId: currentTab.id })

        const updateState = newState => this.setState(oldState => ({ ...oldState, ...newState }))
        const noop = f => f // Don't do anything if error; state doesn't change

        updateState({ url: currentTab.url })
        this.getInitPauseState().then(updateState).catch(noop)
        this.getInitBlacklistBtnState(currentTab.url).then(updateState).catch(noop)
        this.getInitArchiveBtnState(currentTab.url).then(updateState).catch(noop)
    }

    async getInitPauseState() {
        return { isPaused: await getPauseState() }
    }

    async getInitArchiveBtnState(url) {
        const currentTabPageDocId = await getPageDocId(url)
        return {
            currentTabPageDocId,
            archiveBtnDisabled: false,
        }
    }

    async getInitBlacklistBtnState(url) {
        const blacklist = await blacklistI.fetchBlacklist()

        const result = {
            loggable: blacklistI.isLoggable(url),
            blacklist: !blacklistI.isURLBlacklisted(url, blacklist),
        }

        return { blacklistBtn: getBlacklistButtonState(result) }
    }

    onBlacklistBtnClick(domain = false) {
        const url = domain ? new URL(this.state.url).hostname : this.state.url

        return event => {
            event.preventDefault()
            blacklistI.addToBlacklist(url)
            this.blacklistConfirm(url)
            window.close()
        }
    }

    onPauseConfirm(event) {
        event.preventDefault()
        const { isPaused, pauseValue } = this.state

        // Tell background script to do on extension level
        this.toggleLoggingPause(pauseValue)

        // Do local level state toggle and reset
        this.setState(state => ({ ...state, isPaused: !isPaused, pauseValue: 20 }))
    }

    onPauseChange(event) {
        const pauseValue = event.target.value
        this.setState(state => ({ ...state, pauseValue }))
    }

    async onArchiveBtnClick(event) {
        event.preventDefault()

        try {
            await updateArchiveFlag(this.state.currentTabPageDocId)
        } catch (error) {
            // Can't do it for whatever reason
        } finally {
            window.close()
        }
    }

    onSearchChange(event) {
        const searchValue = event.target.value
        this.setState(state => ({ ...state, searchValue }))
    }

    onSearchEnter(event) {
        if (event.key === 'Enter') {
            event.preventDefault()

            const { extractedQuery, startDate, endDate } = extractTimeFiltersFromQuery(this.state.searchValue)
            const queryParams = qs.stringify({ query: extractedQuery, startDate, endDate })

            browser.tabs.create({ url: `${constants.OVERVIEW_URL}?${queryParams}` }) // New tab with query
            window.close() // Close the popup
        }
    }

    renderBlacklistButton() {
        const { blacklistChoice, blacklistBtn } = this.state
        const setBlacklistChoice = () => this.setState(state => ({ ...state, blacklistChoice: true }))

        if (!blacklistChoice) { // Standard blacklist button
            return blacklistBtn === constants.BLACKLIST_BTN_STATE.BLACKLISTED ? (
                <LinkButton href={`${constants.OPTIONS_URL}#/settings`} icon='block' btnClass={itemBtnBlacklisted}>
                    Current Page Blacklisted
                </LinkButton>
            ) : (
                <Button icon='block' onClick={setBlacklistChoice} disabled={blacklistBtn === constants.BLACKLIST_BTN_STATE.DISABLED}>
                    Blacklist Current Page
                </Button>
            )
        }

        // Domain vs URL choice button
        return (
            <SplitButton icon='block'>
                <Button onClick={this.onBlacklistBtnClick(true)}>Domain</Button>
                <Button onClick={this.onBlacklistBtnClick(false)}>URL</Button>
            </SplitButton>
        )
    }

    renderPauseChoices() {
        const pauseValueToOption = (val, i) => <option key={i} value={val}>{val === Infinity ? '∞' : val}</option>

        return this.props.pauseValues.map(pauseValueToOption)
    }

    render() {
        const { searchValue, archiveBtnDisabled, pauseValue, isPaused } = this.state

        return (
            <Popup searchValue={searchValue} onSearchChange={this.onSearchChange} onSearchEnter={this.onSearchEnter}>
                <HistoryPauser
                    onConfirm={this.onPauseConfirm}
                    onChange={this.onPauseChange}
                    value={pauseValue}
                    isPaused={isPaused}
                >
                    {this.renderPauseChoices()}
                </HistoryPauser>
                {this.renderBlacklistButton()}
                <Button icon='archive' onClick={this.onArchiveBtnClick} disabled={archiveBtnDisabled}>
                    Archive Current Page
                </Button>

                <LinkButton href={`${constants.OPTIONS_URL}#/settings`} icon='settings'>
                    Settings
                </LinkButton>
                <LinkButton href={`${constants.OPTIONS_URL}#/import`} icon='file_download'>
                    Import History & Bookmarks
                </LinkButton>
                <LinkButton href={constants.FEEDBACK_URL} icon='feedback'>
                    Feedback
                </LinkButton>
            </Popup>
        )
    }
}

PopupContainer.propTypes = { pauseValues: PropTypes.arrayOf(PropTypes.number).isRequired }
PopupContainer.defaultProps = { pauseValues: [5, 10, 20, 30, 60, 120, 180, Infinity] }

export default PopupContainer
<commit_msg>Write container state + logic for new blacklist confirm<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import qs from 'query-string'

import extractTimeFiltersFromQuery from 'src/util/nlp-time-filter'
import { remoteFunction } from 'src/util/webextensionRPC'
import { getPauseState } from 'src/activity-logger'
import * as blacklistI from 'src/blacklist'
import { getPageDocId, updateArchiveFlag } from './archive-button'
import Popup from './components/Popup'
import Button from './components/Button'
import BlacklistConfirm from './components/BlacklistConfirm'
import HistoryPauser from './components/HistoryPauser'
import LinkButton from './components/LinkButton'
import SplitButton from './components/SplitButton'
import * as constants from './constants'

import { itemBtnBlacklisted } from './components/Button.css'

// Transforms URL checking results to state types
const getBlacklistButtonState = ({ loggable, blacklist }) => {
    if (!blacklist) return constants.BLACKLIST_BTN_STATE.BLACKLISTED
    if (!loggable) return constants.BLACKLIST_BTN_STATE.DISABLED
    return constants.BLACKLIST_BTN_STATE.UNLISTED
}

class PopupContainer extends Component {
    constructor(props) {
        super(props)

        this.state = {
            url: '',
            searchValue: '',
            pauseValue: 20,
            currentTabPageDocId: '',
            blacklistBtn: constants.BLACKLIST_BTN_STATE.DISABLED,
            isPaused: false,
            archiveBtnDisabled: true,
            blacklistChoice: false,
            blacklistConfirm: false,
        }

        this.toggleLoggingPause = remoteFunction('toggleLoggingPause')
        this.cleanupBlacklist = remoteFunction('cleanupBlacklist')

        this.onArchiveBtnClick = this.onArchiveBtnClick.bind(this)
        this.onSearchChange = this.onSearchChange.bind(this)
        this.onPauseChange = this.onPauseChange.bind(this)
        this.onSearchEnter = this.onSearchEnter.bind(this)
        this.onPauseConfirm = this.onPauseConfirm.bind(this)
    }

    async componentDidMount() {
        const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })

        // If we can't get the tab data, then can't init action button states
        if (!currentTab || !currentTab.url) { return }

        const updateState = newState => this.setState(oldState => ({ ...oldState, ...newState }))
        const noop = f => f // Don't do anything if error; state doesn't change

        updateState({ url: currentTab.url })
        this.getInitPauseState().then(updateState).catch(noop)
        this.getInitBlacklistBtnState(currentTab.url).then(updateState).catch(noop)
        this.getInitArchiveBtnState(currentTab.url).then(updateState).catch(noop)
    }

    async getInitPauseState() {
        return { isPaused: await getPauseState() }
    }

    async getInitArchiveBtnState(url) {
        const currentTabPageDocId = await getPageDocId(url)
        return {
            currentTabPageDocId,
            archiveBtnDisabled: false,
        }
    }

    async getInitBlacklistBtnState(url) {
        const blacklist = await blacklistI.fetchBlacklist()

        const result = {
            loggable: blacklistI.isLoggable(url),
            blacklist: !blacklistI.isURLBlacklisted(url, blacklist),
        }

        return { blacklistBtn: getBlacklistButtonState(result) }
    }

    onBlacklistBtnClick(domain = false) {
        const url = domain ? new URL(this.state.url).hostname : this.state.url

        return event => {
            event.preventDefault()
            blacklistI.addToBlacklist(url)
            this.setState(state => ({
                ...state,
                blacklistChoice: false,
                blacklistConfirm: true,
                blacklistBtn: constants.BLACKLIST_BTN_STATE.BLACKLISTED,
                url,
            }))
        }
    }

    onPauseConfirm(event) {
        event.preventDefault()
        const { isPaused, pauseValue } = this.state

        // Tell background script to do on extension level
        this.toggleLoggingPause(pauseValue)

        // Do local level state toggle and reset
        this.setState(state => ({ ...state, isPaused: !isPaused, pauseValue: 20 }))
    }

    onPauseChange(event) {
        const pauseValue = event.target.value
        this.setState(state => ({ ...state, pauseValue }))
    }

    async onArchiveBtnClick(event) {
        event.preventDefault()

        try {
            await updateArchiveFlag(this.state.currentTabPageDocId)
        } catch (error) {
            // Can't do it for whatever reason
        } finally {
            window.close()
        }
    }

    onSearchChange(event) {
        const searchValue = event.target.value
        this.setState(state => ({ ...state, searchValue }))
    }

    onSearchEnter(event) {
        if (event.key === 'Enter') {
            event.preventDefault()

            const { extractedQuery, startDate, endDate } = extractTimeFiltersFromQuery(this.state.searchValue)
            const queryParams = qs.stringify({ query: extractedQuery, startDate, endDate })

            browser.tabs.create({ url: `${constants.OVERVIEW_URL}?${queryParams}` }) // New tab with query
            window.close() // Close the popup
        }
    }

    // Hides full-popup confirm
    resetBlacklistConfirmState = () => this.setState(state => ({ ...state, blacklistConfirm: false }))

    handleDeleteBlacklistData = () => {
        this.cleanupBlacklist(this.state.url)
        this.resetBlacklistConfirmState()
    }

    renderBlacklistButton() {
        const { blacklistChoice, blacklistBtn } = this.state
        const setBlacklistChoice = () => this.setState(state => ({ ...state, blacklistChoice: true }))

        if (!blacklistChoice) { // Standard blacklist button
            return blacklistBtn === constants.BLACKLIST_BTN_STATE.BLACKLISTED ? (
                <LinkButton href={`${constants.OPTIONS_URL}#/settings`} icon='block' btnClass={itemBtnBlacklisted}>
                    Current Page Blacklisted
                </LinkButton>
            ) : (
                <Button icon='block' onClick={setBlacklistChoice} disabled={blacklistBtn === constants.BLACKLIST_BTN_STATE.DISABLED}>
                    Blacklist Current Page
                </Button>
            )
        }

        // Domain vs URL choice button
        return (
            <SplitButton icon='block'>
                <Button onClick={this.onBlacklistBtnClick(true)}>Domain</Button>
                <Button onClick={this.onBlacklistBtnClick(false)}>URL</Button>
            </SplitButton>
        )
    }

    renderPauseChoices() {
        const pauseValueToOption = (val, i) => <option key={i} value={val}>{val === Infinity ? '∞' : val}</option>

        return this.props.pauseValues.map(pauseValueToOption)
    }

    renderChildren() {
        const { blacklistConfirm, archiveBtnDisabled, pauseValue, isPaused } = this.state

        if (blacklistConfirm) {
            return (
                <BlacklistConfirm
                    onConfirmClick={this.handleDeleteBlacklistData}
                    onDenyClick={this.resetBlacklistConfirmState}
                />
            )
        }

        return (

                <HistoryPauser
                    onConfirm={this.onPauseConfirm}
                    onChange={this.onPauseChange}
                    value={pauseValue}
                    isPaused={isPaused}
                >
                    {this.renderPauseChoices()}
                </HistoryPauser>
                {this.renderBlacklistButton()}
                <Button icon='archive' onClick={this.onArchiveBtnClick} disabled={archiveBtnDisabled}>
                    Archive Current Page
                </Button>

                <LinkButton href={`${constants.OPTIONS_URL}#/settings`} icon='settings'>
                    Settings
                </LinkButton>
                <LinkButton href={`${constants.OPTIONS_URL}#/import`} icon='file_download'>
                    Import History & Bookmarks
                </LinkButton>
                <LinkButton href={constants.FEEDBACK_URL} icon='feedback'>
                    Feedback
                </LinkButton>

        )
    }

    render() {
        const { searchValue } = this.state

        return (
            <Popup searchValue={searchValue} onSearchChange={this.onSearchChange} onSearchEnter={this.onSearchEnter}>
                {this.renderChildren()}
            </Popup>
        )
    }
}

PopupContainer.propTypes = { pauseValues: PropTypes.arrayOf(PropTypes.number).isRequired }
PopupContainer.defaultProps = { pauseValues: [5, 10, 20, 30, 60, 120, 180, Infinity] }

export default PopupContainer
<|endoftext|>


<commit_before>/*
 * Total cost of the products in the cart
 */
var totalCost = 0;

/*
 * Number of distinct drinks -identified by beer_id- present in the cart
 */
var numberDistinctDrinks = 0;

/*
 * Products in the shopping cart
 * Each product is an object with beer_id, name, price and quantity properties
 */
var productsData = [];

/**
 * Function which adds a product to the shoppingcart table and adjusts the total
 * @param id The beer_id of the beverage
 * @param name The name of the beverage
 * @param price The price of the beverage
 */
function addProduct(id, name, price) {
    'use strict';
    /**
     * Function which forms an array of products which have been placed in the shoppingcart.
     * @return An array with product objects as elements
     */
    function updateProductsArray() {
        var updatedProductsArray = productsData;
        /**
         * Loops through the total amount of separate products and checks if a certain
         * product already is added to the products array. If so the quantity of the
         * product is just incremented by 1.
         */
        // @ to-do: check id instead of name
        for (var i = 0; i < numberDistinctDrinks; i++) {
            var row = productsData[i];
            if (row.name == name) {
                row.quantity += 1;
                return updatedProductsArray;
            }
        }
        numberDistinctDrinks += 1;
        /**
         * Adding a new product into the array of products
         */
        productsData.push({
            beer_id: id,
            name: name,
            quantity: 1,
            price: price
        });
        return updatedProductsArray;
    }

    //Add the product to the list and modify the table
    var updatedProductsArray = updateProductsArray();
    writeIntoTable(updatedProductsArray);
    totalCost += price;
    $('#total').html('Total: ' + totalCost + ' kr');
}

/**
 * Function which handles writing into shoppingcart.
 * @param an array of product objects
 */
function writeIntoTable(productsArray) {
    'use strict';
    var tbody = $('#tableBody');
    $('#tableBody').empty();
    for (var i = 0; i < productsArray.length; i++) {
        var tr = $('
').appendTo(tbody);
        var currentProduct = productsArray[i];
        // Loops through all the own properties of the object
        for (var property in currentProduct) {
            if (currentProduct.hasOwnProperty(property) && property != 'beer_id') {
                tr.append('' + currentProduct[property] + '	');
            }
        }
    }
}

/**
 * Purchase the drinks in the shopping cart
 */
function purchaseTable() {
    'use strict';
    var i = 0, //Loop index
        j, //Loop index
        singleProductsArray = [], //Transformed productData in a list of beer_id representing each single beverage
        product, //Product object
        purchased = 0;

    //Transform productData into a list of beer_id for each drink
    for (; i < productsData.length; i++) {
        //For each distinct beverage
        product = productsData[i];
        for (j = 0; j < product.quantity; j++) {
            singleProductsArray.push(product.beer_id);
        }
    }

    //Send the purchase requests to the api
    for (i = 0; i < singleProductsArray.length; i++) {
        user.appendPurchases(product.beer_id, function(answer) {
            if (answer.type === "error") {
                alert("Apparently, something went wrong. Please try again.");
                console.log(answer); //Debug in case of error
            }

            purchased += 1;
            //Update display and machine content when all purchases have been done
            if (purchased === singleProductsArray.length) {
                //Update user debt
                updateBalanceDisplay();

                //Update machine content and slot displays for each distinct beverage
                for (i = 0; i < productsData.length; i++) {
                    updateMachineContentQuantities(productsData[i].beer_id, productsData[i].quantity);
                }
            }
        });
    }
}

/**
 * Clear the table
 */
function clearTable() {
    'use strict';
    productsData = [];
    numberDistinctDrinks = 0;
    totalCost = 0;
    writeIntoTable([]);
    $('#total').html('Total: ' + totalCost + ' kr');
}

/**
 * Wire the Clear Table and Purchase button
 */
$(function() {
    'use strict';
    $('#clearAllCart').click(function() {
        clearTable();
    });
    $('#purchaseCart').click(function() {
        console.log("ADD PURCHASE");
        purchaseTable();
        clearTable();
    });
});<commit_msg>Solving issue with display of cart total<commit_after>/*
 * Total cost of the products in the cart
 */
var totalCost = 0;

/*
 * Number of distinct drinks -identified by beer_id- present in the cart
 */
var numberDistinctDrinks = 0;

/*
 * Products in the shopping cart
 * Each product is an object with beer_id, name, price and quantity properties
 */
var productsData = [];

/**
 * Function which adds a product to the shoppingcart table and adjusts the total
 * @param id The beer_id of the beverage
 * @param name The name of the beverage
 * @param price The price of the beverage
 */
function addProduct(id, name, price) {
    'use strict';
    /**
     * Function which forms an array of products which have been placed in the shoppingcart.
     * @return An array with product objects as elements
     */
    function updateProductsArray() {
        var updatedProductsArray = productsData;
        /**
         * Loops through the total amount of separate products and checks if a certain
         * product already is added to the products array. If so the quantity of the
         * product is just incremented by 1.
         */
        // @ to-do: check id instead of name
        for (var i = 0; i < numberDistinctDrinks; i++) {
            var row = productsData[i];
            if (row.name == name) {
                row.quantity += 1;
                return updatedProductsArray;
            }
        }
        numberDistinctDrinks += 1;
        /**
         * Adding a new product into the array of products
         */
        productsData.push({
            beer_id: id,
            name: name,
            quantity: 1,
            price: price
        });
        return updatedProductsArray;
    }

    //Add the product to the list and modify the table
    var updatedProductsArray = updateProductsArray();
    writeIntoTable(updatedProductsArray);
    totalCost += price;
    displayTotal();
}

/**
 * Function which handles writing into shoppingcart.
 * @param an array of product objects
 */
function writeIntoTable(productsArray) {
    'use strict';
    var tbody = $('#tableBody');
    $('#tableBody').empty();
    for (var i = 0; i < productsArray.length; i++) {
        var tr = $('
').appendTo(tbody);
        var currentProduct = productsArray[i];
        // Loops through all the own properties of the object
        for (var property in currentProduct) {
            if (currentProduct.hasOwnProperty(property) && property != 'beer_id') {
                tr.append('' + currentProduct[property] + '	');
            }
        }
    }
}

/**
 * Display the total cost with max one decimal
 */
function displayTotal() {
    totalCost = +totalCost.toFixed(1);
    $('#total').html('Total: ' + totalCost + ' kr');
}

/**
 * Purchase the drinks in the shopping cart
 */
function purchaseTable() {
    'use strict';
    var i = 0, //Loop index
        j, //Loop index
        singleProductsArray = [], //Transformed productData in a list of beer_id representing each single beverage
        product, //Product object
        purchased = 0;

    //Transform productData into a list of beer_id for each drink
    for (; i < productsData.length; i++) {
        //For each distinct beverage
        product = productsData[i];
        for (j = 0; j < product.quantity; j++) {
            singleProductsArray.push(product.beer_id);
        }
    }

    //Send the purchase requests to the api
    for (i = 0; i < singleProductsArray.length; i++) {
        user.appendPurchases(product.beer_id, function(answer) {
            if (answer.type === "error") {
                alert("Apparently, something went wrong. Please try again.");
                console.log(answer); //Debug in case of error
            }

            purchased += 1;
            //Update display and machine content when all purchases have been done
            if (purchased === singleProductsArray.length) {
                //Update user debt
                updateBalanceDisplay();

                //Update machine content and slot displays for each distinct beverage
                for (i = 0; i < productsData.length; i++) {
                    updateMachineContentQuantities(productsData[i].beer_id, productsData[i].quantity);
                }
            }
        });
    }
}

/**
 * Clear the table
 */
function clearTable() {
    'use strict';
    productsData = [];
    numberDistinctDrinks = 0;
    totalCost = 0;
    writeIntoTable([]);
    displayTotal();
}

/**
 * Wire the Clear Table and Purchase button
 */
$(function() {
    'use strict';
    $('#clearAllCart').click(function() {
        clearTable();
    });
    $('#purchaseCart').click(function() {
        purchaseTable();
        clearTable();
    });
});<|endoftext|>


<commit_before>import gql from './gql';

function createPostbackAction(label, input, issuedAt) {
  return {
    type: 'postback',
    label,
    data: JSON.stringify({
      input, issuedAt,
    }),
  };
}

// State diagram:
// http://bit.ly/2kZY6kL
//
export default async function processMessages(
  { state = '__INIT__', data = {} },
  event,
  issuedAt, // When this request is issued. Will be written in postback replies.
  userId,
) {
  let replies;

  if (event.input.length >= 3) {
    // If input contains more than 3 words,
    // consider it as a new query and start over.
    data = {};
    state = '__INIT__';
  }

  // Sets state, data and replies
  //
  switch (state) {
    case '__INIT__': {
      // Store user input into context
      data.searchedText = event.input;

      // Search for articles
      const { data: { SearchArticles } } = await gql`query($text: String!) {
        SearchArticles(text: $text, orderBy: [{_score: DESC}]) {
          edges {
            node {
              text
              id
            }
          }
        }
      }`({
        text: event.input,
      });

      const articleSummary = `${event.input.slice(0, 10)}${event.input.length > 10 ? '⋯⋯' : ''}`;

      if (SearchArticles.edges.length) {
        const templateMessage = {
          type: 'template',
          altText: SearchArticles.edges.map(
            ({ node: { text } }, idx) => `選擇請打 ${idx + 1}> ${text.slice(0, 20)}`,
          ).concat(['若以上皆非，請打 0。']).join('\n\n'),
          template: {
            type: 'carousel',
            columns: SearchArticles.edges.map(({ node: { text } }, idx) => ({
              text: text.slice(0, 119),
              actions: [
                createPostbackAction('選擇此則', idx + 1, issuedAt),
              ],
            })).concat([{
              text: '這裡沒有一篇是我傳的訊息。',
              actions: [
                createPostbackAction('選擇', 0, issuedAt),
              ],
            }]),
          },
        };

        // Store article ids
        data.foundArticleIds = SearchArticles.edges.map(({ node: { id } }) => id);

        replies = [
          {
            type: 'text',
            text: `幫您查詢「${articleSummary}」的相關回應。`,
          },
          {
            type: 'text',
            text: '請問下列文章中，哪一篇是您剛才傳送的訊息呢？',
          },
          templateMessage,
        ];
        state = 'CHOOSING_ARTICLE';
      } else {
        replies = [{
          type: 'text',
          text: `找不到關於「${articleSummary}」文章耶 QQ`,
        }, {
          type: 'template',
          altText: '請問要將這份文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
          template: {
            type: 'buttons',
            text: '請問要將這份文章送出到資料庫嗎？',
            actions: [
              createPostbackAction('是', 'y', issuedAt),
              createPostbackAction('否', 'n', issuedAt),
            ],
          },
        }];
        state = 'ASKING_ARTICLE_SUBMISSION';
      }

      break;
    }
    case 'CHOOSING_ARTICLE': {
      if (!data.foundArticleIds) {
        throw new Error('foundArticleIds not set in data');
      }

      const selectedArticleId = data.foundArticleIds[event.input - 1];

      if (+event.input === 0) {
        replies = [{
          type: 'template',
          altText: '請問要將文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
          template: {
            type: 'buttons',
            text: '請問要將文章送出到資料庫嗎？',
            actions: [
              createPostbackAction('是', 'y', issuedAt),
              createPostbackAction('否', 'n', issuedAt),
            ],
          },
        }];

        state = 'ASKING_ARTICLE_SUBMISSION';
      } else if (!selectedArticleId) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundArticleIds.length} 的數字。` },
        ];

        state = 'CHOOSING_ARTICLE';
      } else {
        const { data: { GetArticle } } = await gql`query($id: String!) {
          GetArticle(id: $id) {
            replyCount
            replyConnections {
              id
              reply {
                id
                versions(limit: 1) {
                  type
                  text
                }
              }
            }
          }
        }`({
          id: selectedArticleId,
        });

        const { rumorReplies, notRumorReplies } = GetArticle.replyConnections.reduce(
          (result, { reply, id }) => {
            if (reply.versions[0].type === 'RUMOR') {
              result.rumorReplies.push({ ...reply, replyConnectionId: id });
            } else if (reply.versions[0].type === 'NOT_RUMOR') {
              result.notRumorReplies.push({ ...reply, replyConnectionId: id });
            }
            return result;
          },
          { rumorReplies: [], notRumorReplies: [] },
        );

        replies = [];
        if (notRumorReplies.length) {
          replies.push({ type: 'text', text: `這篇文章有 ${notRumorReplies.length} 個回應表示查無不實：` });
          replies.push({
            type: 'template',
            altText: notRumorReplies.map(
              ({ versions }, idx) => `閱讀請傳 ${idx + 1}> ${versions[0].text.slice(0, 20)}`,
            ).join('\n\n'),
            template: {
              type: 'carousel',
              columns: notRumorReplies.map(({ versions }, idx) => ({
                text: versions[0].text.slice(0, 119),
                actions: [
                  createPostbackAction('閱讀此回應', idx + 1, issuedAt),
                ],
              })),
            },
          });
        }
        if (rumorReplies.length) {
          replies.push({ type: 'text', text: `這篇文章有 ${rumorReplies.length} 個回應覺得有不實之處：` });
          replies.push({
            type: 'template',
            altText: rumorReplies.map(
              ({ versions }, idx) => `閱讀請傳 ${notRumorReplies.length + idx + 1}> ${versions[0].text.slice(0, 20)}`,
            ).join('\n\n'),
            template: {
              type: 'carousel',
              columns: rumorReplies.map(({ versions }, idx) => ({
                text: versions[0].text.slice(0, 119),
                actions: [
                  createPostbackAction('閱讀此回應', notRumorReplies.length + idx + 1, issuedAt),
                ],
              })),
            },
          });
        }

        if (GetArticle.replyCount === 0) {
          // No one has replied to this yet.
          //

          const { data: { CreateReplyRequest }, errors } = await gql`mutation($id: String!) {
            CreateReplyRequest(articleId: $id) {
              replyRequestCount
            }
          }`({ id: selectedArticleId }, { userId });

          replies = [
            { type: 'text', text: `目前還沒有人回應這篇文章唷。${errors ? '' : `已經將您的需求記錄下來了，共有 ${CreateReplyRequest.replyRequestCount} 人跟您一樣渴望看到針對這篇文章的回應。`}` },
            { type: 'text', text: `若有最新回應，會寫在這個地方：http://rumors.hacktabl.org/article/${selectedArticleId}` },
          ];

          state = '__INIT__';
        } else if (replies.length === 0) {
          // Someone reported that it is not an article,
          // and there are no other replies available.
          //
          replies = [
            { type: 'text', text: '有人認為您傳送的這則訊息並不是完整的文章內容，或認為「真的假的」不應該處理這則訊息。' },
            { type: 'text', text: `詳情請見：http://rumors.hacktabl.org/article/${selectedArticleId}` },
          ];
          state = '__INIT__';
        } else {
          data.foundReplies = notRumorReplies
            .map(({ replyConnectionId, id }) => ({ id, replyConnectionId }))
            .concat(
              rumorReplies.map(({ replyConnectionId, id }) => ({ id, replyConnectionId })),
            );
          state = 'CHOOSING_REPLY';
        }
      }

      break;
    }
    case 'CHOOSING_REPLY': {
      if (!data.foundReplies) {
        throw new Error('foundReplies not set in data');
      }

      const selectedReply = data.foundReplies[event.input - 1];

      if (!selectedReply) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundReplies.length} 的數字。` },
        ];

        state = 'CHOOSING_REPLY';
      } else {
        const { data: { GetReply } } = await gql`query($id: String!) {
          GetReply(id: $id) {
            versions(limit: 1) {
              type
              text
              reference
              createdAt
            }
          }
        }`({ id: selectedReply.id });

        replies = [
          { type: 'text', text: `這則回應認為文章${GetReply.versions[0].type === 'RUMOR' ? '含有不實訊息' : '不含不實訊息'}，理由為：` },
          { type: 'text', text: GetReply.versions[0].text },
          { type: 'text', text: `出處：${GetReply.versions[0].reference}` },
          {
            type: 'template',
            altText: '請問這則回應是否有解答原文章？\n「是」請輸入「y」，「否」請輸入其他任何訊息。',
            template: {
              type: 'buttons',
              text: '請問這則回應是否有解答原文章？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];

        data.selectedReply = selectedReply;
        state = 'ASKING_REPLY_FEEDBACK';
      }

      break;
    }
    case 'ASKING_REPLY_FEEDBACK': {
      if (!data.selectedReply) {
        throw new Error('selectedReply not set in data');
      }

      const { data: { action: { feedbackCount } } } = await gql`mutation($vote: FeedbackVote!, $id: String!){
        action: CreateOrUpdateReplyConnectionFeedback(
          vote: $vote
          replyConnectionId: $id
        ) {
          feedbackCount
        }
      }`({
        id: data.selectedReply.replyConnectionId,
        vote: event.input === 'y' ? 'UPVOTE' : 'DOWNVOTE',
      }, { userId });

      replies = [
        { type: 'text',
          text: feedbackCount > 1 ?
          `感謝您與其他 ${feedbackCount - 1} 人的回饋。` :
          '感謝您的回饋，您是第一個評論這份文章與回應的人 :)' },
      ];

      state = '__INIT__';
      break;
    }
    case 'ASKING_ARTICLE_SUBMISSION': {
      if (!data.searchedText) {
        throw new Error('searchText not set in data');
      }

      if (event.input === 'y') {
        const { data: { CreateArticle } } = await gql`mutation($text: String!){
          CreateArticle(text: $text, reference: {type: LINE}) {
            id
          }
        }`({ text: data.searchedText }, { userId });

        replies = [
          { type: 'text', text: `您回報的文章已經被收錄至：http://rumors.hacktabl.org/article/${CreateArticle.id}` },
          { type: 'text', text: '感謝您的回報！' },
        ];
      } else {
        replies = [
          { type: 'text', text: '感謝您的使用。' },
        ];
      }
      state = '__INIT__';

      break;
    }

    default:
      replies = [{
        type: 'text',
        text: '我們看不懂 QQ\n大俠請重新來過。',
      }];
      state = '__INIT__';
  }

  return {
    context: { state, data },
    replies,
  };
}
<commit_msg>Fix the bug that too many article returned will break carousel<commit_after>import gql from './gql';

function createPostbackAction(label, input, issuedAt) {
  return {
    type: 'postback',
    label,
    data: JSON.stringify({
      input, issuedAt,
    }),
  };
}

// State diagram:
// http://bit.ly/2kZY6kL
//
export default async function processMessages(
  { state = '__INIT__', data = {} },
  event,
  issuedAt, // When this request is issued. Will be written in postback replies.
  userId,
) {
  let replies;

  if (event.input.length >= 3) {
    // If input contains more than 3 words,
    // consider it as a new query and start over.
    data = {};
    state = '__INIT__';
  }

  // Sets state, data and replies
  //
  switch (state) {
    case '__INIT__': {
      // Store user input into context
      data.searchedText = event.input;

      // Search for articles
      const { data: { SearchArticles } } = await gql`query($text: String!) {
        SearchArticles(text: $text, orderBy: [{_score: DESC}], first: 4) {
          edges {
            node {
              text
              id
            }
          }
        }
      }`({
        text: event.input,
      });

      const articleSummary = `${event.input.slice(0, 10)}${event.input.length > 10 ? '⋯⋯' : ''}`;

      if (SearchArticles.edges.length) {
        const templateMessage = {
          type: 'template',
          altText: SearchArticles.edges.map(
            ({ node: { text } }, idx) => `選擇請打 ${idx + 1}> ${text.slice(0, 20)}`,
          ).concat(['若以上皆非，請打 0。']).join('\n\n'),
          template: {
            type: 'carousel',
            columns: SearchArticles.edges.map(({ node: { text } }, idx) => ({
              text: text.slice(0, 119),
              actions: [
                createPostbackAction('選擇此則', idx + 1, issuedAt),
              ],
            })).concat([{
              text: '這裡沒有一篇是我傳的訊息。',
              actions: [
                createPostbackAction('選擇', 0, issuedAt),
              ],
            }]),
          },
        };

        // Store article ids
        data.foundArticleIds = SearchArticles.edges.map(({ node: { id } }) => id);

        replies = [
          {
            type: 'text',
            text: `幫您查詢「${articleSummary}」的相關回應。`,
          },
          {
            type: 'text',
            text: '請問下列文章中，哪一篇是您剛才傳送的訊息呢？',
          },
          templateMessage,
        ];
        state = 'CHOOSING_ARTICLE';
      } else {
        replies = [{
          type: 'text',
          text: `找不到關於「${articleSummary}」文章耶 QQ`,
        }, {
          type: 'template',
          altText: '請問要將這份文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
          template: {
            type: 'buttons',
            text: '請問要將這份文章送出到資料庫嗎？',
            actions: [
              createPostbackAction('是', 'y', issuedAt),
              createPostbackAction('否', 'n', issuedAt),
            ],
          },
        }];
        state = 'ASKING_ARTICLE_SUBMISSION';
      }

      break;
    }
    case 'CHOOSING_ARTICLE': {
      if (!data.foundArticleIds) {
        throw new Error('foundArticleIds not set in data');
      }

      const selectedArticleId = data.foundArticleIds[event.input - 1];

      if (+event.input === 0) {
        replies = [{
          type: 'template',
          altText: '請問要將文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
          template: {
            type: 'buttons',
            text: '請問要將文章送出到資料庫嗎？',
            actions: [
              createPostbackAction('是', 'y', issuedAt),
              createPostbackAction('否', 'n', issuedAt),
            ],
          },
        }];

        state = 'ASKING_ARTICLE_SUBMISSION';
      } else if (!selectedArticleId) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundArticleIds.length} 的數字。` },
        ];

        state = 'CHOOSING_ARTICLE';
      } else {
        const { data: { GetArticle } } = await gql`query($id: String!) {
          GetArticle(id: $id) {
            replyCount
            replyConnections {
              id
              reply {
                id
                versions(limit: 1) {
                  type
                  text
                }
              }
            }
          }
        }`({
          id: selectedArticleId,
        });

        const { rumorReplies, notRumorReplies } = GetArticle.replyConnections.reduce(
          (result, { reply, id }) => {
            if (reply.versions[0].type === 'RUMOR') {
              result.rumorReplies.push({ ...reply, replyConnectionId: id });
            } else if (reply.versions[0].type === 'NOT_RUMOR') {
              result.notRumorReplies.push({ ...reply, replyConnectionId: id });
            }
            return result;
          },
          { rumorReplies: [], notRumorReplies: [] },
        );

        replies = [];
        if (notRumorReplies.length) {
          replies.push({ type: 'text', text: `這篇文章有 ${notRumorReplies.length} 個回應表示查無不實：` });
          replies.push({
            type: 'template',
            altText: notRumorReplies.map(
              ({ versions }, idx) => `閱讀請傳 ${idx + 1}> ${versions[0].text.slice(0, 20)}`,
            ).join('\n\n'),
            template: {
              type: 'carousel',
              columns: notRumorReplies.map(({ versions }, idx) => ({
                text: versions[0].text.slice(0, 119),
                actions: [
                  createPostbackAction('閱讀此回應', idx + 1, issuedAt),
                ],
              })),
            },
          });
        }
        if (rumorReplies.length) {
          replies.push({ type: 'text', text: `這篇文章有 ${rumorReplies.length} 個回應覺得有不實之處：` });
          replies.push({
            type: 'template',
            altText: rumorReplies.map(
              ({ versions }, idx) => `閱讀請傳 ${notRumorReplies.length + idx + 1}> ${versions[0].text.slice(0, 20)}`,
            ).join('\n\n'),
            template: {
              type: 'carousel',
              columns: rumorReplies.map(({ versions }, idx) => ({
                text: versions[0].text.slice(0, 119),
                actions: [
                  createPostbackAction('閱讀此回應', notRumorReplies.length + idx + 1, issuedAt),
                ],
              })),
            },
          });
        }

        if (GetArticle.replyCount === 0) {
          // No one has replied to this yet.
          //

          const { data: { CreateReplyRequest }, errors } = await gql`mutation($id: String!) {
            CreateReplyRequest(articleId: $id) {
              replyRequestCount
            }
          }`({ id: selectedArticleId }, { userId });

          replies = [
            { type: 'text', text: `目前還沒有人回應這篇文章唷。${errors ? '' : `已經將您的需求記錄下來了，共有 ${CreateReplyRequest.replyRequestCount} 人跟您一樣渴望看到針對這篇文章的回應。`}` },
            { type: 'text', text: `若有最新回應，會寫在這個地方：http://rumors.hacktabl.org/article/${selectedArticleId}` },
          ];

          state = '__INIT__';
        } else if (replies.length === 0) {
          // Someone reported that it is not an article,
          // and there are no other replies available.
          //
          replies = [
            { type: 'text', text: '有人認為您傳送的這則訊息並不是完整的文章內容，或認為「真的假的」不應該處理這則訊息。' },
            { type: 'text', text: `詳情請見：http://rumors.hacktabl.org/article/${selectedArticleId}` },
          ];
          state = '__INIT__';
        } else {
          data.foundReplies = notRumorReplies
            .map(({ replyConnectionId, id }) => ({ id, replyConnectionId }))
            .concat(
              rumorReplies.map(({ replyConnectionId, id }) => ({ id, replyConnectionId })),
            );
          state = 'CHOOSING_REPLY';
        }
      }

      break;
    }
    case 'CHOOSING_REPLY': {
      if (!data.foundReplies) {
        throw new Error('foundReplies not set in data');
      }

      const selectedReply = data.foundReplies[event.input - 1];

      if (!selectedReply) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundReplies.length} 的數字。` },
        ];

        state = 'CHOOSING_REPLY';
      } else {
        const { data: { GetReply } } = await gql`query($id: String!) {
          GetReply(id: $id) {
            versions(limit: 1) {
              type
              text
              reference
              createdAt
            }
          }
        }`({ id: selectedReply.id });

        replies = [
          { type: 'text', text: `這則回應認為文章${GetReply.versions[0].type === 'RUMOR' ? '含有不實訊息' : '不含不實訊息'}，理由為：` },
          { type: 'text', text: GetReply.versions[0].text },
          { type: 'text', text: `出處：${GetReply.versions[0].reference}` },
          {
            type: 'template',
            altText: '請問這則回應是否有解答原文章？\n「是」請輸入「y」，「否」請輸入其他任何訊息。',
            template: {
              type: 'buttons',
              text: '請問這則回應是否有解答原文章？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];

        data.selectedReply = selectedReply;
        state = 'ASKING_REPLY_FEEDBACK';
      }

      break;
    }
    case 'ASKING_REPLY_FEEDBACK': {
      if (!data.selectedReply) {
        throw new Error('selectedReply not set in data');
      }

      const { data: { action: { feedbackCount } } } = await gql`mutation($vote: FeedbackVote!, $id: String!){
        action: CreateOrUpdateReplyConnectionFeedback(
          vote: $vote
          replyConnectionId: $id
        ) {
          feedbackCount
        }
      }`({
        id: data.selectedReply.replyConnectionId,
        vote: event.input === 'y' ? 'UPVOTE' : 'DOWNVOTE',
      }, { userId });

      replies = [
        { type: 'text',
          text: feedbackCount > 1 ?
          `感謝您與其他 ${feedbackCount - 1} 人的回饋。` :
          '感謝您的回饋，您是第一個評論這份文章與回應的人 :)' },
      ];

      state = '__INIT__';
      break;
    }
    case 'ASKING_ARTICLE_SUBMISSION': {
      if (!data.searchedText) {
        throw new Error('searchText not set in data');
      }

      if (event.input === 'y') {
        const { data: { CreateArticle } } = await gql`mutation($text: String!){
          CreateArticle(text: $text, reference: {type: LINE}) {
            id
          }
        }`({ text: data.searchedText }, { userId });

        replies = [
          { type: 'text', text: `您回報的文章已經被收錄至：http://rumors.hacktabl.org/article/${CreateArticle.id}` },
          { type: 'text', text: '感謝您的回報！' },
        ];
      } else {
        replies = [
          { type: 'text', text: '感謝您的使用。' },
        ];
      }
      state = '__INIT__';

      break;
    }

    default:
      replies = [{
        type: 'text',
        text: '我們看不懂 QQ\n大俠請重新來過。',
      }];
      state = '__INIT__';
  }

  return {
    context: { state, data },
    replies,
  };
}
<|endoftext|>


<commit_before>// Author: Bricklen
// Trufflehunter project home: https://github.com/bricklen/trufflehunter

var http = require('http'),
    util = require('util'),
    formidable = require('../node_modules/formidable'),
    server,
    serverport = 8080,  // If changed, reflect the change in the crontab entry,
                        // the entry which executes exec_pgbadger.sh.
    fs = require('fs');
    path = require("path"),
    extensions = {
        ".html": "text/html",
        ".css": "text/css",
        ".js": "application/javascript",
        ".png": "image/png",
        ".gif": "image/gif",
        ".jpg": "image/jpeg",
        ".log": "text/plain",
        ".gz": "application/x-gzip"
    };
var rootpath = __dirname,
    uploadDir = rootpath + '/../public/uploads',
    config_file, fname, dir, ext;

// TODO: Add security measures to prevent users from reading other directories.

server = http.createServer(function(req, res) {

    var fname = path.basename(req.url) || "index.html",
        dir = path.dirname(req.url).substring(1),
        ext = path.extname(fname);

    if (req.url == '/') {
        res.writeHead(200, {'content-type': 'text/html'});
        fs.readFile(rootpath + '/../public/' + fname,function(error,data) {
            res.end(data);
        });

    } else if (req.url == '/upload') {
        var form = new formidable.IncomingForm(),
            files = [],
            fields = [],
            stream, filename, filename_no_ext, log_line_prefix, email, top_num_queries, send_email_link,
            randomnumber = Math.floor(Math.random()*100000);

        form.uploadDir = uploadDir;

        form
            .on('field', function(field, value) {
                // All input fields of the form MUST be processed here if they
                // will be used in the .config file, so if you add any to the
                // form, then add them here too.
                // Note: add the form elements "on ('end..." section too.

                fields.push([field, value]);
                if ( field == 'log_line_prefix' ) {
                    log_line_prefix = value;
                }
                if ( field == 'email' ) {
                    email = value;
                }
                if ( field == 'top_num_queries' ) {
                    top_num_queries = value;
                }
                if ( field == 'send_email_link' ) {
                    send_email_link = value;
                }
            })

            /* This is where the renaming happens */
            .on ('fileBegin', function(name, file){
                //if ( email == 'undefined' || email == '' || log_line_prefix == 'undefined' || log_line_prefix == '' ) {
                //    console.log('Some inputs are missing. Exiting...');
                //    // TODO: This is not stopping the upload if for example just the email is supplied but the log_line_prefix is not.
                //    // It should stop the upload and redirect to the main page, preferably with a message stating that there are missing values.
                //    // This should also be handled in the form itself, before we get this far.
                //    req.pause();
                //    // The redirect page should be the root, not some html page that I have hardcoded.
                //    res.writeHead(301, {'content-type': 'text/html', 'location' : req.url + '/../index.html'});
                //    res.end();
                //}

                //Rename the incoming file to the file's name
                filename = file.name;
                ext = path.extname(filename);
                filename = filename.replace(/\s+/g,"_");                    // Replace spaces with underscores
                filename = filename.substr(0, filename.lastIndexOf("."));   // Strip the extension
                filename_no_ext = filename + '_' + randomnumber;            // New filename, try make it unique
                filename = filename + '_' + randomnumber + ext;             // Put the extension back on
                file.path = form.uploadDir + '/' + filename;
                config_file = form.uploadDir + '/' + filename + '.config';  // Create the xxx.config filename
            })

            .on('file', function(field, file) {
                files.push([field, file]);
            })

            // Informational only, can be commented out
            .on('progress', function(bytesReceived, bytesExpected) {
                var percent = Math.ceil(bytesReceived / bytesExpected * 100) || 0;
                process.stdout.write('Uploading: %' + percent + '\r');
            })

            .on('end', function() {
                // Create the .config file.
                // This is done *after* the log file has been uploaded so that the
                // scheduled parse process only parses log files after the full log has
                // been uploaded AND the config has been created.
                // Do not move this step before the upload unless a mutex is added.
                stream = fs.createWriteStream(config_file);
                stream.once('open', function(fd) {

                // Ideally this would be a loop over the fields from the "field" section further up
                stream.write('log_line_prefix="' + log_line_prefix + '"' + "\n");
                stream.write('email="' + email + '"' + "\n");
                stream.write('logfile="' + filename + '"' + "\n");
                stream.write('top_num_queries="' + top_num_queries + '"' + "\n");
                stream.write('send_email_link="' + send_email_link + '"' + "\n");

                stream.end();
            });

            res.writeHead(200, {'content-type': 'text/html'});
            res.write('');
            res.write('<title>Redirect to generated HTML results</title>');
            res.write('');
            res.write('');
            res.write('');
            res.write('<link href="http://fonts.googleapis.com/css?family=Trykker|Bubbler+One" rel="stylesheet" type="text/css">');
            res.write('');
            res.write('');
            res.write('');
            res.write('

');
            res.write('When your generated HTML file is finished you will be redirected automatically.');
            res.write('');
            res.write('');
            res.end();
        });

        form.parse(req);

    } else if (ext === '.html') {
        // TODO: Tighten this up so other html files in other directories cannot be read
        res.writeHead(200, {'content-type': 'text/html'});
        fs.readFile(rootpath + '/../public/' + req.url,function(error,data){
            res.end(data);
        });

    // TODO: Clean up the conditional statements for extensions, bundle them up
    //       and have them processed dynamically, except where there are custom
    //       scripts that need handling.
    } else if (ext === '.js') {
// This stuff is unnecessary.
//        fs.readFile(__dirname + '/public/js/jquery-1.11.0.js', function (err, data) {
//            if (err) console.log(err);
//            res.writeHead(200, {'Content-Type': 'text/javascript'});
//            res.write(data);
//            res.end(data);
//        });

    } else if (ext === '.css') {
        fs.readFile(rootpath + '/../css/style.css', function (err, data) {
            if (err) console.log(err);
            res.writeHead(200, {'Content-Type': 'text/css'});
            res.write(data);
            res.end(data);
        });

    } else if (ext === '.gif') {
        fs.readFile(rootpath + '/../img/' + fname, function (err, data) {
            if (err) console.log(err);
            res.writeHead(200, {'Content-Type': 'image/gif'});
            res.write(data);
            res.end(data);
        });

    } else {
        res.writeHead(404, {'content-type': 'text/plain'});
        res.end('404');
    }
});
server.listen(serverport);

console.log('Listening on port:' + serverport );
<commit_msg>Added missing conditional statement for .png.<commit_after>// Author: Bricklen
// Trufflehunter project home: https://github.com/bricklen/trufflehunter

var http = require('http'),
    util = require('util'),
    formidable = require('../node_modules/formidable'),
    server,
    serverport = 8080,  // If changed, reflect the change in the crontab entry,
                        // the entry which executes exec_pgbadger.sh.
    fs = require('fs');
    path = require("path"),
    extensions = {
        ".html": "text/html",
        ".css": "text/css",
        ".js": "application/javascript",
        ".png": "image/png",
        ".gif": "image/gif",
        ".jpg": "image/jpeg",
        ".log": "text/plain",
        ".gz": "application/x-gzip"
    };
var rootpath = __dirname,
    uploadDir = rootpath + '/../public/uploads',
    config_file, fname, dir, ext;

// TODO: Add security measures to prevent users from reading other directories.

server = http.createServer(function(req, res) {

    var fname = path.basename(req.url) || "index.html",
        dir = path.dirname(req.url).substring(1),
        ext = path.extname(fname);

    if (req.url == '/') {
        res.writeHead(200, {'content-type': 'text/html'});
        fs.readFile(rootpath + '/../public/' + fname,function(error,data) {
            res.end(data);
        });

    } else if (req.url == '/upload') {
        var form = new formidable.IncomingForm(),
            files = [],
            fields = [],
            stream, filename, filename_no_ext, log_line_prefix, email, top_num_queries, send_email_link,
            randomnumber = Math.floor(Math.random()*100000);

        form.uploadDir = uploadDir;

        form
            .on('field', function(field, value) {
                // All input fields of the form MUST be processed here if they
                // will be used in the .config file, so if you add any to the
                // form, then add them here too.
                // Note: add the form elements "on ('end..." section too.

                fields.push([field, value]);
                if ( field == 'log_line_prefix' ) {
                    log_line_prefix = value;
                }
                if ( field == 'email' ) {
                    email = value;
                }
                if ( field == 'top_num_queries' ) {
                    top_num_queries = value;
                }
                if ( field == 'send_email_link' ) {
                    send_email_link = value;
                }
            })

            /* This is where the renaming happens */
            .on ('fileBegin', function(name, file){
                //if ( email == 'undefined' || email == '' || log_line_prefix == 'undefined' || log_line_prefix == '' ) {
                //    console.log('Some inputs are missing. Exiting...');
                //    // TODO: This is not stopping the upload if for example just the email is supplied but the log_line_prefix is not.
                //    // It should stop the upload and redirect to the main page, preferably with a message stating that there are missing values.
                //    // This should also be handled in the form itself, before we get this far.
                //    req.pause();
                //    // The redirect page should be the root, not some html page that I have hardcoded.
                //    res.writeHead(301, {'content-type': 'text/html', 'location' : req.url + '/../index.html'});
                //    res.end();
                //}

                //Rename the incoming file to the file's name
                filename = file.name;
                ext = path.extname(filename);
                filename = filename.replace(/\s+/g,"_");                    // Replace spaces with underscores
                filename = filename.substr(0, filename.lastIndexOf("."));   // Strip the extension
                filename_no_ext = filename + '_' + randomnumber;            // New filename, try make it unique
                filename = filename + '_' + randomnumber + ext;             // Put the extension back on
                file.path = form.uploadDir + '/' + filename;
                config_file = form.uploadDir + '/' + filename + '.config';  // Create the xxx.config filename
            })

            .on('file', function(field, file) {
                files.push([field, file]);
            })

            // Informational only, can be commented out
            .on('progress', function(bytesReceived, bytesExpected) {
                var percent = Math.ceil(bytesReceived / bytesExpected * 100) || 0;
                process.stdout.write('Uploading: %' + percent + '\r');
            })

            .on('end', function() {
                // Create the .config file.
                // This is done *after* the log file has been uploaded so that the
                // scheduled parse process only parses log files after the full log has
                // been uploaded AND the config has been created.
                // Do not move this step before the upload unless a mutex is added.
                stream = fs.createWriteStream(config_file);
                stream.once('open', function(fd) {

                // Ideally this would be a loop over the fields from the "field" section further up
                stream.write('log_line_prefix="' + log_line_prefix + '"' + "\n");
                stream.write('email="' + email + '"' + "\n");
                stream.write('logfile="' + filename + '"' + "\n");
                stream.write('top_num_queries="' + top_num_queries + '"' + "\n");
                stream.write('send_email_link="' + send_email_link + '"' + "\n");

                stream.end();
            });

            res.writeHead(200, {'content-type': 'text/html'});
            res.write('');
            res.write('<title>Redirect to generated HTML results</title>');
            res.write('');
            res.write('');
            res.write('');
            res.write('<link href="http://fonts.googleapis.com/css?family=Trykker|Bubbler+One" rel="stylesheet" type="text/css">');
            res.write('');
            res.write('');
            res.write('');
            res.write('

');
            res.write('When your generated HTML file is finished you will be redirected automatically.');
            res.write('');
            res.write('');
            res.end();
        });

        form.parse(req);

    } else if (ext === '.html') {
        // TODO: Tighten this up so other html files in other directories cannot be read
        res.writeHead(200, {'content-type': 'text/html'});
        fs.readFile(rootpath + '/../public/' + req.url,function(error,data){
            res.end(data);
        });

    // TODO: Clean up the conditional statements for extensions, bundle them up
    //       and have them processed dynamically, except where there are custom
    //       scripts that need handling.
    } else if (ext === '.js') {
// This stuff is unnecessary.
//        fs.readFile(__dirname + '/public/js/jquery-1.11.0.js', function (err, data) {
//            if (err) console.log(err);
//            res.writeHead(200, {'Content-Type': 'text/javascript'});
//            res.write(data);
//            res.end(data);
//        });

    } else if (ext === '.css') {
        fs.readFile(rootpath + '/../css/style.css', function (err, data) {
            if (err) console.log(err);
            res.writeHead(200, {'Content-Type': 'text/css'});
            res.write(data);
            res.end(data);
        });

    } else if (ext === '.gif') {
        fs.readFile(rootpath + '/../img/' + fname, function (err, data) {
            if (err) console.log(err);
            res.writeHead(200, {'Content-Type': 'image/gif'});
            res.write(data);
            res.end(data);
        });

    } else if (ext === '.png') {
        fs.readFile(rootpath + '/../img/' + fname, function (err, data) {
            if (err) console.log(err);
            res.writeHead(200, {'Content-Type': 'image/png'});
            res.write(data);
            res.end(data);
        });

    } else {
        res.writeHead(404, {'content-type': 'text/plain'});
        res.end('404');
    }
});
server.listen(serverport);

console.log('Listening on port:' + serverport );
<|endoftext|>