<commit_before>/**
 * @file MaterialTextArea component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TextArea from '../TextArea';
import MaterialProvider from '../MaterialProvider';
import Theme from '../Theme';

import Util from '../_vendors/Util';

class MaterialTextArea extends Component {

    static Type = TextArea.Type;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: props.value
        };

        this.triggerChangeHandler = ::this.triggerChangeHandler;

    }

    triggerChangeHandler(value) {
        this.setState({
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value);
        });
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.state.value) {
            this.setState({
                value: nextProps.value
            });
        }
    }

    render() {

        const {
                className, style, theme, label, isLabelAnimate, wordCountVisible, required,
                ...restProps
            } = this.props,
            {value} = this.state,

            wrapperClassName = classNames('material-text-area', {
                'has-word-count': wordCountVisible,
                [className]: className
            });

        return (
            <MaterialProvider className={wrapperClassName}
                              style={style}
                              theme={theme}
                              label={label}
                              isLabelAnimate={isLabelAnimate}
                              hasValue={!!value}
                              required={required}>

                <TextArea {...restProps}
                          theme={theme}
                          value={value}
                          required={required}
                          wordCountVisible={wordCountVisible}
                          onChange={this.triggerChangeHandler}/>

            </MaterialProvider>
        );

    }
};

MaterialTextArea.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The TextArea theme.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * Specifies the type of input to display such as "password" or "text".
     */
    type: PropTypes.oneOf(Util.enumerateValue(TextArea.Type)),

    /**
     * The name of the text area.
     */
    name: PropTypes.string,

    /**
     * The label of the text field.
     */
    label: PropTypes.any,

    /**
     * The animate of the text field.
     */
    isLabelAnimate: PropTypes.bool,

    /**
     * The placeholder of the text area.
     */
    placeholder: PropTypes.string,

    /**
     * The value of the text area. Type can be string or number.
     */
    value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

    /**
     * Use this property to display an icon.
     */
    iconCls: PropTypes.string,

    /**
     * Disables the textField if set to true.
     */
    disabled: PropTypes.bool,

    /**
     * If true,the textField will can't editable.
     */
    readOnly: PropTypes.bool,

    /**
     * If true,the textField will autoFocus.
     */
    autoFocus: PropTypes.bool,

    /**
     * The message of the textField.
     */
    infoMsg: PropTypes.string,

    /**
     * If true,clearButton will display when the textField is not empty.
     */
    clearButtonVisible: PropTypes.bool,

    /**
     * Use this property to display an icon.
     */
    rightIconCls: PropTypes.string,

    /**
     * If true, passwordButton will display.
     */
    passwordButtonVisible: PropTypes.bool,

    autoHeight: PropTypes.bool,

    wordCountVisible: PropTypes.bool,

    // valid
    /**
     * If true,the textField must be required.
     */
    required: PropTypes.bool,

    /**
     * The maximum length of textField to enter.
     */
    maxLength: PropTypes.number,

    /**
     * Maximum number of textField that can be entered.
     */
    max: PropTypes.number,

    /**
     * Minimum number of textField that can be entered.
     */
    min: PropTypes.number,

    /**
     * The matching rule of textField.
     */
    pattern: PropTypes.object,

    /**
     * The invalid message of textField.
     */
    patternInvalidMsg: PropTypes.string,

    fieldMsgVisible: PropTypes.bool,

    /**
     * Callback function fired when the textField is changed.
     */
    onChange: PropTypes.func,

    /**
     * Callback function fired when the press enter.
     */
    onPressEnter: PropTypes.func,

    /**
     * Callback function fired when textField valid.
     */
    onValid: PropTypes.func,

    /**
     * Callback function fired when textField invalid.
     */
    onInvalid: PropTypes.func,

    /**
     * Callback function fired when textField focus.
     */
    onFocus: PropTypes.func,

    /**
     * Callback function fired when textField blur.
     */
    onBlur: PropTypes.func,

    /**
     * Callback function fired when textField clear.
     */
    onClear: PropTypes.func,

    /**
     * Callback function fired when password visible.
     */
    onPasswordVisible: PropTypes.func,

    /**
     * Callback function fired when password invisible.
     */
    onPasswordInvisible: PropTypes.func,

    onIconTouchTap: PropTypes.func,
    onRightIconTouchTap: PropTypes.func,

    onMouseOver: PropTypes.func,
    onMouseOut: PropTypes.func

};

MaterialTextArea.defaultProps = {

    className: null,
    style: null,
    theme: Theme.DEFAULT,

    type: 'text',
    name: '',
    label: '',
    isLabelAnimate: true,
    placeholder: '',
    value: '',
    iconCls: '',
    rightIconCls: '',
    disabled: false,
    readOnly: false,
    autoFocus: false,
    infoMsg: '',
    autoHeight: false,
    wordCountVisible: false,

    clearButtonVisible: false,
    passwordButtonVisible: false,

    // valid
    required: false,
    patternInvalidMsg: '',
    preventInvalidInput: false,

    fieldMsgVisible: false

};

export default MaterialTextArea;<commit_msg>update MaterialTextArea<commit_after>/**
 * @file MaterialTextArea component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TextArea from '../TextArea';
import MaterialProvider from '../MaterialProvider';
import Theme from '../Theme';

import Util from '../_vendors/Util';

class MaterialTextArea extends Component {

    static Type = TextArea.Type;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: props.value
        };

        this.triggerChangeHandler = ::this.triggerChangeHandler;

    }

    triggerChangeHandler(value) {
        this.setState({
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value);
        });
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.state.value) {
            this.setState({
                value: nextProps.value
            });
        }
    }

    render() {

        const {
                className, style, theme, label, isLabelAnimate, wordCountVisible, disabled, required,
                ...restProps
            } = this.props,
            {value} = this.state,

            wrapperClassName = classNames('material-text-area', {
                'has-word-count': wordCountVisible,
                [className]: className
            });

        return (
            <MaterialProvider className={wrapperClassName}
                              style={style}
                              theme={theme}
                              label={label}
                              isLabelAnimate={isLabelAnimate}
                              hasValue={!!value}
                              disabled={disabled}
                              required={required}>

                <TextArea {...restProps}
                          theme={theme}
                          value={value}
                          disabled={disabled}
                          required={required}
                          wordCountVisible={wordCountVisible}
                          onChange={this.triggerChangeHandler}/>

            </MaterialProvider>
        );

    }
};

MaterialTextArea.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The TextArea theme.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * Specifies the type of input to display such as "password" or "text".
     */
    type: PropTypes.oneOf(Util.enumerateValue(TextArea.Type)),

    /**
     * The name of the text area.
     */
    name: PropTypes.string,

    /**
     * The label of the text field.
     */
    label: PropTypes.any,

    /**
     * The animate of the text field.
     */
    isLabelAnimate: PropTypes.bool,

    /**
     * The placeholder of the text area.
     */
    placeholder: PropTypes.string,

    /**
     * The value of the text area. Type can be string or number.
     */
    value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

    /**
     * Use this property to display an icon.
     */
    iconCls: PropTypes.string,

    /**
     * Disables the textField if set to true.
     */
    disabled: PropTypes.bool,

    /**
     * If true,the textField will can't editable.
     */
    readOnly: PropTypes.bool,

    /**
     * If true,the textField will autoFocus.
     */
    autoFocus: PropTypes.bool,

    /**
     * The message of the textField.
     */
    infoMsg: PropTypes.string,

    /**
     * If true,clearButton will display when the textField is not empty.
     */
    clearButtonVisible: PropTypes.bool,

    /**
     * Use this property to display an icon.
     */
    rightIconCls: PropTypes.string,

    /**
     * If true, passwordButton will display.
     */
    passwordButtonVisible: PropTypes.bool,

    autoHeight: PropTypes.bool,

    wordCountVisible: PropTypes.bool,

    // valid
    /**
     * If true,the textField must be required.
     */
    required: PropTypes.bool,

    /**
     * The maximum length of textField to enter.
     */
    maxLength: PropTypes.number,

    /**
     * Maximum number of textField that can be entered.
     */
    max: PropTypes.number,

    /**
     * Minimum number of textField that can be entered.
     */
    min: PropTypes.number,

    /**
     * The matching rule of textField.
     */
    pattern: PropTypes.object,

    /**
     * The invalid message of textField.
     */
    patternInvalidMsg: PropTypes.string,

    fieldMsgVisible: PropTypes.bool,

    /**
     * Callback function fired when the textField is changed.
     */
    onChange: PropTypes.func,

    /**
     * Callback function fired when the press enter.
     */
    onPressEnter: PropTypes.func,

    /**
     * Callback function fired when textField valid.
     */
    onValid: PropTypes.func,

    /**
     * Callback function fired when textField invalid.
     */
    onInvalid: PropTypes.func,

    /**
     * Callback function fired when textField focus.
     */
    onFocus: PropTypes.func,

    /**
     * Callback function fired when textField blur.
     */
    onBlur: PropTypes.func,

    /**
     * Callback function fired when textField clear.
     */
    onClear: PropTypes.func,

    /**
     * Callback function fired when password visible.
     */
    onPasswordVisible: PropTypes.func,

    /**
     * Callback function fired when password invisible.
     */
    onPasswordInvisible: PropTypes.func,

    onIconTouchTap: PropTypes.func,
    onRightIconTouchTap: PropTypes.func,

    onMouseOver: PropTypes.func,
    onMouseOut: PropTypes.func

};

MaterialTextArea.defaultProps = {

    className: null,
    style: null,
    theme: Theme.DEFAULT,

    type: 'text',
    name: '',
    label: '',
    isLabelAnimate: true,
    placeholder: '',
    value: '',
    iconCls: '',
    rightIconCls: '',
    disabled: false,
    readOnly: false,
    autoFocus: false,
    infoMsg: '',
    autoHeight: false,
    wordCountVisible: false,

    clearButtonVisible: false,
    passwordButtonVisible: false,

    // valid
    required: false,
    patternInvalidMsg: '',
    preventInvalidInput: false,

    fieldMsgVisible: false

};

export default MaterialTextArea;<|endoftext|>


<commit_before>// ==UserScript==
// @author      Robert Slifka (github @rslifka)
// @connect     docs.google.com
// @connect     rslifka.github.io
// @copyright   2017-2018, Robert Slifka (https://github.com/rslifka/fate_of_all_fools)
// @description Enhancements to the Destiny Item Manager
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getValue
// @grant       GM_log
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @homepage    https://github.com/rslifka/fate_of_all_fools
// @license     MIT; https://raw.githubusercontent.com/rslifka/fate_of_all_fools/master/LICENSE.txt
// @match       https://*.destinyitemmanager.com/*
// @name        Fate of All Fools
// @require     https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.js
// @resource    fateOfAllFoolsCSS https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.css
// @run-at      document-start
// @supportURL  https://github.com/rslifka/fate_of_all_fools/issues
// @version     273
// ==/UserScript==
<commit_msg>AUTO-CREATED Release Artifact v274 c/o Travis<commit_after>// ==UserScript==
// @author      Robert Slifka (github @rslifka)
// @connect     docs.google.com
// @connect     rslifka.github.io
// @copyright   2017-2018, Robert Slifka (https://github.com/rslifka/fate_of_all_fools)
// @description Enhancements to the Destiny Item Manager
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getValue
// @grant       GM_log
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @homepage    https://github.com/rslifka/fate_of_all_fools
// @license     MIT; https://raw.githubusercontent.com/rslifka/fate_of_all_fools/master/LICENSE.txt
// @match       https://*.destinyitemmanager.com/*
// @name        Fate of All Fools
// @require     https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.js
// @resource    fateOfAllFoolsCSS https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.css
// @run-at      document-start
// @supportURL  https://github.com/rslifka/fate_of_all_fools/issues
// @version     274
// ==/UserScript==
<|endoftext|>


<commit_before>import { t } from '../util/locale';

import {
    event as d3_event,
    select as d3_select
} from 'd3-selection';

import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';

import {
    actionAddMidpoint,
    actionMoveNode,
    actionNoop
} from '../actions';

import { behaviorDraw } from './draw';
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
import { modeBrowse, modeSelect } from '../modes';
import { osmNode } from '../osm';

export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
    var origWay = context.entity(wayId);
    var annotation = t((origWay.isDegenerate() ?
        'operations.start.annotation.' :
        'operations.continue.annotation.') + context.geometry(wayId)
    );
    var behavior = behaviorDraw(context);
    var _tempEdits = 0;

    var end = osmNode({ loc: context.map().mouseCoordinates() });

    // Push an annotated state for undo to return back to.
    // We must make sure to remove this edit later.
    context.perform(actionNoop(), annotation);
    _tempEdits++;

    // Add the drawing node to the graph.
    // We must make sure to remove this edit later.
    context.perform(_actionAddDrawNode());
    _tempEdits++;

    function keydown() {
        if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
            if (context.surface().classed('nope')) {
                context.surface()
                    .classed('nope-suppressed', true);
            }
            context.surface()
                .classed('nope', false)
                .classed('nope-disabled', true);
        }
    }

    function keyup() {
        if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
            if (context.surface().classed('nope-suppressed')) {
                context.surface()
                    .classed('nope', true);
            }
            context.surface()
                .classed('nope-suppressed', false)
                .classed('nope-disabled', false);
        }
    }

    // related code
    // - `mode/drag_node.js`     `doMode()`
    // - `behavior/draw.js`      `click()`
    // - `behavior/draw_way.js`  `move()`
    function move(datum) {
        context.surface().classed('nope-disabled', d3_event.altKey);

        var targetLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc;
        var targetNodes = datum && datum.properties && datum.properties.nodes;
        var loc = context.map().mouseCoordinates();

        if (targetLoc) {   // snap to node/vertex - a point target with `.loc`
            loc = targetLoc;

        } else if (targetNodes) {   // snap to way - a line target with `.nodes`
            var choice = geoChooseEdge(targetNodes, context.mouse(), context.projection, end.id);
            if (choice) {
                loc = choice.loc;
            }
        }

        context.replace(actionMoveNode(end.id, loc));
        end = context.entity(end.id);
        checkGeometry(origWay.isClosed());    // skipLast = true when drawing areas
    }

    // Check whether this edit causes the geometry to break.
    // If so, class the surface with a nope cursor.
    // `skipLast` - include closing segment in the check, see #4655
    function checkGeometry(skipLast) {
        var nopeDisabled = context.surface().classed('nope-disabled');
        var isInvalid = isInvalidGeometry(end, context.graph(), skipLast);

        if (nopeDisabled) {
            context.surface()
                .classed('nope', false)
                .classed('nope-suppressed', isInvalid);
        } else {
            context.surface()
                .classed('nope', isInvalid)
                .classed('nope-suppressed', false);
        }
    }

    function isInvalidGeometry(entity, graph, skipLast) {
        var parents = graph.parentWays(entity);

        for (var i = 0; i < parents.length; i++) {
            var parent = parents[i];
            var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });
            if (skipLast)  nodes.pop();   // disregard closing segment - #4655
            if (geoHasSelfIntersections(nodes, entity.id)) {
                return true;
            }
        }

        return false;
    }

    function undone() {
        // Undo popped the history back to the initial annotated no-op edit.
        // Remove initial no-op edit and whatever edit happened immediately before it.
        context.pop(2);
        _tempEdits = 0;

        if (context.hasEntity(wayId)) {
            context.enter(mode);
        } else {
            context.enter(modeBrowse(context));
        }
    }

    function setActiveElements() {
        context.surface().selectAll('.' + end.id)
            .classed('active', true);
    }

    var drawWay = function(surface) {
        behavior
            .on('move', move)
            .on('click', drawWay.add)
            .on('clickWay', drawWay.addWay)
            .on('clickNode', drawWay.addNode)
            .on('undo', context.undo)
            .on('cancel', drawWay.cancel)
            .on('finish', drawWay.finish);

        d3_select(window)
            .on('keydown.drawWay', keydown)
            .on('keyup.drawWay', keyup);

        context.map()
            .dblclickEnable(false)
            .on('drawn.draw', setActiveElements);

        setActiveElements();

        surface.call(behavior);

        context.history()
            .on('undone.draw', undone);
    };

    drawWay.off = function(surface) {
        // Drawing was interrupted unexpectedly.
        // This can happen if the user changes modes,
        // clicks geolocate button, a hashchange event occurs, etc.
        if (_tempEdits) {
            context.pop(_tempEdits);
            while (context.graph() !== startGraph) {
                context.pop();
            }
        }

        context.map()
            .on('drawn.draw', null);

        surface.call(behavior.off)
            .selectAll('.active')
            .classed('active', false);

        surface
            .classed('nope', false)
            .classed('nope-suppressed', false)
            .classed('nope-disabled', false);

        d3_select(window)
            .on('keydown.hover', null)
            .on('keyup.hover', null);

        context.history()
            .on('undone.draw', null);
    };

    function _actionAddDrawNode() {
        return function(graph) {
            return graph
                .replace(end)
                .replace(origWay.addNode(end.id, index));
        };
    }

    function _actionReplaceDrawNode(newNode) {
        return function(graph) {
            return graph
                .replace(origWay.addNode(newNode.id, index))
                .remove(end);
        };
    }

    // Accept the current position of the drawing node and continue drawing.
    drawWay.add = function(loc, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            annotation
        );

        checkGeometry(false);   // skipLast = false
        context.enter(mode);
    };

    // Connect the way to an existing way.
    drawWay.addWay = function(loc, edge, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            actionAddMidpoint({ loc: loc, edge: edge }, end),
            annotation
        );

        checkGeometry(false);   // skipLast = false
        context.enter(mode);
    };

    // Connect the way to an existing node and continue drawing.
    drawWay.addNode = function(node, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionReplaceDrawNode(node),
            annotation
        );

        checkGeometry(false);   // skipLast = false
        context.enter(mode);
    };

    // Finish the draw operation, removing the temporary edits.
    // If the way has enough nodes to be valid, it's selected.
    // Otherwise, delete everything and return to browse mode.
    drawWay.finish = function() {
        checkGeometry(true);   // skipLast = true
        if (context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        var way = context.hasEntity(wayId);
        if (!way || way.isDegenerate()) {
            drawWay.cancel();
            return;
        }

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);

        context.enter(modeSelect(context, [wayId]).newFeature(true));
    };

    // Cancel the draw operation, delete everything, and return to browse mode.
    drawWay.cancel = function() {
        context.pop(_tempEdits);
        _tempEdits = 0;

        while (context.graph() !== startGraph) {
            context.pop();
        }

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);

        context.surface()
            .classed('nope', false)
            .classed('nope-disabled', false)
            .classed('nope-suppressed', false);

        context.enter(modeBrowse(context));
    };

    drawWay.activeID = function() {
        if (!arguments.length) return end.id;
        // no assign
        return drawWay;
    };

    drawWay.tail = function(text) {
        behavior.tail(text);
        return drawWay;
    };

    return drawWay;
}
<commit_msg>Can finish drawing when cursor is in invalid position  - When drawing an Area, the leading edge is allowed to temporarily be in an invalid position (see #4655)..     .. Finishing a drawing is disabled if doing so would close with the invalid leading edge<commit_after>import { t } from '../util/locale';

import {
    event as d3_event,
    select as d3_select
} from 'd3-selection';

import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';

import {
    actionAddMidpoint,
    actionMoveNode,
    actionNoop
} from '../actions';

import { behaviorDraw } from './draw';
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
import { modeBrowse, modeSelect } from '../modes';
import { osmNode } from '../osm';

export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
    var origWay = context.entity(wayId);
    var annotation = t((origWay.isDegenerate() ?
        'operations.start.annotation.' :
        'operations.continue.annotation.') + context.geometry(wayId)
    );
    var behavior = behaviorDraw(context);
    var _tempEdits = 0;

    var end = osmNode({ loc: context.map().mouseCoordinates() });

    // Push an annotated state for undo to return back to.
    // We must make sure to remove this edit later.
    context.perform(actionNoop(), annotation);
    _tempEdits++;

    // Add the drawing node to the graph.
    // We must make sure to remove this edit later.
    context.perform(_actionAddDrawNode());
    _tempEdits++;

    function keydown() {
        if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
            if (context.surface().classed('nope')) {
                context.surface()
                    .classed('nope-suppressed', true);
            }
            context.surface()
                .classed('nope', false)
                .classed('nope-disabled', true);
        }
    }

    function keyup() {
        if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
            if (context.surface().classed('nope-suppressed')) {
                context.surface()
                    .classed('nope', true);
            }
            context.surface()
                .classed('nope-suppressed', false)
                .classed('nope-disabled', false);
        }
    }

    // related code
    // - `mode/drag_node.js`     `doMode()`
    // - `behavior/draw.js`      `click()`
    // - `behavior/draw_way.js`  `move()`
    function move(datum) {
        context.surface().classed('nope-disabled', d3_event.altKey);

        var targetLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc;
        var targetNodes = datum && datum.properties && datum.properties.nodes;
        var loc = context.map().mouseCoordinates();

        if (targetLoc) {   // snap to node/vertex - a point target with `.loc`
            loc = targetLoc;

        } else if (targetNodes) {   // snap to way - a line target with `.nodes`
            var choice = geoChooseEdge(targetNodes, context.mouse(), context.projection, end.id);
            if (choice) {
                loc = choice.loc;
            }
        }

        context.replace(actionMoveNode(end.id, loc));
        end = context.entity(end.id);
        checkGeometry(false);
    }

    // Check whether this edit causes the geometry to break.
    // If so, class the surface with a nope cursor.
    // `finishDraw` - Only checks the relevant line segments if finishing drawing
    function checkGeometry(finishDraw) {
        var nopeDisabled = context.surface().classed('nope-disabled');
        var isInvalid = isInvalidGeometry(end, context.graph(), finishDraw);

        if (nopeDisabled) {
            context.surface()
                .classed('nope', false)
                .classed('nope-suppressed', isInvalid);
        } else {
            context.surface()
                .classed('nope', isInvalid)
                .classed('nope-suppressed', false);
        }
    }

    function isInvalidGeometry(entity, graph, finishDraw) {
        var parents = graph.parentWays(entity);

        for (var i = 0; i < parents.length; i++) {
            var parent = parents[i];
            var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });

            if (origWay.isClosed()) { // Check if Area
                if (finishDraw) {
                    nodes.splice(-2, 1);
                    entity = nodes[nodes.length-2];
                } else {
                    nodes.pop();
                }
            } else { // Line
                if (finishDraw) {
                    nodes.pop();
                }
            }

            if (geoHasSelfIntersections(nodes, entity.id)) {
                return true;
            }
        }

        return false;
    }

    function undone() {
        // Undo popped the history back to the initial annotated no-op edit.
        // Remove initial no-op edit and whatever edit happened immediately before it.
        context.pop(2);
        _tempEdits = 0;

        if (context.hasEntity(wayId)) {
            context.enter(mode);
        } else {
            context.enter(modeBrowse(context));
        }
    }

    function setActiveElements() {
        context.surface().selectAll('.' + end.id)
            .classed('active', true);
    }

    var drawWay = function(surface) {
        behavior
            .on('move', move)
            .on('click', drawWay.add)
            .on('clickWay', drawWay.addWay)
            .on('clickNode', drawWay.addNode)
            .on('undo', context.undo)
            .on('cancel', drawWay.cancel)
            .on('finish', drawWay.finish);

        d3_select(window)
            .on('keydown.drawWay', keydown)
            .on('keyup.drawWay', keyup);

        context.map()
            .dblclickEnable(false)
            .on('drawn.draw', setActiveElements);

        setActiveElements();

        surface.call(behavior);

        context.history()
            .on('undone.draw', undone);
    };

    drawWay.off = function(surface) {
        // Drawing was interrupted unexpectedly.
        // This can happen if the user changes modes,
        // clicks geolocate button, a hashchange event occurs, etc.
        if (_tempEdits) {
            context.pop(_tempEdits);
            while (context.graph() !== startGraph) {
                context.pop();
            }
        }

        context.map()
            .on('drawn.draw', null);

        surface.call(behavior.off)
            .selectAll('.active')
            .classed('active', false);

        surface
            .classed('nope', false)
            .classed('nope-suppressed', false)
            .classed('nope-disabled', false);

        d3_select(window)
            .on('keydown.hover', null)
            .on('keyup.hover', null);

        context.history()
            .on('undone.draw', null);
    };

    function _actionAddDrawNode() {
        return function(graph) {
            return graph
                .replace(end)
                .replace(origWay.addNode(end.id, index));
        };
    }

    function _actionReplaceDrawNode(newNode) {
        return function(graph) {
            return graph
                .replace(origWay.addNode(newNode.id, index))
                .remove(end);
        };
    }

    // Accept the current position of the drawing node and continue drawing.
    drawWay.add = function(loc, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Connect the way to an existing way.
    drawWay.addWay = function(loc, edge, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            actionAddMidpoint({ loc: loc, edge: edge }, end),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Connect the way to an existing node and continue drawing.
    drawWay.addNode = function(node, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionReplaceDrawNode(node),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Finish the draw operation, removing the temporary edits.
    // If the way has enough nodes to be valid, it's selected.
    // Otherwise, delete everything and return to browse mode.
    drawWay.finish = function() {
        checkGeometry(true);   // finishDraw = true
        if (context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        var way = context.hasEntity(wayId);
        if (!way || way.isDegenerate()) {
            drawWay.cancel();
            return;
        }

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);

        context.enter(modeSelect(context, [wayId]).newFeature(true));
    };

    // Cancel the draw operation, delete everything, and return to browse mode.
    drawWay.cancel = function() {
        context.pop(_tempEdits);
        _tempEdits = 0;

        while (context.graph() !== startGraph) {
            context.pop();
        }

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);

        context.surface()
            .classed('nope', false)
            .classed('nope-disabled', false)
            .classed('nope-suppressed', false);

        context.enter(modeBrowse(context));
    };

    drawWay.activeID = function() {
        if (!arguments.length) return end.id;
        // no assign
        return drawWay;
    };

    drawWay.tail = function(text) {
        behavior.tail(text);
        return drawWay;
    };

    return drawWay;
}
<|endoftext|>


<commit_before>const BaseWidget = require('./base');
const debug = require('debug')('windshaft:widget:aggregation');

const filteredQueryTpl = ctx => `
    filtered_source AS (
        SELECT *
        FROM (${ctx._query}) _cdb_filtered_source
        ${ctx._aggregationColumn && ctx._isFloatColumn ? `
        WHERE
            ${ctx._aggregationColumn} != 'infinity'::float
        AND
            ${ctx._aggregationColumn} != '-infinity'::float
        AND
            ${ctx._aggregationColumn} != 'NaN'::float` :
        ''
        }
    )
`;

const summaryQueryTpl = ctx => `
    summary AS (
        SELECT
            count(1) AS count,
            sum(CASE WHEN ${ctx._column} IS NULL THEN 1 ELSE 0 END) AS nulls_count
            ${ctx._isFloatColumn ? `,
            sum(
                CASE
                    WHEN ${ctx._aggregationColumn} = 'infinity'::float OR ${ctx._aggregationColumn} = '-infinity'::float
                    THEN 1
                    ELSE 0
                    END
            ) AS infinities_count,
            sum(CASE WHEN ${ctx._aggregationColumn} = 'NaN'::float THEN 1 ELSE 0 END) AS nans_count` :
            ''
            }
        FROM (${ctx._query}) _cdb_aggregation_nulls
    )
`;

const rankedCategoriesQueryTpl = ctx => `
    categories AS(
        SELECT
            ${ctx._column} AS category,
            ${ctx._aggregation} AS value,
            row_number() OVER (ORDER BY ${ctx._aggregation} desc) as rank
        FROM filtered_source
        ${ctx._aggregationColumn !== null ? `WHERE ${ctx._aggregationColumn} IS NOT NULL` : ''}
        GROUP BY ${ctx._column}
        ORDER BY 2 DESC
    )
`;

const categoriesSummaryMinMaxQueryTpl = () => `
    categories_summary_min_max AS(
        SELECT
            max(value) max_val,
            min(value) min_val
        FROM categories
    )
`;

const categoriesSummaryCountQueryTpl = ctx => `
    categories_summary_count AS(
        SELECT count(1) AS categories_count
        FROM (
            SELECT ${ctx._column} AS category
            FROM (${ctx._query}) _cdb_categories
            GROUP BY ${ctx._column}
        ) _cdb_categories_count
    )
`;

const specialNumericValuesColumns = () => `, nans_count, infinities_count`;

const rankedAggregationQueryTpl = ctx => `
    SELECT
        CAST(category AS text),
        value,
        false as agg,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
        FROM categories, summary, categories_summary_min_max, categories_summary_count
        WHERE rank < ${ctx._limit}
    UNION ALL
    SELECT
        'Other' category,
        ${ctx._aggregationFn}(value) as value,
        true as agg,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
        FROM categories, summary, categories_summary_min_max, categories_summary_count
        WHERE rank >= ${ctx._limit}
    GROUP BY
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
`;

const aggregationQueryTpl = ctx => `
    SELECT
        CAST(${ctx._column} AS text) AS category,
        ${ctx._aggregation} AS value,
        false as agg,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
    FROM (${ctx._query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
    GROUP BY
        category,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
    ORDER BY value DESC
`;

const CATEGORIES_LIMIT = 6;

const VALID_OPERATIONS = {
    count: [],
    sum: ['aggregationColumn'],
    avg: ['aggregationColumn'],
    min: ['aggregationColumn'],
    max: ['aggregationColumn']
};

const TYPE = 'aggregation';

/**
 {
     type: 'aggregation',
     options: {
         column: 'name',
         aggregation: 'count' // it could be, e.g., sum if column is numeric
     }
 }
 */
function Aggregation(query, options = {}, queries = {}) {
    if (typeof options.column !== 'string') {
        throw new Error(`Aggregation expects 'column' in widget options`);
    }

    if (typeof options.aggregation !== 'string') {
        throw new Error(`Aggregation expects 'aggregation' operation in widget options`);
    }

    if (!VALID_OPERATIONS[options.aggregation]) {
       throw new Error(`Aggregation does not support '${options.aggregation}' operation`);
    }

    var requiredOptions = VALID_OPERATIONS[options.aggregation];
    var missingOptions = requiredOptions.filter(requiredOption => !options.hasOwnProperty(requiredOption));

    if (missingOptions.length > 0) {
        throw new Error(`Aggregation '${options.aggregation}' is missing some options: ${missingOptions.join(',')}`);
    }

    BaseWidget.apply(this);

    this.query = query;
    this.queries = queries;
    this.column = options.column;
    this.aggregation = options.aggregation;
    this.aggregationColumn = options.aggregationColumn;
    this._isFloatColumn = null;
}

Aggregation.prototype = new BaseWidget();
Aggregation.prototype.constructor = Aggregation;

module.exports = Aggregation;

Aggregation.prototype.sql = function(psql, override, callback) {
    var self = this;

    if (!callback) {
        callback = override;
        override = {};
    }

    if (this.aggregationColumn && this._isFloatColumn === null) {
        this._isFloatColumn = false;
        this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, function (err, type) {
            if (!err && !!type) {
                self._isFloatColumn = type.float;
            }
            self.sql(psql, override, callback);
        });
        return null;
    }

    var _query = this.query;

    var aggregationSql;

    if (!!override.ownFilter) {
        aggregationSql = [
            this.getCategoriesCTESql(
                _query,
                this.column,
                this.aggregation,
                this.aggregationColumn,
                this._isFloatColumn
            ),
            aggregationQueryTpl({
                _isFloatColumn: this._isFloatColumn,
                _query: _query,
                _column: this.column,
                _aggregation: this.getAggregationSql(),
                _limit: CATEGORIES_LIMIT
            })
        ].join('\n');
    } else {
        aggregationSql = [
            this.getCategoriesCTESql(
                _query,
                this.column,
                this.aggregation,
                this.aggregationColumn,
                this._isFloatColumn
            ),
            rankedAggregationQueryTpl({
                _isFloatColumn: this._isFloatColumn,
                _query: _query,
                _column: this.column,
                _aggregationFn: this.aggregation !== 'count' ? this.aggregation : 'sum',
                _limit: CATEGORIES_LIMIT
            })
        ].join('\n');
    }

    debug(aggregationSql);

    return callback(null, aggregationSql);
};

Aggregation.prototype.getCategoriesCTESql = function(query, column, aggregation, aggregationColumn, isFloatColumn) {
    return [
        "WITH",
        [
            filteredQueryTpl({
                _isFloatColumn: isFloatColumn,
                _query: this.query,
                _column: this.column,
                _aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
            }),
            summaryQueryTpl({
                _isFloatColumn: isFloatColumn,
                _query: query,
                _column: column,
                _aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
            }),
            rankedCategoriesQueryTpl({
                _query: query,
                _column: column,
                _aggregation: this.getAggregationSql(),
                _aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
            }),
            categoriesSummaryMinMaxQueryTpl({
                _query: query,
                _column: column
            }),
            categoriesSummaryCountQueryTpl({
                _query: query,
                _column: column
            })
        ].join(',\n')
    ].join('\n');
};

const aggregationFnQueryTpl = ctx => `${ctx._aggregationFn}(${ctx._aggregationColumn})`;

Aggregation.prototype.getAggregationSql = function() {
    return aggregationFnQueryTpl({
        _aggregationFn: this.aggregation,
        _aggregationColumn: this.aggregationColumn || 1
    });
};

Aggregation.prototype.format = function(result) {
    var categories = [];
    var count = 0;
    var nulls = 0;
    var nans = 0;
    var infinities = 0;
    var minValue = 0;
    var maxValue = 0;
    var categoriesCount = 0;

    if (result.rows.length) {
        var firstRow = result.rows[0];
        count = firstRow.count;
        nulls = firstRow.nulls_count;
        nans = firstRow.nans_count;
        infinities = firstRow.infinities_count;
        minValue = firstRow.min_val;
        maxValue = firstRow.max_val;
        categoriesCount = firstRow.categories_count;
        categories = result.rows.map(({ category, value, agg }) => ({ category, value, agg }));
    }

    return {
        aggregation: this.aggregation,
        count: count,
        nulls: nulls,
        nans: nans,
        infinities: infinities,
        min: minValue,
        max: maxValue,
        categoriesCount: categoriesCount,
        categories: categories
    };
};

const filterCategoriesQueryTpl = ctx => `
    SELECT
        ${ctx._column} AS category,
        ${ctx._value} AS value
    FROM (${ctx._query}) _cdb_aggregation_search
    WHERE CAST(${ctx._column} as text) ILIKE ${ctx._userQuery}
    GROUP BY ${ctx._column}
`;

const searchQueryTpl = ctx => `
    WITH
    search_unfiltered AS (
        ${ctx._searchUnfiltered}
    ),
    search_filtered AS (
        ${ctx._searchFiltered}
    ),
    search_union AS (
        SELECT * FROM search_unfiltered
        UNION ALL
        SELECT * FROM search_filtered
    )
    SELECT category, sum(value) AS value
    FROM search_union
    GROUP BY category
    ORDER BY value desc
`;

Aggregation.prototype.search = function(psql, userQuery, callback) {
    var self = this;

    var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
    var _value = this.aggregation !== 'count' && this.aggregationColumn ?
        this.aggregation + '(' + this.aggregationColumn + ')' : 'count(1)';

    // TODO unfiltered will be wrong as filters are already applied at this point
    var query = searchQueryTpl({
        _searchUnfiltered: filterCategoriesQueryTpl({
            _query: this.query,
            _column: this.column,
            _value: '0',
            _userQuery: _userQuery
        }),
        _searchFiltered: filterCategoriesQueryTpl({
            _query: this.query,
            _column: this.column,
            _value: _value,
            _userQuery: _userQuery
        })
    });

    psql.query(query, function(err, result) {
        if (err) {
            return callback(err, result);
        }

        return callback(null, {type: self.getType(), categories: result.rows });
    }, true); // use read-only transaction
};

Aggregation.prototype.getType = function() {
    return TYPE;
};

Aggregation.prototype.toString = function() {
    return JSON.stringify({
        _type: TYPE,
        _query: this.query,
        _column: this.column,
        _aggregation: this.aggregation
    });
};
<commit_msg>Use string templates to build categories CTE sql<commit_after>const BaseWidget = require('./base');
const debug = require('debug')('windshaft:widget:aggregation');

const filteredQueryTpl = ctx => `
    filtered_source AS (
        SELECT *
        FROM (${ctx._query}) _cdb_filtered_source
        ${ctx._aggregationColumn && ctx._isFloatColumn ? `
        WHERE
            ${ctx._aggregationColumn} != 'infinity'::float
        AND
            ${ctx._aggregationColumn} != '-infinity'::float
        AND
            ${ctx._aggregationColumn} != 'NaN'::float` :
        ''
        }
    )
`;

const summaryQueryTpl = ctx => `
    summary AS (
        SELECT
            count(1) AS count,
            sum(CASE WHEN ${ctx._column} IS NULL THEN 1 ELSE 0 END) AS nulls_count
            ${ctx._isFloatColumn ? `,
            sum(
                CASE
                    WHEN ${ctx._aggregationColumn} = 'infinity'::float OR ${ctx._aggregationColumn} = '-infinity'::float
                    THEN 1
                    ELSE 0
                    END
            ) AS infinities_count,
            sum(CASE WHEN ${ctx._aggregationColumn} = 'NaN'::float THEN 1 ELSE 0 END) AS nans_count` :
            ''
            }
        FROM (${ctx._query}) _cdb_aggregation_nulls
    )
`;

const rankedCategoriesQueryTpl = ctx => `
    categories AS(
        SELECT
            ${ctx._column} AS category,
            ${ctx._aggregation} AS value,
            row_number() OVER (ORDER BY ${ctx._aggregation} desc) as rank
        FROM filtered_source
        ${ctx._aggregationColumn !== null ? `WHERE ${ctx._aggregationColumn} IS NOT NULL` : ''}
        GROUP BY ${ctx._column}
        ORDER BY 2 DESC
    )
`;

const categoriesSummaryMinMaxQueryTpl = () => `
    categories_summary_min_max AS(
        SELECT
            max(value) max_val,
            min(value) min_val
        FROM categories
    )
`;

const categoriesSummaryCountQueryTpl = ctx => `
    categories_summary_count AS(
        SELECT count(1) AS categories_count
        FROM (
            SELECT ${ctx._column} AS category
            FROM (${ctx._query}) _cdb_categories
            GROUP BY ${ctx._column}
        ) _cdb_categories_count
    )
`;

const specialNumericValuesColumns = () => `, nans_count, infinities_count`;

const rankedAggregationQueryTpl = ctx => `
    SELECT
        CAST(category AS text),
        value,
        false as agg,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
        FROM categories, summary, categories_summary_min_max, categories_summary_count
        WHERE rank < ${ctx._limit}
    UNION ALL
    SELECT
        'Other' category,
        ${ctx._aggregationFn}(value) as value,
        true as agg,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
        FROM categories, summary, categories_summary_min_max, categories_summary_count
        WHERE rank >= ${ctx._limit}
    GROUP BY
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
`;

const aggregationQueryTpl = ctx => `
    SELECT
        CAST(${ctx._column} AS text) AS category,
        ${ctx._aggregation} AS value,
        false as agg,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
    FROM (${ctx._query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
    GROUP BY
        category,
        nulls_count,
        min_val,
        max_val,
        count,
        categories_count
        ${ctx._isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
    ORDER BY value DESC
`;

const CATEGORIES_LIMIT = 6;

const VALID_OPERATIONS = {
    count: [],
    sum: ['aggregationColumn'],
    avg: ['aggregationColumn'],
    min: ['aggregationColumn'],
    max: ['aggregationColumn']
};

const TYPE = 'aggregation';

/**
 {
     type: 'aggregation',
     options: {
         column: 'name',
         aggregation: 'count' // it could be, e.g., sum if column is numeric
     }
 }
 */
function Aggregation(query, options = {}, queries = {}) {
    if (typeof options.column !== 'string') {
        throw new Error(`Aggregation expects 'column' in widget options`);
    }

    if (typeof options.aggregation !== 'string') {
        throw new Error(`Aggregation expects 'aggregation' operation in widget options`);
    }

    if (!VALID_OPERATIONS[options.aggregation]) {
       throw new Error(`Aggregation does not support '${options.aggregation}' operation`);
    }

    var requiredOptions = VALID_OPERATIONS[options.aggregation];
    var missingOptions = requiredOptions.filter(requiredOption => !options.hasOwnProperty(requiredOption));

    if (missingOptions.length > 0) {
        throw new Error(`Aggregation '${options.aggregation}' is missing some options: ${missingOptions.join(',')}`);
    }

    BaseWidget.apply(this);

    this.query = query;
    this.queries = queries;
    this.column = options.column;
    this.aggregation = options.aggregation;
    this.aggregationColumn = options.aggregationColumn;
    this._isFloatColumn = null;
}

Aggregation.prototype = new BaseWidget();
Aggregation.prototype.constructor = Aggregation;

module.exports = Aggregation;

Aggregation.prototype.sql = function(psql, override, callback) {
    var self = this;

    if (!callback) {
        callback = override;
        override = {};
    }

    if (this.aggregationColumn && this._isFloatColumn === null) {
        this._isFloatColumn = false;
        this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, function (err, type) {
            if (!err && !!type) {
                self._isFloatColumn = type.float;
            }
            self.sql(psql, override, callback);
        });
        return null;
    }

    var _query = this.query;

    var aggregationSql;

    if (!!override.ownFilter) {
        aggregationSql = [
            this.getCategoriesCTESql(
                _query,
                this.column,
                this.aggregation,
                this.aggregationColumn,
                this._isFloatColumn
            ),
            aggregationQueryTpl({
                _isFloatColumn: this._isFloatColumn,
                _query: _query,
                _column: this.column,
                _aggregation: this.getAggregationSql(),
                _limit: CATEGORIES_LIMIT
            })
        ].join('\n');
    } else {
        aggregationSql = [
            this.getCategoriesCTESql(
                _query,
                this.column,
                this.aggregation,
                this.aggregationColumn,
                this._isFloatColumn
            ),
            rankedAggregationQueryTpl({
                _isFloatColumn: this._isFloatColumn,
                _query: _query,
                _column: this.column,
                _aggregationFn: this.aggregation !== 'count' ? this.aggregation : 'sum',
                _limit: CATEGORIES_LIMIT
            })
        ].join('\n');
    }

    debug(aggregationSql);

    return callback(null, aggregationSql);
};

Aggregation.prototype.getCategoriesCTESql = function(query, column, aggregation, aggregationColumn, isFloatColumn) {
    return `
        WITH
            ${filteredQueryTpl({
                _isFloatColumn: isFloatColumn,
                _query: this.query,
                _column: this.column,
                _aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
            })},
            ${summaryQueryTpl({
                _isFloatColumn: isFloatColumn,
                _query: query,
                _column: column,
                _aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
            })},
            ${rankedCategoriesQueryTpl({
                _query: query,
                _column: column,
                _aggregation: this.getAggregationSql(),
                _aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
            })},
            ${categoriesSummaryMinMaxQueryTpl({
                _query: query,
                _column: column
            })},
            ${categoriesSummaryCountQueryTpl({
                _query: query,
                _column: column
            })}
    `;
};

const aggregationFnQueryTpl = ctx => `${ctx._aggregationFn}(${ctx._aggregationColumn})`;

Aggregation.prototype.getAggregationSql = function() {
    return aggregationFnQueryTpl({
        _aggregationFn: this.aggregation,
        _aggregationColumn: this.aggregationColumn || 1
    });
};

Aggregation.prototype.format = function(result) {
    var categories = [];
    var count = 0;
    var nulls = 0;
    var nans = 0;
    var infinities = 0;
    var minValue = 0;
    var maxValue = 0;
    var categoriesCount = 0;

    if (result.rows.length) {
        var firstRow = result.rows[0];
        count = firstRow.count;
        nulls = firstRow.nulls_count;
        nans = firstRow.nans_count;
        infinities = firstRow.infinities_count;
        minValue = firstRow.min_val;
        maxValue = firstRow.max_val;
        categoriesCount = firstRow.categories_count;
        categories = result.rows.map(({ category, value, agg }) => ({ category, value, agg }));
    }

    return {
        aggregation: this.aggregation,
        count: count,
        nulls: nulls,
        nans: nans,
        infinities: infinities,
        min: minValue,
        max: maxValue,
        categoriesCount: categoriesCount,
        categories: categories
    };
};

const filterCategoriesQueryTpl = ctx => `
    SELECT
        ${ctx._column} AS category,
        ${ctx._value} AS value
    FROM (${ctx._query}) _cdb_aggregation_search
    WHERE CAST(${ctx._column} as text) ILIKE ${ctx._userQuery}
    GROUP BY ${ctx._column}
`;

const searchQueryTpl = ctx => `
    WITH
    search_unfiltered AS (
        ${ctx._searchUnfiltered}
    ),
    search_filtered AS (
        ${ctx._searchFiltered}
    ),
    search_union AS (
        SELECT * FROM search_unfiltered
        UNION ALL
        SELECT * FROM search_filtered
    )
    SELECT category, sum(value) AS value
    FROM search_union
    GROUP BY category
    ORDER BY value desc
`;

Aggregation.prototype.search = function(psql, userQuery, callback) {
    var self = this;

    var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
    var _value = this.aggregation !== 'count' && this.aggregationColumn ?
        this.aggregation + '(' + this.aggregationColumn + ')' : 'count(1)';

    // TODO unfiltered will be wrong as filters are already applied at this point
    var query = searchQueryTpl({
        _searchUnfiltered: filterCategoriesQueryTpl({
            _query: this.query,
            _column: this.column,
            _value: '0',
            _userQuery: _userQuery
        }),
        _searchFiltered: filterCategoriesQueryTpl({
            _query: this.query,
            _column: this.column,
            _value: _value,
            _userQuery: _userQuery
        })
    });

    psql.query(query, function(err, result) {
        if (err) {
            return callback(err, result);
        }

        return callback(null, {type: self.getType(), categories: result.rows });
    }, true); // use read-only transaction
};

Aggregation.prototype.getType = function() {
    return TYPE;
};

Aggregation.prototype.toString = function() {
    return JSON.stringify({
        _type: TYPE,
        _query: this.query,
        _column: this.column,
        _aggregation: this.aggregation
    });
};
<|endoftext|>


<commit_before>var M = require('./m');

var win = window;
// 状态缓存
var stateCache = {};
// url缓存
var urlCache = [];

// 得到basepath
var parseBasePath = function(path) {
	path = path.replace(/[^\/]+$/, '');
	return ('/' + path + '/').replace(/^\/+|\/+$/g, '/');
};
var getDefBase = function() {
	// 默认base path
	var defBase = M.document.getElementsByTagName('base');
	if (defBase && defBase.length) {
		defBase = parseBasePath(defBase[0].getAttribute('href'));
	} else {
		defBase = '/';
	}
	return defBase;
};

var locationObj = M.parseLocation();
var locationOrigin = locationObj.origin;

var history = win.history;
var agent = (win.navigator || {}).userAgent;
if (!agent) agent = '';
var android = parseInt((/android (\d+)/.exec(agent.toLowerCase()) || [])[1], 10);
if (isNaN(android)) android = undefined;
var boxee = /Boxee/i.test(agent);

var supportPushState = !!(history && history.pushState && history.replaceState && !(android < 4) && !boxee);
var supportHashChange = !!('onhashchange' in win);

var MODE_MAP = {
	hashbang: 1,
	history: 2,
	abstract: 3
};

var hashbangPrefix = '#!';

var hashCacheState = '';

var History = {

	mode: MODE_MAP.hashbang,

	supportPushState: supportPushState,
	supportHashChange: supportHashChange,
	/*是否支持*/
	support: supportPushState || supportHashChange,

	/*是否已启动*/
	startd: false,

	/*在urlCache中位置*/
	index: -1,
	preIndex: -1,

	/*base path*/
	base: '/',

	checkMode: function() {
		var that = this;
		'abstract,history,hashbang'.replace(M.rword, function(mode) {
			if (that.options[mode]) {
				that.mode = MODE_MAP[mode];
			}
		});
		if (this.mode === MODE_MAP.history && !this.supportPushState) {
			// history 模式 但是不支持 pushstate
			this.mode = MODE_MAP.hashbang;
		}
		if (!this.support) {
			// 都不支持
			this.mode = MODE_MAP.abstract;
		}
	},

	/**
	 * 启动
	 * @param  {Object} options 配置参数
	 */
	start: function(options) {
		if (this.startd) return;
		if (!options) options = {};
		var base = options.base;
		if (M.isDefined(base) && M.isString(base)) {
			this.base = parseBasePath(base);
		} else {
			this.base = getDefBase();
		}
		this.startd = true;

		this.options = M.extend({}, options);

		// 检查设置的模式
		this.checkMode();

		// 根据模式做处理
		if (this.mode !== MODE_MAP.abstract) {
			win.addEventListener(this.mode === MODE_MAP.history ? 'popstate' : 'hashchange', this.onChange);
		}
		M.document.addEventListener('click', this.onDocClick);

		// 需要初始化一次当前的state
		this.onChange();
		this.trigger('inited');
	},

	/**
	 * 停止监听
	 */
	stop: function() {
		if (this.mode !== MODE_MAP.abstract) {
			win.removeEventListener(this.mode === MODE_MAP.history ? 'popstate' : 'hashchange', this.onChange);
		}
		M.document.removeEventListener('click', this.onDocClick);
		this.startd = false;
		this.index = -1;
		stateCache = {};
		urlCache = [];
	},

	/**
	 * 去掉url中base之后的path
	 * @param  {String}  url      url
	 * @param  {Boolean} noSearch 不加search信息
	 * @return {String}           去掉base之后的URL
	 */
	getPath: function(url, noSearch) {
		var urlDetail = M.parseUrl(url);
		var path = decodeURIComponent(urlDetail.pathname + (noSearch ? '' : urlDetail.search));
		// 去掉最后/
		var root = this.base.slice(0, -1);
		if (!path.indexOf(root)) path = path.slice(root.length);
		path = path.slice(1);
		if (path || History.base) {
			if (!path || path !== '/') {
				path = '/' + (path || '');
			}
		}
		return path;
	},

	/**
	 * document点击的事件处理函数
	 * @param  {Event}   e 事件对象
	 * @return {Boolean}   是否被阻止了
	 */
	onDocClick: function(e) {
		if (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
			return true;
		}

		var targetEle = e.target;
		var hrefTarget = M.getHrefAndTarget(targetEle);
		targetEle = hrefTarget.target;
		var href = hrefTarget.href;
		if (!href || !targetEle) {
			return;
		}
		// 存在 href 且和当前是同源
		// 且不带 target download rel!=external 且不是以 javascript: 开头
		var noSpes = !targetEle.target && !targetEle.hasAttribute('download') && targetEle.getAttribute('rel') !== 'external';
		if (History.checkUrl(href) && noSpes) {
			var datasetObj =  M.getDatesetObj(targetEle);
			var state = History.createStateObject(href, datasetObj);
			e.preventDefault();
			History.pushState(state, true);
			return false;
		}
	},

	/**
	 * 检测url是否可以走自定义pushState
	 * @param  {String}  url       url
	 * @param  {String}  urlOrigin 检测url的origin
	 * @return {Boolean}           检测结果
	 */
	checkUrl: function(url, urlOrigin) {
		if (!urlOrigin) {
			urlOrigin = M.parseUrl(url).origin;
		}
		var rext = /^(javascript|mailto|tel):/i;
		return url && locationOrigin === urlOrigin && !rext.test(url);
	},

	/**
	 * 添加新的url
	 * @param  {String}      url  新的url
	 * @param  {Object|Null} data 附加数据
	 */
	push: function(url, data) {
		var state = this.createStateObject(url, data);
		this.pushState(state);
	},

	currentHref: function() {
		return M.location.href.replace(hashbangPrefix + '/', '');
	},

	/**
	 * 添加新的state
	 * @param  {Object}  state   state数据对象
	 * @param  {Boolean} checked 是否已经校验过该state
	 */
	pushState: function(state, checked) {
		if (!checked && !this.checkUrl(state.url, state.origin)) return;
		if (state.url === History.getCurrentState().url) return;
		// 如果是允许改变history 且其dataset中不包含href的话才会改变history
		// 规则就是：
		// data-href="newUrl"会被认为是在当前页中切换，也就是局部禁用pushstate
		if (this.mode !== MODE_MAP.abstract && state.url !== History.currentHref()) {
			if (this.mode === MODE_MAP.hashbang) {
				if (M.isUndefined(state.data.href)) {
					hashCacheState = state;
					// 更改hash后会自动触发hashchange事件
					M.location.hash = hashbangPrefix + state.rpath;
					return;
				}
			} else {
				M.isUndefined(state.data.href) && history[state.replace ? 'replaceState' : 'pushState'](state, state.title, state.url);
			}
		}
		this.onChange({
			state: state
		});
	},

	/**
	 * history改变回调
	 */
	onChange: function(e) {
		if (e && e.type === 'hashchange') {
			e.state = hashCacheState;
		}
		var state = e && e.state || History.getUrlState(History.currentHref());
		var oldState = History.getCurrentState();

		hashCacheState = null;

		// 如果新的url和旧的url只是hash不同，那么应该走scrollIntoView
		var scrollToEle;
		if (oldState && state.rurl === oldState.rurl) {
			if (e.type !== 'popstate' && state.hash) {
				scrollToEle = M.document.getElementById(state.hash);
				scrollToEle && scrollToEle.scrollIntoView();
			}
			return;
		}

		var newIndex = History.storeState(state);
		var type = '';
		if (newIndex > History.index) {
			// 可以认为是往前 不管是新的 还是已经存在的
			type = 'forward';
		} else if (newIndex < History.index) {
			// 后退
			type = 'back';
		} else {
			e.preventDefault();
			return false;
		}

		// 对于 有data-rel 是back的 调整其顺序
		// 一般的场景就是 类似 返回按钮
		if (type !== 'back' && state.data.rel === 'back') {
			type = 'back';
		} else if (type === 'back' && state.data.rel !== 'back' && oldState.data.rel === 'back') {
			type = 'forward';
		}
		M.document.title = state.title;
		if (newIndex !== History.index) {
			History.preIndex = History.index;
		}
		History.index = newIndex;
		// 触发改变事件
		History.trigger(type, state, oldState);
		History.trigger('change', type, state, oldState);
	},

	/**
	 * 得到url的state 如果不存在则创建新的
	 * @param  {String} url url
	 * @return {Object}     包装的state对象
	 */
	getUrlState: function(url) {
		url = M.getFullUrl(url);
		return stateCache[url] || this.createStateObject(url);
	},

	/**
	 * 得到当前的state
	 * @return {Object|Null} 当前的state
	 */
	getCurrentState: function() {
		var url = urlCache[this.index];
		if (!url) return null;
		return stateCache[url] || null;
	},

	/**
	 * 得到上一个状态
	 * @return {Object|Null} 当前的state
	 */
	getPrevState: function() {
		var url = urlCache[History.preIndex];
		if (!url) {
			url = urlCache[this.index - 1];
			if (!url) {
				return null;
			}
		}
		return stateCache[url] || null;
	},

	/**
	 * 创建包装state对象
	 * @param  {String} url   url
	 * @param  {object} data  附带数据
	 * @return {Object}       包装的state对象
	 */
	createStateObject: function(url, data) {
		if (url) {
			if (url.charAt(0) === '/') {
				url = url.slice(1);
				url = this.base + url;
			}
		} else {
			url = History.currentHref();
		}
		var parsedUrl = M.parseUrl(url);
		return {
			data: data || (data = {}),
			title: data.title || M.document.title,
			rurl: parsedUrl.rurl,
			url: parsedUrl.url,
			path: parsedUrl.pathname,
			rpath: History.getPath(url),
			origin: parsedUrl.origin,
			hash: parsedUrl.hash,
			replace: M.isString(data.replace) ? data.replace === 'true' : !!data.replace
		};
	},

	/**
	 * url是否在缓存中
	 * @param  {String}  url url
	 * @return {Boolean}     是否在缓存中
	 */
	hasUrl: function(url) {
		return !!stateCache[url];
	},

	/**
	 * 存储state
	 * @param  {Object}            state 包装state对象
	 * @return {Number}            在urlCache中位置
	 */
	storeState: function(state) {
		var i = -1;
		var url = state.rurl;
		var toIndex = urlCache.indexOf(url);
		if (this.hasUrl(url)) {
			// 之前有
			i = toIndex;
		} else {
			i = this.index + 1;
			urlCache.splice(i, 0, url);
		}
		stateCache[url] = M.extend(true, stateCache[url], state);
		this.clearCache(i);
		return i;
	},

	/**
	 * 清除指定位置之后的cache
	 * @param  {Number} index 指定位置
	 */
	clearCache: function(index) {
		if (M.isUndefined(index)) index = -1;
		// 最多保留50条记录
		var num = index + 50;
		var clearUrls = urlCache.slice(num);
		clearUrls.forEach(function(url) {
			delete stateCache[url];
		});
		if (urlCache.length > num) urlCache.length = num;
	},

	/**
	 * 得到缓存信息
	 * @return {Object} 缓存
	 */
	getCache: function() {
		return {
			urlCache: urlCache,
			stateCache: stateCache
		};
	},

	parseUrl: parseUrl
};

// 增加事件机制
M.extendByBase(History);

M.history = History;

module.exports = M;

function parseUrl(url) {
	var path = History.getPath(url);
	// 如果path为空 但是有base 说明 可以path为/
	if (path) {
		return M.parseQuery(path);
	}
	return null;
}
<commit_msg>fix url different, hash same<commit_after>var M = require('./m');

var win = window;
// 状态缓存
var stateCache = {};
// url缓存
var urlCache = [];

// 得到basepath
var parseBasePath = function(path) {
	path = path.replace(/[^\/]+$/, '');
	return ('/' + path + '/').replace(/^\/+|\/+$/g, '/');
};
var getDefBase = function() {
	// 默认base path
	var defBase = M.document.getElementsByTagName('base');
	if (defBase && defBase.length) {
		defBase = parseBasePath(defBase[0].getAttribute('href'));
	} else {
		defBase = '/';
	}
	return defBase;
};

var locationObj = M.parseLocation();
var locationOrigin = locationObj.origin;

var history = win.history;
var agent = (win.navigator || {}).userAgent;
if (!agent) agent = '';
var android = parseInt((/android (\d+)/.exec(agent.toLowerCase()) || [])[1], 10);
if (isNaN(android)) android = undefined;
var boxee = /Boxee/i.test(agent);

var supportPushState = !!(history && history.pushState && history.replaceState && !(android < 4) && !boxee);
var supportHashChange = !!('onhashchange' in win);

var MODE_MAP = {
	hashbang: 1,
	history: 2,
	abstract: 3
};

var hashbangPrefix = '#!';

var hashCacheState = '';

var History = {

	mode: MODE_MAP.hashbang,

	supportPushState: supportPushState,
	supportHashChange: supportHashChange,
	/*是否支持*/
	support: supportPushState || supportHashChange,

	/*是否已启动*/
	startd: false,

	/*在urlCache中位置*/
	index: -1,
	preIndex: -1,

	/*base path*/
	base: '/',

	checkMode: function() {
		var that = this;
		'abstract,history,hashbang'.replace(M.rword, function(mode) {
			if (that.options[mode]) {
				that.mode = MODE_MAP[mode];
			}
		});
		if (this.mode === MODE_MAP.history && !this.supportPushState) {
			// history 模式 但是不支持 pushstate
			this.mode = MODE_MAP.hashbang;
		}
		if (!this.support) {
			// 都不支持
			this.mode = MODE_MAP.abstract;
		}
	},

	/**
	 * 启动
	 * @param  {Object} options 配置参数
	 */
	start: function(options) {
		if (this.startd) return;
		if (!options) options = {};
		var base = options.base;
		if (M.isDefined(base) && M.isString(base)) {
			this.base = parseBasePath(base);
		} else {
			this.base = getDefBase();
		}
		this.startd = true;

		this.options = M.extend({}, options);

		// 检查设置的模式
		this.checkMode();

		// 根据模式做处理
		if (this.mode !== MODE_MAP.abstract) {
			win.addEventListener(this.mode === MODE_MAP.history ? 'popstate' : 'hashchange', this.onChange);
		}
		M.document.addEventListener('click', this.onDocClick);

		// 需要初始化一次当前的state
		this.onChange();
		this.trigger('inited');
	},

	/**
	 * 停止监听
	 */
	stop: function() {
		if (this.mode !== MODE_MAP.abstract) {
			win.removeEventListener(this.mode === MODE_MAP.history ? 'popstate' : 'hashchange', this.onChange);
		}
		M.document.removeEventListener('click', this.onDocClick);
		this.startd = false;
		this.index = -1;
		stateCache = {};
		urlCache = [];
	},

	/**
	 * 去掉url中base之后的path
	 * @param  {String}  url      url
	 * @param  {Boolean} noSearch 不加search信息
	 * @return {String}           去掉base之后的URL
	 */
	getPath: function(url, noSearch) {
		var urlDetail = M.parseUrl(url);
		var path = decodeURIComponent(urlDetail.pathname + (noSearch ? '' : urlDetail.search));
		// 去掉最后/
		var root = this.base.slice(0, -1);
		if (!path.indexOf(root)) path = path.slice(root.length);
		path = path.slice(1);
		if (path || History.base) {
			if (!path || path !== '/') {
				path = '/' + (path || '');
			}
		}
		return path;
	},

	/**
	 * document点击的事件处理函数
	 * @param  {Event}   e 事件对象
	 * @return {Boolean}   是否被阻止了
	 */
	onDocClick: function(e) {
		if (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
			return true;
		}

		var targetEle = e.target;
		var hrefTarget = M.getHrefAndTarget(targetEle);
		targetEle = hrefTarget.target;
		var href = hrefTarget.href;
		if (!href || !targetEle) {
			return;
		}
		// 存在 href 且和当前是同源
		// 且不带 target download rel!=external 且不是以 javascript: 开头
		var noSpes = !targetEle.target && !targetEle.hasAttribute('download') && targetEle.getAttribute('rel') !== 'external';
		if (History.checkUrl(href) && noSpes) {
			var datasetObj =  M.getDatesetObj(targetEle);
			var state = History.createStateObject(href, datasetObj);
			e.preventDefault();
			History.pushState(state, true);
			return false;
		}
	},

	/**
	 * 检测url是否可以走自定义pushState
	 * @param  {String}  url       url
	 * @param  {String}  urlOrigin 检测url的origin
	 * @return {Boolean}           检测结果
	 */
	checkUrl: function(url, urlOrigin) {
		if (!urlOrigin) {
			urlOrigin = M.parseUrl(url).origin;
		}
		var rext = /^(javascript|mailto|tel):/i;
		return url && locationOrigin === urlOrigin && !rext.test(url);
	},

	/**
	 * 添加新的url
	 * @param  {String}      url  新的url
	 * @param  {Object|Null} data 附加数据
	 */
	push: function(url, data) {
		var state = this.createStateObject(url, data);
		this.pushState(state);
	},

	currentHref: function() {
		return M.location.href.replace(hashbangPrefix + '/', '');
	},

	/**
	 * 添加新的state
	 * @param  {Object}  state   state数据对象
	 * @param  {Boolean} checked 是否已经校验过该state
	 */
	pushState: function(state, checked) {
		if (!checked && !this.checkUrl(state.url, state.origin)) return;
		if (state.url === History.getCurrentState().url) return;
		// 如果是允许改变history 且其dataset中不包含href的话才会改变history
		// 规则就是：
		// data-href="newUrl"会被认为是在当前页中切换，也就是局部禁用pushstate
		if (this.mode !== MODE_MAP.abstract && state.url !== History.currentHref()) {
			if (this.mode === MODE_MAP.hashbang) {
				if (M.isUndefined(state.data.href)) {
					hashCacheState = state;
					// 更改hash后会自动触发hashchange事件
					var newHash = hashbangPrefix + state.rpath;
					if (newHash !== M.location.hash) {
						// 不相等才会触发 hashchange
						M.location.hash = newHash;
						return;
					}
				}
			} else {
				M.isUndefined(state.data.href) && history[state.replace ? 'replaceState' : 'pushState'](state, state.title, state.url);
			}
		}
		this.onChange({
			state: state
		});
	},

	/**
	 * history改变回调
	 */
	onChange: function(e) {
		if (e && e.type === 'hashchange') {
			e.state = hashCacheState;
		}
		var state = e && e.state || History.getUrlState(History.currentHref());
		var oldState = History.getCurrentState();

		hashCacheState = null;

		// 如果新的url和旧的url只是hash不同，那么应该走scrollIntoView
		var scrollToEle;
		if (oldState && state.rurl === oldState.rurl) {
			if (e.type !== 'popstate' && state.hash) {
				scrollToEle = M.document.getElementById(state.hash);
				scrollToEle && scrollToEle.scrollIntoView();
			}
			return;
		}

		var newIndex = History.storeState(state);
		var type = '';
		if (newIndex > History.index) {
			// 可以认为是往前 不管是新的 还是已经存在的
			type = 'forward';
		} else if (newIndex < History.index) {
			// 后退
			type = 'back';
		} else {
			e.preventDefault();
			return false;
		}

		// 对于 有data-rel 是back的 调整其顺序
		// 一般的场景就是 类似 返回按钮
		if (type !== 'back' && state.data.rel === 'back') {
			type = 'back';
		} else if (type === 'back' && state.data.rel !== 'back' && oldState.data.rel === 'back') {
			type = 'forward';
		}
		M.document.title = state.title;
		if (newIndex !== History.index) {
			History.preIndex = History.index;
		}
		History.index = newIndex;
		// 触发改变事件
		History.trigger(type, state, oldState);
		History.trigger('change', type, state, oldState);
	},

	/**
	 * 得到url的state 如果不存在则创建新的
	 * @param  {String} url url
	 * @return {Object}     包装的state对象
	 */
	getUrlState: function(url) {
		url = M.getFullUrl(url);
		return stateCache[url] || this.createStateObject(url);
	},

	/**
	 * 得到当前的state
	 * @return {Object|Null} 当前的state
	 */
	getCurrentState: function() {
		var url = urlCache[this.index];
		if (!url) return null;
		return stateCache[url] || null;
	},

	/**
	 * 得到上一个状态
	 * @return {Object|Null} 当前的state
	 */
	getPrevState: function() {
		var url = urlCache[History.preIndex];
		if (!url) {
			url = urlCache[this.index - 1];
			if (!url) {
				return null;
			}
		}
		return stateCache[url] || null;
	},

	/**
	 * 创建包装state对象
	 * @param  {String} url   url
	 * @param  {object} data  附带数据
	 * @return {Object}       包装的state对象
	 */
	createStateObject: function(url, data) {
		if (url) {
			if (url.charAt(0) === '/') {
				url = url.slice(1);
				url = this.base + url;
			}
		} else {
			url = History.currentHref();
		}
		var parsedUrl = M.parseUrl(url);
		return {
			data: data || (data = {}),
			title: data.title || M.document.title,
			rurl: parsedUrl.rurl,
			url: parsedUrl.url,
			path: parsedUrl.pathname,
			rpath: History.getPath(url),
			origin: parsedUrl.origin,
			hash: parsedUrl.hash,
			replace: M.isString(data.replace) ? data.replace === 'true' : !!data.replace
		};
	},

	/**
	 * url是否在缓存中
	 * @param  {String}  url url
	 * @return {Boolean}     是否在缓存中
	 */
	hasUrl: function(url) {
		return !!stateCache[url];
	},

	/**
	 * 存储state
	 * @param  {Object}            state 包装state对象
	 * @return {Number}            在urlCache中位置
	 */
	storeState: function(state) {
		var i = -1;
		var url = state.rurl;
		var toIndex = urlCache.indexOf(url);
		if (this.hasUrl(url)) {
			// 之前有
			i = toIndex;
		} else {
			i = this.index + 1;
			urlCache.splice(i, 0, url);
		}
		stateCache[url] = M.extend(true, stateCache[url], state);
		this.clearCache(i);
		return i;
	},

	/**
	 * 清除指定位置之后的cache
	 * @param  {Number} index 指定位置
	 */
	clearCache: function(index) {
		if (M.isUndefined(index)) index = -1;
		// 最多保留50条记录
		var num = index + 50;
		var clearUrls = urlCache.slice(num);
		clearUrls.forEach(function(url) {
			delete stateCache[url];
		});
		if (urlCache.length > num) urlCache.length = num;
	},

	/**
	 * 得到缓存信息
	 * @return {Object} 缓存
	 */
	getCache: function() {
		return {
			urlCache: urlCache,
			stateCache: stateCache
		};
	},

	parseUrl: parseUrl
};

// 增加事件机制
M.extendByBase(History);

M.history = History;

module.exports = M;

function parseUrl(url) {
	var path = History.getPath(url);
	// 如果path为空 但是有base 说明 可以path为/
	if (path) {
		return M.parseQuery(path);
	}
	return null;
}
<|endoftext|>


<commit_before>'use strict';

const _ = require('lodash');

/**
 * A set of functions called "actions" for `ContentManager`
 */

module.exports = {
  fetchAll: async (params, query) => {
    const { limit, skip = 0, sort, query : request, queryAttribute, source, page, populate = [] } = query;

    // Find entries using `queries` system
    return await strapi.query(params.model, source).find({
      limit,
      skip,
      sort,
      where: request,
      queryAttribute,
    }, populate);
  },

  count: async (params, source) => {
    return await strapi.query(params.model, source).count();
  },

  fetch: async (params, source, populate, raw = true) => {
    return await strapi.query(params.model, source).findOne({
      id: params.id
    }, populate, raw);
  },

  add: async (params, values, source) => {
    // Multipart/form-data.
    if (values.hasOwnProperty('fields') && values.hasOwnProperty('files')) {
      // Silent recursive parser.
      const parser = (value) => {
        try {
          value = JSON.parse(value);
        } catch (e) {
          // Silent.
        }

        return _.isArray(value) ? value.map(obj => parser(obj)) : value;
      };

      const files = values.files;

      // Parse stringify JSON data.
      values = Object.keys(values.fields).reduce((acc, current) => {
        acc[current] = parser(values.fields[current]);

        return acc;
      }, {});

      // Update JSON fields.
      const entry = await strapi.query(params.model, source).create({
        values
      });

      // Then, request plugin upload.
      if (strapi.plugins.upload && !_.isEmpty(values.files)) {
        // Upload new files and attach them to this entity.
        await strapi.plugins.upload.services.upload.uploadToEntity({
          id: entry.id || entry._id,
          model: params.model
        }, files, source);
      }

      return strapi.query(params.model, source).findOne({
        id: entry.id || entry._id
      });
    }

    // Create an entry using `queries` system
    return await strapi.query(params.model, source).create({
      values
    });
  },

  edit: async (params, values, source) => {
    // Multipart/form-data.
    if (values.hasOwnProperty('fields') && values.hasOwnProperty('files')) {
      // Silent recursive parser.
      const parser = (value) => {
        try {
          value = JSON.parse(value);
        } catch (e) {
          // Silent.
        }

        return _.isArray(value) ? value.map(obj => parser(obj)) : value;
      };

      const files = values.files;

      // Parse stringify JSON data.
      values = Object.keys(values.fields).reduce((acc, current) => {
        acc[current] = parser(values.fields[current]);

        return acc;
      }, {});

      // Update JSON fields.
      await strapi.query(params.model, source).update({
        id: params.id,
        values
      });

      // Then, request plugin upload.
      if (strapi.plugins.upload) {
        // Upload new files and attach them to this entity.
        await strapi.plugins.upload.services.upload.uploadToEntity(params, files, source);
      }

      return strapi.query(params.model, source).findOne({
        id: params.id
      });
    }

    // Raw JSON.
    return strapi.query(params.model, source).update({
      id: params.id,
      values
    });
  },

  delete: async (params, { source }) => {
    const response = await strapi.query(params.model, source).findOne({
      id: params.id
    });

    params.values = Object.keys(JSON.parse(JSON.stringify(response))).reduce((acc, current) => {
      const association = (strapi.models[params.model] || strapi.plugins[source].models[params.model]).associations.filter(x => x.alias === current)[0];

      // Remove relationships.
      if (association) {
        acc[current] = _.isArray(response[current]) ? [] : null;
      }

      return acc;
    }, {});

    if (!_.isEmpty(params.values)) {
      // Run update to remove all relationships.
      await strapi.query(params.model, source).update(params);
    }

    // Delete an entry using `queries` system
    return await strapi.query(params.model, source).delete({
      id: params.id
    });
  },
};
<commit_msg>Fix uploaded file on create<commit_after>'use strict';

const _ = require('lodash');

/**
 * A set of functions called "actions" for `ContentManager`
 */

module.exports = {
  fetchAll: async (params, query) => {
    const { limit, skip = 0, sort, query : request, queryAttribute, source, page, populate = [] } = query;

    // Find entries using `queries` system
    return await strapi.query(params.model, source).find({
      limit,
      skip,
      sort,
      where: request,
      queryAttribute,
    }, populate);
  },

  count: async (params, source) => {
    return await strapi.query(params.model, source).count();
  },

  fetch: async (params, source, populate, raw = true) => {
    return await strapi.query(params.model, source).findOne({
      id: params.id
    }, populate, raw);
  },

  add: async (params, values, source) => {
    // Multipart/form-data.
    if (values.hasOwnProperty('fields') && values.hasOwnProperty('files')) {
      // Silent recursive parser.
      const parser = (value) => {
        try {
          value = JSON.parse(value);
        } catch (e) {
          // Silent.
        }

        return _.isArray(value) ? value.map(obj => parser(obj)) : value;
      };

      const files = values.files;

      // Parse stringify JSON data.
      values = Object.keys(values.fields).reduce((acc, current) => {
        acc[current] = parser(values.fields[current]);

        return acc;
      }, {});

      // Update JSON fields.
      const entry = await strapi.query(params.model, source).create({
        values
      });

      // Then, request plugin upload.
      if (strapi.plugins.upload && Object.keys(files).length > 0) {
        // Upload new files and attach them to this entity.
        await strapi.plugins.upload.services.upload.uploadToEntity({
          id: entry.id || entry._id,
          model: params.model
        }, files, source);
      }

      return strapi.query(params.model, source).findOne({
        id: entry.id || entry._id
      });
    }

    // Create an entry using `queries` system
    return await strapi.query(params.model, source).create({
      values
    });
  },

  edit: async (params, values, source) => {
    // Multipart/form-data.
    if (values.hasOwnProperty('fields') && values.hasOwnProperty('files')) {
      // Silent recursive parser.
      const parser = (value) => {
        try {
          value = JSON.parse(value);
        } catch (e) {
          // Silent.
        }

        return _.isArray(value) ? value.map(obj => parser(obj)) : value;
      };

      const files = values.files;

      // Parse stringify JSON data.
      values = Object.keys(values.fields).reduce((acc, current) => {
        acc[current] = parser(values.fields[current]);

        return acc;
      }, {});

      // Update JSON fields.
      await strapi.query(params.model, source).update({
        id: params.id,
        values
      });

      // Then, request plugin upload.
      if (strapi.plugins.upload) {
        // Upload new files and attach them to this entity.
        await strapi.plugins.upload.services.upload.uploadToEntity(params, files, source);
      }

      return strapi.query(params.model, source).findOne({
        id: params.id
      });
    }

    // Raw JSON.
    return strapi.query(params.model, source).update({
      id: params.id,
      values
    });
  },

  delete: async (params, { source }) => {
    const response = await strapi.query(params.model, source).findOne({
      id: params.id
    });

    params.values = Object.keys(JSON.parse(JSON.stringify(response))).reduce((acc, current) => {
      const association = (strapi.models[params.model] || strapi.plugins[source].models[params.model]).associations.filter(x => x.alias === current)[0];

      // Remove relationships.
      if (association) {
        acc[current] = _.isArray(response[current]) ? [] : null;
      }

      return acc;
    }, {});

    if (!_.isEmpty(params.values)) {
      // Run update to remove all relationships.
      await strapi.query(params.model, source).update(params);
    }

    // Delete an entry using `queries` system
    return await strapi.query(params.model, source).delete({
      id: params.id
    });
  },
};
<|endoftext|>