<commit_before>import React from "react";
import { difference, filter, find, includes, map, some, sortBy, transform } from "lodash-es";
import { connect } from "react-redux";
import { Col, FormControl, Label, ListGroup, Modal, Overlay, Panel, Popover, Row } from "react-bootstrap";

import { listGroups, createGroup, setGroupPermission, removeGroup } from "../actions";
import { AutoProgressBar, Button, Flex, FlexItem, Icon, ListGroupItem, LoadingPlaceholder } from "../../base";

class Group extends React.Component {

    handleClick = () => {
        this.props.onSelect(this.props.id);
    };

    render () {
        const { id, active } = this.props;

        return (
            <ListGroupItem key={id} active={active} onClick={this.handleClick}>

                    {id}

            </ListGroupItem>
        );
    }

}

class Groups extends React.Component {

    constructor (props) {
        super(props);
        this.state = {
            activeId: null,
            createGroupId: "",
            spaceError: false,
            submitted: false
        };
    }

    componentWillMount () {
        if (this.props.groups === null) {
            this.props.onList();
        } else {
            this.setState({
                activeId: this.props.groups[0].id
            });
        }
    }

    componentWillReceiveProps (nextProps) {
        const state = {};

        // What to do if the active group was removed OR the active group id in state if onList response is incoming.
        if (!some(nextProps.groups, {id: this.state.activeId}) || (this.props.groups === null && nextProps.groups)) {
            state.activeId = nextProps.groups[0].id;
        }

        if (this.props.groups !== null && nextProps.groups.length > this.props.groups.length) {
            state.activeId = difference(nextProps.groups, this.props.groups)[0].id;
            state.createGroupId = "";
        }

        this.setState(state);
    }

    handleModalExited = () => {
        this.setState({
            createGroupId: "",
            spaceError: false,
            submitted: false
        });
    };

    handleSelect = (activeId) => {
        this.setState({
            activeId
        });
    };

    handleSubmit = (e) => {
        e.preventDefault();

        if (this.state.createGroupId !== "") {
            if (includes(this.state.createGroupId, " ")) {
                this.setState({
                    spaceError: true
                });
            } else {
                this.setState({submitted: true}, () => this.props.onCreate(this.state.createGroupId));
            }
        }
    };

    render () {

        if (this.props.groups === null || this.props.users === null) {
            return <LoadingPlaceholder margin="130px" />;
        }

        const groupComponents = map(sortBy(this.props.groups, "id"), group =>
            <Group key={group.id} {...group} active={this.state.activeId === group.id} onSelect={this.handleSelect} />
        );

        const activeGroup = find(this.props.groups, {id: this.state.activeId});

        const members = filter(this.props.users, user => includes(user.groups, activeGroup.id));

        let memberComponents = map(members, member =>

                {member.id}

        );

        if (!memberComponents.length) {
            memberComponents = (

                    <Icon name="info" /> No members found.

            );
        }

        let error;

        if (this.state.submitted && this.props.createError) {
            error = "Group with that name already exists";
        }

        // This error text is shown when the group name contains a space.
        if (this.state.spaceError) {
            error = "Group names may not contain spaces";
        }

        const permissionComponents = transform(activeGroup.permissions, (result, value, key) => {
            const readOnly = activeGroup.id === "administrator";

            result.push(
                <ListGroupItem
                    key={key}
                    onClick={readOnly ? null : () => this.props.onSetPermission(activeGroup.id, key, !value)}
                    disabled={readOnly}
                >
                    {key} <Icon name={`checkbox-${value ? "checked" : "unchecked"}`} pullRight />
                </ListGroupItem>
            );

            return result;
        }, []);

        return (
            <Modal show={this.props.show} onHide={this.props.onHide} onExited={this.handleModalExited}>
                <Modal.Header onHide={this.props.onHide} closeButton>
                    Groups
                </Modal.Header>

                <AutoProgressBar active={this.props.pending} affixed />

                <Modal.Body>
                    <Row>
                        <Col md={5}>
                            <form onSubmit={this.handleSubmit}>
                                <Flex alignItems="stretch" style={{marginBottom: "5px"}}>
                                    <FlexItem grow={1} shrink={1}>
                                        <Overlay
                                            show={!!error}
                                            container={this}
                                            target={() => this.inputNode}
                                            placement="top"
                                        >
                                            <Popover id="create-group-error">
                                                {error}
                                            </Popover>
                                        </Overlay>
                                        <FormControl
                                            type="text"
                                            inputRef={(node) => this.inputNode = node}
                                            value={this.state.createGroupId}
                                            onChange={(e) => this.setState({
                                                createGroupId: e.target.value,
                                                spaceError: this.state.spaceError && includes(e.target.value, " "),
                                                submitted: false
                                            })}
                                        />
                                    </FlexItem>
                                    <FlexItem grow={1} shrink={1}>
                                        <Button
                                            type="submit"
                                            bsStyle="primary"
                                            icon="plus-square"
                                            style={{height: "100%"}}
                                        />
                                    </FlexItem>
                                </Flex>
                            </form>

                            <ListGroup>
                                {groupComponents}
                            </ListGroup>
                        </Col>
                        <Col md={7}>
                            <Panel header="Permissions">
                                <ListGroup style={{marginBottom: "10px"}} fill>
                                    {permissionComponents}
                                </ListGroup>
                            </Panel>

                            <Panel header="Members">
                                {memberComponents}
                            </Panel>

                            <Button
                                icon="remove"
                                bsStyle="danger"
                                disabled={activeGroup.id === "administrator"}
                                onClick={() => this.props.onRemove(activeGroup.id)}
                                block
                            >
                                Remove Group
                            </Button>
                        </Col>
                    </Row>
                </Modal.Body>
            </Modal>
        );
    }
}

const mapStateToProps = (state) => ({
    users: state.users.list,
    groups: state.groups.list,
    pending: state.groups.pending,
    createError: state.groups.createError
});

const mapDispatchToProps = (dispatch) => ({

    onList: () => {
        dispatch(listGroups());
    },

    onCreate: (groupId) => {
        dispatch(createGroup(groupId));
    },

    onSetPermission: (groupId, permission, value) => {
        dispatch(setGroupPermission(groupId, permission, value));
    },

    onRemove: (groupId) => {
        dispatch(removeGroup(groupId));
    }

});

const Container = connect(mapStateToProps, mapDispatchToProps)(Groups);

export default Container;
<commit_msg>Fix group editing modal<commit_after>import React from "react";
import { difference, filter, find, includes, map, some, sortBy, transform } from "lodash-es";
import { Col, FormControl, Label, ListGroup, Modal, Overlay, Panel, Popover, Row } from "react-bootstrap";
import { connect } from "react-redux";
import { push } from "react-router-redux";

import { listGroups, createGroup, setGroupPermission, removeGroup } from "../actions";
import { AutoProgressBar, Button, Flex, FlexItem, Icon, ListGroupItem, LoadingPlaceholder } from "../../base";
import {routerLocationHasState} from "../../utils";

class Group extends React.Component {

    handleClick = () => {
        this.props.onSelect(this.props.id);
    };

    render () {
        const { id, active } = this.props;

        return (
            <ListGroupItem key={id} active={active} onClick={this.handleClick}>

                    {id}

            </ListGroupItem>
        );
    }

}

class Groups extends React.Component {

    constructor (props) {
        super(props);
        this.state = {
            activeId: null,
            createGroupId: "",
            spaceError: false,
            submitted: false
        };
    }

    componentWillMount () {
        if (this.props.groups === null) {
            this.props.onList();
        } else {
            this.setState({
                activeId: this.props.groups[0].id
            });
        }
    }

    componentWillReceiveProps (nextProps) {
        const state = {};

        // What to do if the active group was removed OR the active group id in state if onList response is incoming.
        if (!some(nextProps.groups, {id: this.state.activeId}) || (this.props.groups === null && nextProps.groups)) {
            state.activeId = nextProps.groups[0].id;
        }

        if (this.props.groups !== null && nextProps.groups.length > this.props.groups.length) {
            state.activeId = difference(nextProps.groups, this.props.groups)[0].id;
            state.createGroupId = "";
        }

        this.setState(state);
    }

    handleModalExited = () => {
        this.setState({
            createGroupId: "",
            spaceError: false,
            submitted: false
        });
    };

    handleSelect = (activeId) => {
        this.setState({
            activeId
        });
    };

    handleSubmit = (e) => {
        e.preventDefault();

        if (this.state.createGroupId !== "") {
            if (includes(this.state.createGroupId, " ")) {
                this.setState({
                    spaceError: true
                });
            } else {
                this.setState({submitted: true}, () => this.props.onCreate(this.state.createGroupId));
            }
        }
    };

    render () {

        if (this.props.groups === null || this.props.users === null) {
            return <LoadingPlaceholder margin="130px" />;
        }

        const groupComponents = map(sortBy(this.props.groups, "id"), group =>
            <Group key={group.id} {...group} active={this.state.activeId === group.id} onSelect={this.handleSelect} />
        );

        const activeGroup = find(this.props.groups, {id: this.state.activeId});

        const members = filter(this.props.users, user => includes(user.groups, activeGroup.id));

        let memberComponents = map(members, member =>

                {member.id}

        );

        if (!memberComponents.length) {
            memberComponents = (

                    <Icon name="info" /> No members found.

            );
        }

        let error;

        if (this.state.submitted && this.props.createError) {
            error = "Group with that name already exists";
        }

        // This error text is shown when the group name contains a space.
        if (this.state.spaceError) {
            error = "Group names may not contain spaces";
        }

        const permissionComponents = transform(activeGroup.permissions, (result, value, key) => {
            const readOnly = activeGroup.id === "administrator";

            result.push(
                <ListGroupItem
                    key={key}
                    onClick={readOnly ? null : () => this.props.onSetPermission(activeGroup.id, key, !value)}
                    disabled={readOnly}
                >
                    {key} <Icon name={`checkbox-${value ? "checked" : "unchecked"}`} pullRight />
                </ListGroupItem>
            );

            return result;
        }, []);

        return (
            <Modal show={this.props.show} onHide={this.props.onHide} onExited={this.handleModalExited}>
                <Modal.Header onHide={this.props.onHide} closeButton>
                    Groups
                </Modal.Header>

                <AutoProgressBar active={this.props.pending} affixed />

                <Modal.Body>
                    <Row>
                        <Col md={5}>
                            <form onSubmit={this.handleSubmit}>
                                <Flex alignItems="stretch" style={{marginBottom: "5px"}}>
                                    <FlexItem grow={1} shrink={1}>
                                        <Overlay
                                            show={!!error}
                                            container={this}
                                            target={() => this.inputNode}
                                            placement="top"
                                        >
                                            <Popover id="create-group-error">
                                                {error}
                                            </Popover>
                                        </Overlay>
                                        <FormControl
                                            type="text"
                                            inputRef={(node) => this.inputNode = node}
                                            value={this.state.createGroupId}
                                            onChange={(e) => this.setState({
                                                createGroupId: e.target.value,
                                                spaceError: this.state.spaceError && includes(e.target.value, " "),
                                                submitted: false
                                            })}
                                        />
                                    </FlexItem>
                                    <FlexItem grow={1} shrink={1}>
                                        <Button
                                            type="submit"
                                            bsStyle="primary"
                                            icon="plus-square"
                                            style={{height: "100%"}}
                                        />
                                    </FlexItem>
                                </Flex>
                            </form>

                            <ListGroup>
                                {groupComponents}
                            </ListGroup>
                        </Col>
                        <Col md={7}>
                            <Panel header="Permissions">
                                <ListGroup style={{marginBottom: "10px"}} fill>
                                    {permissionComponents}
                                </ListGroup>
                            </Panel>

                            <Panel header="Members">
                                {memberComponents}
                            </Panel>

                            <Button
                                icon="remove"
                                bsStyle="danger"
                                disabled={activeGroup.id === "administrator"}
                                onClick={() => this.props.onRemove(activeGroup.id)}
                                block
                            >
                                Remove Group
                            </Button>
                        </Col>
                    </Row>
                </Modal.Body>
            </Modal>
        );
    }
}

const mapStateToProps = (state) => ({
    show: routerLocationHasState(state, "groups"),
    users: state.users.list,
    groups: state.groups.list,
    pending: state.groups.pending,
    createError: state.groups.createError
});

const mapDispatchToProps = (dispatch) => ({

    onCreate: (groupId) => {
        dispatch(createGroup(groupId));
    },

    onHide: () => {
        dispatch(push({...window.location, state: {groups: false}}));
    },

    onList: () => {
        dispatch(listGroups());
    },

    onRemove: (groupId) => {
        dispatch(removeGroup(groupId));
    },

    onSetPermission: (groupId, permission, value) => {
        dispatch(setGroupPermission(groupId, permission, value));
    }

});

const Container = connect(mapStateToProps, mapDispatchToProps)(Groups);

export default Container;
<|endoftext|>


<commit_before>import { get, replace } from "lodash-es";
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { AffixedProgressBar, Box, ExternalLink, Icon } from "../../base";
import { checkAdminOrPermission } from "../../utils/utils";
import { installHMMs } from "../actions";
import { getProcess } from "../selectors";
import InstallOption from "./InstallOption";

const HMMInstalling = styled(Box)`
    display: flex;
    justify-content: center;

    h3 {
        font-size: ${props => props.theme.fontSize.xl};
    }

    p {
        text-align: center;
        text-transform: capitalize;
    }
`;

const StyledHMMInstaller = styled(Box)`
    display: flex;
    justify-content: center;
    padding: 35px 0;

    h3 {
        font-size: ${props => props.theme.fontSize.xl};
        margin: 0 0 3px;
    }

    > i {
        font-size: 32px;
        margin: 3px 10px 0;
    }
`;

export const HMMInstaller = ({ installed, process }) => {
    if (process && !installed) {
        const progress = process.progress * 100;
        const step = replace(process.step, "_", " ");

        return (
            <HMMInstalling>
                <AffixedProgressBar color="blue" now={progress} />

Installing

{step}

            </HMMInstalling>
        );
    }

    return (

    );
};

export const mapStateToProps = state => ({
    releaseId: get(state.hmms.status, "release.id"),
    installed: !!state.hmms.status.installed,
    canInstall: checkAdminOrPermission(state, "modify_hmm"),
    process: getProcess(state)
});

export const mapDispatchToProps = dispatch => ({
    onInstall: releaseId => {
        dispatch(installHMMs(releaseId));
    }
});

export default connect(mapStateToProps, mapDispatchToProps)(HMMInstaller);
<commit_msg>Adjust font-weight of HMM installer box<commit_after>import { get, replace } from "lodash-es";
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { AffixedProgressBar, Box, ExternalLink, Icon } from "../../base";
import { checkAdminOrPermission } from "../../utils/utils";
import { installHMMs } from "../actions";
import { getProcess } from "../selectors";
import InstallOption from "./InstallOption";

const HMMInstalling = styled(Box)`
    display: flex;
    justify-content: center;

    h3 {
        font-size: ${props => props.theme.fontSize.xl};
    }

    p {
        text-align: center;
        text-transform: capitalize;
    }
`;

const StyledHMMInstaller = styled(Box)`
    display: flex;
    justify-content: center;
    padding: 35px 0;

    h3 {
        font-size: ${props => props.theme.fontSize.xl};
        font-weight: 500;
        margin: 0 0 3px;
    }

    > i {
        font-size: 32px;
        margin: 3px 10px 0;
    }
`;

export const HMMInstaller = ({ installed, process }) => {
    if (process && !installed) {
        const progress = process.progress * 100;
        const step = replace(process.step, "_", " ");

        return (
            <HMMInstalling>
                <AffixedProgressBar color="blue" now={progress} />

Installing

{step}

            </HMMInstalling>
        );
    }

    return (

    );
};

export const mapStateToProps = state => ({
    releaseId: get(state.hmms.status, "release.id"),
    installed: !!state.hmms.status.installed,
    canInstall: checkAdminOrPermission(state, "modify_hmm"),
    process: getProcess(state)
});

export const mapDispatchToProps = dispatch => ({
    onInstall: releaseId => {
        dispatch(installHMMs(releaseId));
    }
});

export default connect(mapStateToProps, mapDispatchToProps)(HMMInstaller);
<|endoftext|>


<commit_before>Template.introduction.onRendered(function() {
	if (!Assistant.openedIntro()) {
		this.$('.introduction-content').hide();
	}

	var instance = this;
	this.autorun(function() {
		if (Assistant.openedIntro()) {
			instance.$('.introduction-content').slideDown(400);
		} else {
			instance.$('.introduction-content').slideUp(400);
		}
	});

	// use $screen-xxs (from scss) to compare with the width of window
	if ($(window).width()<380) {
		Assistant.closeIntro();
		// dont wait for slideUp
		this.$('.introduction-content').hide();
	}
});

Template.layout.helpers({
	shownIntro: function() {
		return Assistant.shownIntro();
	}
});

Template.introduction.helpers({
	openedIntro: function() {
		return Assistant.openedIntro();
	},

	isInCalendar: function() {
		return Router.current().route.getName() == "calendar";
	}
});

Template.layout.events({
	// Clicks on the logo toggle the intro blurb, but only when already on home
	'click .-toggleIntro': function() {
		if (Router.current().route.options.template === "findWrap") {
			Assistant.showIntro();
		}
	},

	"click .js-introduction-close-btn": function(event, instance) {
		Assistant.doneIntro();
	},

	"click .js-introduction-toggle-btn": function(event, instance) {
		if (Assistant.openedIntro()) {
			Assistant.closeIntro();
		} else {
			Assistant.openIntro();
		}
	}
});
<commit_msg>use viewportWidth session variable in introduction.js<commit_after>Template.introduction.onRendered(function() {
	if (!Assistant.openedIntro()) {
		this.$('.introduction-content').hide();
	}

	var instance = this;
	this.autorun(function() {
		if (Assistant.openedIntro()) {
			instance.$('.introduction-content').slideDown(400);
		} else {
			instance.$('.introduction-content').slideUp(400);
		}
	});

	// use $screen-xxs (from scss) to compare with the width of window
	if (Session.get("viewportWidth") < 380) {
		Assistant.closeIntro();
		// dont wait for slideUp
		this.$('.introduction-content').hide();
	}
});

Template.layout.helpers({
	shownIntro: function() {
		return Assistant.shownIntro();
	}
});

Template.introduction.helpers({
	openedIntro: function() {
		return Assistant.openedIntro();
	},

	isInCalendar: function() {
		return Router.current().route.getName() == "calendar";
	}
});

Template.layout.events({
	// Clicks on the logo toggle the intro blurb, but only when already on home
	'click .-toggleIntro': function() {
		if (Router.current().route.options.template === "findWrap") {
			Assistant.showIntro();
		}
	},

	"click .js-introduction-close-btn": function(event, instance) {
		Assistant.doneIntro();
	},

	"click .js-introduction-toggle-btn": function(event, instance) {
		if (Assistant.openedIntro()) {
			Assistant.closeIntro();
		} else {
			Assistant.openIntro();
		}
	}
});
<|endoftext|>


<commit_before>(function (root, factory) {
	if (typeof customLoader === 'function') {
		customLoader(factory, 'on');
	} else if (typeof define === 'function' && define.amd) {
		define([], factory);
	} else if (typeof exports === 'object') {
		module.exports = factory();
	} else {
		root.returnExports = factory();
		window.on = factory();
	}
}(this, function () {
	'use strict';

	function hasWheelTest () {
		var
			isIE = navigator.userAgent.indexOf('Trident') > -1,
			div = document.createElement('div');
		return "onwheel" in div || "wheel" in div ||
			(isIE && document.implementation.hasFeature("Events.wheel", "3.0")); // IE feature detection
	}

	var
		INVALID_PROPS,
		matches,
		hasWheel = hasWheelTest(),
		isWin = navigator.userAgent.indexOf('Windows') > -1,
		FACTOR = isWin ? 10 : 0.1,
		XLR8 = 0,
		mouseWheelHandle;

	['matches', 'matchesSelector', 'webkit', 'moz', 'ms', 'o'].some(function (name) {
		if (name.length < 7) { // prefix
			name += 'MatchesSelector';
		}
		if (Element.prototype[name]) {
			matches = name;
			return true;
		}
		return false;
	});

	function closest (element, selector, parent) {
		while (element) {
			if (element[matches] && element[matches](selector)) {
				return element;
			}
			if (element === parent) {
				break;
			}
			element = element.parentElement;
		}
		return null;
	}

	function closestFilter (element, selector) {
		return function (e) {
			return closest(e.target, selector, element);
		};
	}

	function makeMultiHandle (handles) {
		return {
			remove: function () {
				handles.forEach(function (h) {
					// allow for a simple function in the list
					if (h.remove) {
						h.remove();
					} else if (typeof h === 'function') {
						h();
					}
				});
				handles = [];
				this.remove = this.pause = this.resume = function () {};
			},
			pause: function () {
				handles.forEach(function (h) {
					if (h.pause) {
						h.pause();
					}
				});
			},
			resume: function () {
				handles.forEach(function (h) {
					if (h.resume) {
						h.resume();
					}
				});
			}
		};
	}

	function onClickoff (node, callback) {
		// important note!
		// starts paused
		//
		var
			handle,
			bHandle = on(document.body, 'click', function (event) {
				var target = event.target;
				if (target.nodeType !== 1) {
					target = target.parentNode;
				}
				if (target && !node.contains(target)) {
					callback(event);
				}
			});

		handle = {
			resume: function () {
				setTimeout(function () {
					bHandle.resume();
				}, 100);
			},
			pause: function () {
				bHandle.pause();
			},
			remove: function () {
				bHandle.remove();
			}
		};

		handle.pause();

		return handle;
	}

	function onImageLoad (img, callback) {
		function onImageLoad (e) {
			var h = setInterval(function () {
				if (img.naturalWidth) {
					e.width = img.naturalWidth;
					e.naturalWidth = img.naturalWidth;
					e.height = img.naturalHeight;
					e.naturalHeight = img.naturalHeight;
					callback(e);
					clearInterval(h);
				}
			}, 100);
			img.removeEventListener('load', onImageLoad);
			img.removeEventListener('error', callback);
		}

		img.addEventListener('load', onImageLoad);
		img.addEventListener('error', callback);
		return {
			pause: function () {},
			resume: function () {},
			remove: function () {
				img.removeEventListener('load', onImageLoad);
				img.removeEventListener('error', callback);
			}
		}
	}

	function getNode (str) {
		if (typeof str !== 'string') {
			return str;
		}
		var node = document.getElementById(str);
		if (!node) {
			console.error('`on` Could not find:', str);
		}
		return node;
	}

	var ieKeys = {
		//a: 'TEST',
		Up: 'ArrowUp',
		Down: 'ArrowDown',
		Left: 'ArrowLeft',
		Right: 'ArrowRight',
		Esc: 'Escape',
		Spacebar: ' ',
		Win: 'Command'
	};

	function normalizeKeyEvent (callback) {
		// IE uses old spec
		return function (e) {
			if (ieKeys[e.key]) {
				var fakeEvent = mix({}, e);
				fakeEvent.key = ieKeys[e.key];
				callback(fakeEvent);
			} else {
				callback(e);
			}
		}
	}

	function normalizeWheelEvent (callback) {
		// normalizes all browsers' events to a standard:
		// delta, wheelY, wheelX
		// also adds acceleration and deceleration to make
		// Mac and Windows behave similarly
		return function (e) {
			XLR8 += FACTOR;
			var
				deltaY = Math.max(-1, Math.min(1, (e.wheelDeltaY || e.deltaY))),
				deltaX = Math.max(-10, Math.min(10, (e.wheelDeltaX || e.deltaX)));

			deltaY = deltaY <= 0 ? deltaY - XLR8 : deltaY + XLR8;

			e.delta = deltaY;
			e.wheelY = deltaY;
			e.wheelX = deltaX;

			clearTimeout(mouseWheelHandle);
			mouseWheelHandle = setTimeout(function () {
				XLR8 = 0;
			}, 300);
			callback(e);
		};
	}

	function isMultiKey (eventName) {
		return /,/.test(eventName) && !/click|mouse|resize|scroll/.test(eventName);
	}

	function keysToRegExp (eventName) {
		return new RegExp(eventName.replace('keydown:', '').replace('keyup:', '').split(',').join('|'));
	}

	function on (node, eventName, filter, handler) {
		var
			callback,
			handles,
			handle,
			keyRegExp;

		if (isMultiKey(eventName)) {
			keyRegExp = keysToRegExp(eventName);
			callback = function (e) {
				if (keyRegExp.test(e.key)) {
					(handler || filter)(e);
				}
			};
			eventName = /keydown/.test(eventName) ? 'keydown' : 'keyup';
		}

		if (/,/.test(eventName)) {
			// handle multiple event types, like:
			// on(node, 'mouseup, mousedown', callback);
			//
			handles = [];
			eventName.split(',').forEach(function (eStr) {
				handles.push(on(node, eStr.trim(), filter, handler));
			});
			return makeMultiHandle(handles);
		}

		if(eventName === 'button'){
			// handle click and Enter
			return makeMultiHandle([
				on(node, 'click', filter, handle),
				on(node, 'keyup:Enter', filter, handle)
			]);
		}

		node = getNode(node);

		if (filter && handler) {
			if (typeof filter === 'string') {
				filter = closestFilter(node, filter);
			}
			// else it is a custom function
			callback = function (e) {
				var result = filter(e);
				if (result) {
					e.filteredTarget = result;
					handler(e, result);
				}
			};
		} else if (!callback) {
			callback = filter || handler;
		}

		if (eventName === 'clickoff') {
			// custom - used for popups 'n stuff
			return onClickoff(node, callback);
		}

		if (eventName === 'load' && node.localName === 'img') {
			return onImageLoad(node, callback);
		}

		if (eventName === 'wheel') {
			// mousewheel events, natch
			if (hasWheel) {
				// pass through, but first curry callback to wheel events
				callback = normalizeWheelEvent(callback);
			} else {
				// old Firefox, old IE, Chrome
				return makeMultiHandle([
					on(node, 'DOMMouseScroll', normalizeWheelEvent(callback)),
					on(node, 'mousewheel', normalizeWheelEvent(callback))
				]);
			}
		}

		if (/key/.test(eventName)) {
			callback = normalizeKeyEvent(callback);
		}

		node.addEventListener(eventName, callback, false);

		handle = {
			remove: function () {
				node.removeEventListener(eventName, callback, false);
				node = callback = null;
				this.remove = this.pause = this.resume = function () {};
			},
			pause: function () {
				node.removeEventListener(eventName, callback, false);
			},
			resume: function () {
				node.addEventListener(eventName, callback, false);
			}
		};

		return handle;
	}

	on.once = function (node, eventName, filter, callback) {
		var h;
		if (filter && callback) {
			h = on(node, eventName, filter, function () {
				callback.apply(window, arguments);
				h.remove();
			});
		} else {
			h = on(node, eventName, function () {
				filter.apply(window, arguments);
				h.remove();
			});
		}
		return h;
	};

	INVALID_PROPS = {
		isTrusted: 1
	};
	function mix (object, value) {
		if (!value) {
			return object;
		}
		if (typeof value === 'object') {
			for(var key in value){
				if (!INVALID_PROPS[key] && typeof value[key] !== 'function') {
					object[key] = value[key];
				}
			}
		} else {
			object.value = value;
		}
		return object;
	}

	on.emit = function (node, eventName, value) {
		node = getNode(node);
		var event = document.createEvent('HTMLEvents');
		event.initEvent(eventName, true, true); // event type, bubbling, cancelable
		return node.dispatchEvent(mix(event, value));
	};

	on.fire = function (node, eventName, eventDetail, bubbles) {
		var event = document.createEvent('CustomEvent');
		event.initCustomEvent(eventName, !!bubbles, true, eventDetail); // event type, bubbling, cancelable, value
		return node.dispatchEvent(event);
	};

	on.isAlphaNumeric = function (str) {
		if (str.length > 1) {
			return false;
		}
		if (str === ' ') {
			return false;
		}
		if (!isNaN(Number(str))) {
			return true;
		}
		var code = str.toLowerCase().charCodeAt(0);
		return code >= 97 && code <= 122;
	};

	on.makeMultiHandle = makeMultiHandle;
	on.closest = closest;
	on.matches = matches;

	return on;

}));
<commit_msg>allow functions in copied event<commit_after>(function (root, factory) {
	if (typeof customLoader === 'function') {
		customLoader(factory, 'on');
	} else if (typeof define === 'function' && define.amd) {
		define([], factory);
	} else if (typeof exports === 'object') {
		module.exports = factory();
	} else {
		root.returnExports = factory();
		window.on = factory();
	}
}(this, function () {
	'use strict';

	function hasWheelTest () {
		var
			isIE = navigator.userAgent.indexOf('Trident') > -1,
			div = document.createElement('div');
		return "onwheel" in div || "wheel" in div ||
			(isIE && document.implementation.hasFeature("Events.wheel", "3.0")); // IE feature detection
	}

	var
		INVALID_PROPS,
		matches,
		hasWheel = hasWheelTest(),
		isWin = navigator.userAgent.indexOf('Windows') > -1,
		FACTOR = isWin ? 10 : 0.1,
		XLR8 = 0,
		mouseWheelHandle;

	['matches', 'matchesSelector', 'webkit', 'moz', 'ms', 'o'].some(function (name) {
		if (name.length < 7) { // prefix
			name += 'MatchesSelector';
		}
		if (Element.prototype[name]) {
			matches = name;
			return true;
		}
		return false;
	});

	function closest (element, selector, parent) {
		while (element) {
			if (element[matches] && element[matches](selector)) {
				return element;
			}
			if (element === parent) {
				break;
			}
			element = element.parentElement;
		}
		return null;
	}

	function closestFilter (element, selector) {
		return function (e) {
			return closest(e.target, selector, element);
		};
	}

	function makeMultiHandle (handles) {
		return {
			remove: function () {
				handles.forEach(function (h) {
					// allow for a simple function in the list
					if (h.remove) {
						h.remove();
					} else if (typeof h === 'function') {
						h();
					}
				});
				handles = [];
				this.remove = this.pause = this.resume = function () {};
			},
			pause: function () {
				handles.forEach(function (h) {
					if (h.pause) {
						h.pause();
					}
				});
			},
			resume: function () {
				handles.forEach(function (h) {
					if (h.resume) {
						h.resume();
					}
				});
			}
		};
	}

	function onClickoff (node, callback) {
		// important note!
		// starts paused
		//
		var
			handle,
			bHandle = on(document.body, 'click', function (event) {
				var target = event.target;
				if (target.nodeType !== 1) {
					target = target.parentNode;
				}
				if (target && !node.contains(target)) {
					callback(event);
				}
			});

		handle = {
			resume: function () {
				setTimeout(function () {
					bHandle.resume();
				}, 100);
			},
			pause: function () {
				bHandle.pause();
			},
			remove: function () {
				bHandle.remove();
			}
		};

		handle.pause();

		return handle;
	}

	function onImageLoad (img, callback) {
		function onImageLoad (e) {
			var h = setInterval(function () {
				if (img.naturalWidth) {
					e.width = img.naturalWidth;
					e.naturalWidth = img.naturalWidth;
					e.height = img.naturalHeight;
					e.naturalHeight = img.naturalHeight;
					callback(e);
					clearInterval(h);
				}
			}, 100);
			img.removeEventListener('load', onImageLoad);
			img.removeEventListener('error', callback);
		}

		img.addEventListener('load', onImageLoad);
		img.addEventListener('error', callback);
		return {
			pause: function () {},
			resume: function () {},
			remove: function () {
				img.removeEventListener('load', onImageLoad);
				img.removeEventListener('error', callback);
			}
		}
	}

	function getNode (str) {
		if (typeof str !== 'string') {
			return str;
		}
		var node = document.getElementById(str);
		if (!node) {
			console.error('`on` Could not find:', str);
		}
		return node;
	}

	var ieKeys = {
		//a: 'TEST',
		Up: 'ArrowUp',
		Down: 'ArrowDown',
		Left: 'ArrowLeft',
		Right: 'ArrowRight',
		Esc: 'Escape',
		Spacebar: ' ',
		Win: 'Command'
	};

	function normalizeKeyEvent (callback) {
		// IE uses old spec
		return function (e) {
			if (ieKeys[e.key]) {
				var fakeEvent = mix({}, e);
				fakeEvent.key = ieKeys[e.key];
				callback(fakeEvent);
			} else {
				callback(e);
			}
		}
	}

	function normalizeWheelEvent (callback) {
		// normalizes all browsers' events to a standard:
		// delta, wheelY, wheelX
		// also adds acceleration and deceleration to make
		// Mac and Windows behave similarly
		return function (e) {
			XLR8 += FACTOR;
			var
				deltaY = Math.max(-1, Math.min(1, (e.wheelDeltaY || e.deltaY))),
				deltaX = Math.max(-10, Math.min(10, (e.wheelDeltaX || e.deltaX)));

			deltaY = deltaY <= 0 ? deltaY - XLR8 : deltaY + XLR8;

			e.delta = deltaY;
			e.wheelY = deltaY;
			e.wheelX = deltaX;

			clearTimeout(mouseWheelHandle);
			mouseWheelHandle = setTimeout(function () {
				XLR8 = 0;
			}, 300);
			callback(e);
		};
	}

	function isMultiKey (eventName) {
		return /,/.test(eventName) && !/click|mouse|resize|scroll/.test(eventName);
	}

	function keysToRegExp (eventName) {
		return new RegExp(eventName.replace('keydown:', '').replace('keyup:', '').split(',').join('|'));
	}

	function on (node, eventName, filter, handler) {
		var
			callback,
			handles,
			handle,
			keyRegExp;

		if (isMultiKey(eventName)) {
			keyRegExp = keysToRegExp(eventName);
			callback = function (e) {
				if (keyRegExp.test(e.key)) {
					(handler || filter)(e);
				}
			};
			eventName = /keydown/.test(eventName) ? 'keydown' : 'keyup';
		}

		if (/,/.test(eventName)) {
			// handle multiple event types, like:
			// on(node, 'mouseup, mousedown', callback);
			//
			handles = [];
			eventName.split(',').forEach(function (eStr) {
				handles.push(on(node, eStr.trim(), filter, handler));
			});
			return makeMultiHandle(handles);
		}

		if(eventName === 'button'){
			// handle click and Enter
			return makeMultiHandle([
				on(node, 'click', filter, handle),
				on(node, 'keyup:Enter', filter, handle)
			]);
		}

		node = getNode(node);

		if (filter && handler) {
			if (typeof filter === 'string') {
				filter = closestFilter(node, filter);
			}
			// else it is a custom function
			callback = function (e) {
				var result = filter(e);
				if (result) {
					e.filteredTarget = result;
					handler(e, result);
				}
			};
		} else if (!callback) {
			callback = filter || handler;
		}

		if (eventName === 'clickoff') {
			// custom - used for popups 'n stuff
			return onClickoff(node, callback);
		}

		if (eventName === 'load' && node.localName === 'img') {
			return onImageLoad(node, callback);
		}

		if (eventName === 'wheel') {
			// mousewheel events, natch
			if (hasWheel) {
				// pass through, but first curry callback to wheel events
				callback = normalizeWheelEvent(callback);
			} else {
				// old Firefox, old IE, Chrome
				return makeMultiHandle([
					on(node, 'DOMMouseScroll', normalizeWheelEvent(callback)),
					on(node, 'mousewheel', normalizeWheelEvent(callback))
				]);
			}
		}

		if (/key/.test(eventName)) {
			callback = normalizeKeyEvent(callback);
		}

		node.addEventListener(eventName, callback, false);

		handle = {
			remove: function () {
				node.removeEventListener(eventName, callback, false);
				node = callback = null;
				this.remove = this.pause = this.resume = function () {};
			},
			pause: function () {
				node.removeEventListener(eventName, callback, false);
			},
			resume: function () {
				node.addEventListener(eventName, callback, false);
			}
		};

		return handle;
	}

	on.once = function (node, eventName, filter, callback) {
		var h;
		if (filter && callback) {
			h = on(node, eventName, filter, function () {
				callback.apply(window, arguments);
				h.remove();
			});
		} else {
			h = on(node, eventName, function () {
				filter.apply(window, arguments);
				h.remove();
			});
		}
		return h;
	};

	INVALID_PROPS = {
		isTrusted: 1
	};
	function mix (object, value) {
		if (!value) {
			return object;
		}
		if (typeof value === 'object') {
			for(var key in value){
				if (!INVALID_PROPS[key]) {
					object[key] = value[key];
				}
			}
		} else {
			object.value = value;
		}
		return object;
	}

	on.emit = function (node, eventName, value) {
		node = getNode(node);
		var event = document.createEvent('HTMLEvents');
		event.initEvent(eventName, true, true); // event type, bubbling, cancelable
		return node.dispatchEvent(mix(event, value));
	};

	on.fire = function (node, eventName, eventDetail, bubbles) {
		var event = document.createEvent('CustomEvent');
		event.initCustomEvent(eventName, !!bubbles, true, eventDetail); // event type, bubbling, cancelable, value
		return node.dispatchEvent(event);
	};

	on.isAlphaNumeric = function (str) {
		if (str.length > 1) {
			return false;
		}
		if (str === ' ') {
			return false;
		}
		if (!isNaN(Number(str))) {
			return true;
		}
		var code = str.toLowerCase().charCodeAt(0);
		return code >= 97 && code <= 122;
	};

	on.makeMultiHandle = makeMultiHandle;
	on.closest = closest;
	on.matches = matches;

	return on;

}));
<|endoftext|>


<commit_before>var fs = require('fs'),
    path = require('path'),
    util = require('util'),

    async = require('async'),
    csv = require('csv'),
    _ = require('lodash'),
    moment = require('moment'),

    helperUtils = require('./helper-utils'),

    __dirname = process.cwd(), //__dirname || path.resolve('./'),
    cwd = __dirname,
    defaults = {
        context: null,
        readPath: [
            path.resolve(process.cwd(), 'tmp/CfO_Animal_Adoption_DB_Model - Cats.csv'),
            path.resolve(process.cwd(), 'tmp/CfO_Animal_Adoption_DB_Model - Dogs.csv')
        ],
        writeDir: path.resolve(cwd, 'data/'),
        cacheName: 'props'
    };

function parseModelCSV(csvModelData) {
    console.log('sanitizing model');

    var newModel = [{
            key: 'images',
            valType: '[Image]',
            defaultVal: ['http://placehold.it/500x500', 'http://placehold.it/720x480', 'http://placehold.it/480x480'],
            example: ['http://placehold.it/500x500', 'http://placehold.it/720x480', 'http://placehold.it/480x480']
        }],
        columnIndices = {
            name: 1,
            fieldLabel: 2,
            type: 3,
            description: 4,
            default: 5,
            required: 6,
            example: 7,
            note: 8
        };
    _.forEachRight(csvModelData, function (csvRow, rowIndex, arr) {
        if (rowIndex == 0) return; //skip the field labels

        var _modelPropData = {};
        if (csvRow[columnIndices['name']] && csvRow[columnIndices['type']]) {
            _.forEach(columnIndices, function (columnIndex, columnIndexName, indices) {
                switch (columnIndexName) {
                    case 'type':
                        if (csvRow[columnIndex].match(/integer/i)) {
                            _modelPropData['valType'] = 'Number';
                        } else {
                            _modelPropData['valType'] = _.capitalize(csvRow[columnIndex]);
                        }
                        break;
                    case 'default':
                        _modelPropData['defaultVal'] = csvRow[columnIndex];
                        break;
                    case 'name':
                        _modelPropData['key'] = csvRow[columnIndex];
                        break;
                    default:
                        _modelPropData[columnIndexName] = csvRow[columnIndex];
                        break;
                }
            });
            if (_modelPropData['key'].match(/(lostGeoL|shelterGeoL)/)) {
                _modelPropData['defaultVal'] = csvRow[columnIndices.example] || 'Location';
                _modelPropData['valType'] = 'Location';
            }
            if (_modelPropData['valType'] == 'Date') {
                if (!moment(_modelPropData['defaultVal']).isValid()) {
                    _modelPropData['defaultVal'] = null;
                }
            }
            newModel.push(_modelPropData);
        }
    });

    console.log('sanitized model');
    // console.log('sanitized model: %j', newModel);
    return newModel;
}

/**
 *
 * @param {Object} speciesDataCollection
 * @param {Object} optionsData
 * @param {Function} callback
 */
function _mergeOptionsAndModel(speciesDataCollection, optionsData, callback) {

    _.forEach(speciesDataCollection, function (speciesProps, speciesName) {

        _.forEach(speciesProps, function (speciesPropData) {
            if (optionsData[speciesName][speciesPropData.key]) {
                speciesPropData.options = optionsData[speciesName][speciesPropData.key];
            } else if (optionsData[speciesName]['breed'] && speciesPropData.key.match(/breed/ig)) {
                speciesPropData.options = optionsData[speciesName]['breed'].sort();
            } else {
                speciesPropData.options = [];
            }
        });
    });

    callback.call(null, speciesDataCollection);
}

module.exports = {
    /**
     *
     * @param options
     * @param {Function} options.done
     * @param {Object} options.context
     * @param {String} options.readPath
     * @param {String} options.writeDir
     */
    parse: function (options) {
        var _options = _.defaults(options, defaults);

        var fileList = _.isArray(_options.readPath) ? _options.readPath : [_options.readPath],
            data = {};

        async.each(fileList,
            function each(filePath, done) {

                function onParsed(err, schemaCSVData) {
                    var namespace = helperUtils.getTypeFromPath(filePath);
                    data[namespace] = parseModelCSV(schemaCSVData);
                    done();
                }

                helperUtils.download(filePath, function (err, content) {
                    if (err) throw err;
                    csv.parse(content, onParsed);
                });
            },
            function complete(error) {
                if (error) throw error;

                // merge in options

                require('./options').parse({
                    cache: true,
                    done: function (optionsData) {
                        _mergeOptionsAndModel(data, optionsData, function onMergeComplete(mergedModelsData) {
                            if (_options.cache === true) {
                                async.eachOf(mergedModelsData, function (speciesData, speciesName, done) {
                                    fs.writeFile(path.join(_options.writeDir, util.format('%s.%s.json', _options.cacheName, speciesName)), JSON.stringify(speciesData), function (err) {
                                        done(err)
                                    })
                                }, function (err) {
                                    if (err) throw err;
                                    _options.done.apply(_options.context, [mergedModelsData, _options]);
                                });
                            } else {
                                _options.done.apply(_options.context, [mergedModelsData, _options]);
                            }
                        });
                    }
                });

            });

    }
};
<commit_msg>refactors species-props csv parser<commit_after>var fs = require('fs'),
    path = require('path'),
    util = require('util'),

    async = require('async'),
    csv = require('csv'),
    _ = require('lodash'),
    moment = require('moment'),

    helperUtils = require('./helper-utils'),

    __dirname = process.cwd(), //__dirname || path.resolve('./'),
    cwd = __dirname,
    defaults = {
        context: null,
        readPath: [
            path.resolve(process.cwd(), 'tmp/CfO_Animal_Adoption_DB_Model - Cats.csv'),
            path.resolve(process.cwd(), 'tmp/CfO_Animal_Adoption_DB_Model - Dogs.csv')
        ],
        writeDir: path.resolve(cwd, 'data/'),
        cacheName: 'props'
    };

function parseModelCSV(csvModelData) {
    console.log('sanitizing model');

    var speciesProps = [{
            key: 'images',
            valType: '[Image]',
            defaultVal: ['http://placehold.it/500x500', 'http://placehold.it/720x480', 'http://placehold.it/480x480'],
            example: ['http://placehold.it/500x500', 'http://placehold.it/720x480', 'http://placehold.it/480x480']
        }],
        columnIndices = {
            key: 1,
            fieldLabel: 2,
            valType: 3,
            description: 4,
            defaultVal: 5,
            required: 6,
            example: 7,
            note: 8
        };

    console.log('sanitized model');
    // console.log('sanitized model: %j', newModel);
    return _.reduce(csvModelData, function (speciesProps, csvRow, rowIndex) {
        if (rowIndex == 0) return; //skip the first row which contains field labels
        if (!(csvRow[columnIndices['key']] && csvRow[columnIndices['valType']])) return; // skip invalid rows

        var speciesProp = {};
        _.forEach(columnIndices, function (columnIndex, columnIndexName) {
            switch (columnIndexName) {
                case 'valType':
                    if (csvRow[columnIndex].match(/integer/i)) {
                        speciesProp['valType'] = 'Number';
                    } else {
                        speciesProp['valType'] = _.capitalize(csvRow[columnIndex]);
                    }
                    break;
                case 'defaultVal':
                    speciesProp['defaultVal'] = csvRow[columnIndex];
                    break;
                case 'key':
                    speciesProp['key'] = csvRow[columnIndex];
                    break;
                default:
                    speciesProp[columnIndexName] = csvRow[columnIndex];
                    break;
            }
        });
        if (speciesProp['key'].match(/(lostGeoL|shelterGeoL)/)) {
            speciesProp['defaultVal'] = csvRow[columnIndices.example] || 'Location';
            speciesProp['valType'] = 'Location';
        }
        if (speciesProp['valType'] == 'Date') {
            if (!moment(speciesProp['defaultVal']).isValid()) {
                speciesProp['defaultVal'] = null;
            }
        }
        speciesProps.push(speciesProp);
    }, []);
}

/**
 *
 * @param {Object} speciesDataCollection
 * @param {Object} optionsData
 * @param {Function} callback
 */
function _mergeOptionsAndModel(speciesDataCollection, optionsData, callback) {

    _.forEach(speciesDataCollection, function (speciesProps, speciesName) {

        _.forEach(speciesProps, function (speciesProp) {
            if (optionsData[speciesName][speciesProp.key]) {
                speciesProp.options = optionsData[speciesName][speciesProp.key];
            } else if (optionsData[speciesName]['breed'] && speciesProp.key.match(/breed/ig)) {
                speciesProp.options = optionsData[speciesName]['breed'].sort();
            } else {
                speciesProp.options = [];
            }
        });
    });

    callback.call(null, speciesDataCollection);
}

module.exports = {
    /**
     *
     * @param options
     * @param {Function} options.done
     * @param {Object} options.context
     * @param {String} options.readPath
     * @param {String} options.writeDir
     */
    parse: function (options) {
        var _options = _.defaults(options, defaults);

        var fileList = _.isArray(_options.readPath) ? _options.readPath : [_options.readPath],
            data = {};

        async.each(fileList,
            function each(filePath, done) {

                function onParsed(err, schemaCSVData) {
                    var namespace = helperUtils.getTypeFromPath(filePath);
                    data[namespace] = parseModelCSV(schemaCSVData);
                    done();
                }

                helperUtils.download(filePath, function (err, content) {
                    if (err) throw err;
                    csv.parse(content, onParsed);
                });
            },
            function complete(error) {
                if (error) throw error;

                // merge in options

                require('./options').parse({
                    cache: true,
                    done: function (optionsData) {
                        _mergeOptionsAndModel(data, optionsData, function onMergeComplete(mergedModelsData) {
                            if (_options.cache === true) {
                                async.eachOf(mergedModelsData, function (speciesData, speciesName, done) {
                                    fs.writeFile(path.join(_options.writeDir, util.format('%s.%s.json', _options.cacheName, speciesName)), JSON.stringify(speciesData), function (err) {
                                        done(err)
                                    })
                                }, function (err) {
                                    if (err) throw err;
                                    _options.done.apply(_options.context, [mergedModelsData, _options]);
                                });
                            } else {
                                _options.done.apply(_options.context, [mergedModelsData, _options]);
                            }
                        });
                    }
                });

            });

    }
};
<|endoftext|>


<commit_before>// Copyright 2014 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview End-to-end tests to check for console errors in
 * library related pages.
 */

var general = require('../protractor_utils/general.js');
var LibraryPage = require('../protractor_utils/LibraryPage.js');
var ExplorationPlayerPage =
  require('../protractor_utils/ExplorationPlayerPage.js');
var users = require('../protractor_utils/users.js');
var workflow = require('../protractor_utils/workflow.js');

describe('Library pages tour', function() {
  var EXPLORATION_TITLE = 'Test Exploration';
  var EXPLORATION_OBJECTIVE = 'To learn testing';
  var EXPLORATION_CATEGORY = 'Algorithms';
  var EXPLORATION_LANGUAGE = 'English';
  var EXPLORATION_RATING = 4;
  var SEARCH_TERM = 'python';
  var libraryPage = null;
  var explorationPlayerPage = null;

  beforeEach(function() {
    libraryPage = new LibraryPage.LibraryPage();
    explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
  });

  var visitLibraryPage = function() {
    libraryPage.get();
  };

  var visitRecentlyPublishedPage = function() {
    browser.get('library/recently_published');
  }

  it('visits the library index page', function() {
    visitLibraryPage();
  });

  it('visits the top rated page', function() {
    // To visit the top rated page, at least one
    // exploration has to be rated by the user
    users.createUser('random@gmail.com', 'random');
    users.login('random@gmail.com');
    workflow.createAndPublishExploration(
      EXPLORATION_TITLE,
      EXPLORATION_CATEGORY,
      EXPLORATION_OBJECTIVE,
      EXPLORATION_LANGUAGE
    );
    visitLibraryPage();
    libraryPage.playExploration(EXPLORATION_TITLE);
    explorationPlayerPage.rateExploration(EXPLORATION_RATING);

    visitLibraryPage();
    element(by.css('.protractor-test-library-top-rated')).click();
    expect(browser.getCurrentUrl()).toContain('library/top_rated');
    users.logout();
  });

  it('visits the recent explorations page', function() {
    visitRecentlyPublishedPage();
    expect(browser.getCurrentUrl()).toContain('library/recently_published');
  });

  it('visits the search page', function() {
    visitLibraryPage();
    element(by.css('.protractor-test-search-input')).sendKeys(SEARCH_TERM);
    browser.driver.wait(function() {
      return browser.getCurrentUrl().then(function(url) {
        return /search/.test(url);
      });
    });
    expect(browser.getCurrentUrl()).toContain('search/find?q=python');
  });

  afterEach(function() {
    general.checkForConsoleErrors([]);
  });
});
<commit_msg>Stabilize search page protractor tests by giving Protractor a bit more time to load the page. (#4192)<commit_after>// Copyright 2014 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview End-to-end tests to check for console errors in
 * library related pages.
 */

var general = require('../protractor_utils/general.js');
var LibraryPage = require('../protractor_utils/LibraryPage.js');
var ExplorationPlayerPage =
  require('../protractor_utils/ExplorationPlayerPage.js');
var users = require('../protractor_utils/users.js');
var workflow = require('../protractor_utils/workflow.js');

describe('Library pages tour', function() {
  var EXPLORATION_TITLE = 'Test Exploration';
  var EXPLORATION_OBJECTIVE = 'To learn testing';
  var EXPLORATION_CATEGORY = 'Algorithms';
  var EXPLORATION_LANGUAGE = 'English';
  var EXPLORATION_RATING = 4;
  var SEARCH_TERM = 'python';
  var libraryPage = null;
  var explorationPlayerPage = null;

  beforeEach(function() {
    libraryPage = new LibraryPage.LibraryPage();
    explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
  });

  var visitLibraryPage = function() {
    libraryPage.get();
  };

  var visitRecentlyPublishedPage = function() {
    browser.get('library/recently_published');
  }

  it('visits the library index page', function() {
    visitLibraryPage();
  });

  it('visits the top rated page', function() {
    // To visit the top rated page, at least one
    // exploration has to be rated by the user
    users.createUser('random@gmail.com', 'random');
    users.login('random@gmail.com');
    workflow.createAndPublishExploration(
      EXPLORATION_TITLE,
      EXPLORATION_CATEGORY,
      EXPLORATION_OBJECTIVE,
      EXPLORATION_LANGUAGE
    );
    visitLibraryPage();
    libraryPage.playExploration(EXPLORATION_TITLE);
    explorationPlayerPage.rateExploration(EXPLORATION_RATING);

    visitLibraryPage();
    element(by.css('.protractor-test-library-top-rated')).click();
    expect(browser.getCurrentUrl()).toContain('library/top_rated');
    users.logout();
  });

  it('visits the recent explorations page', function() {
    visitRecentlyPublishedPage();
    expect(browser.getCurrentUrl()).toContain('library/recently_published');
  });

  it('visits the search page', function() {
    visitLibraryPage();
    element(by.css('.protractor-test-search-input')).sendKeys(SEARCH_TERM);
    browser.driver.wait(function() {
      return browser.getCurrentUrl().then(function(url) {
        return /search/.test(url);
      });
    }, 4000);
    general.waitForSystem();
    expect(browser.getCurrentUrl()).toContain('search/find?q=python');
  });

  afterEach(function() {
    general.checkForConsoleErrors([]);
  });
});
<|endoftext|>


<commit_before>import Artists      from "./ui/artists.js";
import Comments     from "./ui/comments.js";
import ForumPosts   from "./ui/forum_posts.js";;
import ModeMenu     from "./ui/mode_menu.js";
import Pools        from "./ui/pools.js";
import Posts        from "./ui/posts.js";
import PostVersions from "./ui/post_versions.js";
import Users        from "./ui/users.js";
import WikiPages    from "./ui/wiki_pages.js";

import Tag from "./tag.js";

export default class UI {
  static initialize() {
    UI.initialize_moment();
    UI.initialize_patches();

    EX.config.showHeaderBar && UI.initialize_header();
    EX.config.showThumbnailPreviews && UI.initialize_post_thumbnail_previews();
    EX.config.showPostLinkPreviews && UI.initialize_post_link_previews();
    EX.config.styleUsernames && UI.initialize_user_links();
    EX.config.styleWikiLinks && UI.initialize_wiki_links();
    EX.config.useRelativeTimestamps && UI.initialize_relative_times();
    UI.initialize_hotkeys();
  }

  // Prevent middle-click from adding tag when clicking on related tags (open a new tab instead).
  static initialize_patches() {
    const old_toggle_tag = Danbooru.RelatedTag.toggle_tag;
    Danbooru.RelatedTag.toggle_tag = function (e) {
      if (e.which === 1) {
        return old_toggle_tag(e);
      }
    };
  }

  /*
   * Add sticky header.
   */
  static initialize_header() {
    let $sticky = $(`

Danbooru

        <form id="search-box" action="/posts" accept-charset="UTF-8" method="get">
          <input name="utf8" type="hidden" value="✓">
          <input type="text" name="tags" id="tags" size="20" class="ui-autocomplete-input" autocomplete="off">
          <input type="submit" value="Go">
        </form>

          <form action="/">
            <select name="mode">
              <option value="view">View</option>
              <option value="edit">Edit</option>
              <option value="tag-script">Tag script</option>
              <option value="add-fav">Favorite</option>
              <option value="remove-fav">Unfavorite</option>
              <option value="rating-s">Rate safe</option>
              <option value="rating-q">Rate questionable</option>
              <option value="rating-e">Rate explicit</option>
              <option value="vote-up">Vote up</option>
              <option value="vote-down">Vote down</option>
              <option value="lock-rating">Lock rating</option>
              <option value="lock-note">Lock notes</option>
              <option value="approve">Approve</option>
            </select>
          </form>
          <input id="tag-script-field" placeholder="Enter tag script" style="display: none; margin-top: 0.5em;">
          <button type="button">Apply</button>

    `).insertBefore("#top");

    // Initalize sticky header search box.
    $("#sticky-header #tags").val($("#sidebar #tags").val());

    /* Q: Focus search box. */
    /* XXX: Doesn't override site keybinding. */
    /*
    $(document).keydown("keydown", "q", e => {
        let $input = $("#tags, #search_name, #search_name_matches, #query").first();
        console.log($input);

        // Add a space to end if box is non-empty and doesn't already have trailing space.
        $input.val().length && $input.val((i, v) => v.replace(/\s*$/, ' '));
        $input.first().trigger("focus").selectEnd();

        e.preventDefault();
    });
    */

    // Shift+Q: Focus and select all in search box.
    $(document).keydown('shift+q', e => {
      e.preventDefault();

      let $input = $("#tags, #search_name, #search_name_matches, #query").first();

      // Add a space to end if  box is non-empty and doesn't already have trailing space.
      $input.val().length && $input.val((i, v) => v.replace(/\s*$/, ' '));
      $input.focus().selectRange(0, $input.val().length);
    });
  }

  // Use relative times everywhere.
  static initialize_relative_times() {
    const ABS_DATE = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}/;
    const abs_dates = $('time').filter((i, e) => $(e).text().match(ABS_DATE));

    abs_dates.each((i, e) => {
      const time_ago = moment($(e).attr('datetime')).fromNow();
      $(e).text(time_ago);
    });
  }

  static initialize_moment() {
    moment.locale("en-short", {
      relativeTime : {
          future: "in %s",
          past:   "%s",
          s:  "s",
          m:  "1m",
          mm: "%dm",
          h:  "1h",
          hh: "%dh",
          d:  "1d",
          dd: "%dd",
          M:  "1m",
          MM: "%dm",
          y:  "1y",
          yy: "%dy"
      }
    });

    moment.locale("en");
    moment.defaultFormat = "MMMM Do YYYY, h:mm a";
  }

  // Show post previews when hovering over post #1234 links.
  static initialize_post_link_previews() {
    $('a[href^="/posts/"]')
      .filter((i, e) => /post #\d+/.test($(e).text()))
      .addClass('ex-thumbnail-tooltip-link');

    UI.install_tooltips();
  }

  // Show post previews when hovering over thumbnails.
  static initialize_post_thumbnail_previews() {
    // The thumbnail container is .post-preview on every page but comments and
    // the mod queue. Handle those specially.
    if ($("#c-comments").length) {
      $("#c-comments .post-preview .preview img").addClass('ex-thumbnail-tooltip-link');
    } else if ($("#c-post-moderator-queues").length) {
      $("#c-post-moderator-queues .mod-queue-preview aside img").addClass('ex-thumbnail-tooltip-link');
    } else {
      $(".post-preview img").addClass('ex-thumbnail-tooltip-link');
    }

    UI.install_tooltips();
  }

  static install_tooltips(items) {
    const max_size = 450;

    $(".ex-thumbnail-tooltip-link").tooltip({
      items: "*",
      content: `<div style="width: ${max_size}px; height: ${max_size}px"></div>`,
      show: { delay: 350 },
      position: {
        my: "left+10 top",
        at: "right top",
      },
      open: (e, ui) => {
        try {
          let $e = $(e.toElement);
          let $link = $e;

          if ($e.prop("nodeName") === "IMG") {
            $link = $e.closest("a");
          }

          const id = $link.attr('href').match(/\/posts\/(\d+)/)[1];

          $.getJSON(`/posts/${id}.json`).then(post =>
            $(ui.tooltip).html(Posts.preview(post, post.large_file_url, "ex-thumbnail-tooltip"))
          );
        } catch (e) {
          console.log(e);
        }
      }
    });
  }

  // Add data attributes to usernames so that banned users and approvers can be styled.
  static initialize_user_links() {
    const user_ids =
      _($('a[href^="/users"]'))
      .map(e => $(e).attr('href').replace("/users/", ""))
      .map(Number)
      .sortBy()
      .sortedUniq()
      .join(',');

    // XXX should do lookup in batches.
    $.getJSON(`/users.json?limit=1000&search[id]=${user_ids}`).then(users => {
      for (const user of users) {
        let $user = $(`a[href^="/users/${user.id}"]`);

        // $user.addClass(`user-${user.level_string.toLowerCase()}`);

        _(user).forOwn((value, key) =>
          $user.attr(`data-${_(key).kebabCase()}`, value)
        );
      }
    });
  }

  // Color code tags linking to wiki pages. Also add a tooltip showing the tag
  // creation date and post count.
  static initialize_wiki_links() {
    const $wiki_links = $(`a[href^="/wiki_pages/show_or_new?title="]`);

    const tag_names = $wiki_links.map((i, e) =>
      decodeURIComponent($(e).attr('href').match(/^\/wiki_pages\/show_or_new\?title=(.*)/)[1])
    ).toArray();

    // Fetch tag data for each batch of tags, then categorize them and add tooltips.
    //tag_batches.forEach(tag_batch => {
      Tag.search(tag_names).then(tags => {
        _.each(tags, tag => {
          // Encode some extra things manually because Danbooru
          // encodes these things in URLs but encodeURIComponent doesn't.
          const tag_name =
            encodeURIComponent(tag.name)
            .replace(/!/g,  '%21')
            .replace(/'/g,  '%27')
            .replace(/\(/g, '%28')
            .replace(/\)/g, '%29')
            .replace(/~/g,  '%7E');

          const tag_created_at =
            moment(tag.created_at).format('MMMM Do YYYY, h:mm:ss a');

          const tag_title =
            `${Tag.Categories[tag.category]} tag #${tag.id} - ${tag.post_count} posts - created on ${tag_created_at}`;

          const $wiki_link = $(`a[href="/wiki_pages/show_or_new?title=${tag_name}"]`);

          _(tag).forOwn((value, key) =>
            $wiki_link.attr(`data-tag-${_(key).kebabCase()}`, value)
          );

          $wiki_link.addClass(`tag-type-${tag.category}`).attr('title', tag_title);

          if (tag.post_count === 0) {
            $wiki_link.addClass("tag-post-count-empty");
          } else if (tag.post_count < 100) {
            $wiki_link.addClass("tag-post-count-small");
          } else if (tag.post_count < 1000) {
            $wiki_link.addClass("tag-post-count-medium");
          } else if (tag.post_count < 10000) {
            $wiki_link.addClass("tag-post-count-large");
          } else if (tag.post_count < 100000) {
            $wiki_link.addClass("tag-post-count-huge");
          } else {
            $wiki_link.addClass("tag-post-count-gigantic");
          }
        });
      });
    //});
  }

  /*
   * Global keybindings.
   * - Escape: Close notice popups.
   * - W: Smooth scroll up.
   * - S: Smooth scroll down.
   * - Shift+Q: Focus top search bar.
   */
  static initialize_hotkeys() {
    // Escape: Close notice popups.
    $(document).keydown('esc', e => $('#close-notice-link').click());

    // Escape: Unfocus text entry field.
    $('#tag-script-field').attr('type', 'text');
    $('input[type=text],textarea').keydown('esc', e => $(e.currentTarget).blur());

    UI.initialize_scroll_hotkeys();

    if ($(".paginator").length) {
      UI.initialize_paginator_hotkeys();
    }
  }

  static initialize_scroll_hotkeys() {
    let scroll = (direction, duration, distance) =>
      _.throttle(() => {
        const top = $(window).scrollTop() + direction * $(window).height() * distance;
        $('html, body').animate({scrollTop: top}, duration, "linear");
      }, duration);
    /*
    Danbooru.Shortcuts.nav_scroll_down =
        () => Danbooru.scroll_to($(window).scrollTop() + $(window).height() * 0.15);
    Danbooru.Shortcuts.nav_scroll_up =
        () => Danbooru.scroll_to($(window).scrollTop() - $(window).height() * 0.15);
    */

    // Enable smooth scrolling with W/S keys.
    Danbooru.Shortcuts.nav_scroll_down = scroll(+1, 50, 0.06);
    Danbooru.Shortcuts.nav_scroll_up   = scroll(-1, 50, 0.06);
  }

  /*
   * Shift+1..9: Jump to page N.
   * Shift+0: Jump to last page.
   */
  static initialize_paginator_hotkeys() {
    // Add paginator above results.
    // $('.paginator').clone().insertBefore('#post-sections');

    /* Shift+1..9: Jump to page N. */
    [1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n =>
      $(document).keydown(`shift+${n}`, e => {
        UI.gotoPage(n);
        e.preventDefault();
      })
    );

    // Shift+0: Switch to last page if there is one.
    $(document).keydown(`shift+0`, e => {
      e.preventDefault();

      // a:not(a[rel]) - exclude the Previous/Next links seen in the paginator on /favorites et al.
      const last_page = $('div.paginator li:nth-last-child(2) a:not(a[rel])').first().text();

      if (last_page) {
        UI.gotoPage(last_page);
      }
    });
  }

  static linkTo(name, path = "/", params = {}, ...classes) {
    const query = $.param(params);
    const href = (query === "")
               ? path
               : path + "?" + query;

    return `<a class="${_.escape(classes.join(" "))}" href="${href}">${_.escape(name)}</a>`;
  }

  static query(param) {
    return new URL(window.location).searchParams.get(param);
  }

  static openEditPage(controller) {
    // FIXME: Get the ID from the 'Show' link. This is brittle.
    const $show_link =
      $('#nav > menu:nth-child(2) a')
      .filter((i, e) => $(e).text().match(/^Show$/));

    const id = $show_link.attr('href').match(new RegExp(`/${controller}/(\\d+)$`))[1];

    window.location.href = `/${controller}/${id}/edit`;
  }

  // Go to page N.
  static gotoPage(n) {
    if (location.search.match(/page=(\d+)/)) {
      location.search = location.search.replace(/page=(\d+)/, `page=${n}`);
    } else {
      location.search += `&page=${n}`;
    }
  }
}

UI.Artists = Artists;
UI.Comments = Comments;
UI.ForumPosts = ForumPosts;
UI.ModeMenu = ModeMenu;
UI.Pools = Pools;
UI.Posts = Posts;
UI.PostVersions = PostVersions;
UI.Users = Users;
UI.WikiPages = WikiPages;
<commit_msg>[style] Reindent initialize_wiki_links().<commit_after>import Artists      from "./ui/artists.js";
import Comments     from "./ui/comments.js";
import ForumPosts   from "./ui/forum_posts.js";;
import ModeMenu     from "./ui/mode_menu.js";
import Pools        from "./ui/pools.js";
import Posts        from "./ui/posts.js";
import PostVersions from "./ui/post_versions.js";
import Users        from "./ui/users.js";
import WikiPages    from "./ui/wiki_pages.js";

import Tag from "./tag.js";

export default class UI {
  static initialize() {
    UI.initialize_moment();
    UI.initialize_patches();

    EX.config.showHeaderBar && UI.initialize_header();
    EX.config.showThumbnailPreviews && UI.initialize_post_thumbnail_previews();
    EX.config.showPostLinkPreviews && UI.initialize_post_link_previews();
    EX.config.styleUsernames && UI.initialize_user_links();
    EX.config.styleWikiLinks && UI.initialize_wiki_links();
    EX.config.useRelativeTimestamps && UI.initialize_relative_times();
    UI.initialize_hotkeys();
  }

  // Prevent middle-click from adding tag when clicking on related tags (open a new tab instead).
  static initialize_patches() {
    const old_toggle_tag = Danbooru.RelatedTag.toggle_tag;
    Danbooru.RelatedTag.toggle_tag = function (e) {
      if (e.which === 1) {
        return old_toggle_tag(e);
      }
    };
  }

  /*
   * Add sticky header.
   */
  static initialize_header() {
    let $sticky = $(`

Danbooru

        <form id="search-box" action="/posts" accept-charset="UTF-8" method="get">
          <input name="utf8" type="hidden" value="✓">
          <input type="text" name="tags" id="tags" size="20" class="ui-autocomplete-input" autocomplete="off">
          <input type="submit" value="Go">
        </form>

          <form action="/">
            <select name="mode">
              <option value="view">View</option>
              <option value="edit">Edit</option>
              <option value="tag-script">Tag script</option>
              <option value="add-fav">Favorite</option>
              <option value="remove-fav">Unfavorite</option>
              <option value="rating-s">Rate safe</option>
              <option value="rating-q">Rate questionable</option>
              <option value="rating-e">Rate explicit</option>
              <option value="vote-up">Vote up</option>
              <option value="vote-down">Vote down</option>
              <option value="lock-rating">Lock rating</option>
              <option value="lock-note">Lock notes</option>
              <option value="approve">Approve</option>
            </select>
          </form>
          <input id="tag-script-field" placeholder="Enter tag script" style="display: none; margin-top: 0.5em;">
          <button type="button">Apply</button>

    `).insertBefore("#top");

    // Initalize sticky header search box.
    $("#sticky-header #tags").val($("#sidebar #tags").val());

    /* Q: Focus search box. */
    /* XXX: Doesn't override site keybinding. */
    /*
    $(document).keydown("keydown", "q", e => {
        let $input = $("#tags, #search_name, #search_name_matches, #query").first();
        console.log($input);

        // Add a space to end if box is non-empty and doesn't already have trailing space.
        $input.val().length && $input.val((i, v) => v.replace(/\s*$/, ' '));
        $input.first().trigger("focus").selectEnd();

        e.preventDefault();
    });
    */

    // Shift+Q: Focus and select all in search box.
    $(document).keydown('shift+q', e => {
      e.preventDefault();

      let $input = $("#tags, #search_name, #search_name_matches, #query").first();

      // Add a space to end if  box is non-empty and doesn't already have trailing space.
      $input.val().length && $input.val((i, v) => v.replace(/\s*$/, ' '));
      $input.focus().selectRange(0, $input.val().length);
    });
  }

  // Use relative times everywhere.
  static initialize_relative_times() {
    const ABS_DATE = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}/;
    const abs_dates = $('time').filter((i, e) => $(e).text().match(ABS_DATE));

    abs_dates.each((i, e) => {
      const time_ago = moment($(e).attr('datetime')).fromNow();
      $(e).text(time_ago);
    });
  }

  static initialize_moment() {
    moment.locale("en-short", {
      relativeTime : {
          future: "in %s",
          past:   "%s",
          s:  "s",
          m:  "1m",
          mm: "%dm",
          h:  "1h",
          hh: "%dh",
          d:  "1d",
          dd: "%dd",
          M:  "1m",
          MM: "%dm",
          y:  "1y",
          yy: "%dy"
      }
    });

    moment.locale("en");
    moment.defaultFormat = "MMMM Do YYYY, h:mm a";
  }

  // Show post previews when hovering over post #1234 links.
  static initialize_post_link_previews() {
    $('a[href^="/posts/"]')
      .filter((i, e) => /post #\d+/.test($(e).text()))
      .addClass('ex-thumbnail-tooltip-link');

    UI.install_tooltips();
  }

  // Show post previews when hovering over thumbnails.
  static initialize_post_thumbnail_previews() {
    // The thumbnail container is .post-preview on every page but comments and
    // the mod queue. Handle those specially.
    if ($("#c-comments").length) {
      $("#c-comments .post-preview .preview img").addClass('ex-thumbnail-tooltip-link');
    } else if ($("#c-post-moderator-queues").length) {
      $("#c-post-moderator-queues .mod-queue-preview aside img").addClass('ex-thumbnail-tooltip-link');
    } else {
      $(".post-preview img").addClass('ex-thumbnail-tooltip-link');
    }

    UI.install_tooltips();
  }

  static install_tooltips(items) {
    const max_size = 450;

    $(".ex-thumbnail-tooltip-link").tooltip({
      items: "*",
      content: `<div style="width: ${max_size}px; height: ${max_size}px"></div>`,
      show: { delay: 350 },
      position: {
        my: "left+10 top",
        at: "right top",
      },
      open: (e, ui) => {
        try {
          let $e = $(e.toElement);
          let $link = $e;

          if ($e.prop("nodeName") === "IMG") {
            $link = $e.closest("a");
          }

          const id = $link.attr('href').match(/\/posts\/(\d+)/)[1];

          $.getJSON(`/posts/${id}.json`).then(post =>
            $(ui.tooltip).html(Posts.preview(post, post.large_file_url, "ex-thumbnail-tooltip"))
          );
        } catch (e) {
          console.log(e);
        }
      }
    });
  }

  // Add data attributes to usernames so that banned users and approvers can be styled.
  static initialize_user_links() {
    const user_ids =
      _($('a[href^="/users"]'))
      .map(e => $(e).attr('href').replace("/users/", ""))
      .map(Number)
      .sortBy()
      .sortedUniq()
      .join(',');

    // XXX should do lookup in batches.
    $.getJSON(`/users.json?limit=1000&search[id]=${user_ids}`).then(users => {
      for (const user of users) {
        let $user = $(`a[href^="/users/${user.id}"]`);

        // $user.addClass(`user-${user.level_string.toLowerCase()}`);

        _(user).forOwn((value, key) =>
          $user.attr(`data-${_(key).kebabCase()}`, value)
        );
      }
    });
  }

  // Color code tags linking to wiki pages. Also add a tooltip showing the tag
  // creation date and post count.
  static initialize_wiki_links() {
    const $wiki_links = $(`a[href^="/wiki_pages/show_or_new?title="]`);

    const tag_names = $wiki_links.map((i, e) =>
      decodeURIComponent($(e).attr('href').match(/^\/wiki_pages\/show_or_new\?title=(.*)/)[1])
    ).toArray();

    // Fetch tag data for each batch of tags, then categorize them and add tooltips.
    Tag.search(tag_names).then(tags => {
      _.each(tags, tag => {
        // Encode some extra things manually because Danbooru
        // encodes these things in URLs but encodeURIComponent doesn't.
        const tag_name =
          encodeURIComponent(tag.name)
          .replace(/!/g,  '%21')
          .replace(/'/g,  '%27')
          .replace(/\(/g, '%28')
          .replace(/\)/g, '%29')
          .replace(/~/g,  '%7E');

        const tag_created_at =
          moment(tag.created_at).format('MMMM Do YYYY, h:mm:ss a');

        const tag_title =
          `${Tag.Categories[tag.category]} tag #${tag.id} - ${tag.post_count} posts - created on ${tag_created_at}`;

        const $wiki_link = $(`a[href="/wiki_pages/show_or_new?title=${tag_name}"]`);

        _(tag).forOwn((value, key) =>
          $wiki_link.attr(`data-tag-${_(key).kebabCase()}`, value)
        );

        $wiki_link.addClass(`tag-type-${tag.category}`).attr('title', tag_title);

        if (tag.post_count === 0) {
          $wiki_link.addClass("tag-post-count-empty");
        } else if (tag.post_count < 100) {
          $wiki_link.addClass("tag-post-count-small");
        } else if (tag.post_count < 1000) {
          $wiki_link.addClass("tag-post-count-medium");
        } else if (tag.post_count < 10000) {
          $wiki_link.addClass("tag-post-count-large");
        } else if (tag.post_count < 100000) {
          $wiki_link.addClass("tag-post-count-huge");
        } else {
          $wiki_link.addClass("tag-post-count-gigantic");
        }
      });
    });
  }

  /*
   * Global keybindings.
   * - Escape: Close notice popups.
   * - W: Smooth scroll up.
   * - S: Smooth scroll down.
   * - Shift+Q: Focus top search bar.
   */
  static initialize_hotkeys() {
    // Escape: Close notice popups.
    $(document).keydown('esc', e => $('#close-notice-link').click());

    // Escape: Unfocus text entry field.
    $('#tag-script-field').attr('type', 'text');
    $('input[type=text],textarea').keydown('esc', e => $(e.currentTarget).blur());

    UI.initialize_scroll_hotkeys();

    if ($(".paginator").length) {
      UI.initialize_paginator_hotkeys();
    }
  }

  static initialize_scroll_hotkeys() {
    let scroll = (direction, duration, distance) =>
      _.throttle(() => {
        const top = $(window).scrollTop() + direction * $(window).height() * distance;
        $('html, body').animate({scrollTop: top}, duration, "linear");
      }, duration);
    /*
    Danbooru.Shortcuts.nav_scroll_down =
        () => Danbooru.scroll_to($(window).scrollTop() + $(window).height() * 0.15);
    Danbooru.Shortcuts.nav_scroll_up =
        () => Danbooru.scroll_to($(window).scrollTop() - $(window).height() * 0.15);
    */

    // Enable smooth scrolling with W/S keys.
    Danbooru.Shortcuts.nav_scroll_down = scroll(+1, 50, 0.06);
    Danbooru.Shortcuts.nav_scroll_up   = scroll(-1, 50, 0.06);
  }

  /*
   * Shift+1..9: Jump to page N.
   * Shift+0: Jump to last page.
   */
  static initialize_paginator_hotkeys() {
    // Add paginator above results.
    // $('.paginator').clone().insertBefore('#post-sections');

    /* Shift+1..9: Jump to page N. */
    [1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n =>
      $(document).keydown(`shift+${n}`, e => {
        UI.gotoPage(n);
        e.preventDefault();
      })
    );

    // Shift+0: Switch to last page if there is one.
    $(document).keydown(`shift+0`, e => {
      e.preventDefault();

      // a:not(a[rel]) - exclude the Previous/Next links seen in the paginator on /favorites et al.
      const last_page = $('div.paginator li:nth-last-child(2) a:not(a[rel])').first().text();

      if (last_page) {
        UI.gotoPage(last_page);
      }
    });
  }

  static linkTo(name, path = "/", params = {}, ...classes) {
    const query = $.param(params);
    const href = (query === "")
               ? path
               : path + "?" + query;

    return `<a class="${_.escape(classes.join(" "))}" href="${href}">${_.escape(name)}</a>`;
  }

  static query(param) {
    return new URL(window.location).searchParams.get(param);
  }

  static openEditPage(controller) {
    // FIXME: Get the ID from the 'Show' link. This is brittle.
    const $show_link =
      $('#nav > menu:nth-child(2) a')
      .filter((i, e) => $(e).text().match(/^Show$/));

    const id = $show_link.attr('href').match(new RegExp(`/${controller}/(\\d+)$`))[1];

    window.location.href = `/${controller}/${id}/edit`;
  }

  // Go to page N.
  static gotoPage(n) {
    if (location.search.match(/page=(\d+)/)) {
      location.search = location.search.replace(/page=(\d+)/, `page=${n}`);
    } else {
      location.search += `&page=${n}`;
    }
  }
}

UI.Artists = Artists;
UI.Comments = Comments;
UI.ForumPosts = ForumPosts;
UI.ModeMenu = ModeMenu;
UI.Pools = Pools;
UI.Posts = Posts;
UI.PostVersions = PostVersions;
UI.Users = Users;
UI.WikiPages = WikiPages;
<|endoftext|>