<commit_before>import escapeAttr from '../utils/escapeAttr';
import isInArray from '../utils/isInArray';
import dasherize from '../utils/dasherize';
import { IS_DEBUG } from '../utils/debug';

function setAttr(node, name, val) {
    if(name === 'type' && node.tagName === 'INPUT') {
        const { value } = node; // value will be lost in IE if type is changed

        node.setAttribute(name, '' + val);
        node.value = value;
    }
    else {
        node.setAttribute(getAttrName(name), '' + val);
    }
}

function setBooleanAttr(node, name, val) {
    if(val) {
        setAttr(node, name, val);
    }
    else {
        removeAttr(node, name);
    }
}

function setProp(node, name, val) {
    node[name] = val;
}

function setObjProp(node, name, val) {
    if(IS_DEBUG) {
        const typeOfVal = typeof val;

        if(typeOfVal !== 'object') {
            throw TypeError(`vidom: "${name}" attribute value must be an object, not a ${typeOfVal}`);
        }
    }

    const prop = node[name];

    for(const i in val) {
        prop[i] = val[i] == null? '' : val[i];
    }
}

function setPropWithCheck(node, name, val) {
    if(name === 'value' && node.tagName === 'SELECT') {
        setSelectValue(node, val);
    }
    else if(node[name] !== val) {
        node[name] = val;
    }
}

function removeAttr(node, name) {
    node.removeAttribute(getAttrName(name));
}

function removeProp(node, name) {
    if(name === 'style') {
        node[name].cssText = '';
    }
    else if(name === 'value' && node.tagName === 'SELECT') {
        removeSelectValue(node);
    }
    else {
        node[name] = getDefaultPropVal(node.tagName, name);
    }
}

function setSelectValue(node, value) {
    const isMultiple = Array.isArray(value);

    if(isMultiple) {
        const { options } = node,
            len = options.length;
        let i = 0,
            optionNode;

        while(i < len) {
            optionNode = options[i++];
            optionNode.selected = value != null && isInArray(value, optionNode.value);
        }
    }
    else {
        node.value = value;
    }
}

function removeSelectValue(node) {
    const { options } = node,
        len = options.length;
    let i = 0;

    while(i < len) {
        options[i++].selected = false;
    }
}

function attrToString(name, value) {
    const attrName = getAttrName(name);

    return attrName &&
        attrName + '="' + escapeAttr(value) + '"';
}

function booleanAttrToString(name, value) {
    return value? getAttrName(name) : '';
}

function overloadedBooleanAttrToString(name, value) {
    return typeof value === 'boolean'?
        booleanAttrToString(name, value) :
        attrToString(name, value);
}

function stylePropToString(name, value) {
    let styles = '',
        i;

    for(i in value) {
        if(value[i] != null) {
            styles += dasherize(i) + ':' + value[i] + ';';
        }
    }

    return styles? name + '="' + styles + '"' : styles;
}

const defaultNodes = Object.create(null),
    defaultPropVals = Object.create(null);

function getDefaultPropVal(tag, attrName) {
    const key = `${tag}:${attrName}`;

    if(key in defaultPropVals) {
        return defaultPropVals[key];
    }

    const node = tag in defaultNodes?
        defaultNodes[tag] :
        defaultNodes[tag] = document.createElement(tag);

    return defaultPropVals[key] = node[attrName];
}

const VALID_ATTRIBUTE_NAME_START_CHAR =
        ':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD',
    VALID_ATTRIBUTE_NAME_CHAR = `${VALID_ATTRIBUTE_NAME_START_CHAR}\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040`,
    VALID_ATTRIBUTE_NAME_RE = new RegExp(`^[${VALID_ATTRIBUTE_NAME_START_CHAR}][${VALID_ATTRIBUTE_NAME_CHAR}]*$`),
    attrNames = Object.create(null);

function getAttrName(attrName) {
    return attrName in attrNames?
        attrNames[attrName] :
        attrNames[attrName] = VALID_ATTRIBUTE_NAME_RE.test(attrName)?
            attrName.toLowerCase() :
            '';
}

const DEFAULT_ATTR_CFG = {
        set : setAttr,
        remove : removeAttr,
        toString : attrToString
    },
    BOOLEAN_ATTR_CFG = {
        ...DEFAULT_ATTR_CFG,
        set : setBooleanAttr,
        toString : booleanAttrToString
    },
    OVERLOADED_BOOLEAN_ATTR_CFG = {
        ...BOOLEAN_ATTR_CFG,
        toString : overloadedBooleanAttrToString
    },
    DEFAULT_PROP_CFG = {
        set : setProp,
        remove : removeProp,
        toString : attrToString
    },
    BOOLEAN_PROP_CFG = {
        ...DEFAULT_PROP_CFG,
        toString : booleanAttrToString
    },
    attrsCfg = Object.create(null);

attrNames.acceptCharset = 'accept-charset';
attrNames.className = 'class';
attrNames.htmlFor = 'for';
attrNames.httpEquiv = 'http-equiv';

attrsCfg.autoPlay = BOOLEAN_ATTR_CFG;
attrsCfg.checked = BOOLEAN_PROP_CFG;
attrsCfg.controls = DEFAULT_PROP_CFG;
attrsCfg.disabled = BOOLEAN_ATTR_CFG;
attrsCfg.download = OVERLOADED_BOOLEAN_ATTR_CFG;
attrsCfg.id = DEFAULT_PROP_CFG;
attrsCfg.ismap = BOOLEAN_ATTR_CFG;
attrsCfg.loop = DEFAULT_PROP_CFG;
attrsCfg.multiple = BOOLEAN_PROP_CFG;
attrsCfg.muted = DEFAULT_PROP_CFG;
attrsCfg.open = BOOLEAN_ATTR_CFG;
attrsCfg.readOnly = BOOLEAN_PROP_CFG;
attrsCfg.selected = BOOLEAN_PROP_CFG;
attrsCfg.srcDoc = DEFAULT_PROP_CFG;
attrsCfg.style = {
    set : setObjProp,
    remove : removeProp,
    toString : stylePropToString
};
attrsCfg.value = {
    set : setPropWithCheck,
    remove : removeProp,
    toString : attrToString
};

export default function(attrName) {
    return attrName in attrsCfg?
        attrsCfg[attrName] :
        DEFAULT_ATTR_CFG;
}
<commit_msg>Use `merge` to get rid of `_extends`<commit_after>import escapeAttr from '../utils/escapeAttr';
import isInArray from '../utils/isInArray';
import dasherize from '../utils/dasherize';
import merge from '../utils/merge';
import { IS_DEBUG } from '../utils/debug';

function setAttr(node, name, val) {
    if(name === 'type' && node.tagName === 'INPUT') {
        const { value } = node; // value will be lost in IE if type is changed

        node.setAttribute(name, '' + val);
        node.value = value;
    }
    else {
        node.setAttribute(getAttrName(name), '' + val);
    }
}

function setBooleanAttr(node, name, val) {
    if(val) {
        setAttr(node, name, val);
    }
    else {
        removeAttr(node, name);
    }
}

function setProp(node, name, val) {
    node[name] = val;
}

function setObjProp(node, name, val) {
    if(IS_DEBUG) {
        const typeOfVal = typeof val;

        if(typeOfVal !== 'object') {
            throw TypeError(`vidom: "${name}" attribute value must be an object, not a ${typeOfVal}`);
        }
    }

    const prop = node[name];

    for(const i in val) {
        prop[i] = val[i] == null? '' : val[i];
    }
}

function setPropWithCheck(node, name, val) {
    if(name === 'value' && node.tagName === 'SELECT') {
        setSelectValue(node, val);
    }
    else if(node[name] !== val) {
        node[name] = val;
    }
}

function removeAttr(node, name) {
    node.removeAttribute(getAttrName(name));
}

function removeProp(node, name) {
    if(name === 'style') {
        node[name].cssText = '';
    }
    else if(name === 'value' && node.tagName === 'SELECT') {
        removeSelectValue(node);
    }
    else {
        node[name] = getDefaultPropVal(node.tagName, name);
    }
}

function setSelectValue(node, value) {
    const isMultiple = Array.isArray(value);

    if(isMultiple) {
        const { options } = node,
            len = options.length;
        let i = 0,
            optionNode;

        while(i < len) {
            optionNode = options[i++];
            optionNode.selected = value != null && isInArray(value, optionNode.value);
        }
    }
    else {
        node.value = value;
    }
}

function removeSelectValue(node) {
    const { options } = node,
        len = options.length;
    let i = 0;

    while(i < len) {
        options[i++].selected = false;
    }
}

function attrToString(name, value) {
    const attrName = getAttrName(name);

    return attrName &&
        attrName + '="' + escapeAttr(value) + '"';
}

function booleanAttrToString(name, value) {
    return value? getAttrName(name) : '';
}

function overloadedBooleanAttrToString(name, value) {
    return typeof value === 'boolean'?
        booleanAttrToString(name, value) :
        attrToString(name, value);
}

function stylePropToString(name, value) {
    let styles = '',
        i;

    for(i in value) {
        if(value[i] != null) {
            styles += dasherize(i) + ':' + value[i] + ';';
        }
    }

    return styles? name + '="' + styles + '"' : styles;
}

const defaultNodes = Object.create(null),
    defaultPropVals = Object.create(null);

function getDefaultPropVal(tag, attrName) {
    const key = `${tag}:${attrName}`;

    if(key in defaultPropVals) {
        return defaultPropVals[key];
    }

    const node = tag in defaultNodes?
        defaultNodes[tag] :
        defaultNodes[tag] = document.createElement(tag);

    return defaultPropVals[key] = node[attrName];
}

const VALID_ATTRIBUTE_NAME_START_CHAR =
        ':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD',
    VALID_ATTRIBUTE_NAME_CHAR = `${VALID_ATTRIBUTE_NAME_START_CHAR}\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040`,
    VALID_ATTRIBUTE_NAME_RE = new RegExp(`^[${VALID_ATTRIBUTE_NAME_START_CHAR}][${VALID_ATTRIBUTE_NAME_CHAR}]*$`),
    attrNames = Object.create(null);

function getAttrName(attrName) {
    return attrName in attrNames?
        attrNames[attrName] :
        attrNames[attrName] = VALID_ATTRIBUTE_NAME_RE.test(attrName)?
            attrName.toLowerCase() :
            '';
}

const DEFAULT_ATTR_CFG = {
        set : setAttr,
        remove : removeAttr,
        toString : attrToString
    },
    BOOLEAN_ATTR_CFG = merge(
        DEFAULT_ATTR_CFG,
        {
            set : setBooleanAttr,
            toString : booleanAttrToString
        }),
    OVERLOADED_BOOLEAN_ATTR_CFG = merge(
        BOOLEAN_ATTR_CFG,
        {
            toString : overloadedBooleanAttrToString
        }),
    DEFAULT_PROP_CFG = {
        set : setProp,
        remove : removeProp,
        toString : attrToString
    },
    BOOLEAN_PROP_CFG = merge(
        DEFAULT_PROP_CFG,
        {
            toString : booleanAttrToString
        }),
    attrsCfg = Object.create(null);

attrNames.acceptCharset = 'accept-charset';
attrNames.className = 'class';
attrNames.htmlFor = 'for';
attrNames.httpEquiv = 'http-equiv';

attrsCfg.autoPlay = BOOLEAN_ATTR_CFG;
attrsCfg.checked = BOOLEAN_PROP_CFG;
attrsCfg.controls = DEFAULT_PROP_CFG;
attrsCfg.disabled = BOOLEAN_ATTR_CFG;
attrsCfg.download = OVERLOADED_BOOLEAN_ATTR_CFG;
attrsCfg.id = DEFAULT_PROP_CFG;
attrsCfg.ismap = BOOLEAN_ATTR_CFG;
attrsCfg.loop = DEFAULT_PROP_CFG;
attrsCfg.multiple = BOOLEAN_PROP_CFG;
attrsCfg.muted = DEFAULT_PROP_CFG;
attrsCfg.open = BOOLEAN_ATTR_CFG;
attrsCfg.readOnly = BOOLEAN_PROP_CFG;
attrsCfg.selected = BOOLEAN_PROP_CFG;
attrsCfg.srcDoc = DEFAULT_PROP_CFG;
attrsCfg.style = {
    set : setObjProp,
    remove : removeProp,
    toString : stylePropToString
};
attrsCfg.value = {
    set : setPropWithCheck,
    remove : removeProp,
    toString : attrToString
};

export default function(attrName) {
    return attrName in attrsCfg?
        attrsCfg[attrName] :
        DEFAULT_ATTR_CFG;
}
<|endoftext|>


<commit_before>#!/usr/bin/env node

var spacey = require('./../index');

/**
 * Start Spacey Application
 */
spacey.express.get('/', function(req, res){
    res.render( 'index', { title: 'Hey', message: 'Theme Index!'});
});

/**
 * Start Spacey Application
 */

spacey.start();<commit_msg>remove route<commit_after>#!/usr/bin/env node

var spacey = require('./../index');

/**
 * Start Spacey Application
 */
// spacey.express.get('/', function(req, res){
//     res.render( 'index', { title: 'Hey', message: 'Theme Index!'});
// });

/**
 * Start Spacey Application
 */

spacey.start();<|endoftext|>


<commit_before>const url = {

  /**
   * Convert a query string in to an object
   */
  decodeUriArgs : function(queryStr) {
    const args = {}
    const a = queryStr.split('&')
    for (const i in a) {
      if (a.hasOwnProperty(i)) {
        const b = a[i].split('=')
        args[decodeURIComponent(b[0])] = decodeURIComponent(b[1])
      }
    }
    return args
  },

  /**
   * Convert an arguments object in to a query string
   */
  encodeUriArgs : function(args) {
    const queryParts = []
    for (const i in args) {
      queryParts.push(encodeURIComponent(i)+'='+encodeURIComponent(args[i]))
    }
    return queryParts.join('&')
  },

  /**
   * Merges query strings or objects into a single query string. Accepts a variable number of query string/objects
   * to merge. The latter overrides the former.
   */
  mergeQueryStr : function(queryStr1) {
    const args = (typeof queryStr1 === 'string' ? this.decodeUriArgs(queryStr1) : queryStr1)
    for (let i = 1; i < arguments.length; i++) {
      if (arguments[i] !== undefined) {
        const nextArgs = (typeof arguments[i] === 'string' ? this.decodeUriArgs(arguments[i]) : arguments[i])
        for (const a in nextArgs) {
          args[a] = nextArgs[a]
        }
      }
    }
    return this.encodeUriArgs(args)
  },

  /**
   * Takes an existing URL and merges additional data into the query string
   */
  augmentUri : function(uri, args) {
    const m = /^([^\?]+)\?(.+)+$/.exec(uri)
    if (m) {
      return m[1]+'?'+this.mergeQueryStr(m[2], args)
    } else {
      return uri+'?'+this.encodeUriArgs(args)
    }
  },

  /**
   * Get a parameter from the URL
   */
  getURLParameter : function(name) {
    return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,''])[1].replace(/\+/g, '%20'))||null
  }
}

const isFunction = x => typeof x === "function";

class GoogleAnalytics {
  constructor(id, param = 'q') {
    if (id !== undefined) {
      this.id = id;
    } else if (isFunction(window.ga)) {
      this.id = "ga";
    } else if (isFunction(window._ua)) {
      this.id = "_ua";
    } else {
      this.id = null;
    }
    this.param = param;
  }

  sendGAPageView(body) {
    if (
      this.id &&
      typeof window[this.id] === "function" &&
      process.env.NODE_ENV.environment !== 'development'
    ) {
      // Merge the body in with the existing query params in the url
      const pageAddress = url.augmentUri(
        // Take only the portion of the url following the domain
        location.href.substring(location.origin.length),
        { [this.param]: body }
      )
      window[this.id]('send', 'pageview', pageAddress)
    }
  }

  onBodyReset(previousBody) {
    this.sendGAPageView(previousBody)
  }

  onResultClicked(body) {
    this.sendGAPageView(body)
  }

  onPageClose(body) {
    this.sendGAPageView(body)
  }
}

export default GoogleAnalytics
<commit_msg>analytics: use available isFunction check<commit_after>const url = {

  /**
   * Convert a query string in to an object
   */
  decodeUriArgs : function(queryStr) {
    const args = {}
    const a = queryStr.split('&')
    for (const i in a) {
      if (a.hasOwnProperty(i)) {
        const b = a[i].split('=')
        args[decodeURIComponent(b[0])] = decodeURIComponent(b[1])
      }
    }
    return args
  },

  /**
   * Convert an arguments object in to a query string
   */
  encodeUriArgs : function(args) {
    const queryParts = []
    for (const i in args) {
      queryParts.push(encodeURIComponent(i)+'='+encodeURIComponent(args[i]))
    }
    return queryParts.join('&')
  },

  /**
   * Merges query strings or objects into a single query string. Accepts a variable number of query string/objects
   * to merge. The latter overrides the former.
   */
  mergeQueryStr : function(queryStr1) {
    const args = (typeof queryStr1 === 'string' ? this.decodeUriArgs(queryStr1) : queryStr1)
    for (let i = 1; i < arguments.length; i++) {
      if (arguments[i] !== undefined) {
        const nextArgs = (typeof arguments[i] === 'string' ? this.decodeUriArgs(arguments[i]) : arguments[i])
        for (const a in nextArgs) {
          args[a] = nextArgs[a]
        }
      }
    }
    return this.encodeUriArgs(args)
  },

  /**
   * Takes an existing URL and merges additional data into the query string
   */
  augmentUri : function(uri, args) {
    const m = /^([^\?]+)\?(.+)+$/.exec(uri)
    if (m) {
      return m[1]+'?'+this.mergeQueryStr(m[2], args)
    } else {
      return uri+'?'+this.encodeUriArgs(args)
    }
  },

  /**
   * Get a parameter from the URL
   */
  getURLParameter : function(name) {
    return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,''])[1].replace(/\+/g, '%20'))||null
  }
}

const isFunction = x => typeof x === "function";

class GoogleAnalytics {
  constructor(id, param = 'q') {
    if (id !== undefined) {
      this.id = id;
    } else if (isFunction(window.ga)) {
      this.id = "ga";
    } else if (isFunction(window._ua)) {
      this.id = "_ua";
    } else {
      this.id = null;
    }
    this.param = param;
  }

  sendGAPageView(body) {
    if (
      this.id &&
      isFunction(window[this.id]) &&
      process.env.NODE_ENV.environment !== 'development'
    ) {
      // Merge the body in with the existing query params in the url
      const pageAddress = url.augmentUri(
        // Take only the portion of the url following the domain
        location.href.substring(location.origin.length),
        { [this.param]: body }
      )
      window[this.id]('send', 'pageview', pageAddress)
    }
  }

  onBodyReset(previousBody) {
    this.sendGAPageView(previousBody)
  }

  onResultClicked(body) {
    this.sendGAPageView(body)
  }

  onPageClose(body) {
    this.sendGAPageView(body)
  }
}

export default GoogleAnalytics
<|endoftext|>


<commit_before>import React from "react";
import PropTypes from "prop-types";

import { Filter } from "../../controllers";

class CheckboxFacet extends React.Component {
  /**
   * propTypes
   * @property {Filter} filter
   * @property {string} name
   */
  static get propTypes() {
    return {
      filter: PropTypes.instanceOf(Filter).isRequired,
      name: PropTypes.string.isRequired
    };
  }

  render() {
    return <InputFacet type="checkbox" {...this.props} />;
  }
}

class RadioFacet extends React.Component {
  /**
   * propTypes
   * @property {Filter} filter
   * @property {string} name
   */
  static get propTypes() {
    return {
      filter: PropTypes.instanceOf(Filter).isRequired,
      name: PropTypes.string.isRequired
    };
  }

  render() {
    return <InputFacet type="radio" {...this.props} />;
  }
}

class SelectFacet extends React.Component {
  /**
   * propTypes
   * @property {Filter} filter
   * @property {string} name
   * @property {Object} options
   */
  static get propTypes() {
    return {
      filter: PropTypes.instanceOf(Filter).isRequired,
      name: PropTypes.string.isRequired,
      options: PropTypes.object.isRequired
    };
  }

  constructor(props) {
    super(props);

    this.state = { active: props.filter.isSet(props.name) };
  }

  componentDidMount() {
    this.unregister = this.props.filter.register(this.filterChanged);
  }

  componentWillUnmount() {
    this.unregister();
  }

  filterChanged = filter => {
    this.setState({ active: filter.get() });
  };

  handleChange = e => {
    filter.set(e.target.value, true);
  };

  render() {
    const { filter, name, options, ...rest } = this.props;

    const optionsRendered = Object.keys(options).map(o => {
      return (
        <option value={o} key={o}>
          {options[o]}
        </option>
      );
    });
    return (
      <select value={this.state.active} onChange={this.handleChange}>
        {optionsRendered}
      </select>
    );
  }
}

class InputFacet extends React.Component {
  constructor(props) {
    super(props);

    this.state = { active: props.filter.isSet(props.name) };
  }

  componentDidMount() {
    this.unregister = this.props.filter.register(this.filterChanged);
  }

  componentWillUnmount() {
    this.unregister();
  }

  filterChanged = filter => {
    this.setState({ active: filter.isSet(this.props.name) });
  };

  handleChange = () => {
    this.props.filter.set(this.props.name, !this.state.active);
  };

  render() {
    const { filter, name, children, ...rest } = this.props;
    return (
      <input
        onChange={this.handleChange}
        checked={this.state.active}
        {...rest}
      />
    );
  }
}

export { CheckboxFacet, RadioFacet, SelectFacet };
<commit_msg>ui/facets: fix SelectFacet<commit_after>import React from "react";
import PropTypes from "prop-types";

import { Filter } from "../../controllers";

class CheckboxFacet extends React.Component {
  /**
   * propTypes
   * @property {Filter} filter
   * @property {string} name
   */
  static get propTypes() {
    return {
      filter: PropTypes.instanceOf(Filter).isRequired,
      name: PropTypes.string.isRequired
    };
  }

  render() {
    return <InputFacet type="checkbox" {...this.props} />;
  }
}

class RadioFacet extends React.Component {
  /**
   * propTypes
   * @property {Filter} filter
   * @property {string} name
   */
  static get propTypes() {
    return {
      filter: PropTypes.instanceOf(Filter).isRequired,
      name: PropTypes.string.isRequired
    };
  }

  render() {
    return <InputFacet type="radio" {...this.props} />;
  }
}

class SelectFacet extends React.Component {
  /**
   * propTypes
   * @property {Filter} filter
   * @property {Object} options
   */
  static get propTypes() {
    return {
      filter: PropTypes.instanceOf(Filter).isRequired,
      options: PropTypes.object.isRequired
    };
  }

  constructor(props) {
    super(props);

    const current = props.filter.get();
    if (current.length > 0) {
      this.state = { active: current[0] };
    }
  }

  componentDidMount() {
    this.unregister = this.props.filter.register(this.filterChanged);
  }

  componentWillUnmount() {
    this.unregister();
  }

  filterChanged = (filter) => {
    const current = this.props.filter.get();
    if (current.length > 0) {
      this.setState({ active: current[0] });
    }
  };

  handleChange = (e) => {
    this.props.filter.set(e.target.value, true);
  };

  render() {
    const { filter, name, options, ...rest } = this.props;

    const optionsRendered = Object.keys(options).map(o => {
      return (
        <option value={o} key={o}>
          {options[o]}
        </option>
      );
    });
    return (
      <select value={this.state.active} onChange={this.handleChange}>
        {optionsRendered}
      </select>
    );
  }
}

class InputFacet extends React.Component {
  constructor(props) {
    super(props);

    this.state = { active: props.filter.isSet(props.name) };
  }

  componentDidMount() {
    this.unregister = this.props.filter.register(this.filterChanged);
  }

  componentWillUnmount() {
    this.unregister();
  }

  filterChanged = filter => {
    this.setState({ active: filter.isSet(this.props.name) });
  };

  handleChange = () => {
    this.props.filter.set(this.props.name, !this.state.active);
  };

  render() {
    const { filter, name, children, ...rest } = this.props;
    return (
      <input
        onChange={this.handleChange}
        checked={this.state.active}
        {...rest}
      />
    );
  }
}

export { CheckboxFacet, RadioFacet, SelectFacet };
<|endoftext|>


<commit_before>#!/usr/bin/env node
'use strict';

/**
 * Update only expired packages (those with nextUpdate previous to current date)
 * (C) 2015 Diego Lafuente.
 */

// requires
require('prototypes');
var config = require('../config.js');
var db = require('../lib/db.js');
var estimator = require('../lib/estimation.js');
var async = require('async');
var moment = require('moment');
var Log = require('log');
var testing = require('testing');

// globals
var log = new Log(config.logLevel);
var limit = config.limit;
var packagesCollection;

/**
 * Go over all the packages in all.json and update mongo if required.
 * There are two constraints:
 *   - Github only allows 5000 requests per hour.
 *   - We should use config.limit to avoid RangeErrors in the stack size
 * @param offset[optional]: offset over the all.json file. Defaults to zero.
 * @param callback[required]: a function(error, result) with the results of the process
 */
exports.goOver = function(offset, callback)
{
    if (typeof offset === 'function') {
        callback = offset;
        offset = 0;
    }
    var all;
    log.info('loading all.json...');
    try
    {
        all = require('../all.json');
        delete all._updated;
    }
    catch(exception)
    {
        return callback('Could not parse all.json: ' + exception);
    }
    log.info('all.json loaded');
    var names = Object.keys(all);
    log.info('All packages: ' + names.length);
    if (offset)
    {
        log.info('Offset ' + offset);
        for (var i=0; i<offset; i++)
        {
            delete all[names.shift()];
        }
    }
    var numberOfPackages = names.length;
    log.info('All packages after offset: ' + numberOfPackages);
    limit = (limit === null) || (numberOfPackages < limit) ? numberOfPackages : limit;
    var chunks = [];
    for (var j = 0; j < Math.ceil(numberOfPackages/limit); j++)
    {
        chunks.push([]);
    }
    var packageCount = 0;
    for (var name in all)
    {
        var entry = all[name];
        var index = Math.floor(packageCount/limit);
        chunks[index].push(getEstimator(entry));
        packageCount++;
        delete all[name];
    }
    log.debug('number of chunks: ' + chunks.length);
    db.addCallback(function(error, result)
    {
        if (error) {
            return callback(error);
        }
        packagesCollection = result.collection(config.packagesCollection);
        var series = [];
        while (chunks.length)
        {
            series.push(getChunkProcessor(chunks.shift()));
        }
        log.debug('series has length ' + series.length);
        async.series(series, function(error)
        {
            if (error)
            {
                callback(error);
            }
            callback(null, series.length);
        });
    });
};

function getEstimator(entry)
{
    return function(callback)
    {
        var name = entry.name;
        var now = moment();
        packagesCollection.findOne({name: name}, function (error, item)
        {
            var isNewEntry = error || !item;
            var shouldUpdate = isNewEntry || (moment(item.nextUpdate) < now);
            // return if we should not update
            if (!shouldUpdate)
            {
                log.info('Discarding package: ' + name);
                return callback(null);
            }
            // update!!
            estimator.estimate(entry, function (error, estimation)
            {
                // new entry?
                if (!isNewEntry && estimation)
                {
                    delete estimation.created;
                    estimation.timesUpdated = item.timesUpdated + 1;
                    // should we defer the update one year
                    var created = moment(item.created);
                    var lastUpdated = moment(estimation.lastUpdated);
                    var monthsAgo = lastUpdated.diff(created, 'months');
                    if ((monthsAgo > 11) && (estimation.timesUpdated <= (monthsAgo + 1)))
                    {
                        estimation.nextUpdate = moment(lastUpdated).add(1, 'years');
                    }
                }
                return callback (error, estimation);
            });
        });
    };
}

function getChunkProcessor(chunk)
{
    return function(callback)
    {
        log.info('About to process chunk: ' + chunk.length);
        async.parallel(chunk, function(error, estimations)
        {
            if (error)
            {
                log.error('Chunk processed with error ', error);
                return callback(error);
            }
            log.info('Chunk processed.');
            // Adjust pending, remaining calls, etc
            var pendings = [];
            var updates = [];
            var githubApiRemainingCalls = 9999999;
            var githubApiResetLimit;
            estimations.forEach(function (estimation)
            {
                if (estimation.githubApiRemainingCalls < githubApiRemainingCalls)
                {
                    githubApiRemainingCalls = estimation.githubApiRemainingCalls;
                    githubApiResetLimit = estimation.githubApiResetLimit;
                }
                delete estimation.githubApiRemainingCalls;
                delete estimation.githubApiResetLimit;
                if (estimation.pending)
                {
                    var item = {
                        pending: estimation.pending
                    };
                    delete estimation.pending;
                    item.previousEstimation = estimation;
                    pendings.push(item);
                }
                else
                {
                    var finalEstimation = estimator.addQuality(estimation);
                    updates.push(finalEstimation);
                }
            });
            // process updates, and then pendings
            var updatesPendingsStream = [];
            updatesPendingsStream.push(function (callback)
            {
                processUpdates(updates, callback);
            });
            updatesPendingsStream.push(function (result, callback)
            {
                processPendings(pendings, githubApiRemainingCalls, githubApiResetLimit, callback);
            });
            async.waterfall(updatesPendingsStream, function (error, result)
            {
                githubApiRemainingCalls = result.githubApiRemainingCalls;
                githubApiResetLimit = result.githubApiResetLimit;
                // check remaining api calls.
                if (githubApiRemainingCalls < limit)
                {
                    var now = moment().unix();
                    if (githubApiResetLimit > now)
                    {
                        var millisecondsToWait = (githubApiResetLimit - now) * 1000;
                        log.info('Waiting ' + millisecondsToWait + ' milliseconds until next chunk');
                        setTimeout(function()
                        {
                            return callback(null);
                        }, millisecondsToWait);
                    }
                    else
                    {
                        return callback(null);
                    }
                }
                else
                {
                    return callback(null);
                }
            });
        });
    };
}

function processUpdates(estimations, callback)
{
    var updatesStream = [];
    estimations.forEach(function (estimation)
    {
        updatesStream.push(function (callback)
        {
            packagesCollection.update({name: estimation.name}, {'$set':estimation}, {upsert: true}, function(error)
            {
                if (error)
                {
                    log.error('Package ' + estimation.name + ' could not be upserted in the database: ' + JSON.stringify(error));
                }
                return callback(null);
            });
        });
    });
    async.parallel(updatesStream, callback);
}

function processPendings(pendings, githubApiRemainingCalls, githubApiResetLimit, callback)
{
    // stream the pending stuff
    var pendingStream = [];
    pendings.forEach(function (pendingItem)
    {
        // only pending issues so far
        pendingStream.push(function (callback)
        {
            log.info('Processing pending for ' + pendingItem.previousEstimation.name);
            // function to process pending item
            function processPendingItem(pendingItem)
            {
                estimator.pending(pendingItem.pending, function (error, pendingEstimation)
                {
                    githubApiRemainingCalls = pendingEstimation.githubApiRemainingCalls;
                    githubApiResetLimit = pendingEstimation.githubApiResetLimit;
                    delete pendingEstimation.githubApiRemainingCalls;
                    delete pendingEstimation.githubApiResetLimit;
                    var finalEstimation = estimator.addQuality(pendingItem.previousEstimation.concat(pendingEstimation));
                    packagesCollection.update({name: finalEstimation.name}, {'$set':finalEstimation}, {upsert: true}, function(error)
                    {
                        if (error)
                        {
                            log.error('Package ' + finalEstimation.name + ' could not be upserted in the database: ' + JSON.stringify(error));
                        }
                        return callback(null);
                    });
                });
            }
            // pending API calls
            var apiCallsForThisPending = pendingItem.pending[0].pages[1] - pendingItem.pending[0].pages[0] + 1;
            // check remaining api calls.
            if (githubApiRemainingCalls < apiCallsForThisPending)
            {
                var now = moment().unix();
                if (githubApiResetLimit > now)
                {
                    var millisecondsToWait = (githubApiResetLimit - now) * 1000;
                    log.info('Waiting in pendings ' + millisecondsToWait + ' milliseconds until next pending');
                    setTimeout(function()
                    {
                        processPendingItem(pendingItem);
                    }, millisecondsToWait);
                }
                else
                {
                    processPendingItem(pendingItem);
                }
            }
            else
            {
                processPendingItem(pendingItem);
            }
        });
    });
    // run pending stream
    async.series(pendingStream, function()
    {
        return callback (null, {
            githubApiRemainingCalls: githubApiRemainingCalls,
            githubApiResetLimit: githubApiResetLimit
        });
    });
}

/**
 * Unit tests.
 */
function testEstimatorNewEntry(callback)
{
    var newEntry = {name: 'newEntry'};
    var now = moment().format();
    var nextUpdate = moment(now).add(1, 'month').format();
    // stubs
    estimator = {
        estimate: function(entry, internalCallback) {
            testing.assertEquals(entry.name, newEntry.name, 'wrong entry passed to estimate', callback);
            return internalCallback(null, {
                name: entry.name,
                created: now,
                nextUpdate: nextUpdate,
                timesUpdated: 0
            });
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, newEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(true);
        }
    };
    var theEstimator = getEstimator(newEntry);
    theEstimator(function(error, estimation) {
        testing.check(error, callback);
        testing.assertEquals(estimation.name, newEntry.name, 'wrong name returned by the estimator', callback);
        testing.assertEquals(estimation.created, now, 'wrong created returned by the estimator', callback);
        testing.assertEquals(estimation.nextUpdate, nextUpdate, 'wrong nextUpdate returned by the estimator', callback);
        testing.assertEquals(estimation.timesUpdated, 0, 'wrong timesUpdated returned by the estimator', callback);
        testing.success(callback);
    });
}

function testEstimatorExistingEntryShouldUpdate(callback)
{
    var existingEntry = {name: 'existingEntry'};
    var now = moment().format();
    var nextUpdate = moment(now).add(1, 'month').format();
    // stubs
    estimator = {
        estimate: function(entry, internalCallback) {
            testing.assertEquals(entry.name, existingEntry.name, 'wrong entry passed to estimate', callback);
            return internalCallback(null, {
                name: entry.name,
                created: now,
                nextUpdate: nextUpdate,
                timesUpdated: 0
            });
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, existingEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(null, {
                name: query.name,
                nextUpdate: moment(now).subtract(1, 'second').format(),
                timesUpdated: 7
            });
        }
    };
    var theEstimator = getEstimator(existingEntry);
    theEstimator(function(error, estimation) {
        testing.check(error, callback);
        testing.assertEquals(estimation.name, existingEntry.name, 'wrong name returned by the estimator', callback);
        testing.check(estimation.created, 'created should be deleted in existing entries', callback);
        testing.assertEquals(estimation.nextUpdate, nextUpdate, 'wrong nextUpdate returned by the estimator', callback);
        testing.assertEquals(estimation.timesUpdated, 8, 'wrong timesUpdated returned by the estimator', callback);
        testing.success(callback);
    });
}

function testEstimatorExistingEntryShouldUpdateAndDefer(callback)
{
    var existingEntry = {name: 'existingEntry'};
    var now = moment().format();
    var nextUpdate = moment(now).add(1, 'month').format();
    // stubs
    estimator = {
        estimate: function(entry, internalCallback) {
            testing.assertEquals(entry.name, existingEntry.name, 'wrong entry passed to estimate', callback);
            return internalCallback(null, {
                name: entry.name,
                created: now,
                lastUpdated: now,
                nextUpdate: nextUpdate,
                timesUpdated: 0
            });
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, existingEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(null, {
                name: query.name,
                created: moment(now).subtract(12, 'months').format(),
                nextUpdate: moment(now).subtract(1, 'second').format(),
                timesUpdated: 12
            });
        }
    };
    var theEstimator = getEstimator(existingEntry);
    theEstimator(function(error, estimation) {
        testing.check(error, callback);
        testing.assertEquals(estimation.name, existingEntry.name, 'wrong name returned by the estimator', callback);
        testing.check(estimation.created, 'created should be deleted in existing entries', callback);
        testing.assertEquals(moment(estimation.nextUpdate).diff(now, 'years'), 1, 'wrong nextUpdate returned by the estimator', callback);
        testing.assertEquals(estimation.timesUpdated, 13, 'wrong timesUpdated returned by the estimator', callback);
        testing.success(callback);
    });
}

function testEstimatorExistingEntryShouldNotUpdate(callback)
{
    var existingEntry = {name: 'existingEntry'};
    var now = moment();
    // stubs
    estimator = {
        estimate: function() {
            testing.check(true, 'estimate should never be called', callback);
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, existingEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(null, {
                name: query.name,
                nextUpdate: moment(now).add(1, 'second').format(),
            });
        }
    };
    var theEstimator = getEstimator(existingEntry);
    theEstimator(function(error) {
        testing.check(error, callback);
        testing.success(callback);
    });
}

/**
 * Run all tests.
 */
exports.test = function(callback)
{
    testing.run([
        testEstimatorNewEntry,
        testEstimatorExistingEntryShouldUpdate,
        testEstimatorExistingEntryShouldUpdateAndDefer,
        testEstimatorExistingEntryShouldNotUpdate
    ], function() {
        db.close(function(error) {
            callback(error);
        });
    });
};

// run script if invoked directly
if (__filename == process.argv[1])
{
    var offset = process.argv[2];
    exports.goOver(offset, function(error, result)
    {
        if (error)
        {
            log.error('Could not evaluate all: %s', error);
            return db.close(function () {
                log.info('DB closed');
                process.exit(1);
            });
        }
        log.info('Processed ' + result + ' chunks of approx ' + limit + ' elements each.');
        db.close(function () {
            log.info('DB closed');
            process.exit(0);
        });
    });
}<commit_msg>changed unit test header<commit_after>#!/usr/bin/env node
'use strict';

/**
 * Update only expired packages (those with nextUpdate previous to current date)
 * (C) 2015 Diego Lafuente.
 */

// requires
require('prototypes');
var config = require('../config.js');
var db = require('../lib/db.js');
var estimator = require('../lib/estimation.js');
var async = require('async');
var moment = require('moment');
var Log = require('log');
var testing = require('testing');

// globals
var log = new Log(config.logLevel);
var limit = config.limit;
var packagesCollection;

/**
 * Go over all the packages in all.json and update mongo if required.
 * There are two constraints:
 *   - Github only allows 5000 requests per hour.
 *   - We should use config.limit to avoid RangeErrors in the stack size
 * @param offset[optional]: offset over the all.json file. Defaults to zero.
 * @param callback[required]: a function(error, result) with the results of the process
 */
exports.goOver = function(offset, callback)
{
    if (typeof offset === 'function') {
        callback = offset;
        offset = 0;
    }
    var all;
    log.info('loading all.json...');
    try
    {
        all = require('../all.json');
        delete all._updated;
    }
    catch(exception)
    {
        return callback('Could not parse all.json: ' + exception);
    }
    log.info('all.json loaded');
    var names = Object.keys(all);
    log.info('All packages: ' + names.length);
    if (offset)
    {
        log.info('Offset ' + offset);
        for (var i=0; i<offset; i++)
        {
            delete all[names.shift()];
        }
    }
    var numberOfPackages = names.length;
    log.info('All packages after offset: ' + numberOfPackages);
    limit = (limit === null) || (numberOfPackages < limit) ? numberOfPackages : limit;
    var chunks = [];
    for (var j = 0; j < Math.ceil(numberOfPackages/limit); j++)
    {
        chunks.push([]);
    }
    var packageCount = 0;
    for (var name in all)
    {
        var entry = all[name];
        var index = Math.floor(packageCount/limit);
        chunks[index].push(getEstimator(entry));
        packageCount++;
        delete all[name];
    }
    log.debug('number of chunks: ' + chunks.length);
    db.addCallback(function(error, result)
    {
        if (error) {
            return callback(error);
        }
        packagesCollection = result.collection(config.packagesCollection);
        var series = [];
        while (chunks.length)
        {
            series.push(getChunkProcessor(chunks.shift()));
        }
        log.debug('series has length ' + series.length);
        async.series(series, function(error)
        {
            if (error)
            {
                callback(error);
            }
            callback(null, series.length);
        });
    });
};

function getEstimator(entry)
{
    return function(callback)
    {
        var name = entry.name;
        var now = moment();
        packagesCollection.findOne({name: name}, function (error, item)
        {
            var isNewEntry = error || !item;
            var shouldUpdate = isNewEntry || (moment(item.nextUpdate) < now);
            // return if we should not update
            if (!shouldUpdate)
            {
                log.info('Discarding package: ' + name);
                return callback(null);
            }
            // update!!
            estimator.estimate(entry, function (error, estimation)
            {
                // new entry?
                if (!isNewEntry && estimation)
                {
                    delete estimation.created;
                    estimation.timesUpdated = item.timesUpdated + 1;
                    // should we defer the update one year
                    var created = moment(item.created);
                    var lastUpdated = moment(estimation.lastUpdated);
                    var monthsAgo = lastUpdated.diff(created, 'months');
                    if ((monthsAgo > 11) && (estimation.timesUpdated <= (monthsAgo + 1)))
                    {
                        estimation.nextUpdate = moment(lastUpdated).add(1, 'years');
                    }
                }
                return callback (error, estimation);
            });
        });
    };
}

function getChunkProcessor(chunk)
{
    return function(callback)
    {
        log.info('About to process chunk: ' + chunk.length);
        async.parallel(chunk, function(error, estimations)
        {
            if (error)
            {
                log.error('Chunk processed with error ', error);
                return callback(error);
            }
            log.info('Chunk processed.');
            // Adjust pending, remaining calls, etc
            var pendings = [];
            var updates = [];
            var githubApiRemainingCalls = 9999999;
            var githubApiResetLimit;
            estimations.forEach(function (estimation)
            {
                if (estimation.githubApiRemainingCalls < githubApiRemainingCalls)
                {
                    githubApiRemainingCalls = estimation.githubApiRemainingCalls;
                    githubApiResetLimit = estimation.githubApiResetLimit;
                }
                delete estimation.githubApiRemainingCalls;
                delete estimation.githubApiResetLimit;
                if (estimation.pending)
                {
                    var item = {
                        pending: estimation.pending
                    };
                    delete estimation.pending;
                    item.previousEstimation = estimation;
                    pendings.push(item);
                }
                else
                {
                    var finalEstimation = estimator.addQuality(estimation);
                    updates.push(finalEstimation);
                }
            });
            // process updates, and then pendings
            var updatesPendingsStream = [];
            updatesPendingsStream.push(function (callback)
            {
                processUpdates(updates, callback);
            });
            updatesPendingsStream.push(function (result, callback)
            {
                processPendings(pendings, githubApiRemainingCalls, githubApiResetLimit, callback);
            });
            async.waterfall(updatesPendingsStream, function (error, result)
            {
                githubApiRemainingCalls = result.githubApiRemainingCalls;
                githubApiResetLimit = result.githubApiResetLimit;
                // check remaining api calls.
                if (githubApiRemainingCalls < limit)
                {
                    var now = moment().unix();
                    if (githubApiResetLimit > now)
                    {
                        var millisecondsToWait = (githubApiResetLimit - now) * 1000;
                        log.info('Waiting ' + millisecondsToWait + ' milliseconds until next chunk');
                        setTimeout(function()
                        {
                            return callback(null);
                        }, millisecondsToWait);
                    }
                    else
                    {
                        return callback(null);
                    }
                }
                else
                {
                    return callback(null);
                }
            });
        });
    };
}

function processUpdates(estimations, callback)
{
    var updatesStream = [];
    estimations.forEach(function (estimation)
    {
        updatesStream.push(function (callback)
        {
            packagesCollection.update({name: estimation.name}, {'$set':estimation}, {upsert: true}, function(error)
            {
                if (error)
                {
                    log.error('Package ' + estimation.name + ' could not be upserted in the database: ' + JSON.stringify(error));
                }
                return callback(null);
            });
        });
    });
    async.parallel(updatesStream, callback);
}

function processPendings(pendings, githubApiRemainingCalls, githubApiResetLimit, callback)
{
    // stream the pending stuff
    var pendingStream = [];
    pendings.forEach(function (pendingItem)
    {
        // only pending issues so far
        pendingStream.push(function (callback)
        {
            log.info('Processing pending for ' + pendingItem.previousEstimation.name);
            // function to process pending item
            function processPendingItem(pendingItem)
            {
                estimator.pending(pendingItem.pending, function (error, pendingEstimation)
                {
                    githubApiRemainingCalls = pendingEstimation.githubApiRemainingCalls;
                    githubApiResetLimit = pendingEstimation.githubApiResetLimit;
                    delete pendingEstimation.githubApiRemainingCalls;
                    delete pendingEstimation.githubApiResetLimit;
                    var finalEstimation = estimator.addQuality(pendingItem.previousEstimation.concat(pendingEstimation));
                    packagesCollection.update({name: finalEstimation.name}, {'$set':finalEstimation}, {upsert: true}, function(error)
                    {
                        if (error)
                        {
                            log.error('Package ' + finalEstimation.name + ' could not be upserted in the database: ' + JSON.stringify(error));
                        }
                        return callback(null);
                    });
                });
            }
            // pending API calls
            var apiCallsForThisPending = pendingItem.pending[0].pages[1] - pendingItem.pending[0].pages[0] + 1;
            // check remaining api calls.
            if (githubApiRemainingCalls < apiCallsForThisPending)
            {
                var now = moment().unix();
                if (githubApiResetLimit > now)
                {
                    var millisecondsToWait = (githubApiResetLimit - now) * 1000;
                    log.info('Waiting in pendings ' + millisecondsToWait + ' milliseconds until next pending');
                    setTimeout(function()
                    {
                        processPendingItem(pendingItem);
                    }, millisecondsToWait);
                }
                else
                {
                    processPendingItem(pendingItem);
                }
            }
            else
            {
                processPendingItem(pendingItem);
            }
        });
    });
    // run pending stream
    async.series(pendingStream, function()
    {
        return callback (null, {
            githubApiRemainingCalls: githubApiRemainingCalls,
            githubApiResetLimit: githubApiResetLimit
        });
    });
}

/************************************************
 **************** UNIT TESTS ********************
 ************************************************/
function testEstimatorNewEntry(callback)
{
    var newEntry = {name: 'newEntry'};
    var now = moment().format();
    var nextUpdate = moment(now).add(1, 'month').format();
    // stubs
    estimator = {
        estimate: function(entry, internalCallback) {
            testing.assertEquals(entry.name, newEntry.name, 'wrong entry passed to estimate', callback);
            return internalCallback(null, {
                name: entry.name,
                created: now,
                nextUpdate: nextUpdate,
                timesUpdated: 0
            });
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, newEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(true);
        }
    };
    var theEstimator = getEstimator(newEntry);
    theEstimator(function(error, estimation) {
        testing.check(error, callback);
        testing.assertEquals(estimation.name, newEntry.name, 'wrong name returned by the estimator', callback);
        testing.assertEquals(estimation.created, now, 'wrong created returned by the estimator', callback);
        testing.assertEquals(estimation.nextUpdate, nextUpdate, 'wrong nextUpdate returned by the estimator', callback);
        testing.assertEquals(estimation.timesUpdated, 0, 'wrong timesUpdated returned by the estimator', callback);
        testing.success(callback);
    });
}

function testEstimatorExistingEntryShouldUpdate(callback)
{
    var existingEntry = {name: 'existingEntry'};
    var now = moment().format();
    var nextUpdate = moment(now).add(1, 'month').format();
    // stubs
    estimator = {
        estimate: function(entry, internalCallback) {
            testing.assertEquals(entry.name, existingEntry.name, 'wrong entry passed to estimate', callback);
            return internalCallback(null, {
                name: entry.name,
                created: now,
                nextUpdate: nextUpdate,
                timesUpdated: 0
            });
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, existingEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(null, {
                name: query.name,
                nextUpdate: moment(now).subtract(1, 'second').format(),
                timesUpdated: 7
            });
        }
    };
    var theEstimator = getEstimator(existingEntry);
    theEstimator(function(error, estimation) {
        testing.check(error, callback);
        testing.assertEquals(estimation.name, existingEntry.name, 'wrong name returned by the estimator', callback);
        testing.check(estimation.created, 'created should be deleted in existing entries', callback);
        testing.assertEquals(estimation.nextUpdate, nextUpdate, 'wrong nextUpdate returned by the estimator', callback);
        testing.assertEquals(estimation.timesUpdated, 8, 'wrong timesUpdated returned by the estimator', callback);
        testing.success(callback);
    });
}

function testEstimatorExistingEntryShouldUpdateAndDefer(callback)
{
    var existingEntry = {name: 'existingEntry'};
    var now = moment().format();
    var nextUpdate = moment(now).add(1, 'month').format();
    // stubs
    estimator = {
        estimate: function(entry, internalCallback) {
            testing.assertEquals(entry.name, existingEntry.name, 'wrong entry passed to estimate', callback);
            return internalCallback(null, {
                name: entry.name,
                created: now,
                lastUpdated: now,
                nextUpdate: nextUpdate,
                timesUpdated: 0
            });
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, existingEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(null, {
                name: query.name,
                created: moment(now).subtract(12, 'months').format(),
                nextUpdate: moment(now).subtract(1, 'second').format(),
                timesUpdated: 12
            });
        }
    };
    var theEstimator = getEstimator(existingEntry);
    theEstimator(function(error, estimation) {
        testing.check(error, callback);
        testing.assertEquals(estimation.name, existingEntry.name, 'wrong name returned by the estimator', callback);
        testing.check(estimation.created, 'created should be deleted in existing entries', callback);
        testing.assertEquals(moment(estimation.nextUpdate).diff(now, 'years'), 1, 'wrong nextUpdate returned by the estimator', callback);
        testing.assertEquals(estimation.timesUpdated, 13, 'wrong timesUpdated returned by the estimator', callback);
        testing.success(callback);
    });
}

function testEstimatorExistingEntryShouldNotUpdate(callback)
{
    var existingEntry = {name: 'existingEntry'};
    var now = moment();
    // stubs
    estimator = {
        estimate: function() {
            testing.check(true, 'estimate should never be called', callback);
        }
    };
    packagesCollection = {
        findOne: function(query, internalCallback) {
            testing.assertEquals(query.name, existingEntry.name, 'wrong name passed to findOne', callback);
            return internalCallback(null, {
                name: query.name,
                nextUpdate: moment(now).add(1, 'second').format(),
            });
        }
    };
    var theEstimator = getEstimator(existingEntry);
    theEstimator(function(error) {
        testing.check(error, callback);
        testing.success(callback);
    });
}

/**
 * Run all tests.
 */
exports.test = function(callback)
{
    testing.run([
        testEstimatorNewEntry,
        testEstimatorExistingEntryShouldUpdate,
        testEstimatorExistingEntryShouldUpdateAndDefer,
        testEstimatorExistingEntryShouldNotUpdate
    ], function() {
        db.close(function(error) {
            callback(error);
        });
    });
};

// run script if invoked directly
if (__filename == process.argv[1])
{
    var offset = process.argv[2];
    exports.goOver(offset, function(error, result)
    {
        if (error)
        {
            log.error('Could not evaluate all: %s', error);
            return db.close(function () {
                log.info('DB closed');
                process.exit(1);
            });
        }
        log.info('Processed ' + result + ' chunks of approx ' + limit + ' elements each.');
        db.close(function () {
            log.info('DB closed');
            process.exit(0);
        });
    });
}<|endoftext|>


<commit_before>/*
 * fixmystreet.js
 * FixMyStreet JavaScript
 */

/*
 * general height fixing function
 *
 * elem1: element to check against
 * elem2: target element
 * offset: this will be added (if present) to the final value, useful for height errors
 */
function heightFix(elem1, elem2, offset){
    var h1 = $(elem1).height(),
        h2 = $(elem2).height();
    if(offset === undefined){
        offset = 0;
    }
    if(h1 > h2){
        $(elem2).css({'min-height':h1+offset});
    }
}

/*
 * very simple tab function
 *
 * elem: trigger element, must have an href attribute (so probably needs to be an )
 */
function tabs(elem, indirect) {
    var href = elem.attr('href');
    //stupid IE sometimes adds the full uri into the href attr, so trim
    var start = href.indexOf('#'),
        target = href.slice(start, href.length);

    if (indirect) {
        elem = $(target + '_tab');
    }

    if(!$(target).hasClass('open'))
    {
        //toggle class on nav
        $('.tab-nav .active').removeClass('active');
        elem.addClass('active');

        //hide / show the right tab
        $('.tab.open').hide().removeClass('open');
        $(target).show().addClass('open');
    }
}

$(function(){
    var $html = $('html');

    var cobrand;
    if (window.location.href.indexOf('bromley') != -1) {
        cobrand = 'bromley';
    }

    // Deal with switching between mobile and desktop versions on resize
    var last_type;
    $(window).resize(function(){
        var type = $('#site-header').css('borderTopWidth');
        if (type == '4px') { type = 'mobile'; }
        else if (type == '0px') { type = 'desktop'; }
        else { return; }
        if (last_type == type) { return; }
        if (type == 'mobile') {
            $html.addClass('mobile');
            $('#map_box').prependTo('.content').css({
                zIndex: '', position: '',
                top: '', left: '', right: '', bottom: '',
                width: '', height: '10em',
                margin: ''
            });
            if (typeof fixmystreet !== 'undefined') {
                fixmystreet.state_map = ''; // XXX
            }
            if (typeof fixmystreet !== 'undefined' && fixmystreet.page == 'around') {
                // Immediately go full screen map if on around page
                if (cobrand != 'bromley') {
                    $('#site-header').hide();
                }
                $('#map_box').prependTo('.wrapper').css({
                    position: 'absolute',
                    top: 0, left: 0, right: 0, bottom: 0,
                    height: 'auto',
                    margin: 0
                });
                $('#fms_pan_zoom').css({ top: '2.75em !important' });
                $('.big-green-banner')
                    .addClass('mobile-map-banner')
                    .appendTo('#map_box')
                    .html('Home Place pin on map');
            }
            $('span.report-a-problem-btn').on('click.reportBtn', function(){
                $('html, body').animate({scrollTop:0}, 500);
            }).css({ cursor:'pointer' }).on('hover.reportBtn', function(){
                $(this).toggleClass('hover');
            });
        } else {
            // Make map full screen on non-mobile sizes.
            $html.removeClass('mobile');
            position_map_box();
            if (typeof fixmystreet !== 'undefined') {
                if (cobrand == 'bromley') {
                    //$('#bromley-footer').hide();
                } else {
                    fixmystreet.state_map = 'full';
                }
            }
            if (typeof fixmystreet !== 'undefined' && fixmystreet.page == 'around') {
                // Remove full-screen-ness
                var banner_text;
                if (cobrand == 'bromley') {
                    banner_text = 'Click map to report a problemYellow pins show existing reports';
                } else {
                    $('#site-header').show();
                    banner_text = 'Click map to report a problem';
                }
                $('#fms_pan_zoom').css({ top: '4.75em !important' });
                $('.big-green-banner')
                    .removeClass('mobile-map-banner')
                    .prependTo('#side')
                    .html(banner_text);
            }
            $('span.report-a-problem-btn').css({ cursor:'' }).off('.reportBtn');
        }
        last_type = type;
    }).resize();

    /*
     * Report a problem page
     */
    //desktop
    if ($('#report-a-problem-sidebar').is(':visible')) {
        heightFix('#report-a-problem-sidebar', '.content', 26);
    }

    //show/hide notes on mobile
    $('.mobile #report-a-problem-sidebar').after('How to send successful reports').hide();
    $('.rap-notes-trigger').click(function(e){
        e.preventDefault();
        //check if we've already moved the notes
        if($('.rap-notes').length > 0){
            //if we have, show and hide .content
            $('.content').hide();
            $('.rap-notes').show();
        }else{
            //if not, move them and show, hiding .content
            $('.content').after('

').hide();
            $('#report-a-problem-sidebar').appendTo('.rap-notes').show().after('Back');
        }
        $('html, body').scrollTop($('#report-a-problem-sidebar').offset().top);
        location.hash = 'rap-notes';
    });
    $('.mobile').on('click', '.rap-notes-close', function(e){
        e.preventDefault();
        //hide notes, show .content
        $('.content').show();
        $('.rap-notes').hide();
        $('html, body').scrollTop($('#mob_ok').offset().top);
        location.hash = 'report';
    });

    //move 'skip this step' link on mobile
    $('.mobile #skip-this-step').hide();
    $('.mobile #skip-this-step a').addClass('chevron').wrap('
').appendTo('#key-tools');

    /*
     * Tabs
     */
    //make initial tab active
    $('.tab-nav a').first().addClass('active');
    $('.tab').first().addClass('open');

    //hide other tabs
    $('.tab').not('.open').hide();

    //set up click event
    $(".tab-nav").on('click', 'a', function(e){
        e.preventDefault();
        tabs($(this));
    });
    $('.tab_link').click(function(e) {
        e.preventDefault();
        tabs($(this), 1);
    });

    /*
     * Skip to nav on mobile
     */
    $('.mobile').on('click', '#nav-link', function(e){
        e.preventDefault();
        var offset = $('#main-nav').offset().top;
        $('html, body').animate({scrollTop:offset}, 1000);
    });

    /*
     * Show stuff on input focus
     */
    var form_focus_data = $('.form-focus-trigger').map(function() {
        return $(this).val();
    }).get().join('');
    if (!form_focus_data) {
        $('.form-focus-hidden').hide();
        $('.form-focus-trigger').on('focus', function(){
            $('.form-focus-hidden').fadeIn(500);
        });
    }

    /*
     * Show on click - pretty generic
     */
    $('.hideshow-trigger').on('click', function(e){
        e.preventDefault();
        var href = $(this).attr('href'),
            //stupid IE sometimes adds the full uri into the href attr, so trim
            start = href.indexOf('#'),
            target = href.slice(start, href.length);

        $(target).removeClass('hidden-js');

        $(this).hide();
    });

    /*
     * nicetable - on mobile shift 'name' col to be a row
     */
    $('.mobile .nicetable th.title').remove();
    $('.mobile .nicetable td.title').each(function(i){
        $(this).attr('colspan', 5).insertBefore($(this).parent('tr')).wrap('
');
    });
    // $('.mobile .nicetable tr.heading > td.title').css({'min-width':'300px'});
    // $('.mobile .nicetable tr > td.data').css({'max-width':'12%'});

    /*
     * Map controls prettiness
     */
    //add links container (if its not there)
    if($('#sub_map_links').length === 0){
        $('
').insertAfter($('#map'));
    }

// A sliding drawer from the bottom of the page, small version
// that doesn't change the main content at all.
(function($){

    var opened;

    $.fn.small_drawer = function(id) {
        this.toggle(function(){
            if (opened) {
                opened.click();
            }
            var $this = $(this), d = $('#' + id);
            if (!$this.addClass('hover').data('setup')) {
                d.hide().removeClass('hidden-js').css({
                padding: '1em',
                background: '#fff'
                });
                $this.data('setup', true);
            }
            d.slideDown();
            opened = $this;
        }, function(e){
            var $this = $(this), d = $('#' + id);
            $this.removeClass('hover');
            d.slideUp();
            opened = null;
        });
    };

})(jQuery);

// A sliding drawer from the bottom of the page, large version
$.fn.drawer = function(id, ajax) {
    // IE7 positions the fixed tool bar 1em to the left unless it comes after
    // the full-width section, ho-hum. Move it to where it would be after an
    // open/close anyway
    if ($('html.ie7').length) {
        var $sw = $('.shadow-wrap'), $content = $('.content[role="main"]');
        $sw.appendTo($content);
    }
    this.toggle(function(){
        var $this = $(this), d = $('#' + id), $content = $('.content[role="main"]');
        if (!$this.addClass('hover').data('setup')) {
            // make a drawer div with an innerDiv
            if (!d.length) {
                d = $('
');
            }
            var innerDiv = $('
');
            d.wrapInner(innerDiv);

            // if ajax, load it with a spinner
            if (ajax) {
                var href = $this.attr('href') + ';ajax=1';
                $this.prepend(' ');
                innerDiv.load(href, function(){
                    $('.spinner').remove();
                });
            }

            // Tall drawer - put after .content for scrolling to work okay.
            // position over the top of the main .content in precisely the right location
            d.insertAfter($content).addClass('content').css({
                position: 'absolute',
                zIndex: '1100',
                marginTop: $('html.ie6, html.ie7').length ? '-3em' : 0, // IE6/7 otherwise include the 3em padding and stay too low
                left: 0,
                top: $(window).height() - $content.offset().top
            }).removeClass('hidden-js').find('h2').css({ marginTop: 0 });
            $this.data('setup', true);
        }

        //do the animation
        $('.shadow-wrap').prependTo(d).addClass('static');
        d.show().animate({top:'3em'}, 1000, function(){
            $content.fadeOut(function() {
                d.css({ position: 'relative' });
            });
        });
    }, function(e){
        var $this = $(this), d = $('#' + id), $sw = $('.shadow-wrap'),
            $content = $('.content[role="main"]'),
            tot_height = $(window).height() - d.offset().top;
        $this.removeClass('hover');
        d.css({ position: 'absolute' }).animate({ top: tot_height }, 1000, function(){
            d.hide();
            $sw.appendTo($content).removeClass('static');
        });
        $content.show();
    });
};

    if ($('html.mobile').length || slide_wards_down ) {
        $('#council_wards').hide().removeClass('hidden-js').find('h2').hide();
        $('#key-tool-wards').click(function(e){
            e.preventDefault();
            $('#council_wards').slideToggle('800', function(){
              $('#key-tool-wards').toggleClass('hover');
            });
        });
    } else {
        $('#key-tool-wards').drawer('council_wards', false);
        $('#key-tool-around-updates').drawer('updates_ajax', true);
    }
    $('#key-tool-report-updates').small_drawer('report-updates-data');
    $('#key-tool-report-share').small_drawer('report-share');

    // Go directly to RSS feed if RSS button clicked on alert page
    // (due to not wanting around form to submit, though good thing anyway)
    $('.container').on('click', '#alert_rss_button', function(e){
        e.preventDefault();
        var feed = $('input[name=feed][type=radio]:checked').nextAll('a').attr('href');
        window.location.href = feed;
    });
    $('.container').on('click', '#alert_email_button', function(e){
        e.preventDefault();
        var form = $('<form/>').attr({ method:'post', action:"/alert/subscribe" });
        form.append($('<input name="alert" value="Subscribe me to an email alert" type="hidden" />'));
        $('#alerts input[type=text], #alerts input[type=hidden], #alerts input[type=radio]:checked').each(function() {
            var $v = $(this);
            $('<input/>').attr({ name:$v.attr('name'), value:$v.val(), type:'hidden' }).appendTo(form);
        });
        $('body').append(form);
        form.submit();
    });

    //add permalink on desktop, force hide on mobile
    $('#sub_map_links').append('Permalink');
    if($('.mobile').length){
        $('#map_permalink').hide();
        $('#key-tools a.feed').appendTo('#sub_map_links');
        $('#key-tools li:empty').remove();
        $('#report-updates-data').insertAfter($('#map_box'));
    }
    //add open/close toggle button on desk
    $('#sub_map_links').prepend(' ');

    //set up map_links_toggle click event
    $('#map_links_toggle').on('click', function(){
        var maplinks_width = $('#sub_map_links').width();

        if($(this).hasClass('closed')){
            $(this).removeClass('closed');
            $('#sub_map_links').animate({'right':'0'}, 1200);
        }else{
            $(this).addClass('closed');
            $('#sub_map_links').animate({'right':-maplinks_width}, 1200);
        }
    });

    /*
     * Add close buttons for .promo's
     */
    if($('.promo').length){
        $('.promo').append('x');
    }
    //only close its own parent
    $('.promo').on('click', '.close-promo', function(e){
        e.preventDefault();
        $(this).parent('.promo').animate({
            'height':0,
            'margin-bottom':0,
            'padding-top':0,
            'padding-bottom':0
        },{
            duration:500,
            queue:false
        }).fadeOut(500);
    });

    /*
     * Fancybox fullscreen images
     */
    if (typeof $.fancybox == 'function') {
        $('a[rel=fancy]').fancybox({
            'overlayColor': '#000000'
        });
    }

    /*
     * heightfix the desktop .content div
     *
     * this must be kept near the end so that the
     * rendered height is used after any page manipulation (such as tabs)
     */
    if (!$('html.mobile').length) {
        if (!($('body').hasClass('frontpage'))){
            var offset = -176;
            if (cobrand == 'bromley') {
                offset = -110;
            }
            heightFix(window, '.content', offset);
            // in case we have a map that isn't full screen
            map_fix();
        }
    }

});

/*
XXX Disabled because jerky on Android and makes map URL bar height too small on iPhone.
// Hide URL bar
$(window).load(function(){
    window.setTimeout(function(){
        var s = window.pageYOffset || document.compatMode === "CSS1Compat" && document.documentElement.scrollTop || document.body.scrollTop || 0;
        if (s < 20 && !location.hash) {
            window.scrollTo(0, 1);
        }
    }, 0);
});
*/

<commit_msg>add oxfordshire 'small map' behaviour to fixmystreet.js<commit_after>/*
 * fixmystreet.js
 * FixMyStreet JavaScript
 */

/*
 * general height fixing function
 *
 * elem1: element to check against
 * elem2: target element
 * offset: this will be added (if present) to the final value, useful for height errors
 */
function heightFix(elem1, elem2, offset){
    var h1 = $(elem1).height(),
        h2 = $(elem2).height();
    if(offset === undefined){
        offset = 0;
    }
    if(h1 > h2){
        $(elem2).css({'min-height':h1+offset});
    }
}

/*
 * very simple tab function
 *
 * elem: trigger element, must have an href attribute (so probably needs to be an )
 */
function tabs(elem, indirect) {
    var href = elem.attr('href');
    //stupid IE sometimes adds the full uri into the href attr, so trim
    var start = href.indexOf('#'),
        target = href.slice(start, href.length);

    if (indirect) {
        elem = $(target + '_tab');
    }

    if(!$(target).hasClass('open'))
    {
        //toggle class on nav
        $('.tab-nav .active').removeClass('active');
        elem.addClass('active');

        //hide / show the right tab
        $('.tab.open').hide().removeClass('open');
        $(target).show().addClass('open');
    }
}

$(function(){
    var $html = $('html');

    var cobrand;
    var is_small_map = false;
    if (window.location.href.indexOf('bromley') != -1) {
        cobrand = 'bromley';
        is_small_map = true;
    } else if (window.location.href.indexOf('oxfordshire') != -1) {
        cobrand = 'oxfordshire';
        is_small_map = true;
    }

    // Deal with switching between mobile and desktop versions on resize
    var last_type;
    $(window).resize(function(){
        var type = $('#site-header').css('borderTopWidth');
        if (type == '4px') { type = 'mobile'; }
        else if (type == '0px') { type = 'desktop'; }
        else { return; }
        if (last_type == type) { return; }
        if (type == 'mobile') {
            $html.addClass('mobile');
            $('#map_box').prependTo('.content').css({
                zIndex: '', position: '',
                top: '', left: '', right: '', bottom: '',
                width: '', height: '10em',
                margin: ''
            });
            if (typeof fixmystreet !== 'undefined') {
                fixmystreet.state_map = ''; // XXX
            }
            if (typeof fixmystreet !== 'undefined' && fixmystreet.page == 'around') {
                // Immediately go full screen map if on around page
                if (cobrand != 'bromley') {
                    $('#site-header').hide();
                }
                $('#map_box').prependTo('.wrapper').css({
                    position: 'absolute',
                    top: 0, left: 0, right: 0, bottom: 0,
                    height: 'auto',
                    margin: 0
                });
                $('#fms_pan_zoom').css({ top: '2.75em !important' });
                $('.big-green-banner')
                    .addClass('mobile-map-banner')
                    .appendTo('#map_box')
                    .html('Home Place pin on map');
            }
            $('span.report-a-problem-btn').on('click.reportBtn', function(){
                $('html, body').animate({scrollTop:0}, 500);
            }).css({ cursor:'pointer' }).on('hover.reportBtn', function(){
                $(this).toggleClass('hover');
            });
        } else {
            // Make map full screen on non-mobile sizes.
            $html.removeClass('mobile');
            position_map_box();
            if (typeof fixmystreet !== 'undefined') {
                if (is_small_map) {
                    //$('#bromley-footer').hide();
                } else {
                    fixmystreet.state_map = 'full';
                }
            }
            if (typeof fixmystreet !== 'undefined' && fixmystreet.page == 'around') {
                // Remove full-screen-ness
                var banner_text = 'Click map to report a problem';
                if (cobrand == 'bromley') {
                    banner_text += 'Yellow pins show existing reports';
                }
                if (! is_small_map) {
                    $('#site-header').show();
                }
                $('#fms_pan_zoom').css({ top: '4.75em !important' });
                $('.big-green-banner')
                    .removeClass('mobile-map-banner')
                    .prependTo('#side')
                    .html(banner_text);
            }
            $('span.report-a-problem-btn').css({ cursor:'' }).off('.reportBtn');
        }
        last_type = type;
    }).resize();

    /*
     * Report a problem page
     */
    //desktop
    if ($('#report-a-problem-sidebar').is(':visible')) {
        heightFix('#report-a-problem-sidebar', '.content', 26);
    }

    //show/hide notes on mobile
    $('.mobile #report-a-problem-sidebar').after('How to send successful reports').hide();
    $('.rap-notes-trigger').click(function(e){
        e.preventDefault();
        //check if we've already moved the notes
        if($('.rap-notes').length > 0){
            //if we have, show and hide .content
            $('.content').hide();
            $('.rap-notes').show();
        }else{
            //if not, move them and show, hiding .content
            $('.content').after('

').hide();
            $('#report-a-problem-sidebar').appendTo('.rap-notes').show().after('Back');
        }
        $('html, body').scrollTop($('#report-a-problem-sidebar').offset().top);
        location.hash = 'rap-notes';
    });
    $('.mobile').on('click', '.rap-notes-close', function(e){
        e.preventDefault();
        //hide notes, show .content
        $('.content').show();
        $('.rap-notes').hide();
        $('html, body').scrollTop($('#mob_ok').offset().top);
        location.hash = 'report';
    });

    //move 'skip this step' link on mobile
    $('.mobile #skip-this-step').hide();
    $('.mobile #skip-this-step a').addClass('chevron').wrap('
').appendTo('#key-tools');

    /*
     * Tabs
     */
    //make initial tab active
    $('.tab-nav a').first().addClass('active');
    $('.tab').first().addClass('open');

    //hide other tabs
    $('.tab').not('.open').hide();

    //set up click event
    $(".tab-nav").on('click', 'a', function(e){
        e.preventDefault();
        tabs($(this));
    });
    $('.tab_link').click(function(e) {
        e.preventDefault();
        tabs($(this), 1);
    });

    /*
     * Skip to nav on mobile
     */
    $('.mobile').on('click', '#nav-link', function(e){
        e.preventDefault();
        var offset = $('#main-nav').offset().top;
        $('html, body').animate({scrollTop:offset}, 1000);
    });

    /*
     * Show stuff on input focus
     */
    var form_focus_data = $('.form-focus-trigger').map(function() {
        return $(this).val();
    }).get().join('');
    if (!form_focus_data) {
        $('.form-focus-hidden').hide();
        $('.form-focus-trigger').on('focus', function(){
            $('.form-focus-hidden').fadeIn(500);
        });
    }

    /*
     * Show on click - pretty generic
     */
    $('.hideshow-trigger').on('click', function(e){
        e.preventDefault();
        var href = $(this).attr('href'),
            //stupid IE sometimes adds the full uri into the href attr, so trim
            start = href.indexOf('#'),
            target = href.slice(start, href.length);

        $(target).removeClass('hidden-js');

        $(this).hide();
    });

    /*
     * nicetable - on mobile shift 'name' col to be a row
     */
    $('.mobile .nicetable th.title').remove();
    $('.mobile .nicetable td.title').each(function(i){
        $(this).attr('colspan', 5).insertBefore($(this).parent('tr')).wrap('
');
    });
    // $('.mobile .nicetable tr.heading > td.title').css({'min-width':'300px'});
    // $('.mobile .nicetable tr > td.data').css({'max-width':'12%'});

    /*
     * Map controls prettiness
     */
    //add links container (if its not there)
    if($('#sub_map_links').length === 0){
        $('
').insertAfter($('#map'));
    }

// A sliding drawer from the bottom of the page, small version
// that doesn't change the main content at all.
(function($){

    var opened;

    $.fn.small_drawer = function(id) {
        this.toggle(function(){
            if (opened) {
                opened.click();
            }
            var $this = $(this), d = $('#' + id);
            if (!$this.addClass('hover').data('setup')) {
                d.hide().removeClass('hidden-js').css({
                padding: '1em',
                background: '#fff'
                });
                $this.data('setup', true);
            }
            d.slideDown();
            opened = $this;
        }, function(e){
            var $this = $(this), d = $('#' + id);
            $this.removeClass('hover');
            d.slideUp();
            opened = null;
        });
    };

})(jQuery);

// A sliding drawer from the bottom of the page, large version
$.fn.drawer = function(id, ajax) {
    // IE7 positions the fixed tool bar 1em to the left unless it comes after
    // the full-width section, ho-hum. Move it to where it would be after an
    // open/close anyway
    if ($('html.ie7').length) {
        var $sw = $('.shadow-wrap'), $content = $('.content[role="main"]');
        $sw.appendTo($content);
    }
    this.toggle(function(){
        var $this = $(this), d = $('#' + id), $content = $('.content[role="main"]');
        if (!$this.addClass('hover').data('setup')) {
            // make a drawer div with an innerDiv
            if (!d.length) {
                d = $('
');
            }
            var innerDiv = $('
');
            d.wrapInner(innerDiv);

            // if ajax, load it with a spinner
            if (ajax) {
                var href = $this.attr('href') + ';ajax=1';
                $this.prepend(' ');
                innerDiv.load(href, function(){
                    $('.spinner').remove();
                });
            }

            // Tall drawer - put after .content for scrolling to work okay.
            // position over the top of the main .content in precisely the right location
            d.insertAfter($content).addClass('content').css({
                position: 'absolute',
                zIndex: '1100',
                marginTop: $('html.ie6, html.ie7').length ? '-3em' : 0, // IE6/7 otherwise include the 3em padding and stay too low
                left: 0,
                top: $(window).height() - $content.offset().top
            }).removeClass('hidden-js').find('h2').css({ marginTop: 0 });
            $this.data('setup', true);
        }

        //do the animation
        $('.shadow-wrap').prependTo(d).addClass('static');
        d.show().animate({top:'3em'}, 1000, function(){
            $content.fadeOut(function() {
                d.css({ position: 'relative' });
            });
        });
    }, function(e){
        var $this = $(this), d = $('#' + id), $sw = $('.shadow-wrap'),
            $content = $('.content[role="main"]'),
            tot_height = $(window).height() - d.offset().top;
        $this.removeClass('hover');
        d.css({ position: 'absolute' }).animate({ top: tot_height }, 1000, function(){
            d.hide();
            $sw.appendTo($content).removeClass('static');
        });
        $content.show();
    });
};

    if ($('html.mobile').length || slide_wards_down ) {
        $('#council_wards').hide().removeClass('hidden-js').find('h2').hide();
        $('#key-tool-wards').click(function(e){
            e.preventDefault();
            $('#council_wards').slideToggle('800', function(){
              $('#key-tool-wards').toggleClass('hover');
            });
        });
    } else {
        $('#key-tool-wards').drawer('council_wards', false);
        $('#key-tool-around-updates').drawer('updates_ajax', true);
    }
    $('#key-tool-report-updates').small_drawer('report-updates-data');
    $('#key-tool-report-share').small_drawer('report-share');

    // Go directly to RSS feed if RSS button clicked on alert page
    // (due to not wanting around form to submit, though good thing anyway)
    $('.container').on('click', '#alert_rss_button', function(e){
        e.preventDefault();
        var feed = $('input[name=feed][type=radio]:checked').nextAll('a').attr('href');
        window.location.href = feed;
    });
    $('.container').on('click', '#alert_email_button', function(e){
        e.preventDefault();
        var form = $('<form/>').attr({ method:'post', action:"/alert/subscribe" });
        form.append($('<input name="alert" value="Subscribe me to an email alert" type="hidden" />'));
        $('#alerts input[type=text], #alerts input[type=hidden], #alerts input[type=radio]:checked').each(function() {
            var $v = $(this);
            $('<input/>').attr({ name:$v.attr('name'), value:$v.val(), type:'hidden' }).appendTo(form);
        });
        $('body').append(form);
        form.submit();
    });

    //add permalink on desktop, force hide on mobile
    $('#sub_map_links').append('Permalink');
    if($('.mobile').length){
        $('#map_permalink').hide();
        $('#key-tools a.feed').appendTo('#sub_map_links');
        $('#key-tools li:empty').remove();
        $('#report-updates-data').insertAfter($('#map_box'));
    }
    //add open/close toggle button on desk
    $('#sub_map_links').prepend(' ');

    //set up map_links_toggle click event
    $('#map_links_toggle').on('click', function(){
        var maplinks_width = $('#sub_map_links').width();

        if($(this).hasClass('closed')){
            $(this).removeClass('closed');
            $('#sub_map_links').animate({'right':'0'}, 1200);
        }else{
            $(this).addClass('closed');
            $('#sub_map_links').animate({'right':-maplinks_width}, 1200);
        }
    });

    /*
     * Add close buttons for .promo's
     */
    if($('.promo').length){
        $('.promo').append('x');
    }
    //only close its own parent
    $('.promo').on('click', '.close-promo', function(e){
        e.preventDefault();
        $(this).parent('.promo').animate({
            'height':0,
            'margin-bottom':0,
            'padding-top':0,
            'padding-bottom':0
        },{
            duration:500,
            queue:false
        }).fadeOut(500);
    });

    /*
     * Fancybox fullscreen images
     */
    if (typeof $.fancybox == 'function') {
        $('a[rel=fancy]').fancybox({
            'overlayColor': '#000000'
        });
    }

    /*
     * heightfix the desktop .content div
     *
     * this must be kept near the end so that the
     * rendered height is used after any page manipulation (such as tabs)
     */
    if (!$('html.mobile').length) {
        if (!($('body').hasClass('frontpage'))){
            var offset = -176;
            if (cobrand == 'bromley') {
                offset = -110;
            }
            heightFix(window, '.content', offset);
            // in case we have a map that isn't full screen
            map_fix();
        }
    }

});

/*
XXX Disabled because jerky on Android and makes map URL bar height too small on iPhone.
// Hide URL bar
$(window).load(function(){
    window.setTimeout(function(){
        var s = window.pageYOffset || document.compatMode === "CSS1Compat" && document.documentElement.scrollTop || document.body.scrollTop || 0;
        if (s < 20 && !location.hash) {
            window.scrollTo(0, 1);
        }
    }, 0);
});
*/

<|endoftext|>