<commit_before>import {
  React, PropTypes, observer, styled, action, css, moment,
} from 'vendor';
import { last } from 'lodash';
import { Button } from 'react-bootstrap';
import TaskStep from '../../../models/student-tasks/step';
import ResponseValidation from '../../../models/response_validation';
import Question from 'shared/model/exercise/question';
import TaskUX from '../ux';
import QuestionStem from './question-stem';
import Theme from '../../../theme';
import Course from '../../../models/course';
import { StepFooter } from './footer';
import { ResponseValidationUX } from '../response-validation-ux';
import { NudgeMessages, NudgeMessage } from './nudge-message';

const StyledFreeResponse = styled.div`
  display: flex;
  flex-direction: column;
`;

const TextAreaErrorStyle = css`
  background-color: #f5e9ea;
`;

const InfoRow = styled.div`
  margin: 8px 0;
  display: flex;
  justify-content: ${props => props.isDisplayingLastSaved ? 'space-between' : 'flex-end'};

  span {
    font-size: 12px;
    line-height: 16px;
  }
`;

const ControlsRow = styled.div`
  margin: 24px 0;
  display: flex;
  justify-content: ${props => props.isDisplayingNudge ? 'space-between' : 'flex-end'};
`;

const TextArea = styled.textarea`
  width: 100%;
  min-height: 10.5em;
  line-height: 1.5em;
  margin: 2.5rem 0 0 0;
  padding: 0.5em;
  border: 1px solid ${Theme.colors.neutral.std};
  ${props => props.isErrored && TextAreaErrorStyle}
`;
TextArea.displayName = 'TextArea';

const AnswerButton = styled(Button)`
  align-self: flex-start;
  margin: 0;
`;
AnswerButton.displayName = 'AnswerButton';

@observer
class FreeResponseReview extends React.Component {
  static propTypes = {
    step: PropTypes.instanceOf(TaskStep).isRequired,
  };
  render() {
    const { step } = this.props;
    if (!step.free_response) { return null; }
    return (

{step.free_response}

    );
  }
}

@observer
class FreeResponseInput extends React.Component {

  static propTypes = {
    questionNumber: PropTypes.number,
    course: PropTypes.instanceOf(Course).isRequired,
    step: PropTypes.instanceOf(TaskStep).isRequired,
    question: PropTypes.instanceOf(Question).isRequired,
    taskUX: PropTypes.instanceOf(TaskUX).isRequired,
    response_validation: PropTypes.instanceOf(ResponseValidation),
  };

  ux = new ResponseValidationUX({
    step: this.props.step,
    taskUX: this.props.taskUX,
    messages: NudgeMessages,
    validator: this.props.response_validation,
  });

  textArea = React.createRef();

  @action.bound onSave() {
    const { taskUX, step } = this.props;
    taskUX.setCurrentMultiPartStep(step);
    this.ux.onSave();
  }

  componentDidMount() {
    // focus on the textarea when question is loaded
    this.textArea.current.focus();
  }

  render() {
    const { ux, props: { questionNumber, course, step, question } } = this;
    return (

    );
  }

}

export { FreeResponseInput, FreeResponseReview };
<commit_msg>do not autofocus the text area when rendering a question. the question might be long or have a video and it will scroll down.<commit_after>import {
  React, PropTypes, observer, styled, action, css, moment,
} from 'vendor';
import { last } from 'lodash';
import { Button } from 'react-bootstrap';
import TaskStep from '../../../models/student-tasks/step';
import ResponseValidation from '../../../models/response_validation';
import Question from 'shared/model/exercise/question';
import TaskUX from '../ux';
import QuestionStem from './question-stem';
import Theme from '../../../theme';
import Course from '../../../models/course';
import { StepFooter } from './footer';
import { ResponseValidationUX } from '../response-validation-ux';
import { NudgeMessages, NudgeMessage } from './nudge-message';

const StyledFreeResponse = styled.div`
  display: flex;
  flex-direction: column;
`;

const TextAreaErrorStyle = css`
  background-color: #f5e9ea;
`;

const InfoRow = styled.div`
  margin: 8px 0;
  display: flex;
  justify-content: ${props => props.isDisplayingLastSaved ? 'space-between' : 'flex-end'};

  span {
    font-size: 12px;
    line-height: 16px;
  }
`;

const ControlsRow = styled.div`
  margin: 24px 0;
  display: flex;
  justify-content: ${props => props.isDisplayingNudge ? 'space-between' : 'flex-end'};
`;

const TextArea = styled.textarea`
  width: 100%;
  min-height: 10.5em;
  line-height: 1.5em;
  margin: 2.5rem 0 0 0;
  padding: 0.5em;
  border: 1px solid ${Theme.colors.neutral.std};
  ${props => props.isErrored && TextAreaErrorStyle}
`;
TextArea.displayName = 'TextArea';

const AnswerButton = styled(Button)`
  align-self: flex-start;
  margin: 0;
`;
AnswerButton.displayName = 'AnswerButton';

@observer
class FreeResponseReview extends React.Component {
  static propTypes = {
    step: PropTypes.instanceOf(TaskStep).isRequired,
  };
  render() {
    const { step } = this.props;
    if (!step.free_response) { return null; }
    return (

{step.free_response}

    );
  }
}

@observer
class FreeResponseInput extends React.Component {

  static propTypes = {
    questionNumber: PropTypes.number,
    course: PropTypes.instanceOf(Course).isRequired,
    step: PropTypes.instanceOf(TaskStep).isRequired,
    question: PropTypes.instanceOf(Question).isRequired,
    taskUX: PropTypes.instanceOf(TaskUX).isRequired,
    response_validation: PropTypes.instanceOf(ResponseValidation),
  };

  ux = new ResponseValidationUX({
    step: this.props.step,
    taskUX: this.props.taskUX,
    messages: NudgeMessages,
    validator: this.props.response_validation,
  });

  @action.bound onSave() {
    const { taskUX, step } = this.props;
    taskUX.setCurrentMultiPartStep(step);
    this.ux.onSave();
  }

  render() {
    const { ux, props: { questionNumber, course, step, question } } = this;
    return (

    );
  }

}

export { FreeResponseInput, FreeResponseReview };
<|endoftext|>


<commit_before>import React from 'react';
import preloader from 'spectacle/lib/utils/preloader';
import createTheme from 'spectacle/lib/themes/default';

import {
    Deck,
    Heading,
    Link,
    Slide,
    Text
} from 'spectacle';

require('normalize.css');
require('spectacle/lib/themes/default/index.css');

const images = {};

preloader(images);

const theme = createTheme(
    {
        primary: '#FFF',
        secondary: '#1F2022',
        tertiary: '#FF6D00',
        quarternary: '#FF6D00'
    },
    {
        primary: 'Roboto',
        secondary: 'Helvetica'
    }
);

export default class Presentation extends React.Component {
    render() {
        return (
            <Deck transition={['fade']} transitionDuration={500} theme={theme}>
                <Slide transition={['fade']} bgColor="primary">
                    <Heading size={1} fit caps lineHeight={1} textColor="secondary">
                        Virtual House Assistant
                    </Heading>
                    <Text margin="30px" textColor="tertiary" textSize={48}>
                        Yash Kulshrestha
                    </Text>
                </Slide>
                <Slide transition={['fade']} bgColor="primary">
                    <Link href="https://github.com/yashdalfthegray" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Github
                    </Link>
                    <Link href="https://twitter.com/yashdalfthegray" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Twitter
                    </Link>
                    <Link href="https://yashkulshrestha.carrd.co" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Website
                    </Link>
                    <Text textColor="secondary" textSize={48} margin="24px ">
                        ~~
                    </Text>
                    <Link href="https://yashdalfthegray.github.io/talks/assistant/notes" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Presentation Notes
                    </Link>
                </Slide>
                <Slide transition={['fade']} bgColor="primary">
                    <Heading size={3} caps lineHeight={1} textColor="secondary">
                        Thanks for listening!
                    </Heading>
                </Slide>
            </Deck>
        );
    }
}
<commit_msg>added the first couple slides<commit_after>import React from 'react';
import preloader from 'spectacle/lib/utils/preloader';
import createTheme from 'spectacle/lib/themes/default';

import {
    Deck,
    Heading,
    Image,
    Link,
    Slide,
    Text
} from 'spectacle';

require('normalize.css');
require('spectacle/lib/themes/default/index.css');

const images = {
    hubot: require('../assets/hubot.svg'),
    particle: require('../assets/particle.svg'),
    slack: require('../assets/slack.svg')
};

preloader(images);

const theme = createTheme(
    {
        primary: '#FFF',
        secondary: '#1F2022',
        tertiary: '#FF6D00',
        quarternary: '#FF6D00'
    },
    {
        primary: 'Roboto',
        secondary: 'Helvetica'
    }
);

export default class Presentation extends React.Component {
    render() {
        return (
            <Deck transition={['fade']} transitionDuration={500} theme={theme}>
                <Slide transition={['fade']} bgColor="primary">
                    <Heading size={1} fit caps lineHeight={1} textColor="secondary">
                        Virtual House Assistant
                    </Heading>
                    <Text margin="30px" textColor="tertiary" textSize={48}>
                        Yash Kulshrestha
                    </Text>
                </Slide>
                <Slide>
                    <Text margin="36px" textColor="secondary" textSize={48}>
                        Hey, Jarvis...
                    </Text>
                </Slide>
                <Slide>
                    <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
                        <Image src={images.particle} width="25%" style={{ transform: 'rotate(90deg)' }} />
                        <Image src={images.hubot} width="25%" />
                        <Image src={images.slack} width="25%" />
                    </div>
                </Slide>
                <Slide transition={['fade']} bgColor="primary">
                    <Link href="https://github.com/yashdalfthegray" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Github
                    </Link>
                    <Link href="https://twitter.com/yashdalfthegray" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Twitter
                    </Link>
                    <Link href="https://yashkulshrestha.carrd.co" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Website
                    </Link>
                    <Text textColor="secondary" textSize={48} margin="24px ">
                        ~~
                    </Text>
                    <Link href="https://yashdalfthegray.github.io/talks/assistant/notes" target="_blank" textColor="tertiary" textSize={48} margin="24px">
                        Presentation Notes
                    </Link>
                </Slide>
                <Slide transition={['fade']} bgColor="primary">
                    <Heading size={3} caps lineHeight={1} textColor="secondary">
                        Thanks for listening!
                    </Heading>
                </Slide>
            </Deck>
        );
    }
}
<|endoftext|>


<commit_before>/**
 * PowerTip TooltipController
 *
 * @fileoverview  TooltipController object that manages tips for an instance.
 * @link          https://stevenbenner.github.io/jquery-powertip/
 * @author        Steven Benner (http://stevenbenner.com/)
 * @requires      jQuery 1.7+
 */

/**
 * Creates a new tooltip controller.
 * @private
 * @constructor
 * @param {Object} options Options object containing settings.
 */
function TooltipController(options) {
	var placementCalculator = new PlacementCalculator(),
		tipElement = $('#' + options.popupId);

	// build and append tooltip div if it does not already exist
	if (tipElement.length === 0) {
		tipElement = $('<div/>', { id: options.popupId });
		// grab body element if it was not populated when the script loaded
		// note: this hack exists solely for jsfiddle support
		if ($body.length === 0) {
			$body = $('body');
		}
		$body.append(tipElement);
		// remember the tooltip elements that the plugin has created
		session.tooltips = session.tooltips ? session.tooltips.add(tipElement) : tipElement;
	}

	// hook mousemove for cursor follow tooltips
	if (options.followMouse) {
		// only one positionTipOnCursor hook per tooltip element, please
		if (!tipElement.data(DATA_HASMOUSEMOVE)) {
			$document.on('mousemove' + EVENT_NAMESPACE, positionTipOnCursor);
			$window.on('scroll' + EVENT_NAMESPACE, positionTipOnCursor);
			tipElement.data(DATA_HASMOUSEMOVE, true);
		}
	}

	/**
	 * Gives the specified element the active-hover state and queues up the
	 * showTip function.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function beginShowTip(element) {
		element.data(DATA_HASACTIVEHOVER, true);
		// show tooltip, asap
		tipElement.queue(function queueTipInit(next) {
			showTip(element);
			next();
		});
	}

	/**
	 * Shows the tooltip, as soon as possible.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function showTip(element) {
		var tipContent;

		// it is possible, especially with keyboard navigation, to move on to
		// another element with a tooltip during the queue to get to this point
		// in the code. if that happens then we need to not proceed or we may
		// have the fadeout callback for the last tooltip execute immediately
		// after this code runs, causing bugs.
		if (!element.data(DATA_HASACTIVEHOVER)) {
			return;
		}

		// if the tooltip is open and we got asked to open another one then the
		// old one is still in its fadeOut cycle, so wait and try again
		if (session.isTipOpen) {
			if (!session.isClosing) {
				hideTip(session.activeHover);
			}
			tipElement.delay(100).queue(function queueTipAgain(next) {
				showTip(element);
				next();
			});
			return;
		}

		// trigger powerTipPreRender event
		element.trigger('powerTipPreRender');

		// set tooltip content
		tipContent = getTooltipContent(element);
		if (tipContent) {
			tipElement.empty().append(tipContent);
		} else {
			// we have no content to display, give up
			return;
		}

		// trigger powerTipRender event
		element.trigger('powerTipRender');

		session.activeHover = element;
		session.isTipOpen = true;

		tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup);

		// add custom class to tooltip element
		tipElement.addClass(options.popupClass);

		// set tooltip position
		// revert to static placement when the "force open" flag was set because
		// that flag means that we do not have accurate mouse position info
		if (!options.followMouse || element.data(DATA_FORCEDOPEN)) {
			positionTipOnElement(element);
			session.isFixedTipOpen = true;
		} else {
			positionTipOnCursor();
		}

		// close tooltip when clicking anywhere on the page, with the exception
		// of the tooltip's trigger element and any elements that are within a
		// tooltip that has 'mouseOnToPopup' option enabled
		// always enable this feature when the "force open" flag is set on a
		// followMouse tooltip because we reverted to static placement above
		if (!element.data(DATA_FORCEDOPEN) && !options.followMouse) {
			$document.on('click' + EVENT_NAMESPACE, function documentClick(event) {
				var target = event.target;
				if (target !== element[0]) {
					if (options.mouseOnToPopup) {
						if (target !== tipElement[0] && !$.contains(tipElement[0], target)) {
							$.powerTip.hide();
						}
					} else {
						$.powerTip.hide();
					}
				}
			});
		}

		// if we want to be able to mouse on to the tooltip then we need to
		// attach hover events to the tooltip that will cancel a close request
		// on mouseenter and start a new close request on mouseleave
		// only hook these listeners if we're not in manual mode
		if (options.mouseOnToPopup && !options.manual) {
			tipElement.on('mouseenter' + EVENT_NAMESPACE, function tipMouseEnter() {
				// check activeHover in case the mouse cursor entered the
				// tooltip during the fadeOut and close cycle
				if (session.activeHover) {
					session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel();
				}
			});
			tipElement.on('mouseleave' + EVENT_NAMESPACE, function tipMouseLeave() {
				// check activeHover in case the mouse cursor left the tooltip
				// during the fadeOut and close cycle
				if (session.activeHover) {
					session.activeHover.data(DATA_DISPLAYCONTROLLER).hide();
				}
			});
		}

		// fadein
		tipElement.fadeIn(options.fadeInTime, function fadeInCallback() {
			// start desync polling
			if (!session.desyncTimeout) {
				session.desyncTimeout = setInterval(closeDesyncedTip, 500);
			}

			// trigger powerTipOpen event
			element.trigger('powerTipOpen');
		});
	}

	/**
	 * Hides the tooltip.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function hideTip(element) {
		// reset session
		session.isClosing = true;
		session.isTipOpen = false;

		// stop desync polling
		session.desyncTimeout = clearInterval(session.desyncTimeout);

		// reset element state
		element.data(DATA_HASACTIVEHOVER, false);
		element.data(DATA_FORCEDOPEN, false);

		// remove document click handler
		$document.off('click' + EVENT_NAMESPACE);

		// unbind the mouseOnToPopup events if they were set
		tipElement.off(EVENT_NAMESPACE);

		// fade out
		tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() {
			var coords = new CSSCoordinates();

			// reset session and tooltip element
			session.activeHover = null;
			session.isClosing = false;
			session.isFixedTipOpen = false;
			tipElement.removeClass();

			// support mouse-follow and fixed position tips at the same time by
			// moving the tooltip to the last cursor location after it is hidden
			coords.set('top', session.currentY + options.offset);
			coords.set('left', session.currentX + options.offset);
			tipElement.css(coords);

			// trigger powerTipClose event
			element.trigger('powerTipClose');
		});
	}

	/**
	 * Moves the tooltip to the users mouse cursor.
	 * @private
	 */
	function positionTipOnCursor() {
		var tipWidth,
			tipHeight,
			coords,
			collisions,
			collisionCount;

		// to support having fixed tooltips on the same page as cursor tooltips,
		// where both instances are referencing the same tooltip element, we
		// need to keep track of the mouse position constantly, but we should
		// only set the tip location if a fixed tip is not currently open, a tip
		// open is imminent or active, and the tooltip element in question does
		// have a mouse-follow using it.
		if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) {
			// grab measurements
			tipWidth = tipElement.outerWidth();
			tipHeight = tipElement.outerHeight();
			coords = new CSSCoordinates();

			// grab collisions
			coords.set('top', session.currentY + options.offset);
			coords.set('left', session.currentX + options.offset);
			collisions = getViewportCollisions(
				coords,
				tipWidth,
				tipHeight
			);

			// handle tooltip view port collisions
			if (collisions !== Collision.none) {
				collisionCount = countFlags(collisions);
				if (collisionCount === 1) {
					// if there is only one collision (bottom or right) then
					// simply constrain the tooltip to the view port
					if (collisions === Collision.right) {
						coords.set('left', session.scrollLeft + session.windowWidth - tipWidth);
					} else if (collisions === Collision.bottom) {
						coords.set('top', session.scrollTop + session.windowHeight - tipHeight);
					}
				} else {
					// if the tooltip has more than one collision then it is
					// trapped in the corner and should be flipped to get it out
					// of the users way
					coords.set('left', session.currentX - tipWidth - options.offset);
					coords.set('top', session.currentY - tipHeight - options.offset);
				}
			}

			// position the tooltip
			tipElement.css(coords);
		}
	}

	/**
	 * Sets the tooltip to the correct position relative to the specified target
	 * element. Based on options settings.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function positionTipOnElement(element) {
		var priorityList,
			finalPlacement;

		// when the followMouse option is enabled and the "force open" flag is
		// set we revert to static positioning. since the developer may not have
		// considered this scenario we should use smart placement
		if (options.smartPlacement || (options.followMouse && element.data(DATA_FORCEDOPEN))) {
			priorityList = $.fn.powerTip.smartPlacementLists[options.placement];

			// iterate over the priority list and use the first placement option
			// that does not collide with the view port. if they all collide
			// then the last placement in the list will be used.
			$.each(priorityList, function(idx, pos) {
				// place tooltip and find collisions
				var collisions = getViewportCollisions(
					placeTooltip(element, pos),
					tipElement.outerWidth(),
					tipElement.outerHeight()
				);

				// update the final placement variable
				finalPlacement = pos;

				// break if there were no collisions
				return collisions !== Collision.none;
			});
		} else {
			// if we're not going to use the smart placement feature then just
			// compute the coordinates and do it
			placeTooltip(element, options.placement);
			finalPlacement = options.placement;
		}

		// add placement as class for CSS arrows
		tipElement.removeClass('w nw sw e ne se n s w se-alt sw-alt ne-alt nw-alt');
		tipElement.addClass(finalPlacement);
	}

	/**
	 * Sets the tooltip position to the appropriate values to show the tip at
	 * the specified placement. This function will iterate and test the tooltip
	 * to support elastic tooltips.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 * @param {string} placement The placement for the tooltip.
	 * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and
	 *     right position values.
	 */
	function placeTooltip(element, placement) {
		var iterationCount = 0,
			tipWidth,
			tipHeight,
			coords = new CSSCoordinates();

		// set the tip to 0,0 to get the full expanded width
		coords.set('top', 0);
		coords.set('left', 0);
		tipElement.css(coords);

		// to support elastic tooltips we need to check for a change in the
		// rendered dimensions after the tooltip has been positioned
		do {
			// grab the current tip dimensions
			tipWidth = tipElement.outerWidth();
			tipHeight = tipElement.outerHeight();

			// get placement coordinates
			coords = placementCalculator.compute(
				element,
				placement,
				tipWidth,
				tipHeight,
				options.offset
			);

			// place the tooltip
			tipElement.css(coords);
		} while (
			// sanity check: limit to 5 iterations, and...
			++iterationCount <= 5 &&
			// try again if the dimensions changed after placement
			(tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight())
		);

		return coords;
	}

	/**
	 * Checks for a tooltip desync and closes the tooltip if one occurs.
	 * @private
	 */
	function closeDesyncedTip() {
		var isDesynced = false,
			hasDesyncableCloseEvent = $.grep(
				[ 'mouseleave', 'mouseout', 'blur', 'focusout' ],
				function(eventType) {
					return $.inArray(options.closeEvents, eventType) !== -1;
				}
			).length > 0;

		// It is possible for the mouse cursor to leave an element without
		// firing the mouseleave or blur event. This most commonly happens when
		// the element is disabled under mouse cursor. If this happens it will
		// result in a desynced tooltip because the tooltip was never asked to
		// close. So we should periodically check for a desync situation and
		// close the tip if such a situation arises.
		if (session.isTipOpen && !session.isClosing && !session.delayInProgress && hasDesyncableCloseEvent) {
			if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) {
				// user moused onto another tip or active hover is disabled
				isDesynced = true;
			} else if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) {
				// hanging tip - have to test if mouse position is not over the
				// active hover and not over a tooltip set to let the user
				// interact with it.
				// for keyboard navigation: this only counts if the element does
				// not have focus.
				// for tooltips opened via the api: we need to check if it has
				// the forcedOpen flag.
				if (tipElement.data(DATA_MOUSEONTOTIP)) {
					if (!isMouseOver(tipElement)) {
						isDesynced = true;
					}
				} else {
					isDesynced = true;
				}
			}

			if (isDesynced) {
				// close the desynced tip
				hideTip(session.activeHover);
			}
		}
	}

	// expose methods
	this.showTip = beginShowTip;
	this.hideTip = hideTip;
	this.resetPosition = positionTipOnElement;
}
<commit_msg>Corrected backwards parameters in $.inArray() call<commit_after>/**
 * PowerTip TooltipController
 *
 * @fileoverview  TooltipController object that manages tips for an instance.
 * @link          https://stevenbenner.github.io/jquery-powertip/
 * @author        Steven Benner (http://stevenbenner.com/)
 * @requires      jQuery 1.7+
 */

/**
 * Creates a new tooltip controller.
 * @private
 * @constructor
 * @param {Object} options Options object containing settings.
 */
function TooltipController(options) {
	var placementCalculator = new PlacementCalculator(),
		tipElement = $('#' + options.popupId);

	// build and append tooltip div if it does not already exist
	if (tipElement.length === 0) {
		tipElement = $('<div/>', { id: options.popupId });
		// grab body element if it was not populated when the script loaded
		// note: this hack exists solely for jsfiddle support
		if ($body.length === 0) {
			$body = $('body');
		}
		$body.append(tipElement);
		// remember the tooltip elements that the plugin has created
		session.tooltips = session.tooltips ? session.tooltips.add(tipElement) : tipElement;
	}

	// hook mousemove for cursor follow tooltips
	if (options.followMouse) {
		// only one positionTipOnCursor hook per tooltip element, please
		if (!tipElement.data(DATA_HASMOUSEMOVE)) {
			$document.on('mousemove' + EVENT_NAMESPACE, positionTipOnCursor);
			$window.on('scroll' + EVENT_NAMESPACE, positionTipOnCursor);
			tipElement.data(DATA_HASMOUSEMOVE, true);
		}
	}

	/**
	 * Gives the specified element the active-hover state and queues up the
	 * showTip function.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function beginShowTip(element) {
		element.data(DATA_HASACTIVEHOVER, true);
		// show tooltip, asap
		tipElement.queue(function queueTipInit(next) {
			showTip(element);
			next();
		});
	}

	/**
	 * Shows the tooltip, as soon as possible.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function showTip(element) {
		var tipContent;

		// it is possible, especially with keyboard navigation, to move on to
		// another element with a tooltip during the queue to get to this point
		// in the code. if that happens then we need to not proceed or we may
		// have the fadeout callback for the last tooltip execute immediately
		// after this code runs, causing bugs.
		if (!element.data(DATA_HASACTIVEHOVER)) {
			return;
		}

		// if the tooltip is open and we got asked to open another one then the
		// old one is still in its fadeOut cycle, so wait and try again
		if (session.isTipOpen) {
			if (!session.isClosing) {
				hideTip(session.activeHover);
			}
			tipElement.delay(100).queue(function queueTipAgain(next) {
				showTip(element);
				next();
			});
			return;
		}

		// trigger powerTipPreRender event
		element.trigger('powerTipPreRender');

		// set tooltip content
		tipContent = getTooltipContent(element);
		if (tipContent) {
			tipElement.empty().append(tipContent);
		} else {
			// we have no content to display, give up
			return;
		}

		// trigger powerTipRender event
		element.trigger('powerTipRender');

		session.activeHover = element;
		session.isTipOpen = true;

		tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup);

		// add custom class to tooltip element
		tipElement.addClass(options.popupClass);

		// set tooltip position
		// revert to static placement when the "force open" flag was set because
		// that flag means that we do not have accurate mouse position info
		if (!options.followMouse || element.data(DATA_FORCEDOPEN)) {
			positionTipOnElement(element);
			session.isFixedTipOpen = true;
		} else {
			positionTipOnCursor();
		}

		// close tooltip when clicking anywhere on the page, with the exception
		// of the tooltip's trigger element and any elements that are within a
		// tooltip that has 'mouseOnToPopup' option enabled
		// always enable this feature when the "force open" flag is set on a
		// followMouse tooltip because we reverted to static placement above
		if (!element.data(DATA_FORCEDOPEN) && !options.followMouse) {
			$document.on('click' + EVENT_NAMESPACE, function documentClick(event) {
				var target = event.target;
				if (target !== element[0]) {
					if (options.mouseOnToPopup) {
						if (target !== tipElement[0] && !$.contains(tipElement[0], target)) {
							$.powerTip.hide();
						}
					} else {
						$.powerTip.hide();
					}
				}
			});
		}

		// if we want to be able to mouse on to the tooltip then we need to
		// attach hover events to the tooltip that will cancel a close request
		// on mouseenter and start a new close request on mouseleave
		// only hook these listeners if we're not in manual mode
		if (options.mouseOnToPopup && !options.manual) {
			tipElement.on('mouseenter' + EVENT_NAMESPACE, function tipMouseEnter() {
				// check activeHover in case the mouse cursor entered the
				// tooltip during the fadeOut and close cycle
				if (session.activeHover) {
					session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel();
				}
			});
			tipElement.on('mouseleave' + EVENT_NAMESPACE, function tipMouseLeave() {
				// check activeHover in case the mouse cursor left the tooltip
				// during the fadeOut and close cycle
				if (session.activeHover) {
					session.activeHover.data(DATA_DISPLAYCONTROLLER).hide();
				}
			});
		}

		// fadein
		tipElement.fadeIn(options.fadeInTime, function fadeInCallback() {
			// start desync polling
			if (!session.desyncTimeout) {
				session.desyncTimeout = setInterval(closeDesyncedTip, 500);
			}

			// trigger powerTipOpen event
			element.trigger('powerTipOpen');
		});
	}

	/**
	 * Hides the tooltip.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function hideTip(element) {
		// reset session
		session.isClosing = true;
		session.isTipOpen = false;

		// stop desync polling
		session.desyncTimeout = clearInterval(session.desyncTimeout);

		// reset element state
		element.data(DATA_HASACTIVEHOVER, false);
		element.data(DATA_FORCEDOPEN, false);

		// remove document click handler
		$document.off('click' + EVENT_NAMESPACE);

		// unbind the mouseOnToPopup events if they were set
		tipElement.off(EVENT_NAMESPACE);

		// fade out
		tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() {
			var coords = new CSSCoordinates();

			// reset session and tooltip element
			session.activeHover = null;
			session.isClosing = false;
			session.isFixedTipOpen = false;
			tipElement.removeClass();

			// support mouse-follow and fixed position tips at the same time by
			// moving the tooltip to the last cursor location after it is hidden
			coords.set('top', session.currentY + options.offset);
			coords.set('left', session.currentX + options.offset);
			tipElement.css(coords);

			// trigger powerTipClose event
			element.trigger('powerTipClose');
		});
	}

	/**
	 * Moves the tooltip to the users mouse cursor.
	 * @private
	 */
	function positionTipOnCursor() {
		var tipWidth,
			tipHeight,
			coords,
			collisions,
			collisionCount;

		// to support having fixed tooltips on the same page as cursor tooltips,
		// where both instances are referencing the same tooltip element, we
		// need to keep track of the mouse position constantly, but we should
		// only set the tip location if a fixed tip is not currently open, a tip
		// open is imminent or active, and the tooltip element in question does
		// have a mouse-follow using it.
		if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) {
			// grab measurements
			tipWidth = tipElement.outerWidth();
			tipHeight = tipElement.outerHeight();
			coords = new CSSCoordinates();

			// grab collisions
			coords.set('top', session.currentY + options.offset);
			coords.set('left', session.currentX + options.offset);
			collisions = getViewportCollisions(
				coords,
				tipWidth,
				tipHeight
			);

			// handle tooltip view port collisions
			if (collisions !== Collision.none) {
				collisionCount = countFlags(collisions);
				if (collisionCount === 1) {
					// if there is only one collision (bottom or right) then
					// simply constrain the tooltip to the view port
					if (collisions === Collision.right) {
						coords.set('left', session.scrollLeft + session.windowWidth - tipWidth);
					} else if (collisions === Collision.bottom) {
						coords.set('top', session.scrollTop + session.windowHeight - tipHeight);
					}
				} else {
					// if the tooltip has more than one collision then it is
					// trapped in the corner and should be flipped to get it out
					// of the users way
					coords.set('left', session.currentX - tipWidth - options.offset);
					coords.set('top', session.currentY - tipHeight - options.offset);
				}
			}

			// position the tooltip
			tipElement.css(coords);
		}
	}

	/**
	 * Sets the tooltip to the correct position relative to the specified target
	 * element. Based on options settings.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 */
	function positionTipOnElement(element) {
		var priorityList,
			finalPlacement;

		// when the followMouse option is enabled and the "force open" flag is
		// set we revert to static positioning. since the developer may not have
		// considered this scenario we should use smart placement
		if (options.smartPlacement || (options.followMouse && element.data(DATA_FORCEDOPEN))) {
			priorityList = $.fn.powerTip.smartPlacementLists[options.placement];

			// iterate over the priority list and use the first placement option
			// that does not collide with the view port. if they all collide
			// then the last placement in the list will be used.
			$.each(priorityList, function(idx, pos) {
				// place tooltip and find collisions
				var collisions = getViewportCollisions(
					placeTooltip(element, pos),
					tipElement.outerWidth(),
					tipElement.outerHeight()
				);

				// update the final placement variable
				finalPlacement = pos;

				// break if there were no collisions
				return collisions !== Collision.none;
			});
		} else {
			// if we're not going to use the smart placement feature then just
			// compute the coordinates and do it
			placeTooltip(element, options.placement);
			finalPlacement = options.placement;
		}

		// add placement as class for CSS arrows
		tipElement.removeClass('w nw sw e ne se n s w se-alt sw-alt ne-alt nw-alt');
		tipElement.addClass(finalPlacement);
	}

	/**
	 * Sets the tooltip position to the appropriate values to show the tip at
	 * the specified placement. This function will iterate and test the tooltip
	 * to support elastic tooltips.
	 * @private
	 * @param {jQuery} element The element that the tooltip should target.
	 * @param {string} placement The placement for the tooltip.
	 * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and
	 *     right position values.
	 */
	function placeTooltip(element, placement) {
		var iterationCount = 0,
			tipWidth,
			tipHeight,
			coords = new CSSCoordinates();

		// set the tip to 0,0 to get the full expanded width
		coords.set('top', 0);
		coords.set('left', 0);
		tipElement.css(coords);

		// to support elastic tooltips we need to check for a change in the
		// rendered dimensions after the tooltip has been positioned
		do {
			// grab the current tip dimensions
			tipWidth = tipElement.outerWidth();
			tipHeight = tipElement.outerHeight();

			// get placement coordinates
			coords = placementCalculator.compute(
				element,
				placement,
				tipWidth,
				tipHeight,
				options.offset
			);

			// place the tooltip
			tipElement.css(coords);
		} while (
			// sanity check: limit to 5 iterations, and...
			++iterationCount <= 5 &&
			// try again if the dimensions changed after placement
			(tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight())
		);

		return coords;
	}

	/**
	 * Checks for a tooltip desync and closes the tooltip if one occurs.
	 * @private
	 */
	function closeDesyncedTip() {
		var isDesynced = false,
			hasDesyncableCloseEvent = $.grep(
				[ 'mouseleave', 'mouseout', 'blur', 'focusout' ],
				function(eventType) {
					return $.inArray(eventType, options.closeEvents) !== -1;
				}
			).length > 0;

		// It is possible for the mouse cursor to leave an element without
		// firing the mouseleave or blur event. This most commonly happens when
		// the element is disabled under mouse cursor. If this happens it will
		// result in a desynced tooltip because the tooltip was never asked to
		// close. So we should periodically check for a desync situation and
		// close the tip if such a situation arises.
		if (session.isTipOpen && !session.isClosing && !session.delayInProgress && hasDesyncableCloseEvent) {
			if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) {
				// user moused onto another tip or active hover is disabled
				isDesynced = true;
			} else if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) {
				// hanging tip - have to test if mouse position is not over the
				// active hover and not over a tooltip set to let the user
				// interact with it.
				// for keyboard navigation: this only counts if the element does
				// not have focus.
				// for tooltips opened via the api: we need to check if it has
				// the forcedOpen flag.
				if (tipElement.data(DATA_MOUSEONTOTIP)) {
					if (!isMouseOver(tipElement)) {
						isDesynced = true;
					}
				} else {
					isDesynced = true;
				}
			}

			if (isDesynced) {
				// close the desynced tip
				hideTip(session.activeHover);
			}
		}
	}

	// expose methods
	this.showTip = beginShowTip;
	this.hideTip = hideTip;
	this.resetPosition = positionTipOnElement;
}
<|endoftext|>


<commit_before>import { A as emberA } from '@ember/array';
import EmberObject from '@ember/object';
import { groupByPath } from 'ember-cli-group-by/macros';
import { module, skip, test } from 'ember-qunit';

module('Unit | Macro | group by path', {
  unit: true,
});

const TestSingle = EmberObject.extend({
  array: null,
  singleGroup: groupByPath('array', 'category'),
});

test('It groups by given single path', function (assert) {
  const subject = TestSingle.create({
    array: emberA([
      { name: '1', category: 'A', },
      { name: '2', category: 'A', },
      { name: '3', category: 'B', },
      { name: '4', category: 'B', },
    ]),
  });
  return subject.get('singleGroup').then((grouped) => {
    assert.deepEqual(Object.keys(grouped), ['A', 'B']);
  });
});

const TestNested = EmberObject.extend({
  array: null,
  doubleGroup: groupByPath('array', 'category.type'),
});

skip('It groups by given nested path', function (assert) {
  const subject = TestNested.create({
    array: emberA([
      { name: '1', category: { type: 'A' }, },
      { name: '2', category: { type: 'A' }, },
      { name: '3', category: { type: 'B' }, },
      { name: '4', category: { type: 'B' }, },
    ]),
  });
  return subject.get('doubleGroup').then((grouped) => {
    assert.deepEqual(Object.keys(grouped), ['A', 'B']);
  });
});
<commit_msg>Fix groups by given nested path test<commit_after>import EmberObject from '@ember/object';
import { groupByPath } from 'ember-cli-group-by/macros';
import { module, setupTest, test } from 'ember-qunit';

module('Unit | Macro | group by path', function (hooks) {
  setupTest(hooks);

  const TestSingle = EmberObject.extend({
    items: undefined,
    grouped: groupByPath('items', 'category'),
  });

  test('It groups by given single path', async function (assert) {
    const subject = TestSingle.create({
      items: [
        { name: '1', category: 'A', },
        { name: '2', category: 'A', },
        { name: '3', category: 'B', },
        { name: '4', category: 'B', },
      ],
    });
    const grouped = await subject.get('grouped');

    const keys = Object.keys(grouped);
    assert.deepEqual(keys, ['A', 'B']);
  });

  const TestNested = EmberObject.extend({
    items: undefined,
    grouped: groupByPath('items', 'category.type'),
  });

  test('It groups by given nested path', async function (assert) {
    const subject = TestNested.create({
      items: [
        { name: '1', category: { type: 'A' }, },
        { name: '2', category: { type: 'A' }, },
        { name: '3', category: { type: 'B' }, },
        { name: '4', category: { type: 'B' }, },
      ],
    });
    const grouped = await subject.get('grouped');

    const keys = Object.keys(await grouped);
    assert.deepEqual(keys, ['A', 'B']);
  });
});
<|endoftext|>


<commit_before>'use strict';

const http = require('http');
const https = require('https');
const { transform } = require('../transforms/transform');
const { removeStyles } = require('../transforms/style');
const { toJSON, maxDepth } = require('../transforms/object');

module.exports = remoteTransportFactory;

function remoteTransportFactory(logger) {
  return Object.assign(transport, {
    client: { name: 'electron-application' },
    depth: 6,
    level: false,
    requestOptions: {},
    onError: null,
    transforms: [removeStyles, toJSON, maxDepth],

    makeBodyFn({ message }) {
      return JSON.stringify({
        client: transport.client,
        data: message.data,
        date: message.date.getTime(),
        level: message.level,
        scope: message.scope,
        variables: message.variables,
      });
    },
  });

  function transport(message) {
    if (!transport.url) {
      return;
    }

    const body = transport.makeBodyFn({
      logger,
      message: { ...message, data: transform({ logger, message, transport }) },
      transport,
    });

    const request = post(
      transport.url,
      transport.requestOptions,
      Buffer.from(body, 'utf8'),
    );

    request.on('error', transport.onError || onError);

    function onError(error) {
      logger.processMessage(
        {
          data: [`electron-log: can't POST ${transport.url}`, error],
          level: 'warn',
        },
        { transports: ['console', 'file'] },
      );
    }
  }
}

function post(serverUrl, requestOptions, body) {
  const httpTransport = serverUrl.startsWith('https:') ? https : http;

  const request = httpTransport.request(serverUrl, {
    method: 'POST',
    ...requestOptions,
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': body.length,
      ...requestOptions.headers,
    },
  });

  request.write(body);
  request.end();

  return request;
}
<commit_msg>feat(remote): Rename option onError -> processErrorFn<commit_after>'use strict';

const http = require('http');
const https = require('https');
const { transform } = require('../transforms/transform');
const { removeStyles } = require('../transforms/style');
const { toJSON, maxDepth } = require('../transforms/object');

module.exports = remoteTransportFactory;

function remoteTransportFactory(logger) {
  return Object.assign(transport, {
    client: { name: 'electron-application' },
    depth: 6,
    level: false,
    requestOptions: {},
    transforms: [removeStyles, toJSON, maxDepth],

    makeBodyFn({ message }) {
      return JSON.stringify({
        client: transport.client,
        data: message.data,
        date: message.date.getTime(),
        level: message.level,
        scope: message.scope,
        variables: message.variables,
      });
    },

    processErrorFn({ error }) {
      logger.processMessage(
        {
          data: [`electron-log: can't POST ${transport.url}`, error],
          level: 'warn',
        },
        { transports: ['console', 'file'] },
      );
    },
  });

  function transport(message) {
    if (!transport.url) {
      return;
    }

    const body = transport.makeBodyFn({
      logger,
      message: { ...message, data: transform({ logger, message, transport }) },
      transport,
    });

    const request = post(
      transport.url,
      transport.requestOptions,
      Buffer.from(body, 'utf8'),
    );

    request.on('error', (error) => transport.processErrorFn({
      error,
      logger,
      message,
      request,
      transport,
    }));
  }
}

function post(serverUrl, requestOptions, body) {
  const httpTransport = serverUrl.startsWith('https:') ? https : http;

  const request = httpTransport.request(serverUrl, {
    method: 'POST',
    ...requestOptions,
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': body.length,
      ...requestOptions.headers,
    },
  });

  request.write(body);
  request.end();

  return request;
}
<|endoftext|>


<commit_before>/*

To do:

Configurables?
 - value coercian, e.g. try throw '12345' catch 12345 succeeds - default disabled
 - primitive coercian, e.g. try throw 12345 catch Number succeeds - default enabled
 - case-sensitivity - not yet implemented - also, what if there are both string and String objects... catch('string')...?

? catch(/regex-pattern/)

? by value deep equal

Notes:

 - catching precedence (contrived example): {throw 12345} {throw 'Number'} {catch 'Number' gets the second}
 - instanceof fails across frames, e.g. inside an iframe: [] instance of window.top.Array === false

*/

define(function defineTryCatchFinally() {

	String.prototype.__coerceToObject__ =
	Number.prototype.__coerceToObject__ =
	Boolean.prototype.__coerceToObject__ =
	function __coerceToObject__() { return this; };

	function canCoerceToObject(obj) {
		return obj !== undefined
			&& obj !== null
			&& typeof obj.__coerceToObject__ === 'function';
	}

	function CatchChecker(caughtError) {
		this.caughtError = caughtError;
	}

	CatchChecker.prototype.byValue = function byValue(value) {
		return this.caughtError === value;
	};

	CatchChecker.prototype.byName = function byName(name) {

		var typeToCatchPattern, errorAsString;

		if (typeof name !== 'string') return false;

		typeToCatchPattern = new RegExp('^\\[object ' + name + '\\]$');

		errorAsString = Object.prototype.toString.call(this.caughtError);

		return typeToCatchPattern.test(errorAsString);
	};

	CatchChecker.prototype.byConstructor = function byConstructor(constructor) {
		var caughtError = this.caughtError,
			errorAsObject = canCoerceToObject(caughtError) ? caughtError.__coerceToObject__() : caughtError;

		return (typeof constructor === 'function') && (errorAsObject instanceof constructor);
	};

	function errorShouldBeCaught(caughtError, toCatch) {
		var errorAsObject,
			canCatch = new CatchChecker(caughtError);

		return canCatch.byValue(toCatch)
			|| canCatch.byName(toCatch)
			|| canCatch.byConstructor(toCatch);

	}

	function TryCatchFinally(tryBlock) {

		var error = {
			raw: undefined,
			exists: false, // undefined can be thrown/caught so cannot check raw for undefined for presence of error
			handled: false
		};

		try {
			tryBlock();
		}
		catch (e) {
			error.raw = e;
			error.exists = true;
		}

		this['catch'] = function (toCatch, handleError) {
			// check catch callback only expecting one argument?
			// configurable to only catch explicitly, i.e. catch(Object) would not catch a Number
			// check arguments more strictly? throw if not function and numArgs === 1?

			function handleSuccessfulCatch() {
				handleError(error.raw);
				setErrorHandled();
			}

			var numArgs = arguments.length;

			if (numArgs > 0 && error.exists && !error.handled) {

				if (numArgs === 1) { // indiscriminate catch
					handleError = toCatch;
					toCatch = undefined;
					handleSuccessfulCatch();
				}
				else if (errorShouldBeCaught(error.raw, toCatch)) { // specific catch
					handleSuccessfulCatch();
				}

			}

			return this;
		};

		this['finally'] = function (finallyBlock) {
			if (finallyBlock) finallyBlock();
			if (error.exists && !error.handled) throw error.raw;
		};

		function setErrorHandled() { error.handled = true; }

	}

	return function _try(tryBlock) {
		return new TryCatchFinally(tryBlock);
	};

});<commit_msg>Update comment notes<commit_after>/*

To do:

Configurables?
 - value coercian, e.g. try throw '12345' catch 12345 succeeds - default disabled
 - primitive coercian, e.g. try throw 12345 catch Number succeeds - default enabled
 - case-sensitivity - not yet implemented - also, what if there are both string and String objects... catch('string')...?
 - hierarchical catching - not yet implemented - only catch explicitly, e.g. catch(Object) would not catch a Number

? catch(/regex-pattern/)

? by value deep equal

? check catch callback only expecting one argument

? check arguments more strictly? throw if not function and numArgs === 1

---

Notes:

 - catching precedence (contrived example): {throw 12345} {throw 'Number'} {catch 'Number' gets the second}
 - instanceof fails across frames, e.g. inside an iframe: [] instance of window.top.Array === false

*/

define(function defineTryCatchFinally() {

	String.prototype.__coerceToObject__ =
	Number.prototype.__coerceToObject__ =
	Boolean.prototype.__coerceToObject__ =
	function __coerceToObject__() { return this; };

	function canCoerceToObject(obj) {
		return obj !== undefined
			&& obj !== null
			&& typeof obj.__coerceToObject__ === 'function';
	}

	function CatchChecker(caughtError) {
		this.caughtError = caughtError;
	}

	CatchChecker.prototype.byValue = function byValue(value) {
		return this.caughtError === value;
	};

	CatchChecker.prototype.byName = function byName(name) {

		var typeToCatchPattern, errorAsString;

		if (typeof name !== 'string') return false;

		typeToCatchPattern = new RegExp('^\\[object ' + name + '\\]$');

		errorAsString = Object.prototype.toString.call(this.caughtError);

		return typeToCatchPattern.test(errorAsString);
	};

	CatchChecker.prototype.byConstructor = function byConstructor(constructor) {

		var caughtError = this.caughtError,
			errorAsObject = canCoerceToObject(caughtError) ? caughtError.__coerceToObject__() : caughtError;

		return (typeof constructor === 'function') && (errorAsObject instanceof constructor);
	};

	function errorShouldBeCaught(caughtError, toCatch) {

		var canCatch = new CatchChecker(caughtError);

		return canCatch.byValue(toCatch)
			|| canCatch.byName(toCatch)
			|| canCatch.byConstructor(toCatch);

	}

	function TryCatchFinally(tryBlock) {

		var error = {
			raw: undefined,
			exists: false, // undefined can be thrown/caught so cannot check raw for undefined for presence of error
			handled: false
		};

		try {
			tryBlock();
		}
		catch (e) {
			error.raw = e;
			error.exists = true;
		}

		this['catch'] = function (toCatch, handleError) {

			function handleSuccessfulCatch() {
				handleError(error.raw);
				setErrorHandled();
			}

			var numArgs = arguments.length;

			if (numArgs > 0 && error.exists && !error.handled) {

				if (numArgs === 1) { // indiscriminate catch
					handleError = toCatch;
					toCatch = undefined;
					handleSuccessfulCatch();
				}
				else if (errorShouldBeCaught(error.raw, toCatch)) { // specific catch
					handleSuccessfulCatch();
				}

			}

			return this;
		};

		this['finally'] = function (finallyBlock) {
			if (finallyBlock) finallyBlock();
			if (error.exists && !error.handled) throw error.raw;
		};

		function setErrorHandled() { error.handled = true; }

	}

	return function _try(tryBlock) {
		return new TryCatchFinally(tryBlock);
	};

});<|endoftext|>


<commit_before>// Load dependencies
var amanda = require('../../../dist/latest.js');

/**
 * Test #1
 */
exports['Test #1'] = function(test) {

  var count = 0;

  var schema = {
    required: true,
    format: 'alphanumeric'
  };

  [
    'abc123',
    '123abc',
    'a1b2c3',
    'abc',
    'ABC',
    'Abc',
    '123',
    123
  ].forEach(function(data) {
    amanda.validate(data, schema, function(error) {
      count += 1;
      test.equal(error, undefined);
    });
  });

  [
    '+@#$~^*{}',
    'lorem ipsum',
    ' ',
    null,
    [],
    {},
    function() {},
    null,
    undefined
  ].forEach(function(data) {
    amanda.validate(data, schema, function(error) {
      count += 1;
      test.ok(error);
    });
  });

  test.equal(count, 17);
  test.done();

};<commit_msg>Remove the obsolete ‘alphanumeric’ test suite<commit_after><|endoftext|>


<commit_before>import React from 'react';
import { NavLink } from 'react-router-dom';
import { themeSettings, text } from '../lib/settings';
import * as helper from '../lib/helper';

const GalleryItemImage = ({ category }) => {
	const imageUrl =
		category.image && category.image.length > 0
			? helper.getThumbnailUrl(
					category.image,
					themeSettings.category_list_thumbnail_width
			  )
			: '';

	if (imageUrl && imageUrl !== '') {
		return (

		);
	} else {
		return null;
	}
};

const GalleryItem = ({ category }) => {
	return (

			<NavLink to={category.path}>

					<GalleryItemImage category={category} />

{category.name}

			</NavLink>

	);
};

const CategoryGallery = ({ categories }) => {
	const items = categories
		.filter(category => category.parent_id === null)
		.map((category, index) => <GalleryItem key={index} category={category} />);
	return
{items}
;
};

export default CategoryGallery;
<commit_msg>Removed unused component<commit_after><|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');

module.exports = function navigationMapper_ERROR(aliasMap, log) {

  var debug = log.debug;

  function errorMapper (pages, key) {
    var res = {
      name: 'Errors',
      title: 'Error Reference',
      type: 'groups',
      href: key,
      navItems: []
    };
    _(pages)
      .filter('module')  // take only the modules on the top level
      .sortBy('module')  // alphabetic sort
      .groupBy('module') // group by name
      .forEach(function (components, moduleName) {
        debug('Processing module: %s', moduleName);
        var mod = _.find(components, {name: moduleName, module: moduleName});

        var navGroup = {
          name: moduleName,
          type: 'section',
          href: mod.path, // 'api/' + moduleName, // conforms path calculation in docgen.js
          navItems: []
        };
        res.navItems.push(navGroup);

        _(components)
        .filter(function (it) {
          return it.docType !== 'module' && it.docType != 'overview';
        })
        .sort('name')
        .forEach(function (error) {
          navGroup.navItems.push({
            name: error.name,
            type: error.docType,
            href: error.path,
          });
        });
      });

    return [res];
  };

  Object.defineProperty(errorMapper, 'area', {
    value: 'error'
  });

  Object.defineProperty(errorMapper, 'title', {
    value: 'Errors'
  });

  return errorMapper;
};
<commit_msg>lodash compatibility fix<commit_after>'use strict';

var _ = require('lodash');

module.exports = function navigationMapper_ERROR(aliasMap, log) {

  var debug = log.debug;

  function errorMapper (pages, key) {
    var res = {
      name: 'Errors',
      title: 'Error Reference',
      type: 'groups',
      href: key,
      navItems: []
    };
    _(pages)
      .filter('module')  // take only the modules on the top level
      .sortBy('module')  // alphabetic sort
      .groupBy('module') // group by name
      .forEach(function (components, moduleName) {
        debug('Processing module: %s', moduleName);
        var mod = _.find(components, {name: moduleName, module: moduleName});

        var navGroup = {
          name: moduleName,
          type: 'section',
          href: mod.path, // 'api/' + moduleName, // conforms path calculation in docgen.js
          navItems: []
        };
        res.navItems.push(navGroup);

        _(components)
        .filter(function (it) {
          return it.docType !== 'module' && it.docType != 'overview';
        })
        .sortBy('name')
        .forEach(function (error) {
          navGroup.navItems.push({
            name: error.name,
            type: error.docType,
            href: error.path,
          });
        });
      });

    return [res];
  };

  Object.defineProperty(errorMapper, 'area', {
    value: 'error'
  });

  Object.defineProperty(errorMapper, 'title', {
    value: 'Errors'
  });

  return errorMapper;
};
<|endoftext|>


<commit_before>describe('module:hashMap', function() {
    var hashMap;

    beforeEach(function() {
        hashMap = new ne.util.HashMap();
    });

    describe('HashMap이 존재한다.', function() {
        it('HashMap은 정의되어있다.', function() {
            expect(ne.util.HashMap).toBeDefined();
        });

        it('hashMap의 인스턴스가 존재한다.', function() {
            expect(hashMap).toBeDefined();
        });
    });

    describe('setKeyValue()', function() {
        it('key와 value를 전달하여 데이터를 저장한다..', function() {
            hashMap.setKeyValue('dataKey', 'data');
            hashMap.setKeyValue('dataKey2', 'data');
            expect(hashMap.get('dataKey')).toEqual('data');
            expect(hashMap.length).toEqual(2);
        });

        it('같은 key에 값을 두번 셋팅하면 length에 변함이 없다.', function() {
            hashMap.setKeyValue('dataKey', 'data');
            hashMap.setKeyValue('dataKey', 'data2');
            expect(hashMap.get('dataKey')).toEqual('data2');
            expect(hashMap.length).toEqual(1);
        });
    });

    describe('setObject()', function() {
        it('객체를 전달하여 데이터를 저장한다..', function() {
            hashMap.setObject({
                'dataKey': 'data',
                'dataKey2': 'data'
            });
            expect(hashMap.get('dataKey')).toEqual('data');
            expect(hashMap.length).toEqual(2);
        });
    });

    describe('merge()', function() {
        it('해쉬맵을 인자로 받아 병합한다.', function() {
            var mergedHashMap = new ne.HashMap();

            hashMap.setObject({
                'dataKey': 'data',
                'dataKey2': 'data'
            });

            mergedHashMap.merge(hashMap);

            expect(mergedHashMap.get('dataKey')).toEqual('data');
            expect(mergedHashMap.length).toEqual(2);
        });
    });

    describe('set()', function() {
        it('key와 value를 전달하여 데이터를 저장한다..', function() {
            hashMap.set('dataKey', 'data');
            expect(hashMap.get('dataKey')).toEqual('data');
        });
        it('객체를 전달하여 데이터를 저장한다..', function() {
            hashMap.set({
                'dataKey': 'data'
            });
            expect(hashMap.get('dataKey')).toEqual('data');
        });
    });

    describe('get()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
        });

        it('키를 넘겨 값을 가져온다.', function() {
            var v = hashMap.get('dataKey');
            expect(v).toEqual('data');
        });
    });

    describe('has()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
        });

        it('존재하는 키에대해 true를 리턴한다.', function() {
            var v = hashMap.has('dataKey');
            expect(v).toEqual(true);
        });

        it('존재하지 않는 키에대해 false를 리턴한다.', function() {
            var v = hashMap.has('notAKey');
            expect(v).toEqual(false);
        });
    });

    describe('encodeKey()', function() {
        it('키로 사용할 문자열을 넘기면 프리픽스를 인코딩해 리턴한다.', function() {
            var encodedKey = hashMap.encodeKey('dataKey');
            expect(encodedKey).toEqual('ådataKey');
        });
    });

    describe('decodeKey()', function() {
        it('키를 넘기면 프리픽스를 제거해 리턴한다.', function() {
            var encodedKey = hashMap.decodeKey('ådataKey');
            expect(encodedKey).toEqual('dataKey');
        });
    });

    describe('remove()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
            hashMap.set('dataKey2', 'data');
        });

        it('키를 넘겨 데이터를 삭제한다.', function() {
            hashMap.remove('dataKey');
            expect(hashMap.has('dataKey')).toEqual(false);
            expect(hashMap.length).toEqual(1);
        });

        it('키를 여러개 넘겨 데이터를 삭제한다.', function() {
            var v = hashMap.remove('dataKey', 'dataKey2');
            expect(v).toEqual(['data', 'data']);
            expect(hashMap.length).toEqual(0);
        });

        it('키의 배열을 넘겨 데이터를 삭제한다.', function() {
            var v = hashMap.remove(['dataKey', 'dataKey2']);
            expect(v).toEqual(['data', 'data']);
            expect(hashMap.length).toEqual(0);
        });

        it('삭제된 데이터가 리턴된다.', function() {
            var v = hashMap.remove('dataKey');
            expect(v).toEqual('data');
        });

        it('없는키를 삭제하면 null이 리턴되고 length는 변함이 없다.', function() {
            var v = hashMap.remove('dataKey1');
            expect(v).toBe(null);
            expect(hashMap.length).toEqual(2);
        });
    });

    describe('removeByKey()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
        });

        it('키를 넘겨 데이터를 삭제한다.', function() {
            hashMap.removeByKey('dataKey');
            var v = hashMap.has('dataKey');
            expect(v).toEqual(false);
            expect(hashMap.length).toEqual(0);
        });

        it('삭제된 데이터가 리턴된다.', function() {
            var v = hashMap.removeByKey('dataKey');
            expect(v).toEqual('data');
        });

        it('없는키를 삭제하면 null이 리턴되고 length는 변함이 없다.', function() {
            var v = hashMap.removeByKey('dataKey1');
            expect(v).toBe(null);
            expect(hashMap.length).toEqual(1);
        });
    });

    describe('removeByKeyArray()', function() {
        beforeEach(function() {
            hashMap.set('key', 'data');
            hashMap.set('key2', 'data');
        });

        it('키의 목록을 넘겨 데이터를 삭제한다.', function() {
            hashMap.removeByKeyArray(['key', 'key2']);
            expect(hashMap.has('key')).toEqual(false);
            expect(hashMap.has('key2')).toEqual(false);
            expect(hashMap.length).toEqual(0);
        });

        it('삭제된 데이터가 리턴된다.', function() {
            var v = hashMap.removeByKeyArray(['key', 'key2']);
            expect(v).toEqual(['data', 'data']);
        });
    });

    describe('removeAll()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data2');
            hashMap.set('key3', 'data3');
        });

        it('모든 값을 지운다.', function() {
            hashMap.removeAll();

            expect(hashMap.get('key1')).not.toBeDefined();
            expect(hashMap.get('key2')).not.toBeDefined();
            expect(hashMap.get('key3')).not.toBeDefined();
            expect(hashMap.length).toEqual(0);
        });
    });

    describe('each()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data2');
            hashMap.set('key3', 'data3');
        });

        it('데이터를 순회하여 넘겨받은 콜백에 넘겨준다.', function() {
            var sumValue = '';
            var sumKey = '';

            hashMap.each(function(value, key) {
                sumValue += value;
                sumKey += key;
            });

            expect(sumValue).toEqual('data1data2data3');
            expect(sumKey).toEqual('key1key2key3');
        });
    });

    describe('keys()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data2');
            hashMap.set('key3', 'data3');
        });

        it('저장되어있는 Key를 모와서 배열로 리턴해준다.', function() {
            var keys;

            keys = hashMap.keys();

            expect(keys).toEqual(['key1', 'key2', 'key3']);
        });
    });

    describe('find()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data');
            hashMap.set('key3', 'data');
        });

        it('조건을 체크하는 펑션을 넘겨 데이터의 배열을 구할수있다.', function() {
            var dataByValue, dataByKey;

            dataByValue = hashMap.find(function(value) {
                return value === 'data';
            });

            dataByKey = hashMap.find(function(value, key) {
                return key === 'key1';
            });

            expect(dataByValue).toEqual(['data', 'data']);
            expect(dataByKey).toEqual(['data1']);
        });
    });
});

<commit_msg>error fix in test code<commit_after>describe('module:hashMap', function() {
    var hashMap;

    beforeEach(function() {
        hashMap = new ne.util.HashMap();
    });

    describe('HashMap이 존재한다.', function() {
        it('HashMap은 정의되어있다.', function() {
            expect(ne.util.HashMap).toBeDefined();
        });

        it('hashMap의 인스턴스가 존재한다.', function() {
            expect(hashMap).toBeDefined();
        });
    });

    describe('setKeyValue()', function() {
        it('key와 value를 전달하여 데이터를 저장한다..', function() {
            hashMap.setKeyValue('dataKey', 'data');
            hashMap.setKeyValue('dataKey2', 'data');
            expect(hashMap.get('dataKey')).toEqual('data');
            expect(hashMap.length).toEqual(2);
        });

        it('같은 key에 값을 두번 셋팅하면 length에 변함이 없다.', function() {
            hashMap.setKeyValue('dataKey', 'data');
            hashMap.setKeyValue('dataKey', 'data2');
            expect(hashMap.get('dataKey')).toEqual('data2');
            expect(hashMap.length).toEqual(1);
        });
    });

    describe('setObject()', function() {
        it('객체를 전달하여 데이터를 저장한다..', function() {
            hashMap.setObject({
                'dataKey': 'data',
                'dataKey2': 'data'
            });
            expect(hashMap.get('dataKey')).toEqual('data');
            expect(hashMap.length).toEqual(2);
        });
    });

    describe('merge()', function() {
        it('해쉬맵을 인자로 받아 병합한다.', function() {
            var mergedHashMap = new ne.util.HashMap();

            hashMap.setObject({
                'dataKey': 'data',
                'dataKey2': 'data'
            });

            mergedHashMap.merge(hashMap);

            expect(mergedHashMap.get('dataKey')).toEqual('data');
            expect(mergedHashMap.length).toEqual(2);
        });
    });

    describe('set()', function() {
        it('key와 value를 전달하여 데이터를 저장한다..', function() {
            hashMap.set('dataKey', 'data');
            expect(hashMap.get('dataKey')).toEqual('data');
        });
        it('객체를 전달하여 데이터를 저장한다..', function() {
            hashMap.set({
                'dataKey': 'data'
            });
            expect(hashMap.get('dataKey')).toEqual('data');
        });
    });

    describe('get()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
        });

        it('키를 넘겨 값을 가져온다.', function() {
            var v = hashMap.get('dataKey');
            expect(v).toEqual('data');
        });
    });

    describe('has()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
        });

        it('존재하는 키에대해 true를 리턴한다.', function() {
            var v = hashMap.has('dataKey');
            expect(v).toEqual(true);
        });

        it('존재하지 않는 키에대해 false를 리턴한다.', function() {
            var v = hashMap.has('notAKey');
            expect(v).toEqual(false);
        });
    });

    describe('encodeKey()', function() {
        it('키로 사용할 문자열을 넘기면 프리픽스를 인코딩해 리턴한다.', function() {
            var encodedKey = hashMap.encodeKey('dataKey');
            expect(encodedKey).toEqual('ådataKey');
        });
    });

    describe('decodeKey()', function() {
        it('키를 넘기면 프리픽스를 제거해 리턴한다.', function() {
            var encodedKey = hashMap.decodeKey('ådataKey');
            expect(encodedKey).toEqual('dataKey');
        });
    });

    describe('remove()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
            hashMap.set('dataKey2', 'data');
        });

        it('키를 넘겨 데이터를 삭제한다.', function() {
            hashMap.remove('dataKey');
            expect(hashMap.has('dataKey')).toEqual(false);
            expect(hashMap.length).toEqual(1);
        });

        it('키를 여러개 넘겨 데이터를 삭제한다.', function() {
            var v = hashMap.remove('dataKey', 'dataKey2');
            expect(v).toEqual(['data', 'data']);
            expect(hashMap.length).toEqual(0);
        });

        it('키의 배열을 넘겨 데이터를 삭제한다.', function() {
            var v = hashMap.remove(['dataKey', 'dataKey2']);
            expect(v).toEqual(['data', 'data']);
            expect(hashMap.length).toEqual(0);
        });

        it('삭제된 데이터가 리턴된다.', function() {
            var v = hashMap.remove('dataKey');
            expect(v).toEqual('data');
        });

        it('없는키를 삭제하면 null이 리턴되고 length는 변함이 없다.', function() {
            var v = hashMap.remove('dataKey1');
            expect(v).toBe(null);
            expect(hashMap.length).toEqual(2);
        });
    });

    describe('removeByKey()', function() {
        beforeEach(function() {
            hashMap.set('dataKey', 'data');
        });

        it('키를 넘겨 데이터를 삭제한다.', function() {
            hashMap.removeByKey('dataKey');
            var v = hashMap.has('dataKey');
            expect(v).toEqual(false);
            expect(hashMap.length).toEqual(0);
        });

        it('삭제된 데이터가 리턴된다.', function() {
            var v = hashMap.removeByKey('dataKey');
            expect(v).toEqual('data');
        });

        it('없는키를 삭제하면 null이 리턴되고 length는 변함이 없다.', function() {
            var v = hashMap.removeByKey('dataKey1');
            expect(v).toBe(null);
            expect(hashMap.length).toEqual(1);
        });
    });

    describe('removeByKeyArray()', function() {
        beforeEach(function() {
            hashMap.set('key', 'data');
            hashMap.set('key2', 'data');
        });

        it('키의 목록을 넘겨 데이터를 삭제한다.', function() {
            hashMap.removeByKeyArray(['key', 'key2']);
            expect(hashMap.has('key')).toEqual(false);
            expect(hashMap.has('key2')).toEqual(false);
            expect(hashMap.length).toEqual(0);
        });

        it('삭제된 데이터가 리턴된다.', function() {
            var v = hashMap.removeByKeyArray(['key', 'key2']);
            expect(v).toEqual(['data', 'data']);
        });
    });

    describe('removeAll()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data2');
            hashMap.set('key3', 'data3');
        });

        it('모든 값을 지운다.', function() {
            hashMap.removeAll();

            expect(hashMap.get('key1')).not.toBeDefined();
            expect(hashMap.get('key2')).not.toBeDefined();
            expect(hashMap.get('key3')).not.toBeDefined();
            expect(hashMap.length).toEqual(0);
        });
    });

    describe('each()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data2');
            hashMap.set('key3', 'data3');
        });

        it('데이터를 순회하여 넘겨받은 콜백에 넘겨준다.', function() {
            var sumValue = '';
            var sumKey = '';

            hashMap.each(function(value, key) {
                sumValue += value;
                sumKey += key;
            });

            expect(sumValue).toEqual('data1data2data3');
            expect(sumKey).toEqual('key1key2key3');
        });
    });

    describe('keys()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data2');
            hashMap.set('key3', 'data3');
        });

        it('저장되어있는 Key를 모와서 배열로 리턴해준다.', function() {
            var keys;

            keys = hashMap.keys();

            expect(keys).toEqual(['key1', 'key2', 'key3']);
        });
    });

    describe('find()', function() {
        beforeEach(function() {
            hashMap.set('key1', 'data1');
            hashMap.set('key2', 'data');
            hashMap.set('key3', 'data');
        });

        it('조건을 체크하는 펑션을 넘겨 데이터의 배열을 구할수있다.', function() {
            var dataByValue, dataByKey;

            dataByValue = hashMap.find(function(value) {
                return value === 'data';
            });

            dataByKey = hashMap.find(function(value, key) {
                return key === 'key1';
            });

            expect(dataByValue).toEqual(['data', 'data']);
            expect(dataByKey).toEqual(['data1']);
        });
    });
});

<|endoftext|>


<commit_before>'use strict';

// See original base source for this file (and it's apache2 license) in
// https://googlechrome.github.io/samples/service-worker/custom-offline-page/

// TODO explain this
const CACHE_VERSION = 1;
let CURRENT_CACHES = {
  offline: 'offline-v' + CACHE_VERSION
};

let POST_CMDS = {
  install: 'INSTALL_URL',
  primer: 'PRIMER_STATUS', // TODO emit different stages in `install` handler
};
const FLASHCARD_INDEXES = 'cards.index';
const OFFLINE_URL = 'study.html'
let OFFLINE_URL_DEPS = [
  'study.css',
  'study.js',
  FLASHCARD_INDEXES
];

const CARDSET_URL_PREFIX = 'cards';

function objectVals(obj) {
  return Object.keys(obj).map(function(key) {
    return obj[key];
  });
}

function createCacheBustedRequest(url) {
  let request = new Request(url, {cache: 'reload'});
  // See https://fetch.spec.whatwg.org/#concept-request-mode
  if ('cache' in request) {
    return request;
  }

  // if {cache: 'reload'} didn't have any effect, append a cache-busting URL
  // parameter instead
  let bustedUrl = new URL(url, self.location.href);
  bustedUrl.search += (bustedUrl.search ? '&' : '') + 'cachebust=' + Date.now();
  return new Request(bustedUrl);
}

function refreshDepsCache(cache) {
  return Promise.all(OFFLINE_URL_DEPS.map(depUrl => {
    return fetch(depUrl).then(function(depResp) {
      return cache.put(depUrl, depResp.clone());
    });
  }));
}

let installKeyVal = function(cmd, payload) {
  return self.clients.matchAll({includeUncontrolled: true}).then(allClients => {
    return Promise.all(allClients.map(client => {
      client.postMessage(JSON.stringify({cmd: cmd, payload: payload}));
    }));
  });
};

let parseCardIndex = function(lines, parentUrl) {
  lines.sort((a, b) => {
    const ignoreSideRegexp = /\b(front|back)\b/;
    let aSideless = a.replace(ignoreSideRegexp, '');
    let bSideless = b.replace(ignoreSideRegexp, '');
    if (aSideless  == bSideless) {
      return 0;
    }
    return aSideless < bSideless ? -1 : 1;
  });

  let relCardToUrl = function(relUrl) { return parentUrl + '/' + relUrl; };

  return lines.reduce(function(accum, curVal, curIdx, lns) {
    if (!(curIdx % 2)) {
      return accum; // we're on the front of a card
    }

    let a = lns[curIdx - 1];
    let b = curVal;

    let pair = {};
    if (a.match(/\bfront\b/)) {
      pair.front = relCardToUrl(a);
      pair.back = relCardToUrl(b);
    } else {
      pair.front = relCardToUrl(b);
      pair.back = relCardToUrl(a);
    }

    accum.push(pair);
    return accum;
  }, [/*initialValue*/]);
};

function refreshFlashcards(fileCache) {
  let parseIndexTxt = function(rawIndexTxt) {
    return rawIndexTxt.split("\n").filter(e => {
      return Boolean(
          e &&
          e.length &&
          e.match(/[^\s]/)) // nonempty lines
    });
  };
  return fileCache.match(FLASHCARD_INDEXES).then(function(resp) {
    return resp.clone().text()
        .then(parseIndexTxt)
        .then(cardSets => {
          return installKeyVal(POST_CMDS.install, {url: FLASHCARD_INDEXES, resp: cardSets})
              .then(_ => { return cardSets;});
        })
        .then(cardSets => {
          return Promise.all(cardSets
              .map(cardSet => {
                return CARDSET_URL_PREFIX + '/' + cardSet;
              })
              .map(cardSetUrl => {
                let cardIndex;
                let metadataFetch = function(metadata) {
                  let metadataUrl = cardSetUrl + '/' + metadata;
                  return fetch(metadataUrl).then(resp => {
                    return resp.text().then(body => {
                      let content;
                      if (metadata == 'index') {
                        content = cardIndex = parseCardIndex(
                            parseIndexTxt(body), cardSetUrl);
                      } else {
                        content = body.trim();
                      }
                      return installKeyVal(POST_CMDS.install, {url: metadataUrl, resp: content});
                    });
                  });
                };
                return Promise
                  .all([
                    metadataFetch('index'),
                    metadataFetch('title'),
                    metadataFetch('description'),
                  ]).
                  then(_ => {
                    return Promise.all(cardIndex.map(cardPair => {
                      let cacheCard = function(cardUrl) {
                        return fetch(cardUrl).then(imgResp => {
                          return fileCache.put(cardUrl, imgResp.clone());
                        });
                      };
                      return Promise.all([
                        cacheCard(cardPair.front),
                        cacheCard(cardPair.back),
                      ]);
                    }));
                  });
              }));
        });
  });
}

self.addEventListener('install', event => {
  // We can't use cache.add() here, since we want OFFLINE_URL to be the cache
  // key, but the actual URL we end up requesting might include a cache-busting
  // parameter.
  event.waitUntil(
      fetch(createCacheBustedRequest(OFFLINE_URL))
          .then(function(freshResponse) {
            return caches
                .open(CURRENT_CACHES.offline)
                .then(openCache => {
                  return openCache
                      .put(OFFLINE_URL, freshResponse)
                      .then(_ => refreshDepsCache(openCache))
                      .then(_ => refreshFlashcards(openCache));
                });
          }));
});

// Delete all caches that aren't named in CURRENT_CACHES. While there is only
// one cache in this example, the same logic will handle the case where there
// exists multiple versioned caches
self.addEventListener('activate', event => {
  let expectedCacheNames = objectVals(CURRENT_CACHES);

  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (expectedCacheNames.indexOf(cacheName) === -1) {
            console.log('Deleting out of data cache: "%s"', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

let requestLooksLikePageNav = function(request) {
  // We only want to call event.respondWith() if this is a navigation request
  // for an HTML page.
  // request.mode of 'navigate' is unfortunately not supported in Chrome
  // versions older than 49, so we need to include a less precise fallback,
  // which checks for a GET request with an Accept: text/html header.
  return request.mode === 'navigate' || (
        request.method === 'GET' &&
        request.headers.get('accept').includes('text/html'));
};

let requestLooksLikeImage = function(request) {
  return Boolean(
      request.method === 'GET' &&
      request.headers.get('accept').match(/image\//)
  );
};

self.addEventListener('fetch', event => {
  if (requestLooksLikePageNav(event.request)) {
    event.respondWith(fetch(event.request).catch(error => {
      // The catch is only triggered if fetch() throws an exception which will
      // most likely happen due to the server being unreachable. If fetch()
      // returns a valid HTTP response with a response code in 4xx or 5xx range,
      // the catch() will NOT be called. If you need custom handling for said
      // ranges, see
      // https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker/fallback-response
      console.log('Fetch failed; returning offline page instead.', error);
      return caches.match(OFFLINE_URL);
    }));
  } else if (requestLooksLikeImage(event.request)) {
    return caches.open(CURRENT_CACHES.offline).then(function(rawUrl, openCache) {
      return openCache.match(rawUrl.replace(location.origin + '/', ''));
    }.bind(null /*this*/, event.request.url));

    // TODO: fix and empower the offline-status dashbaord
    //   normal behavior commented out belwo, but for now, we want to ONLY use
    //   offline storage, unless prompted (ie: by user, to refresh)
//  event.respondWith(fetch(event.request).catch(error => {
//    console.log('Cached fetch of image failed; returning offline page instead.', error);
//
//    // TODO above in-use logic should live here
//  }));
  }

  // If the above if() is false, then this fetch handler won't intercept the
  // request. If there are any other fetch handlers registered, they will get a
  // chance to call event.respondWith(). If no fetch handlers call
  // event.respondWith(), the request will be handled by the browser as if there
  // were no service worker involvement.
});
<commit_msg>more explicitly default to offline<commit_after>'use strict';

// See original base source for this file (and it's apache2 license) in
// https://googlechrome.github.io/samples/service-worker/custom-offline-page/

// TODO explain this
const CACHE_VERSION = 1;
let CURRENT_CACHES = {
  offline: 'offline-v' + CACHE_VERSION
};

let POST_CMDS = {
  install: 'INSTALL_URL',
  primer: 'PRIMER_STATUS', // TODO emit different stages in `install` handler
};
const FLASHCARD_INDEXES = 'cards.index';
const OFFLINE_URL = 'study.html'
let OFFLINE_URL_DEPS = [
  'index.css',
  'study.css',
  'study.js',
  FLASHCARD_INDEXES
];

const CARDSET_URL_PREFIX = 'cards';

function objectVals(obj) {
  return Object.keys(obj).map(function(key) {
    return obj[key];
  });
}

function createCacheBustedRequest(url) {
  let request = new Request(url, {cache: 'reload'});
  // See https://fetch.spec.whatwg.org/#concept-request-mode
  if ('cache' in request) {
    return request;
  }

  // if {cache: 'reload'} didn't have any effect, append a cache-busting URL
  // parameter instead
  let bustedUrl = new URL(url, self.location.href);
  bustedUrl.search += (bustedUrl.search ? '&' : '') + 'cachebust=' + Date.now();
  return new Request(bustedUrl);
}

function refreshDepsCache(cache) {
  return Promise.all(OFFLINE_URL_DEPS.map(depUrl => {
    return fetch(depUrl).then(function(depResp) {
      return cache.put(depUrl, depResp.clone());
    });
  }));
}

let installKeyVal = function(cmd, payload) {
  return self.clients.matchAll({includeUncontrolled: true}).then(allClients => {
    return Promise.all(allClients.map(client => {
      client.postMessage(JSON.stringify({cmd: cmd, payload: payload}));
    }));
  });
};

let parseCardIndex = function(lines, parentUrl) {
  lines.sort((a, b) => {
    const ignoreSideRegexp = /\b(front|back)\b/;
    let aSideless = a.replace(ignoreSideRegexp, '');
    let bSideless = b.replace(ignoreSideRegexp, '');
    if (aSideless  == bSideless) {
      return 0;
    }
    return aSideless < bSideless ? -1 : 1;
  });

  let relCardToUrl = function(relUrl) { return parentUrl + '/' + relUrl; };

  return lines.reduce(function(accum, curVal, curIdx, lns) {
    if (!(curIdx % 2)) {
      return accum; // we're on the front of a card
    }

    let a = lns[curIdx - 1];
    let b = curVal;

    let pair = {};
    if (a.match(/\bfront\b/)) {
      pair.front = relCardToUrl(a);
      pair.back = relCardToUrl(b);
    } else {
      pair.front = relCardToUrl(b);
      pair.back = relCardToUrl(a);
    }

    accum.push(pair);
    return accum;
  }, [/*initialValue*/]);
};

function refreshFlashcards(fileCache) {
  let parseIndexTxt = function(rawIndexTxt) {
    return rawIndexTxt.split("\n").filter(e => {
      return Boolean(
          e &&
          e.length &&
          e.match(/[^\s]/)) // nonempty lines
    });
  };
  return fileCache.match(FLASHCARD_INDEXES).then(function(resp) {
    return resp.clone().text()
        .then(parseIndexTxt)
        .then(cardSets => {
          return installKeyVal(POST_CMDS.install, {url: FLASHCARD_INDEXES, resp: cardSets})
              .then(_ => { return cardSets;});
        })
        .then(cardSets => {
          return Promise.all(cardSets
              .map(cardSet => {
                return CARDSET_URL_PREFIX + '/' + cardSet;
              })
              .map(cardSetUrl => {
                let cardIndex;
                let metadataFetch = function(metadata) {
                  let metadataUrl = cardSetUrl + '/' + metadata;
                  return fetch(metadataUrl).then(resp => {
                    return resp.text().then(body => {
                      let content;
                      if (metadata == 'index') {
                        content = cardIndex = parseCardIndex(
                            parseIndexTxt(body), cardSetUrl);
                      } else {
                        content = body.trim();
                      }
                      return installKeyVal(POST_CMDS.install, {url: metadataUrl, resp: content});
                    });
                  });
                };
                return Promise
                  .all([
                    metadataFetch('index'),
                    metadataFetch('title'),
                    metadataFetch('description'),
                  ]).
                  then(_ => {
                    return Promise.all(cardIndex.map(cardPair => {
                      let cacheCard = function(cardUrl) {
                        return fetch(cardUrl).then(imgResp => {
                          return fileCache.put(cardUrl, imgResp.clone());
                        });
                      };
                      return Promise.all([
                        cacheCard(cardPair.front),
                        cacheCard(cardPair.back),
                      ]);
                    }));
                  });
              }));
        });
  });
}

self.addEventListener('install', event => {
  // We can't use cache.add() here, since we want OFFLINE_URL to be the cache
  // key, but the actual URL we end up requesting might include a cache-busting
  // parameter.
  event.waitUntil(
      fetch(createCacheBustedRequest(OFFLINE_URL))
          .then(function(freshResponse) {
            return caches
                .open(CURRENT_CACHES.offline)
                .then(openCache => {
                  return openCache
                      .put(OFFLINE_URL, freshResponse)
                      .then(_ => refreshDepsCache(openCache))
                      .then(_ => refreshFlashcards(openCache));
                });
          }));
});

// Delete all caches that aren't named in CURRENT_CACHES. While there is only
// one cache in this example, the same logic will handle the case where there
// exists multiple versioned caches
self.addEventListener('activate', event => {
  let expectedCacheNames = objectVals(CURRENT_CACHES);

  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (expectedCacheNames.indexOf(cacheName) === -1) {
            console.log('Deleting out of data cache: "%s"', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

self.addEventListener('fetch', event => {
  // Original if-block from forked demo code can be seen here as of
  // 971147490359, on lines 216-227

  let requestUrlToRelative = function(absUrl) {
    return absUrl.replace(location.origin + '/', '');
  };

  let requestLooksLikeImage = function(request) {
    return Boolean(
        request.method === 'GET' &&
        request.headers.get('accept').match(/image\//)
    );
  };

  let requestLooksLikePageNav = function(request) {
    // We only want to call event.respondWith() if this is a navigation request
    // for an HTML page.
    // request.mode of 'navigate' is unfortunately not supported in Chrome
    // versions older than 49, so we need to include a less precise fallback,
    // which checks for a GET request with an Accept: text/html header.
    return request.mode === 'navigate' || (
          request.method === 'GET' &&
          request.headers.get('accept').includes('text/html'));
  };

  let isRequestToNavToOfflinePage = function(request) {
    return Boolean(
        requestLooksLikePageNav(request) &&
        requestUrlToRelative(request.url) == OFFLINE_URL
    );
  };

  // Default to offline (cached) response of:
  // - offline-ready pages (and its immediate dependencies)
  // - heavily used resources (eg: flashcard images)
  if (isRequestToNavToOfflinePage(event.request) ||
      OFFLINE_URL_DEPS.includes(requestUrlToRelative(event.request.url)) ||
      requestLooksLikeImage(event.request)) {
    return event.respondWith(
        caches
          .open(CURRENT_CACHES.offline)
          .then(function(rawUrl, openCache) {
            return openCache.match(requestUrlToRelative(rawUrl));
          }.bind(null /*this*/, event.request.url))
          .catch(function() {
            // Attempt online requests *last*
            return fetch(event.request);
          }.bind(null /*this*/, event.request))
    );
  }
});
<|endoftext|>


<commit_before>(function () {
    "use strict";
    return {
        // Production configuration
        //"backends": ["./backends/repeater"],
        //"flushInterval": 10000,

        // Development configuration
        "debug": false,
        "backends": [ "./backends/repeater" ],
        // , "./backends/console"],
        "flushInterval": 10000,

        "repeater": [ { "host": "statsd-sink", "port": 8125 } ],
        "repeaterProtocol": "upd4"
    };
})();
<commit_msg>Let's just do this the more graceful way, shall we?<commit_after>(function () {
    "use strict";
    return {
        // Production configuration
        "backends": ["./backends/repeater"],
        "flushInterval": 10000,

        // Development configuration
        // "debug": true,
        // "backends": ["./backends/repeater", "./backends/console"],
        // "flushInterval": 1000,

        "repeater": [ { "host": "statsd-sink", "port": 8125 } ],
        "repeaterProtocol": "upd4"
    };
})();
<|endoftext|>