<commit_before>import {BottomBar, Content, SectionLayout, TopBar} from 'widget/sectionLayout'
import {Button} from 'widget/button'
import {Buttons} from 'widget/buttons'
import {FieldSet} from 'widget/form'
import {PageControls, PageData, PageInfo, Pageable} from 'widget/pageable'
import {Scrollable, ScrollableContainer, Unscrollable} from 'widget/scrollable'
import {Shape} from 'widget/shape'
import {compose} from 'compose'
import {connect} from 'store'
import {map, share, zip} from 'rxjs/operators'
import {msg} from 'translate'
import Highlight from 'react-highlighter'
import Icon from 'widget/icon'
import Label from 'widget/label'
import Notifications from 'widget/notifications'
import React from 'react'
import UserDetails from './user'
import _ from 'lodash'
import api from 'api'
import escapeStringRegexp from 'escape-string-regexp'
import format from 'format'
import lookStyles from 'style/look.module.css'
import moment from 'moment'
import styles from './users.module.css'

const getUserList$ = () => api.user.getUserList$().pipe(
    share()
)

const getBudgetReport$ = () => api.user.getBudgetReport$().pipe(
    zip(getUserList$()),
    map(([budgetReport]) => budgetReport)
)

class Users extends React.Component {
    state = {
        users: [],
        sortingOrder: 'updateTime',
        sortingDirection: -1,
        textFilter: '',
        statusFilter: null,
        budgetFilter: false,
        userDetails: null
    }

    search = React.createRef()

    componentDidMount() {
        const setUserList = userList =>
            this.setState({
                users: this.getSortedUsers(userList)
            })

        const mergeBudgetReport = budgetReport =>
            this.setState(({users}) => ({
                users: _.map(users, user => ({
                    ...user,
                    report: budgetReport[user.username || {}]
                }))
            }))

        this.props.stream('LOAD_USER_LIST',
            getUserList$(),
            userList => setUserList(userList)
        )
        this.props.stream('LOAD_BUDGET_REPORT',
            getBudgetReport$(),
            budgetReport => mergeBudgetReport(budgetReport)
        )
    }

    setSorting(sortingOrder) {
        this.setState(prevState => {
            const sortingDirection = sortingOrder === prevState.sortingOrder ? -prevState.sortingDirection : 1
            return {
                ...prevState,
                sortingOrder,
                sortingDirection,
                users: this.getSortedUsers(prevState.users, sortingOrder, sortingDirection)
            }
        })
    }

    getSortedUsers(users, sortingOrder = this.state.sortingOrder, sortingDirection = this.state.sortingDirection) {
        return _.orderBy(users, user => {
            const item = _.get(user, sortingOrder)
            return _.isString(item) ? item.toUpperCase() : item
        }, sortingDirection === 1 ? 'asc' : 'desc')
    }

    setTextFilter(textFilter) {
        this.setState({
            textFilter,
            textFilterRegexp: RegExp(escapeStringRegexp(textFilter), 'i')
        })
    }

    setActiveFilter(activeFilter) {
        this.setState({activeFilter})
    }

    setBudgetFilter(budgetFilter) {
        this.setState({budgetFilter})
    }

    getFilteredUsers() {
        const {users} = this.state
        return users
            .filter(user => this.userMatchesTextFilter(user))
            .filter(user => this.userMatchesStatusFilter(user))
    }

    userMatchesTextFilter(user) {
        const {textFilter, textFilterRegexp} = this.state
        const searchProperties = ['name', 'username', 'email', 'organization']
        return textFilter
            ? _.find(searchProperties, searchProperty =>
                textFilterRegexp.test(user[searchProperty])
            )
            : true
    }

    userMatchesStatusFilter(user) {
        const {statusFilter} = this.state
        return statusFilter
            ? statusFilter === 'OVERBUDGET'
                ? this.isUserOverBudget(user)
                : user.status === statusFilter
            : true
    }

    isUserOverBudget({report: {budget, current}}) {
        return current.instanceSpending > budget.instanceSpending
            || current.storageSpending > budget.storageSpending
            || current.storageQuota > budget.storageQuota
    }

    onKeyDown({key}) {
        const keyMap = {
            Escape: () => {
                this.setTextFilter('')
            }
        }
        const keyAction = keyMap[key]
        keyAction && keyAction()
        this.search.current.focus()
    }

    editUser(user) {
        this.setState({
            userDetails: {
                username: user.username,
                name: user.name,
                email: user.email,
                organization: user.organization,
                monthlyBudgetInstanceSpending: user.report.budget.instanceSpending,
                monthlyBudgetStorageSpending: user.report.budget.storageSpending,
                monthlyBudgetStorageQuota: user.report.budget.storageQuota
            }
        })
    }

    inviteUser() {
        this.setState({
            userDetails: {
                newUser: true,
                monthlyBudgetInstanceSpending: 1,
                monthlyBudgetStorageSpending: 1,
                monthlyBudgetStorageQuota: 20
            }
        })
    }

    cancelUser() {
        this.setState({
            userDetails: null
        })
    }

    updateUser(userDetails) {

        const update$ = userDetails =>
            updateUserDetails$(userDetails).pipe(
                zip(updateUserBudget$(userDetails)),
                map(([userDetails, userBudget]) => ({
                    ...userDetails,
                    report: {
                        budget: userBudget
                    }
                }))
            )

        const updateUserDetails$ = ({newUser, username, name, email, organization}) =>
            newUser
                ? api.user.inviteUser$({username, name, email, organization})
                : api.user.updateUser$({username, name, email, organization})

        const updateUserBudget$ = ({
            username,
            monthlyBudgetInstanceSpending: instanceSpending,
            monthlyBudgetStorageSpending: storageSpending,
            monthlyBudgetStorageQuota: storageQuota
        }) => api.user.updateUserBudget$({
            username,
            instanceSpending,
            storageSpending,
            storageQuota
        })

        const updateLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    const index = users.findIndex(user => user.username === userDetails.username)
                    index === -1
                        ? users.push(userDetails)
                        : users[index] = userDetails
                }
                return {
                    users: this.getSortedUsers(users)
                }
            })

        const removeFromLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    _.remove(users, user => user.username === userDetails.username)
                }
                return {users}
            })

        this.cancelUser()

        updateLocalState({
            username: userDetails.username,
            name: userDetails.name,
            email: userDetails.email,
            organization: userDetails.organization,
            report: {
                budget: {
                    instanceSpending: userDetails.monthlyBudgetInstanceSpending,
                    storageSpending: userDetails.monthlyBudgetStorageSpending,
                    storageQuota: userDetails.monthlyBudgetStorageQuota
                }
            }
        })

        this.props.stream('UPDATE_USER',
            update$(userDetails),
            userDetails => {
                updateLocalState(userDetails)
                Notifications.success({message: msg('user.userDetails.update.success')})
            },
            error => {
                removeFromLocalState(userDetails)
                Notifications.error({message: msg('user.userDetails.update.error'), error})
            }
        )
    }

    renderSortingHandle(column) {
        return this.state.sortingOrder === column
            ? this.state.sortingDirection === 1
                ? <Icon name={'sort-down'}/>
                : <Icon name={'sort-up'}/>
            : <Icon name={'sort'}/>
    }

    renderColumnHeader(column, label, classNames = []) {
        const {sortingOrder} = this.state
        return (

                <Button
                    chromeless
                    shape='none'
                    additionalClassName='itemType'
                    onClick={() => this.setSorting(column)}>

                        {label}

                        {this.renderSortingHandle(column)}

                </Button>

        )
    }

    renderHeader() {
        return (

                    {this.renderInfo()}

                {this.renderColumnHeader('name', msg('user.userDetails.form.name.label'), [styles.name])}
                {this.renderColumnHeader('status', msg('user.userDetails.form.status.label'), [styles.status])}
                {this.renderColumnHeader('updateTime', msg('user.userDetails.form.updateTime.label'), [styles.updateTime])}
                {this.renderColumnHeader('report.budget.instanceSpending', msg('user.report.resources.quota'), [styles.instanceBudgetQuota, styles.number])}
                {this.renderColumnHeader('report.current.instanceSpending', msg('user.report.resources.used'), [styles.instanceBudgetUsed, styles.number])}
                {this.renderColumnHeader('report.budget.storageSpending', msg('user.report.resources.quota'), [styles.storageBudgetQuota, styles.number])}
                {this.renderColumnHeader('report.current.storageSpending', msg('user.report.resources.used'), [styles.storageBudgetUsed, styles.number])}
                {this.renderColumnHeader('report.budget.storageQuota', msg('user.report.resources.quota'), [styles.storageQuota, styles.number])}
                {this.renderColumnHeader('report.current.storageQuota', msg('user.report.resources.used'), [styles.storageUsed, styles.number])}

        )
    }

    renderTextFilter() {
        const {textFilter} = this.state
        return (
            <Shape
                look='transparent'
                size='large'
                shape='pill'
                icon='search'>
                <input
                    className={styles.search}
                    type='search'
                    ref={this.search}
                    value={textFilter}
                    placeholder={msg('users.filter.search.placeholder')}
                    onChange={e => this.setTextFilter(e.target.value)}/>
            </Shape>
        )
    }

    renderStatusFilter() {
        const {statusFilter} = this.state
        const options = [{
            label: msg('users.filter.status.ignore.label'),
            value: null
        }, {
            label: msg('users.filter.status.pending.label'),
            value: 'PENDING'
        }, {
            label: msg('users.filter.status.active.label'),
            value: 'ACTIVE'
        }, {
            label: msg('users.filter.budget.over.label'),
            value: 'OVERBUDGET'
        }]
        return (
            <Buttons
                chromeless
                type='horizontal-tight'
                options={options}
                selected={statusFilter}
                onChange={statusFilter => this.setState({statusFilter})}
            />
        )
    }

    renderInviteUser() {
        return (
            <Button
                additionalClassName={styles.inviteUser}
                look='add'
                size='xx-large'
                shape='circle'
                icon='plus'
                tooltip={msg('users.invite.label')}
                tooltipPlacement='left'
                onClick={() => this.inviteUser()}/>
        )
    }

    renderInfo() {
        const {textFilter, statusFilter, users} = this.state
        const isFiltered = textFilter || statusFilter
        const results = (count, total) =>
            msg(isFiltered ? 'users.countWithFilter' : 'users.countNoFilter', {count, total})
        return (
            <PageInfo>
                {({count}) =>

                        {results(count, users.length)}

                }
            </PageInfo>
        )
    }

    renderUsers() {
        const {textFilter} = this.state
        return (
            <PageData itemKey={user => user.username || user.id}>
                {user =>
                    <User
                        user={user}
                        highlight={textFilter}
                        onClick={() => this.editUser(user)}/>
                }
            </PageData>
        )
    }

    renderUserDetails() {
        const {userDetails} = this.state
        return userDetails ? (
            <UserDetails
                userDetails={userDetails}
                onCancel={() => this.cancelUser()}
                onSave={userDetails => this.updateUser(userDetails)}/>
        ) : null
    }

    render() {
        return (
            <React.Fragment>

 this.onKeyDown(e)}>
                    <Pageable items={this.getFilteredUsers()}>
                        <SectionLayout>
                            <TopBar label={msg('home.sections.users')}/>
                            <Content horizontalPadding verticalPadding menuPadding>
                                <ScrollableContainer>
                                    <Unscrollable>
                                        <FieldSet layout='horizontal' spacing='compact'>
                                            {this.renderTextFilter()}
                                            {this.renderStatusFilter()}
                                        </FieldSet>
                                    </Unscrollable>
                                    <Scrollable direction='x'>
                                        <ScrollableContainer className={styles.content}>
                                            <Unscrollable>
                                                {this.renderHeader()}
                                            </Unscrollable>
                                            <Scrollable direction='y' className={styles.users}>
                                                {this.renderUsers()}
                                            </Scrollable>
                                        </ScrollableContainer>
                                    </Scrollable>
                                </ScrollableContainer>
                                {this.renderInviteUser()}
                            </Content>
                            <BottomBar className={styles.bottomBar}>
                                <PageControls/>
                            </BottomBar>
                        </SectionLayout>
                    </Pageable>

                {this.renderUserDetails()}
            </React.Fragment>
        )
    }
}

export default compose(
    Users,
    connect()
)

class User extends React.Component {
    render() {
        const {
            user: {
                name,
                status,
                updateTime,
                report: {
                    budget = {},
                    current = {}
                } = {}
            },
            highlight,
            onClick
        } = this.props
        return (

 status ? onClick() : null}>

<Highlight search={highlight} matchClass={styles.highlight}>{name}</Highlight>

{status ? msg(`user.userDetails.form.status.${status}`) : <Icon name='spinner'/>}

{moment(updateTime).fromNow()}

{format.dollars(budget.instanceSpending)}

 budget.instanceSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.instanceSpending)}

{format.dollars(budget.storageSpending)}

 budget.storageSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.storageSpending)}

{format.fileSize(budget.storageQuota, {scale: 'G'})}

 budget.storageQuota ? styles.overBudget : null].join(' ')}>
                    {format.fileSize(current.storageQuota, {scale: 'G'})}

        )
    }
}
<commit_msg>Cleanup.<commit_after>import {BottomBar, Content, SectionLayout, TopBar} from 'widget/sectionLayout'
import {Button} from 'widget/button'
import {Buttons} from 'widget/buttons'
import {FieldSet} from 'widget/form'
import {PageControls, PageData, PageInfo, Pageable} from 'widget/pageable'
import {Scrollable, ScrollableContainer, Unscrollable} from 'widget/scrollable'
import {Shape} from 'widget/shape'
import {compose} from 'compose'
import {connect} from 'store'
import {map, share, zip} from 'rxjs/operators'
import {msg} from 'translate'
import Highlight from 'react-highlighter'
import Icon from 'widget/icon'
import Label from 'widget/label'
import Notifications from 'widget/notifications'
import React from 'react'
import UserDetails from './user'
import _ from 'lodash'
import api from 'api'
import escapeStringRegexp from 'escape-string-regexp'
import format from 'format'
import lookStyles from 'style/look.module.css'
import moment from 'moment'
import styles from './users.module.css'

const getUserList$ = () => api.user.getUserList$().pipe(
    share()
)

const getBudgetReport$ = () => api.user.getBudgetReport$().pipe(
    zip(getUserList$()),
    map(([budgetReport]) => budgetReport)
)

class Users extends React.Component {
    state = {
        users: [],
        sortingOrder: 'updateTime',
        sortingDirection: -1,
        textFilter: '',
        statusFilter: null,
        userDetails: null
    }

    search = React.createRef()

    componentDidMount() {
        const setUserList = userList =>
            this.setState({
                users: this.getSortedUsers(userList)
            })

        const mergeBudgetReport = budgetReport =>
            this.setState(({users}) => ({
                users: _.map(users, user => ({
                    ...user,
                    report: budgetReport[user.username || {}]
                }))
            }))

        this.props.stream('LOAD_USER_LIST',
            getUserList$(),
            userList => setUserList(userList)
        )
        this.props.stream('LOAD_BUDGET_REPORT',
            getBudgetReport$(),
            budgetReport => mergeBudgetReport(budgetReport)
        )
    }

    setSorting(sortingOrder) {
        this.setState(prevState => {
            const sortingDirection = sortingOrder === prevState.sortingOrder ? -prevState.sortingDirection : 1
            return {
                ...prevState,
                sortingOrder,
                sortingDirection,
                users: this.getSortedUsers(prevState.users, sortingOrder, sortingDirection)
            }
        })
    }

    getSortedUsers(users, sortingOrder = this.state.sortingOrder, sortingDirection = this.state.sortingDirection) {
        return _.orderBy(users, user => {
            const item = _.get(user, sortingOrder)
            return _.isString(item) ? item.toUpperCase() : item
        }, sortingDirection === 1 ? 'asc' : 'desc')
    }

    setTextFilter(textFilter) {
        this.setState({
            textFilter,
            textFilterRegexp: RegExp(escapeStringRegexp(textFilter), 'i')
        })
    }

    setActiveFilter(activeFilter) {
        this.setState({activeFilter})
    }

    getFilteredUsers() {
        const {users} = this.state
        return users
            .filter(user => this.userMatchesTextFilter(user))
            .filter(user => this.userMatchesStatusFilter(user))
    }

    userMatchesTextFilter(user) {
        const {textFilter, textFilterRegexp} = this.state
        const searchProperties = ['name', 'username', 'email', 'organization']
        return textFilter
            ? _.find(searchProperties, searchProperty =>
                textFilterRegexp.test(user[searchProperty])
            )
            : true
    }

    userMatchesStatusFilter(user) {
        const {statusFilter} = this.state
        return statusFilter
            ? statusFilter === 'OVERBUDGET'
                ? this.isUserOverBudget(user)
                : user.status === statusFilter
            : true
    }

    isUserOverBudget({report: {budget, current}}) {
        return current.instanceSpending > budget.instanceSpending
            || current.storageSpending > budget.storageSpending
            || current.storageQuota > budget.storageQuota
    }

    onKeyDown({key}) {
        const keyMap = {
            Escape: () => {
                this.setTextFilter('')
            }
        }
        const keyAction = keyMap[key]
        keyAction && keyAction()
        this.search.current.focus()
    }

    editUser(user) {
        this.setState({
            userDetails: {
                username: user.username,
                name: user.name,
                email: user.email,
                organization: user.organization,
                monthlyBudgetInstanceSpending: user.report.budget.instanceSpending,
                monthlyBudgetStorageSpending: user.report.budget.storageSpending,
                monthlyBudgetStorageQuota: user.report.budget.storageQuota
            }
        })
    }

    inviteUser() {
        this.setState({
            userDetails: {
                newUser: true,
                monthlyBudgetInstanceSpending: 1,
                monthlyBudgetStorageSpending: 1,
                monthlyBudgetStorageQuota: 20
            }
        })
    }

    cancelUser() {
        this.setState({
            userDetails: null
        })
    }

    updateUser(userDetails) {

        const update$ = userDetails =>
            updateUserDetails$(userDetails).pipe(
                zip(updateUserBudget$(userDetails)),
                map(([userDetails, userBudget]) => ({
                    ...userDetails,
                    report: {
                        budget: userBudget
                    }
                }))
            )

        const updateUserDetails$ = ({newUser, username, name, email, organization}) =>
            newUser
                ? api.user.inviteUser$({username, name, email, organization})
                : api.user.updateUser$({username, name, email, organization})

        const updateUserBudget$ = ({
            username,
            monthlyBudgetInstanceSpending: instanceSpending,
            monthlyBudgetStorageSpending: storageSpending,
            monthlyBudgetStorageQuota: storageQuota
        }) => api.user.updateUserBudget$({
            username,
            instanceSpending,
            storageSpending,
            storageQuota
        })

        const updateLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    const index = users.findIndex(user => user.username === userDetails.username)
                    index === -1
                        ? users.push(userDetails)
                        : users[index] = userDetails
                }
                return {
                    users: this.getSortedUsers(users)
                }
            })

        const removeFromLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    _.remove(users, user => user.username === userDetails.username)
                }
                return {users}
            })

        this.cancelUser()

        updateLocalState({
            username: userDetails.username,
            name: userDetails.name,
            email: userDetails.email,
            organization: userDetails.organization,
            report: {
                budget: {
                    instanceSpending: userDetails.monthlyBudgetInstanceSpending,
                    storageSpending: userDetails.monthlyBudgetStorageSpending,
                    storageQuota: userDetails.monthlyBudgetStorageQuota
                }
            }
        })

        this.props.stream('UPDATE_USER',
            update$(userDetails),
            userDetails => {
                updateLocalState(userDetails)
                Notifications.success({message: msg('user.userDetails.update.success')})
            },
            error => {
                removeFromLocalState(userDetails)
                Notifications.error({message: msg('user.userDetails.update.error'), error})
            }
        )
    }

    renderSortingHandle(column) {
        return this.state.sortingOrder === column
            ? this.state.sortingDirection === 1
                ? <Icon name={'sort-down'}/>
                : <Icon name={'sort-up'}/>
            : <Icon name={'sort'}/>
    }

    renderColumnHeader(column, label, classNames = []) {
        const {sortingOrder} = this.state
        return (

                <Button
                    chromeless
                    shape='none'
                    additionalClassName='itemType'
                    onClick={() => this.setSorting(column)}>

                        {label}

                        {this.renderSortingHandle(column)}

                </Button>

        )
    }

    renderHeader() {
        return (

                    {this.renderInfo()}

                {this.renderColumnHeader('name', msg('user.userDetails.form.name.label'), [styles.name])}
                {this.renderColumnHeader('status', msg('user.userDetails.form.status.label'), [styles.status])}
                {this.renderColumnHeader('updateTime', msg('user.userDetails.form.updateTime.label'), [styles.updateTime])}
                {this.renderColumnHeader('report.budget.instanceSpending', msg('user.report.resources.quota'), [styles.instanceBudgetQuota, styles.number])}
                {this.renderColumnHeader('report.current.instanceSpending', msg('user.report.resources.used'), [styles.instanceBudgetUsed, styles.number])}
                {this.renderColumnHeader('report.budget.storageSpending', msg('user.report.resources.quota'), [styles.storageBudgetQuota, styles.number])}
                {this.renderColumnHeader('report.current.storageSpending', msg('user.report.resources.used'), [styles.storageBudgetUsed, styles.number])}
                {this.renderColumnHeader('report.budget.storageQuota', msg('user.report.resources.quota'), [styles.storageQuota, styles.number])}
                {this.renderColumnHeader('report.current.storageQuota', msg('user.report.resources.used'), [styles.storageUsed, styles.number])}

        )
    }

    renderTextFilter() {
        const {textFilter} = this.state
        return (
            <Shape
                look='transparent'
                size='large'
                shape='pill'
                icon='search'>
                <input
                    className={styles.search}
                    type='search'
                    ref={this.search}
                    value={textFilter}
                    placeholder={msg('users.filter.search.placeholder')}
                    onChange={e => this.setTextFilter(e.target.value)}/>
            </Shape>
        )
    }

    renderStatusFilter() {
        const {statusFilter} = this.state
        const options = [{
            label: msg('users.filter.status.ignore.label'),
            value: null
        }, {
            label: msg('users.filter.status.pending.label'),
            value: 'PENDING'
        }, {
            label: msg('users.filter.status.active.label'),
            value: 'ACTIVE'
        }, {
            label: msg('users.filter.budget.over.label'),
            value: 'OVERBUDGET'
        }]
        return (
            <Buttons
                chromeless
                type='horizontal-tight'
                options={options}
                selected={statusFilter}
                onChange={statusFilter => this.setState({statusFilter})}
            />
        )
    }

    renderInviteUser() {
        return (
            <Button
                additionalClassName={styles.inviteUser}
                look='add'
                size='xx-large'
                shape='circle'
                icon='plus'
                tooltip={msg('users.invite.label')}
                tooltipPlacement='left'
                onClick={() => this.inviteUser()}/>
        )
    }

    renderInfo() {
        const {textFilter, statusFilter, users} = this.state
        const isFiltered = textFilter || statusFilter
        const results = (count, total) =>
            msg(isFiltered ? 'users.countWithFilter' : 'users.countNoFilter', {count, total})
        return (
            <PageInfo>
                {({count}) =>

                        {results(count, users.length)}

                }
            </PageInfo>
        )
    }

    renderUsers() {
        const {textFilter} = this.state
        return (
            <PageData itemKey={user => user.username || user.id}>
                {user =>
                    <User
                        user={user}
                        highlight={textFilter}
                        onClick={() => this.editUser(user)}/>
                }
            </PageData>
        )
    }

    renderUserDetails() {
        const {userDetails} = this.state
        return userDetails ? (
            <UserDetails
                userDetails={userDetails}
                onCancel={() => this.cancelUser()}
                onSave={userDetails => this.updateUser(userDetails)}/>
        ) : null
    }

    render() {
        return (
            <React.Fragment>

 this.onKeyDown(e)}>
                    <Pageable items={this.getFilteredUsers()}>
                        <SectionLayout>
                            <TopBar label={msg('home.sections.users')}/>
                            <Content horizontalPadding verticalPadding menuPadding>
                                <ScrollableContainer>
                                    <Unscrollable>
                                        <FieldSet layout='horizontal' spacing='compact'>
                                            {this.renderTextFilter()}
                                            {this.renderStatusFilter()}
                                        </FieldSet>
                                    </Unscrollable>
                                    <Scrollable direction='x'>
                                        <ScrollableContainer className={styles.content}>
                                            <Unscrollable>
                                                {this.renderHeader()}
                                            </Unscrollable>
                                            <Scrollable direction='y' className={styles.users}>
                                                {this.renderUsers()}
                                            </Scrollable>
                                        </ScrollableContainer>
                                    </Scrollable>
                                </ScrollableContainer>
                                {this.renderInviteUser()}
                            </Content>
                            <BottomBar className={styles.bottomBar}>
                                <PageControls/>
                            </BottomBar>
                        </SectionLayout>
                    </Pageable>

                {this.renderUserDetails()}
            </React.Fragment>
        )
    }
}

export default compose(
    Users,
    connect()
)

class User extends React.Component {
    render() {
        const {
            user: {
                name,
                status,
                updateTime,
                report: {
                    budget = {},
                    current = {}
                } = {}
            },
            highlight,
            onClick
        } = this.props
        return (

 status ? onClick() : null}>

<Highlight search={highlight} matchClass={styles.highlight}>{name}</Highlight>

{status ? msg(`user.userDetails.form.status.${status}`) : <Icon name='spinner'/>}

{moment(updateTime).fromNow()}

{format.dollars(budget.instanceSpending)}

 budget.instanceSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.instanceSpending)}

{format.dollars(budget.storageSpending)}

 budget.storageSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.storageSpending)}

{format.fileSize(budget.storageQuota, {scale: 'G'})}

 budget.storageQuota ? styles.overBudget : null].join(' ')}>
                    {format.fileSize(current.storageQuota, {scale: 'G'})}

        )
    }
}
<|endoftext|>


<commit_before>const cluster = require('cluster');

const EventEmitter = require('events').EventEmitter;

const util           = require('util');
const bunyan         = require('bunyan');
const _              = require('lodash');
const net            = require('net');
const Deque          = require("double-ended-queue");

const RequestDecoder = require('./messages/decoders').RequestDecoder;
const randomstring   = require('randomstring');

const ResponseWriter = require('./lib/pipeline/response_writer');
const through2       = require('through2');
const Response       = require('./messages').Response;
const AWS            = require('aws-sdk');
const enableDestroy  = require('server-destroy');

const defaults = {
  port:     9485,
  hostname: '0.0.0.0',
  logLevel: 'info',
  socketTimeout: 2000,
  metrics: {
    gauge:     _.noop,
    increment: _.noop,
    histogram: _.noop,
    flush:     _.noop
  },
  logger: bunyan.createLogger({
    name:        'baas',
    level:       'error',
    serializers: bunyan.stdSerializers
  })
};

function fork_worker() {
  const worker = cluster.fork();

  worker._pendingRequests = new Map();

  worker.on('message', function (response) {
    const callback = worker._pendingRequests.get(response.request_id);
    worker._pendingRequests.delete(response.request_id);
    worker.emit('drain');
    return callback(null, response);
  });

  worker.sendRequest = function (message, callback) {
    worker._pendingRequests.set(message.id, callback);
    worker.send(message);
  };

  return worker;
}

/*
 * Creates an instance of BaaSServer.
 *
 * Options:
 *
 *  - `port` the port to listen to. Defaults to 9231.
 *  - `hostname` the hostname to bind to. Defaults to INADDR_ANY
 *  - `logLevel` the verbosity of the logs. Defaults to 'info'.
 *
 */
function BaaSServer (options) {
  EventEmitter.call(this);

  this._config = _.extend({}, defaults, options);
  this._logger = this._config.logger;
  this._server = net.createServer(this._handler.bind(this));
  enableDestroy(this._server);
  this._metrics = this._config.metrics;
  this._server.on('error', (err) => {
    this.emit('error', err);
  });

  cluster.setupMaster({
    exec: __dirname + '/worker.js'
  });

  var workers_number;

  if (options.workers) {
    workers_number = options.workers;
  } else if (!isNaN(process.env.WORKERS)) {
    workers_number = parseInt(process.env.WORKERS, 10);
  } else {
    workers_number = Math.max(require('os').cpus().length - 1, 1);
  }

  this._queue = new Deque();
  this._workers = _.range(workers_number).map(fork_worker);

  this._workers.forEach(worker => {
    worker.on('drain', () => {
      const pending = this._queue.shift();
      if (!pending) {
        this._workers.push(worker);
      } else {
        worker.sendRequest(pending.request, pending.done(worker.id, true));
      }
    });
  });

  if (process.env.REPORT_QUEUE_LENGTH) {
    this.cloudWatch = new AWS.CloudWatch();
    setInterval(this._reportQueueLength.bind(this), 1000);
  }
}

util.inherits(BaaSServer, EventEmitter);

BaaSServer.prototype._reportQueueLength = function () {
  this._metrics.gauge('requests.queued', this._queue.length);

  const dimensions = [];

  if (process.env.STACK_NAME) {
    dimensions.push({
      Name: 'StackName',
      Value: process.env.STACK_NAME
    });
  }

  this.cloudWatch.putMetricData({
    MetricData: [
      {
        MetricName: 'QueuedRequests',
        Unit:       'Count',
        Timestamp:  new Date(),
        Value:      this._queue.length,
        Dimensions: dimensions
      }
    ],
    Namespace: 'Auth0/BAAS'
  }, (err) => {
    if (err) {
      this._logger.error({ err }, err.message);
    }
  });
};

BaaSServer.prototype._handler = function (socket) {
  this._metrics.increment('connection.incoming');

  const sockets_details = _.pick(socket, ['remoteAddress', 'remotePort']);

  sockets_details.connection = socket._connection_id = randomstring.generate(5);

  const log = this._logger;

  socket.on('error', (err) => {
    this._metrics.increment('connection.error');
    log.info(_.extend(sockets_details, {
      err: {
        code:    err.code,
        message: err.message
      }
    }), 'connection error');
  }).on('close', () => {
    this._metrics.increment('connection.closed');
    log.debug(sockets_details, 'connection closed');
  });

  if (this._config.socketTimeout) {
    socket.setTimeout(this._config.socketTimeout);
    socket.once('timeout', () => {
      log.info(sockets_details, 'idle connection closed');
      socket.end();
    });
  }

  log.debug(sockets_details, 'connection accepted');

  const decoder = RequestDecoder();

  decoder.on('error',  () => {
    log.info(sockets_details, 'unknown message format');
    return socket.end();
  });

  const responseStream = ResponseWriter();

  responseStream.pipe(socket);

  socket.pipe(decoder)
    .pipe(through2.obj((request, encoding, callback) => {
      const operation = request.operation === 0 ? 'compare' : 'hash';
      const start = new Date();

      const done = (worker_id, enqueued) => {
        return (err, response) => {
          log.info({
            request:    request.id,
            connection: socket._connection_id,
            took:       new Date() - start,
            worker:     worker_id,
            operation:  operation,
            enqueued:   enqueued,
            log_type:   'response'
          }, `${operation} completed`);

          this._metrics.histogram(`requests.processed.${operation}.time`, (new Date() - start));
          this._metrics.increment(`requests.processed.${operation}`);

          responseStream.write(new Response(response));
        };
      };

      this._metrics.increment(`requests.incoming`);

      log.info({
        request:    request.id,
        connection: socket._connection_id,
        operation:  operation,
        log_type:   'request',
        queued:     this._workers.length === 0
      }, `incoming ${operation}`);

      if (!this._workers.length){
        // no available workers, queue and wait
        this._queue.push({request, done});
        return callback();
      }

      // all workers are the same, pop is faster than shift: http://stackoverflow.com/questions/6501160/why-is-pop-faster-than-shift
      const worker = this._workers.pop();

      worker.sendRequest(request, done(worker.id, false));

      callback();
    }));
};

BaaSServer.prototype.start = function (done) {
  const log = this._logger;

  this._server.listen(this._config.port, this._config.hostname, (err) => {
    if (err) {
      log.error(err, 'error starting server');
      this.emit('error', err);
      if (done) { done(err); }
      return;
    }

    const address = this._server.address();

    log.info(address, 'server started');

    this.emit('started', address);

    if (done) { return done(null, address); }
  });

  return this;
};

BaaSServer.prototype.stop = function (done) {
  const log = this._logger;
  const address = this._server.address();

  const timeout = setTimeout(() => {
    this._server.destroy();
  }, 500);

  this._server.close(() => {
    clearTimeout(timeout);
    log.debug(address, 'server closed');
    this.emit('close');
    if (done) { done(); }
  });
};

module.exports = BaaSServer;
<commit_msg>change the queued metric used in the ASG<commit_after>const cluster = require('cluster');

const EventEmitter = require('events').EventEmitter;

const util           = require('util');
const bunyan         = require('bunyan');
const _              = require('lodash');
const net            = require('net');
const Deque          = require("double-ended-queue");

const RequestDecoder = require('./messages/decoders').RequestDecoder;
const randomstring   = require('randomstring');

const ResponseWriter = require('./lib/pipeline/response_writer');
const through2       = require('through2');
const Response       = require('./messages').Response;
const AWS            = require('aws-sdk');
const enableDestroy  = require('server-destroy');

const defaults = {
  port:     9485,
  hostname: '0.0.0.0',
  logLevel: 'info',
  socketTimeout: 2000,
  metrics: {
    gauge:     _.noop,
    increment: _.noop,
    histogram: _.noop,
    flush:     _.noop
  },
  logger: bunyan.createLogger({
    name:        'baas',
    level:       'error',
    serializers: bunyan.stdSerializers
  })
};

function fork_worker() {
  const worker = cluster.fork();

  worker._pendingRequests = new Map();

  worker.on('message', function (response) {
    const callback = worker._pendingRequests.get(response.request_id);
    worker._pendingRequests.delete(response.request_id);
    worker.emit('drain');
    return callback(null, response);
  });

  worker.sendRequest = function (message, callback) {
    worker._pendingRequests.set(message.id, callback);
    worker.send(message);
  };

  return worker;
}

/*
 * Creates an instance of BaaSServer.
 *
 * Options:
 *
 *  - `port` the port to listen to. Defaults to 9231.
 *  - `hostname` the hostname to bind to. Defaults to INADDR_ANY
 *  - `logLevel` the verbosity of the logs. Defaults to 'info'.
 *
 */
function BaaSServer (options) {
  EventEmitter.call(this);

  this._config = _.extend({}, defaults, options);
  this._logger = this._config.logger;
  this._server = net.createServer(this._handler.bind(this));
  enableDestroy(this._server);
  this._metrics = this._config.metrics;
  this._server.on('error', (err) => {
    this.emit('error', err);
  });

  cluster.setupMaster({
    exec: __dirname + '/worker.js'
  });

  var workers_number;

  if (options.workers) {
    workers_number = options.workers;
  } else if (!isNaN(process.env.WORKERS)) {
    workers_number = parseInt(process.env.WORKERS, 10);
  } else {
    workers_number = Math.max(require('os').cpus().length - 1, 1);
  }

  this._queue = new Deque();
  this._workers = _.range(workers_number).map(fork_worker);

  this._workers.forEach(worker => {
    worker.on('drain', () => {
      const pending = this._queue.shift();
      if (!pending) {
        this._workers.push(worker);
      } else {
        worker.sendRequest(pending.request, pending.done(worker.id, true));
      }
    });
  });

  if (process.env.REPORT_QUEUE_LENGTH) {
    this.cloudWatch = new AWS.CloudWatch();
    this._intervalQueued = 0;
    setInterval(this._reportQueueLength.bind(this), 1000);
  }
}

util.inherits(BaaSServer, EventEmitter);

BaaSServer.prototype._reportQueueLength = function () {
  this._metrics.gauge('requests.queued', this._queue.length);

  const dimensions = [];

  if (process.env.STACK_NAME) {
    dimensions.push({
      Name: 'StackName',
      Value: process.env.STACK_NAME
    });
  }

  const intervalQueued = this._intervalQueued;
  this._intervalQueued = 0;

  this.cloudWatch.putMetricData({
    MetricData: [
      {
        MetricName: 'QueuedRequests',
        Unit:       'Count',
        Timestamp:  new Date(),
        Value:      intervalQueued,
        Dimensions: dimensions
      }
    ],
    Namespace: 'Auth0/BAAS'
  }, (err) => {
    if (err) {
      this._logger.error({ err }, err.message);
    }
  });
};

BaaSServer.prototype._handler = function (socket) {
  this._metrics.increment('connection.incoming');

  const sockets_details = _.pick(socket, ['remoteAddress', 'remotePort']);

  sockets_details.connection = socket._connection_id = randomstring.generate(5);

  const log = this._logger;

  socket.on('error', (err) => {
    this._metrics.increment('connection.error');
    log.info(_.extend(sockets_details, {
      err: {
        code:    err.code,
        message: err.message
      }
    }), 'connection error');
  }).on('close', () => {
    this._metrics.increment('connection.closed');
    log.debug(sockets_details, 'connection closed');
  });

  if (this._config.socketTimeout) {
    socket.setTimeout(this._config.socketTimeout);
    socket.once('timeout', () => {
      log.info(sockets_details, 'idle connection closed');
      socket.end();
    });
  }

  log.debug(sockets_details, 'connection accepted');

  const decoder = RequestDecoder();

  decoder.on('error',  () => {
    log.info(sockets_details, 'unknown message format');
    return socket.end();
  });

  const responseStream = ResponseWriter();

  responseStream.pipe(socket);

  socket.pipe(decoder)
    .pipe(through2.obj((request, encoding, callback) => {
      const operation = request.operation === 0 ? 'compare' : 'hash';
      const start = new Date();

      const done = (worker_id, enqueued) => {
        return (err, response) => {
          log.info({
            request:    request.id,
            connection: socket._connection_id,
            took:       new Date() - start,
            worker:     worker_id,
            operation:  operation,
            enqueued:   enqueued,
            log_type:   'response'
          }, `${operation} completed`);

          this._metrics.histogram(`requests.processed.${operation}.time`, (new Date() - start));
          this._metrics.increment(`requests.processed.${operation}`);

          responseStream.write(new Response(response));
        };
      };

      this._metrics.increment(`requests.incoming`);

      log.info({
        request:    request.id,
        connection: socket._connection_id,
        operation:  operation,
        log_type:   'request',
        queued:     this._workers.length === 0
      }, `incoming ${operation}`);

      if (!this._workers.length){
        this._intervalQueued++;
        // no available workers, queue and wait
        this._queue.push({request, done});
        return callback();
      }

      // all workers are the same, pop is faster than shift: http://stackoverflow.com/questions/6501160/why-is-pop-faster-than-shift
      const worker = this._workers.pop();

      worker.sendRequest(request, done(worker.id, false));

      callback();
    }));
};

BaaSServer.prototype.start = function (done) {
  const log = this._logger;

  this._server.listen(this._config.port, this._config.hostname, (err) => {
    if (err) {
      log.error(err, 'error starting server');
      this.emit('error', err);
      if (done) { done(err); }
      return;
    }

    const address = this._server.address();

    log.info(address, 'server started');

    this.emit('started', address);

    if (done) { return done(null, address); }
  });

  return this;
};

BaaSServer.prototype.stop = function (done) {
  const log = this._logger;
  const address = this._server.address();

  const timeout = setTimeout(() => {
    this._server.destroy();
  }, 500);

  this._server.close(() => {
    clearTimeout(timeout);
    log.debug(address, 'server closed');
    this.emit('close');
    if (done) { done(); }
  });
};

module.exports = BaaSServer;
<|endoftext|>


<commit_before>'use strict';

var aws = require('aws-sdk');
var express = require('express');
var config = require('./config.' + process.env.NODE_ENV);
var app = express();
var fs = require('fs')
var morgan = require('morgan');
var bodyParser = require('body-parser');
var winston = require('winston');
var Datastore = require('nedb');
var regService = require('./reg.service.js');

// init SMTP
aws.config.loadFromPath('config.aws.json');
var ses = new aws.SES({
  apiVersion: '2010-12-01'
});

// init database
var dbReg = new Datastore({
  filename: __dirname + '/reg.db',
  autoload: true
});

// event log file init
var eventLogStream = fs.createWriteStream(__dirname + '/event.log', {
  flags: 'a'
});

// app log
winston.add(winston.transports.File, {
  filename: __dirname + '/reg.log',
  handleExceptions: true
});

// middleware config
app.use(bodyParser.json());
// public web
app.use(express.static('public'));
// event log init
app.use(morgan('combined', {
  stream: eventLogStream
}));

// create price
app.route('/api/price')
  .post(function(req, res) {
    winston.info('price in', req.body);
    var reg = regService.transform(req.body);
    var price = regService.getPrice(reg, config.deadlineDiscount);
    winston.info('price out', price);
    res.json({
      'price': price
    });
  });

// registration
app.route('/api/registrations')
  .post(function(req, res) {
    winston.info('registration in', req.body);
    // transform registration data
    var reg = regService.transform(req.body);
    try {
      // validate registration data
      regService.validate(reg);
      // validate unique registration
      dbReg.find({
          'email': reg.email
        },
        function(err, docs) {
          if (docs.length === 0) {
            // registration is unique

            // calculating price
            reg.price = regService.getPrice(reg, config.deadlineDiscount);

            dbReg.insert(reg, function(err, newDoc) {
              if (err) {
                res.json({
                  'errorCode': 'e.reg.insert'
                });
              } else {
                // registration code is _id
                winston.info('registration out', newDoc);
                // email admin and user
                ses.sendEmail({
                  Source: config.email.from,
                  Destination: {
                    ToAddresses: [reg.email],
                    BccAddresses: [config.email.bcc]
                  },
                  Message: {
                    Subject: {
                      Data: config.email.subject
                    },
                    Body: {
                      Text: {
                        Data: regService.toText(newDoc)
                      }
                    }
                  }
                }, function(err, data) {
                  if (err) {
                    winston.error('SMTP: reg. send: ' + err.message);
                  } else {
                    winston.info('Email sent to from reg.: ' + reg.email + ", " + reg._id);
                  }
                });
                res.json(newDoc);
              }
            });
          } else {
            res.json({
              'errorCode': 'v.reg.not.unique'
            });
          }
        });
    } catch (e) {
      winston.error(e.message);
      res.json({
        'errorCode': e.message
      });
    }

  })
  .get(function(req, res) {
    // get all registration data
    winston.info('registration download', req.query.key);
    if (req.query.key === config.adminKey) {
      dbReg.find({}, function(err, docs) {
        ses.sendEmail({
          Source: config.email.from,
          Destination: {
            ToAddresses: [config.email.bcc]
          },
          Message: {
            Subject: {
              Data: 'Minden regisztráció'
            },
            Body: {
              Text: {
                Data: JSON.stringify(docs)
              }
            }
          }
        }, function(err, data) {
          if (err) {
            winston.error('SMTP: all reg. data send: ' + err.message);
          } else {
            winston.info('Email sent to from all reg. data!');
          }
        });
      });
    } else {
      res.sendStatus(404);
    }
  });

app.listen(3000, function() {
  winston.info('Listen on port 3000 NODE_ENV: ' + process.env.NODE_ENV);
});
<commit_msg>change SMTP config path<commit_after>'use strict';

var aws = require('aws-sdk');
var express = require('express');
var config = require('./config.' + process.env.NODE_ENV);
var app = express();
var fs = require('fs')
var morgan = require('morgan');
var bodyParser = require('body-parser');
var winston = require('winston');
var Datastore = require('nedb');
var regService = require('./reg.service.js');

// init SMTP
aws.config.loadFromPath('./config.aws.json');
var ses = new aws.SES({
  apiVersion: '2010-12-01'
});

// init database
var dbReg = new Datastore({
  filename: __dirname + '/reg.db',
  autoload: true
});

// event log file init
var eventLogStream = fs.createWriteStream(__dirname + '/event.log', {
  flags: 'a'
});

// app log
winston.add(winston.transports.File, {
  filename: __dirname + '/reg.log',
  handleExceptions: true
});

// middleware config
app.use(bodyParser.json());
// public web
app.use(express.static('public'));
// event log init
app.use(morgan('combined', {
  stream: eventLogStream
}));

// create price
app.route('/api/price')
  .post(function(req, res) {
    winston.info('price in', req.body);
    var reg = regService.transform(req.body);
    var price = regService.getPrice(reg, config.deadlineDiscount);
    winston.info('price out', price);
    res.json({
      'price': price
    });
  });

// registration
app.route('/api/registrations')
  .post(function(req, res) {
    winston.info('registration in', req.body);
    // transform registration data
    var reg = regService.transform(req.body);
    try {
      // validate registration data
      regService.validate(reg);
      // validate unique registration
      dbReg.find({
          'email': reg.email
        },
        function(err, docs) {
          if (docs.length === 0) {
            // registration is unique

            // calculating price
            reg.price = regService.getPrice(reg, config.deadlineDiscount);

            dbReg.insert(reg, function(err, newDoc) {
              if (err) {
                res.json({
                  'errorCode': 'e.reg.insert'
                });
              } else {
                // registration code is _id
                winston.info('registration out', newDoc);
                // email admin and user
                ses.sendEmail({
                  Source: config.email.from,
                  Destination: {
                    ToAddresses: [reg.email],
                    BccAddresses: [config.email.bcc]
                  },
                  Message: {
                    Subject: {
                      Data: config.email.subject
                    },
                    Body: {
                      Text: {
                        Data: regService.toText(newDoc)
                      }
                    }
                  }
                }, function(err, data) {
                  if (err) {
                    winston.error('SMTP: reg. send: ' + err.message);
                  } else {
                    winston.info('Email sent to from reg.: ' + reg.email + ", " + reg._id);
                  }
                });
                res.json(newDoc);
              }
            });
          } else {
            res.json({
              'errorCode': 'v.reg.not.unique'
            });
          }
        });
    } catch (e) {
      winston.error(e.message);
      res.json({
        'errorCode': e.message
      });
    }

  })
  .get(function(req, res) {
    // get all registration data
    winston.info('registration download', req.query.key);
    if (req.query.key === config.adminKey) {
      dbReg.find({}, function(err, docs) {
        ses.sendEmail({
          Source: config.email.from,
          Destination: {
            ToAddresses: [config.email.bcc]
          },
          Message: {
            Subject: {
              Data: 'Minden regisztráció'
            },
            Body: {
              Text: {
                Data: JSON.stringify(docs)
              }
            }
          }
        }, function(err, data) {
          if (err) {
            winston.error('SMTP: all reg. data send: ' + err.message);
          } else {
            winston.info('Email sent to from all reg. data!');
          }
        });
      });
    } else {
      res.sendStatus(404);
    }
  });

app.listen(3000, function() {
  winston.info('Listen on port 3000 NODE_ENV: ' + process.env.NODE_ENV);
});
<|endoftext|>


<commit_before><commit_msg>Correct validation text for minLength<commit_after><|endoftext|>


<commit_before>/*
 * xhr.js - XMLHttpRequest helper class
 * (c) 2008-2010 Jo-Philipp Wich
 */

XHR = function()
{
	this.reinit = function()
	{
		if (window.XMLHttpRequest) {
			this._xmlHttp = new XMLHttpRequest();
		}
		else if (window.ActiveXObject) {
			this._xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
		}
		else {
			alert("xhr.js: XMLHttpRequest is not supported by this browser!");
		}
	}

	this.busy = function() {
		if (!this._xmlHttp)
			return false;

		switch (this._xmlHttp.readyState)
		{
			case 1:
			case 2:
			case 3:
				return true;

			default:
				return false;
		}
	}

	this.abort = function() {
		if (this.busy())
			this._xmlHttp.abort();
	}

	this.get = function(url,data,callback)
	{
		this.reinit();

		var xhr  = this._xmlHttp;
		var code = this._encode(data);

		url = location.protocol + '//' + location.host + url;

		if (code)
			if (url.substr(url.length-1,1) == '&')
				url += code;
			else
				url += '?' + code;

		xhr.open('GET', url, true);

		xhr.onreadystatechange = function()
		{
			if (xhr.readyState == 4) {
				var json = null;
				if (xhr.getResponseHeader("Content-Type") == "application/json") {
					try {
						json = eval('(' + xhr.responseText + ')');
					}
					catch(e) {
						json = null;
					}
				}

				callback(xhr, json);
			}
		}

		xhr.send(null);
	}

	this.post = function(url,data,callback)
	{
		this.reinit();

		var xhr  = this._xmlHttp;
		var code = this._encode(data);

		xhr.onreadystatechange = function()
		{
			if (xhr.readyState == 4)
				callback(xhr);
		}

		xhr.open('POST', url, true);
		xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
		xhr.setRequestHeader('Content-length', code.length);
		xhr.setRequestHeader('Connection', 'close');
		xhr.send(code);
	}

	this.cancel = function()
	{
		this._xmlHttp.onreadystatechange = function(){};
		this._xmlHttp.abort();
	}

	this.send_form = function(form,callback,extra_values)
	{
		var code = '';

		for (var i = 0; i < form.elements.length; i++)
		{
			var e = form.elements[i];

			if (e.options)
			{
				code += (code ? '&' : '') +
					form.elements[i].name + '=' + encodeURIComponent(
						e.options[e.selectedIndex].value
					);
			}
			else if (e.length)
			{
				for (var j = 0; j < e.length; j++)
					if (e[j].name) {
						code += (code ? '&' : '') +
							e[j].name + '=' + encodeURIComponent(e[j].value);
					}
			}
			else
			{
				code += (code ? '&' : '') +
					e.name + '=' + encodeURIComponent(e.value);
			}
		}

		if (typeof extra_values == 'object')
			for (var key in extra_values)
				code += (code ? '&' : '') +
					key + '=' + encodeURIComponent(extra_values[key]);

		return(
			(form.method == 'get')
				? this.get(form.getAttribute('action'), code, callback)
				: this.post(form.getAttribute('action'), code, callback)
		);
	}

	this._encode = function(obj)
	{
		obj = obj ? obj : { };
		obj['_'] = Math.random();

		if (typeof obj == 'object')
		{
			var code = '';
			var self = this;

			for (var k in obj)
				code += (code ? '&' : '') +
					k + '=' + encodeURIComponent(obj[k]);

			return code;
		}

		return obj;
	}
}

XHR.get = function(url, data, callback)
{
	(new XHR()).get(url, data, callback);
}

XHR.poll = function(interval, url, data, callback)
{
	if (isNaN(interval) || interval < 1)
		interval = 5;

	if (!XHR._q)
	{
		XHR._t = 0;
		XHR._q = [ ];
		XHR._r = function() {
			for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i])
			{
				if (!(XHR._t % e.interval) && !e.xhr.busy())
					e.xhr.get(e.url, e.data, e.callback);
			}

			XHR._t++;
		};
	}

	XHR._q.push({
		interval: interval,
		callback: callback,
		url:      url,
		data:     data,
		xhr:      new XHR()
	});

	XHR.run();
}

XHR.halt = function()
{
	if (XHR._i)
	{
		/* show & set poll indicator */
		try {
			document.getElementById('xhr_poll_status').style.display = '';
			document.getElementById('xhr_poll_status_on').style.display = 'none';
			document.getElementById('xhr_poll_status_off').style.display = '';
		} catch(e) { }

		window.clearInterval(XHR._i);
		XHR._i = null;
	}
}

XHR.run = function()
{
	if (XHR._r && !XHR._i)
	{
		/* show & set poll indicator */
		try {
			document.getElementById('xhr_poll_status').style.display = '';
			document.getElementById('xhr_poll_status_on').style.display = '';
			document.getElementById('xhr_poll_status_off').style.display = 'none';
		} catch(e) { }

		/* kick first round manually to prevent one second lag when setting up
		 * the poll interval */
		XHR._r();
		XHR._i = window.setInterval(XHR._r, 1000);
	}
}

XHR.running = function()
{
	return !!(XHR._r && XHR._i);
}
<commit_msg>remove request headers that are set automatically by browser<commit_after>/*
 * xhr.js - XMLHttpRequest helper class
 * (c) 2008-2010 Jo-Philipp Wich
 */

XHR = function()
{
	this.reinit = function()
	{
		if (window.XMLHttpRequest) {
			this._xmlHttp = new XMLHttpRequest();
		}
		else if (window.ActiveXObject) {
			this._xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
		}
		else {
			alert("xhr.js: XMLHttpRequest is not supported by this browser!");
		}
	}

	this.busy = function() {
		if (!this._xmlHttp)
			return false;

		switch (this._xmlHttp.readyState)
		{
			case 1:
			case 2:
			case 3:
				return true;

			default:
				return false;
		}
	}

	this.abort = function() {
		if (this.busy())
			this._xmlHttp.abort();
	}

	this.get = function(url,data,callback)
	{
		this.reinit();

		var xhr  = this._xmlHttp;
		var code = this._encode(data);

		url = location.protocol + '//' + location.host + url;

		if (code)
			if (url.substr(url.length-1,1) == '&')
				url += code;
			else
				url += '?' + code;

		xhr.open('GET', url, true);

		xhr.onreadystatechange = function()
		{
			if (xhr.readyState == 4) {
				var json = null;
				if (xhr.getResponseHeader("Content-Type") == "application/json") {
					try {
						json = eval('(' + xhr.responseText + ')');
					}
					catch(e) {
						json = null;
					}
				}

				callback(xhr, json);
			}
		}

		xhr.send(null);
	}

	this.post = function(url,data,callback)
	{
		this.reinit();

		var xhr  = this._xmlHttp;
		var code = this._encode(data);

		xhr.onreadystatechange = function()
		{
			if (xhr.readyState == 4)
				callback(xhr);
		}

		xhr.open('POST', url, true);
		xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
		xhr.send(code);
	}

	this.cancel = function()
	{
		this._xmlHttp.onreadystatechange = function(){};
		this._xmlHttp.abort();
	}

	this.send_form = function(form,callback,extra_values)
	{
		var code = '';

		for (var i = 0; i < form.elements.length; i++)
		{
			var e = form.elements[i];

			if (e.options)
			{
				code += (code ? '&' : '') +
					form.elements[i].name + '=' + encodeURIComponent(
						e.options[e.selectedIndex].value
					);
			}
			else if (e.length)
			{
				for (var j = 0; j < e.length; j++)
					if (e[j].name) {
						code += (code ? '&' : '') +
							e[j].name + '=' + encodeURIComponent(e[j].value);
					}
			}
			else
			{
				code += (code ? '&' : '') +
					e.name + '=' + encodeURIComponent(e.value);
			}
		}

		if (typeof extra_values == 'object')
			for (var key in extra_values)
				code += (code ? '&' : '') +
					key + '=' + encodeURIComponent(extra_values[key]);

		return(
			(form.method == 'get')
				? this.get(form.getAttribute('action'), code, callback)
				: this.post(form.getAttribute('action'), code, callback)
		);
	}

	this._encode = function(obj)
	{
		obj = obj ? obj : { };
		obj['_'] = Math.random();

		if (typeof obj == 'object')
		{
			var code = '';
			var self = this;

			for (var k in obj)
				code += (code ? '&' : '') +
					k + '=' + encodeURIComponent(obj[k]);

			return code;
		}

		return obj;
	}
}

XHR.get = function(url, data, callback)
{
	(new XHR()).get(url, data, callback);
}

XHR.poll = function(interval, url, data, callback)
{
	if (isNaN(interval) || interval < 1)
		interval = 5;

	if (!XHR._q)
	{
		XHR._t = 0;
		XHR._q = [ ];
		XHR._r = function() {
			for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i])
			{
				if (!(XHR._t % e.interval) && !e.xhr.busy())
					e.xhr.get(e.url, e.data, e.callback);
			}

			XHR._t++;
		};
	}

	XHR._q.push({
		interval: interval,
		callback: callback,
		url:      url,
		data:     data,
		xhr:      new XHR()
	});

	XHR.run();
}

XHR.halt = function()
{
	if (XHR._i)
	{
		/* show & set poll indicator */
		try {
			document.getElementById('xhr_poll_status').style.display = '';
			document.getElementById('xhr_poll_status_on').style.display = 'none';
			document.getElementById('xhr_poll_status_off').style.display = '';
		} catch(e) { }

		window.clearInterval(XHR._i);
		XHR._i = null;
	}
}

XHR.run = function()
{
	if (XHR._r && !XHR._i)
	{
		/* show & set poll indicator */
		try {
			document.getElementById('xhr_poll_status').style.display = '';
			document.getElementById('xhr_poll_status_on').style.display = '';
			document.getElementById('xhr_poll_status_off').style.display = 'none';
		} catch(e) { }

		/* kick first round manually to prevent one second lag when setting up
		 * the poll interval */
		XHR._r();
		XHR._i = window.setInterval(XHR._r, 1000);
	}
}

XHR.running = function()
{
	return !!(XHR._r && XHR._i);
}
<|endoftext|>


<commit_before>/*
 * ------------------------------------------
 * 瀑布式列表模块实现文件
 * @version  1.0
 * @author   genify(caijf@corp.netease.com)
 * ------------------------------------------
 */
var f = function(){
    var _  = NEJ.P,
        _o = NEJ.O,
        _r = NEJ.R,
        _e = _('nej.e'),
        _v = _('nej.v'),
        _u = _('nej.u'),
        _i = _('nej.ui'),
        _p = _('nej.ut'),
        _proListModuleWF,
        _supListModuleWF;
    if (!!_p._$$ListModuleWF) return;
    /**
     * 瀑布式列表模块
     *
     * 结构举例：
     * [code type="html"]
     *

     *

     *
     *

     *
 load more button

     *

     *
     *
     *   <textarea name="jst" id="jst-list">
     *     {list beg..end as y}
     *       {var x=xlist[y]}
     *

               删除
     *
姓名：${x.name}

     *
联系方式：${x.mobile}

     *

     *     {/list}
     *   </textarea>
     * [/code]
     *
     * 脚本举例：
     * [code]
     *   // 统一定义名字空间简写
     *   var _  = NEJ.P,
     *       _j = _('nej.j'),
     *       _t = _('nej.ut'),
     *       _p = _('t.d'),
     *       _proCustomListCache;
     *   // 自定义列表缓存
     *   _p._$$CustomListCache = NEJ.C();
     *     _proCustomListCache = _p._$$CustomListCache
     *                             ._$extend(_t._$$AbstractListCache);
     *   // 实现数据载入逻辑
     *   _proCustomListCache.__doLoadList = function(_options){
     *       var _onload = _options.onload;
     *       // 补全请求数据，也可在模块层通过cache参数传入
     *       var _data = _options.data||{};
     *       NEJ.X(_data,{uid:'ww',sort:'xx',order:1});
     *       switch(_options.key){
     *              case 'user-list':
     *               // TODO load list from server
     *               _j._$request('/api/user/list',{
     *                   type:'json',
     *                   data:_u._$object2query(_data),
     *                   onload:function(_json){
     *                          // _json.code
     *                       // _json.result
     *                          _onload(_json.code==1?_json.result:null);
     *                   },
     *                   onerror:_onload._$bind(null);
     *               });
     *           break;
     *           // TODO other list load
     *       }
     *   };
     * [/code]
     *
     * 脚本举例：
     * [code]
     *   // 统一定义名字空间简写
     *   var _  = NEJ.P,
     *       _e = _('nej.e'),
     *       _t = _('nej.ut'),
     *       _d = _('t.d');
     *   // 构建列表模块，使用JST模版
     *   _t._$$ListModuleWF._$allocate({
     *       limit:5,
     *       parent:'list-box',
     *       more:'more-btn',
     *       item:'jst-list',
     *       cache:{
     *           key:'user-list',// 此key必须是唯一的，对应了item中的值,也是删除选项的data-id
     *           data:{uid:'ww',sort:'xx',order:1}, // <--- 列表加载时携带数据信息，此数据也可在cache层补全
     *           klass:_d._$$CustomListCache
     *       }
     *   });
     * [/code]
     *
     * @class   {nej.ut._$$ListModuleWF}
     * @extends {nej.ut._$$ListModule}
     * @param   {Object}              可选配置参数
     * @config  {String|Node}  more   添加更多列表项按钮节点
     * @config  {String|Node}  sbody  滚动条所在容器，支持onscroll事件
     * @config  {Number}       delta  触发自动加载更多时距离滚动容器底部的便宜量，单位px，默认30
     * @config  {Number}       count  指定加载多少次后出现分页器
     * @config  {Number}       number 初始加载次数，小于等于count数有效
     *
     */
    _p._$$ListModuleWF = NEJ.C();
      _proListModuleWF = _p._$$ListModuleWF._$extend(_p._$$ListModule);
      _supListModuleWF = _p._$$ListModuleWF._$supro;
    /**
     * 控件重置
     * @protected
     * @method {__reset}
     * @param  {Object} 配置参数
     * @return {Void}
     */
    _proListModuleWF.__reset = function(_options){
        this.__doResetMoreBtn(_options.more);
        this.__sbody = _e._$get(_options.sbody);
        this.__doInitDomEvent([[
            this.__sbody,'scroll',
            this.__onCheckScroll._$bind(this)
        ]]);
        var _delta = parseInt(_options.delta);
        if (isNaN(_delta)) _delta = 30;
        this.__delta = Math.max(0,_delta);
        var _count = parseInt(_options.count)||0;
        this.__count = Math.max(0,_count);
        var _number = parseInt(_options.number)||0;
        if (_number>1&&_number<=_count){
            this.__number = _number;
        }
        this.__supReset(_options);
    };
    /**
     * 控件销毁
     * @protected
     * @method {__destroy}
     * @return {Void}
     */
    _proListModuleWF.__destroy = function(){
        this.__supDestroy();
        delete this.__nmore;
        delete this.__sbody;
        delete this.__endskr;
        delete this.__nexting;
    };
    /**
     * 取当前偏移量的分页信息
     * @param  {Number} 偏移位置
     * @param  {Number} 长度
     * @return {Object} 分页信息，如：{index:1,total:4}
     */
    _proListModuleWF.__getPageInfo = function(_offset,_length){
        var _point = this.__first+(this.__count-1)*this.__limit,
            _limit = this.__count*this.__limit;
        return _supListModuleWF.__getPageInfo.call(
            this,_point,_offset,_limit,_length
        );
    };
    /**
     * 重置载入更多按钮
     * @param  {String|Node} 按钮节点
     * @return {Void}
     */
    _proListModuleWF.__doResetMoreBtn = function(_more){
        this.__nmore = _e._$get(_more);
        this.__doInitDomEvent([[
            this.__nmore,'click',
            this._$next._$bind(this)
        ]]);
    };
    /**
     * 检查滚动条
     * @return {Void}
     */
    _proListModuleWF.__doCheckScroll = function(_element){
        if (this.__endskr||!_element) return;
        if (!_element.scrollHeight)
            _element = _e._$getPageBox();
        var _offset = _e._$offset(this.__lbox),
            _delta = _offset.y+this.__lbox.offsetHeight-
                     _element.scrollTop-_element.clientHeight;
        if (_delta<=this.__delta){
            this._$next();
        }
    };
    /**
     * 检查滚动情况
     * @return {Void}
     */
    _proListModuleWF.__onCheckScroll = function(_event){
        if (this.__endskr) return;
        this.__doCheckScroll(
            _v._$getElement(_event)
        );
    };
    /**
     * 页码变化处理逻辑
     * @protected
     * @method {__doChangePage}
     * @param  {Object} 页码信息
     * @return {Void}
     */
    _proListModuleWF.__doChangePage = function(_event){
        _supListModuleWF.__doChangePage.apply(this,arguments);
        if (!_event.stopped){
            this.__doClearListBox();
            var _offset = 0;
            if (_event.index>1){
                _offset = this.__first+((
                    _event.index-1)*this.__count-1)*this.__limit;
            }
            this.__offset = _offset;
            this._$next();
        }
    };
    /**
     * 生成请求对象信息
     * @param  {Object} 预处理请求信息
     * @return {Object} 处理后请求信息
     */
    _proListModuleWF.__doGenRequestOpt = function(_options){
        if (!!this.__number){
            var _delta = _options.offset>0?this.__limit:this.__first,
                _limit = _delta+this.__limit*(this.__number-1);
            this.__offset = _options.offset+_limit;
            _options.data.limit = _limit;
            _options.limit = _limit;
            delete this.__number;
        }
        return _options;
    };
    /**
     * 数据列表载入完成回调
     * @param  {Object} 请求信息
     * @return {Void}
     */
    _proListModuleWF.__cbListLoad = function(_options){
        delete this.__nexting;
        _supListModuleWF.__cbListLoad.apply(this,arguments);
        // if not scroll check next
        var _element = this.__sbody;
        if (!_element) return;
        if (!_element.scrollHeight)
             _element = _e._$getPageBox();
        if (_element.scrollHeight<=_element.clientHeight){
            this.__doCheckScroll(this.__sbody);
        }
    };
    /**
     * 加载数据之前处理逻辑，显示数据加载中信息
     * @protected
     * @method {__doBeforeListLoad}
     * @return {Void}
     */
    _proListModuleWF.__doBeforeListLoad = function(){
        this.__doShowMessage('onbeforelistload','列表加载中...');
        _e._$setStyle(this.__nmore,'visibility','hidden');
        _e._$setStyle(this.__popt.parent,'visibility','hidden');
    };
    /**
     * 列表绘制之前处理逻辑
     * @protected
     * @method {__doBeforeListRender}
     * @return {Void}
     */
    _proListModuleWF.__doBeforeListRender = function(_list,_offset,_limit){
        var _length = _list.length,
            _ended = _list.loaded
                   ? _offset+_limit>=_length
                   : _offset+_limit>_length;
        this.__offset = Math.min(this.__offset,_length);
        _e._$setStyle(this.__nmore,'visibility',_ended?'hidden':'visible');
        if (this.__count>0){
            // check pager
            var _info = this.__getPageInfo(_offset,_list.length);
            if (this.__doSyncPager(_info.index,_info.total)) return !0;
            // check scroll end
            var _delta = this.__first-this.__limit,
                _number = this.__count*this.__limit;
            this.__endskr = (_offset+_limit-_delta)%_number==0;
            // sync more button and pager
            _e._$setStyle(
                this.__nmore,'display',
                this.__endskr||_ended?'none':''
            );
            _e._$style(this.__popt.parent,{
                visibility:_info.total>1?'visible':'hidden',
                display:this.__endskr||_ended?'':'none'
            });
        }
    };
    /**
     * 列表为空时处理逻辑
     * @protected
     * @method {__doShowEmpty}
     * @return {Void}
     */
    _proListModuleWF.__doShowEmpty = function(){
        this.__doShowMessage('onemptylist','没有列表数据！');
    };
    /**
     * 以jst模版方式绘制列表
     * @protected
     * @method {__doShowListByJST}
     * @return {Void}
     */
    _proListModuleWF.__doShowListByJST = function(_html,_pos){
        this.__lbox.insertAdjacentHTML(_pos||'beforeEnd',_html);
    };
    /**
     * 以item模版方式绘制列表
     * @protected
     * @method {__doShowListByItem}
     * @return {Void}
     */
    _proListModuleWF.__doShowListByItem = function(_items){
        this.__items = this.__items||[];
        if (_u._$isArray(_items)){
            _r.push.apply(this.__items,_items);
        }else{
            this.__items.push(_items);
        }
    };
    /**
     * 添加列表项回调
     * @protected
     * @method {__cbItemAdd}
     * @return {Void}
     */
    _proListModuleWF.__cbItemAdd = function(_event){
        _e._$removeByEC(this.__ntip);
        this.__doCheckResult(_event,'onafteradd');
        var _flag = _event.flag;
        if (_event.stopped||!_flag) return;
        // with pager
        if (this.__count>0){
            this.__doRefreshByPager();
            return;
        }
        // without pager
        this.__offset += 1;
        _flag==-1 ? this._$unshift(_event.data)
                  : this._$append(_event.data);
    };
    /**
     * 删除列表项回调，子类按需实现具体业务逻辑
     * @protected
     * @method {__cbItemDelete}
     * @return {Void}
     */
    _proListModuleWF.__cbItemDelete = function(_event){
        this.__doCheckResult(_event,'onafterdelete');
        if (_event.stopped) return;
        // with pager
        if (this.__count>0){
            this.__doRefreshByPager();
            return;
        }
        // without pager
        var _id = _event.data[this.__iopt.pkey];
        if (!!this.__items){
            var _item = _e._$getItemById(_id),
                _index = _u._$indexOf(this.__items,_item);
            if (_index>=0){
                this.__items.splice(_index,1);
                this.__offset -= 1;
            }
            if (!!_item) _item._$recycle();
        }else{
            var _node = _e._$get(this.
                        __getItemBodyId(_id));
            if (!!_node) this.__offset -= 1;
            _e._$remove(_node);
        }
        if (this.__offset<=0) this._$next();
    };
    /**
     * 更新列表项回调
     * @protected
     * @method {__cbItemUpdate}
     * @return {Void}
     */
    _proListModuleWF.__cbItemUpdate = function(_event){
        this.__doCheckResult(_event,'onafterupdate');
        if (_event.stopped) return;
        var _id = _event.data[this.__iopt.pkey];
        if (!!this.__items){
            var _item = _e._$getItemById(_id);
            if (!!_item) _item._$refresh(_event.data);
        }else{
            var _node = _e._$get(_id+''+
                        _e._$getHtmlTemplateSeed());
            if (!_node) return;
            var _list = this.__cache._$getListInCache(_event.key),
                _index = _u._$indexOf(_list,_event.data);
            if (_index<0) return;
            this.__iopt.list = _list;
            this.__iopt.beg  = _index;
            this.__iopt.end  = _index;
            var _html = _e._$getHtmlTemplate(
                        this.__ikey,this.__iopt);
            _node.insertAdjacentHTML('afterEnd',_html);
            _e._$remove(_node);
        }
    };
    /**
     * 批量添加回调
     * @param  {Object} 偏移量
     * @param  {Object} 数量
     * @return {Void}
     */
    _proListModuleWF.__cbAppendList = function(_offset,_limit){
        // TODO
    };
    /**
     * 载入更多列表
     * @method {_$next}
     * @return {Void}
     */
    _proListModuleWF._$next = function(){
        // lock loading
        if (!!this.__nexting)
            return;
        this.__nexting = !0;
        // update offset first for
        // offset adjust after list loaded
        var _offset = this.__offset;
        this.__offset += _offset==0?
            this.__first:this.__limit;
        this.__doChangeOffset(_offset);
    };
};
NEJ.define('{lib}util/list/module.waterfall.js',
          ['{lib}util/list/module.js'],f);
<commit_msg>support resize for list module<commit_after>/*
 * ------------------------------------------
 * 瀑布式列表模块实现文件
 * @version  1.0
 * @author   genify(caijf@corp.netease.com)
 * ------------------------------------------
 */
var f = function(){
    var _  = NEJ.P,
        _o = NEJ.O,
        _r = NEJ.R,
        _e = _('nej.e'),
        _v = _('nej.v'),
        _u = _('nej.u'),
        _i = _('nej.ui'),
        _p = _('nej.ut'),
        _proListModuleWF,
        _supListModuleWF;
    if (!!_p._$$ListModuleWF) return;
    /**
     * 瀑布式列表模块
     *
     * 结构举例：
     * [code type="html"]
     *

     *

     *
     *

     *
 load more button

     *

     *
     *
     *   <textarea name="jst" id="jst-list">
     *     {list beg..end as y}
     *       {var x=xlist[y]}
     *

               删除
     *
姓名：${x.name}

     *
联系方式：${x.mobile}

     *

     *     {/list}
     *   </textarea>
     * [/code]
     *
     * 脚本举例：
     * [code]
     *   // 统一定义名字空间简写
     *   var _  = NEJ.P,
     *       _j = _('nej.j'),
     *       _t = _('nej.ut'),
     *       _p = _('t.d'),
     *       _proCustomListCache;
     *   // 自定义列表缓存
     *   _p._$$CustomListCache = NEJ.C();
     *     _proCustomListCache = _p._$$CustomListCache
     *                             ._$extend(_t._$$AbstractListCache);
     *   // 实现数据载入逻辑
     *   _proCustomListCache.__doLoadList = function(_options){
     *       var _onload = _options.onload;
     *       // 补全请求数据，也可在模块层通过cache参数传入
     *       var _data = _options.data||{};
     *       NEJ.X(_data,{uid:'ww',sort:'xx',order:1});
     *       switch(_options.key){
     *              case 'user-list':
     *               // TODO load list from server
     *               _j._$request('/api/user/list',{
     *                   type:'json',
     *                   data:_u._$object2query(_data),
     *                   onload:function(_json){
     *                          // _json.code
     *                       // _json.result
     *                          _onload(_json.code==1?_json.result:null);
     *                   },
     *                   onerror:_onload._$bind(null);
     *               });
     *           break;
     *           // TODO other list load
     *       }
     *   };
     * [/code]
     *
     * 脚本举例：
     * [code]
     *   // 统一定义名字空间简写
     *   var _  = NEJ.P,
     *       _e = _('nej.e'),
     *       _t = _('nej.ut'),
     *       _d = _('t.d');
     *   // 构建列表模块，使用JST模版
     *   _t._$$ListModuleWF._$allocate({
     *       limit:5,
     *       parent:'list-box',
     *       more:'more-btn',
     *       item:'jst-list',
     *       cache:{
     *           key:'user-list',// 此key必须是唯一的，对应了item中的值,也是删除选项的data-id
     *           data:{uid:'ww',sort:'xx',order:1}, // <--- 列表加载时携带数据信息，此数据也可在cache层补全
     *           klass:_d._$$CustomListCache
     *       }
     *   });
     * [/code]
     *
     * @class   {nej.ut._$$ListModuleWF}
     * @extends {nej.ut._$$ListModule}
     * @param   {Object}              可选配置参数
     * @config  {String|Node}  more   添加更多列表项按钮节点
     * @config  {String|Node}  sbody  滚动条所在容器，支持onscroll事件
     * @config  {Number}       delta  触发自动加载更多时距离滚动容器底部的便宜量，单位px，默认30
     * @config  {Number}       count  指定加载多少次后出现分页器
     * @config  {Number}       number 初始加载次数，小于等于count数有效
     *
     */
    _p._$$ListModuleWF = NEJ.C();
      _proListModuleWF = _p._$$ListModuleWF._$extend(_p._$$ListModule);
      _supListModuleWF = _p._$$ListModuleWF._$supro;
    /**
     * 控件重置
     * @protected
     * @method {__reset}
     * @param  {Object} 配置参数
     * @return {Void}
     */
    _proListModuleWF.__reset = function(_options){
        this.__doResetMoreBtn(_options.more);
        this.__sbody = _e._$get(_options.sbody);
        this.__doInitDomEvent([[
            this.__sbody,'scroll',
            this.__onCheckScroll._$bind(this)
        ]]);
        var _delta = parseInt(_options.delta);
        if (isNaN(_delta)) _delta = 30;
        this.__delta = Math.max(0,_delta);
        var _count = parseInt(_options.count)||0;
        this.__count = Math.max(0,_count);
        var _number = parseInt(_options.number)||0;
        if (_number>1&&_number<=_count){
            this.__number = _number;
        }
        this.__supReset(_options);
    };
    /**
     * 控件销毁
     * @protected
     * @method {__destroy}
     * @return {Void}
     */
    _proListModuleWF.__destroy = function(){
        this.__supDestroy();
        delete this.__nmore;
        delete this.__sbody;
        delete this.__endskr;
        delete this.__nexting;
    };
    /**
     * 取当前偏移量的分页信息
     * @param  {Number} 偏移位置
     * @param  {Number} 长度
     * @return {Object} 分页信息，如：{index:1,total:4}
     */
    _proListModuleWF.__getPageInfo = function(_offset,_length){
        var _point = this.__first+(this.__count-1)*this.__limit,
            _limit = this.__count*this.__limit;
        return _supListModuleWF.__getPageInfo.call(
            this,_point,_offset,_limit,_length
        );
    };
    /**
     * 重置载入更多按钮
     * @param  {String|Node} 按钮节点
     * @return {Void}
     */
    _proListModuleWF.__doResetMoreBtn = function(_more){
        this.__nmore = _e._$get(_more);
        this.__doInitDomEvent([[
            this.__nmore,'click',
            this._$next._$bind(this)
        ]]);
    };
    /**
     * 检查滚动条
     * @return {Void}
     */
    _proListModuleWF.__doCheckScroll = function(_element){
        if (this.__endskr||!_element) return;
        if (!_element.scrollHeight)
            _element = _e._$getPageBox();
        var _offset = _e._$offset(this.__lbox),
            _delta = _offset.y+this.__lbox.offsetHeight-
                     _element.scrollTop-_element.clientHeight;
        if (_delta<=this.__delta){
            this._$next();
        }
    };
    /**
     * 检查滚动情况
     * @return {Void}
     */
    _proListModuleWF.__onCheckScroll = function(_event){
        if (this.__endskr) return;
        this.__doCheckScroll(
            _v._$getElement(_event)
        );
    };
    /**
     * 页码变化处理逻辑
     * @protected
     * @method {__doChangePage}
     * @param  {Object} 页码信息
     * @return {Void}
     */
    _proListModuleWF.__doChangePage = function(_event){
        _supListModuleWF.__doChangePage.apply(this,arguments);
        if (!_event.stopped){
            this.__doClearListBox();
            var _offset = 0;
            if (_event.index>1){
                _offset = this.__first+((
                    _event.index-1)*this.__count-1)*this.__limit;
            }
            this.__offset = _offset;
            this._$next();
        }
    };
    /**
     * 生成请求对象信息
     * @param  {Object} 预处理请求信息
     * @return {Object} 处理后请求信息
     */
    _proListModuleWF.__doGenRequestOpt = function(_options){
        if (!!this.__number){
            var _delta = _options.offset>0?this.__limit:this.__first,
                _limit = _delta+this.__limit*(this.__number-1);
            this.__offset = _options.offset+_limit;
            _options.data.limit = _limit;
            _options.limit = _limit;
            delete this.__number;
        }
        return _options;
    };
    /**
     * 数据列表载入完成回调
     * @param  {Object} 请求信息
     * @return {Void}
     */
    _proListModuleWF.__cbListLoad = function(_options){
        delete this.__nexting;
        _supListModuleWF.__cbListLoad.apply(this,arguments);
        this._$resize();
    };
    /**
     * 加载数据之前处理逻辑，显示数据加载中信息
     * @protected
     * @method {__doBeforeListLoad}
     * @return {Void}
     */
    _proListModuleWF.__doBeforeListLoad = function(){
        this.__doShowMessage('onbeforelistload','列表加载中...');
        _e._$setStyle(this.__nmore,'visibility','hidden');
        _e._$setStyle(this.__popt.parent,'visibility','hidden');
    };
    /**
     * 列表绘制之前处理逻辑
     * @protected
     * @method {__doBeforeListRender}
     * @return {Void}
     */
    _proListModuleWF.__doBeforeListRender = function(_list,_offset,_limit){
        var _length = _list.length,
            _ended = _list.loaded
                   ? _offset+_limit>=_length
                   : _offset+_limit>_length;
        this.__offset = Math.min(this.__offset,_length);
        _e._$setStyle(this.__nmore,'visibility',_ended?'hidden':'visible');
        if (this.__count>0){
            // check pager
            var _info = this.__getPageInfo(_offset,_list.length);
            if (this.__doSyncPager(_info.index,_info.total)) return !0;
            // check scroll end
            var _delta = this.__first-this.__limit,
                _number = this.__count*this.__limit;
            this.__endskr = (_offset+_limit-_delta)%_number==0||_ended;
            // sync more button and pager
            _e._$setStyle(
                this.__nmore,'display',
                this.__endskr?'none':''
            );
            _e._$style(this.__popt.parent,{
                visibility:_info.total>1?'visible':'hidden',
                display:this.__endskr?'':'none'
            });
        }
    };
    /**
     * 列表为空时处理逻辑
     * @protected
     * @method {__doShowEmpty}
     * @return {Void}
     */
    _proListModuleWF.__doShowEmpty = function(){
        this.__doShowMessage('onemptylist','没有列表数据！');
    };
    /**
     * 以jst模版方式绘制列表
     * @protected
     * @method {__doShowListByJST}
     * @return {Void}
     */
    _proListModuleWF.__doShowListByJST = function(_html,_pos){
        this.__lbox.insertAdjacentHTML(_pos||'beforeEnd',_html);
    };
    /**
     * 以item模版方式绘制列表
     * @protected
     * @method {__doShowListByItem}
     * @return {Void}
     */
    _proListModuleWF.__doShowListByItem = function(_items){
        this.__items = this.__items||[];
        if (_u._$isArray(_items)){
            _r.push.apply(this.__items,_items);
        }else{
            this.__items.push(_items);
        }
    };
    /**
     * 添加列表项回调
     * @protected
     * @method {__cbItemAdd}
     * @return {Void}
     */
    _proListModuleWF.__cbItemAdd = function(_event){
        _e._$removeByEC(this.__ntip);
        this.__doCheckResult(_event,'onafteradd');
        var _flag = _event.flag;
        if (_event.stopped||!_flag) return;
        // with pager
        if (this.__count>0){
            this.__doRefreshByPager();
            return;
        }
        // without pager
        this.__offset += 1;
        _flag==-1 ? this._$unshift(_event.data)
                  : this._$append(_event.data);
    };
    /**
     * 删除列表项回调，子类按需实现具体业务逻辑
     * @protected
     * @method {__cbItemDelete}
     * @return {Void}
     */
    _proListModuleWF.__cbItemDelete = function(_event){
        this.__doCheckResult(_event,'onafterdelete');
        if (_event.stopped) return;
        // with pager
        if (this.__count>0){
            this.__doRefreshByPager();
            return;
        }
        // without pager
        var _id = _event.data[this.__iopt.pkey];
        if (!!this.__items){
            var _item = _e._$getItemById(_id),
                _index = _u._$indexOf(this.__items,_item);
            if (_index>=0){
                this.__items.splice(_index,1);
                this.__offset -= 1;
            }
            if (!!_item) _item._$recycle();
        }else{
            var _node = _e._$get(this.
                        __getItemBodyId(_id));
            if (!!_node) this.__offset -= 1;
            _e._$remove(_node);
        }
        if (this.__offset<=0) this._$next();
    };
    /**
     * 更新列表项回调
     * @protected
     * @method {__cbItemUpdate}
     * @return {Void}
     */
    _proListModuleWF.__cbItemUpdate = function(_event){
        this.__doCheckResult(_event,'onafterupdate');
        if (_event.stopped) return;
        var _id = _event.data[this.__iopt.pkey];
        if (!!this.__items){
            var _item = _e._$getItemById(_id);
            if (!!_item) _item._$refresh(_event.data);
        }else{
            var _node = _e._$get(_id+''+
                        _e._$getHtmlTemplateSeed());
            if (!_node) return;
            var _list = this.__cache._$getListInCache(_event.key),
                _index = _u._$indexOf(_list,_event.data);
            if (_index<0) return;
            this.__iopt.list = _list;
            this.__iopt.beg  = _index;
            this.__iopt.end  = _index;
            var _html = _e._$getHtmlTemplate(
                        this.__ikey,this.__iopt);
            _node.insertAdjacentHTML('afterEnd',_html);
            _e._$remove(_node);
        }
    };
    /**
     * 批量添加回调
     * @param  {Object} 偏移量
     * @param  {Object} 数量
     * @return {Void}
     */
    _proListModuleWF.__cbAppendList = function(_offset,_limit){
        // TODO
    };
    /**
     * 重置大小触发滚动条修正
     * @return {Void}
     */
    _proListModuleWF._$resize = function(){
        // if not scroll check next
        var _element = this.__sbody;
        if (!_element||this.__endskr) return;
        if (!_element.scrollHeight)
             _element = _e._$getPageBox();
        if (_element.scrollHeight<=_element.clientHeight){
            this.__doCheckScroll(this.__sbody);
        }
    };
    /**
     * 载入更多列表
     * @method {_$next}
     * @return {Void}
     */
    _proListModuleWF._$next = function(){
        // lock loading
        if (!!this.__nexting)
            return;
        this.__nexting = !0;
        // update offset first for
        // offset adjust after list loaded
        var _offset = this.__offset;
        this.__offset += _offset==0?
            this.__first:this.__limit;
        this.__doChangeOffset(_offset);
    };
};
NEJ.define('{lib}util/list/module.waterfall.js',
          ['{lib}util/list/module.js'],f);
<|endoftext|>