<commit_before>/* @flow */

import { ZalgoPromise } from 'zalgo-promise/src';
import { values, uniqueID } from 'belter/src';
import { type OrderCreateRequest, type FundingEligibilityType,
    type OrderGetResponse, type OrderCaptureResponse, type OrderAuthorizeResponse } from '@paypal/sdk-client/src';
import { FUNDING, PLATFORM, INTENT, COMMIT, VAULT,
    ENV, COUNTRY, LANG, COUNTRY_LANGS, type LocaleType, CARD, COMPONENTS } from '@paypal/sdk-constants/src';
import { type CrossDomainWindowType } from 'cross-domain-utils/src';
import { LOGO_COLOR } from '@paypal/sdk-logos/src';
import { SUPPORTED_FUNDING_SOURCES } from '@paypal/funding-components/src';
import type { ComponentFunctionType } from 'jsx-pragmatic/src';

import { BUTTON_LABEL, BUTTON_COLOR, BUTTON_LAYOUT, BUTTON_SHAPE, BUTTON_SIZE } from '../../constants';
import { getFundingConfig, isFundingEligible } from '../../funding';

import { BUTTON_SIZE_STYLE } from './config';

export type CreateOrderData = {|

|} | {};

export type CreateOrderActions = {|
    order : {
        create : (OrderCreateRequest) => ZalgoPromise<string>
    }
|};

export type CreateOrder = (CreateOrderData, CreateOrderActions) => ZalgoPromise<string> | string;

export type OnApproveData = {|
    orderID : string,
    payerID : string,
    paymentID? : string
|};

export type CreateBillingAgreement = () => ZalgoPromise<string> | string;

export type CreateSubscriptionRequest = {||};
export type SubscriptionResponse = {||} | {};
export type CreateSubscriptionData = {|
|} | {};

export type CreateSubscriptionActions = {|
    subscription : {
        create : (CreateSubscriptionRequest) => ZalgoPromise<string>,
        revise : (CreateSubscriptionRequest) => ZalgoPromise<string>
    }
|};

export type CreateSubscription = (CreateSubscriptionData, CreateSubscriptionActions) => ZalgoPromise<string> | string;

export type OnApproveActions = {|
    redirect : (string, CrossDomainWindowType) => ZalgoPromise<void>,
    order : {
        capture : () => ZalgoPromise<OrderCaptureResponse>,
        get : () => ZalgoPromise<OrderGetResponse>,
        authorize : () => ZalgoPromise<OrderAuthorizeResponse>
    },
    subscription : {
        get : () => ZalgoPromise<SubscriptionResponse>,
        activate : () => ZalgoPromise<SubscriptionResponse>
    }
|};

export type OnApprove = (data : OnApproveData, actions : OnApproveActions) => ZalgoPromise<void> | void;

type OnShippingChangeAddress = {|
    city : string,
    state : string,
    country_code : string,
    postal_code : string
|};

type OnShippingChangeMethod = {|
    label : string,
    type : string,
    amount : {
        currency_code : string,
        value : string
    }
|};

export type OnShippingChangeData = {|
    orderID : string,
    payerID : string,
    paymentID? : string,
    shipping_address : OnShippingChangeAddress,
    selected_shipping_method : OnShippingChangeMethod
|};

export type OnShippingChangeActions = {|
    order : {
        patch : () => ZalgoPromise<OrderGetResponse>
    }
|};

export type OnShippingChange = (data : OnShippingChangeData, actions : OnShippingChangeActions) => ZalgoPromise<void> | void;

export type OnCancelData = {|
    orderID : string,
    paymentID? : string
|};

export type OnCancelActions = {|
    redirect : (string, CrossDomainWindowType) => ZalgoPromise<void>
|};

export type OnCancel = (OnCancelData, OnCancelActions) => ZalgoPromise<void> | void;

export type OnClickData = {|
    fundingSource : $Values<typeof FUNDING>,
    card? : $Values<typeof CARD>
|};

export type OnClickActions = {|
    resolve : () => ZalgoPromise<void>,
    reject : () => ZalgoPromise<void>
|};

export type OnClick = (OnClickData, OnClickActions) => void;

export type ButtonStyle = {|
    label : ?$Values<typeof BUTTON_LABEL>,
    color : $Values<typeof BUTTON_COLOR>,
    shape : $Values<typeof BUTTON_SHAPE>,
    tagline : boolean,
    layout : $Values<typeof BUTTON_LAYOUT>,
    period? : number,
    height? : number
|};

export type ButtonStyleInputs = {|
    label? : $PropertyType<ButtonStyle, 'label'> | void,
    color? : $PropertyType<ButtonStyle, 'color'> | void,
    shape? : $PropertyType<ButtonStyle, 'shape'> | void,
    tagline? : $PropertyType<ButtonStyle, 'tagline'> | void,
    layout? : $PropertyType<ButtonStyle, 'layout'> | void,
    period? : $PropertyType<ButtonStyle, 'period'> | void,
    height? : $PropertyType<ButtonStyle, 'height'> | void
|};

type PersonalizationComponentProps = {|
   logoColor : $Values<typeof LOGO_COLOR>,
   period : ?number
|};

export type Personalization = {|
    buttonText? : {|
        text : string,
        Component : ?ComponentFunctionType<PersonalizationComponentProps>,
        tracking : {|
            impression : string,
            click : string
        |}
    |},
    tagline? : {|
        text : string,
        Component : ?ComponentFunctionType<PersonalizationComponentProps>,
        tracking : {|
            impression : string,
            click : string
        |}
    |}
|};

export type RenderButtonProps = {|
    style : ButtonStyle,
    locale : LocaleType,
    commit : boolean,
    fundingSource : ?$Values<typeof FUNDING>,
    env : $Values<typeof ENV>,
    stage? : string,
    stageUrl? : string,
    platform : $Values<typeof PLATFORM>,
    fundingEligibility : FundingEligibilityType,
    remembered : $ReadOnlyArray<$Values<typeof FUNDING>>,
    clientID : string,
    sessionID : string,
    buttonSessionID : string,
    nonce : string,
    components : $ReadOnlyArray<$Values<typeof COMPONENTS>>,
    onShippingChange : ?OnShippingChange,
    personalization : ?Personalization,
    clientAccessToken : ?string,
    content : { [string] : string }
|};

export type PrerenderDetails = {|
    win : ?CrossDomainWindowType,
    fundingSource : $Values<typeof FUNDING>,
    card : ?$Values<typeof CARD>
|};

export type GetPrerenderDetails = () => PrerenderDetails | void;

export type ButtonProps = {|
    fundingSource : ?$Values<typeof FUNDING>,
    intent : $Values<typeof INTENT>,
    createOrder : CreateOrder,
    createBillingAgreement : CreateBillingAgreement,
    createSubscription : CreateSubscription,
    oncancel : OnCancel,
    onApprove : OnApprove,
    onClick : OnClick,
    getPrerenderDetails : GetPrerenderDetails,
    style : ButtonStyle,
    locale : LocaleType,
    commit : boolean,
    env : $Values<typeof ENV>,
    stage? : string,
    stageUrl? : string,
    platform : $Values<typeof PLATFORM>,
    fundingEligibility : FundingEligibilityType,
    remembered : $ReadOnlyArray<$Values<typeof FUNDING>>,
    remember : ($ReadOnlyArray<$Values<typeof FUNDING>>) => void,
    clientID : string,
    sessionID : string,
    buttonSessionID : string,
    onShippingChange : ?OnShippingChange,
    clientAccessToken : ?string,
    nonce : string
|};

export type ButtonPropsInputs = {|
    clientID : string,
    fundingSource? : $Values<typeof FUNDING>,
    style? : ButtonStyleInputs | void,
    locale? : $PropertyType<ButtonProps, 'locale'> | void,
    commit? : $PropertyType<ButtonProps, 'commit'> | void,
    env? : $PropertyType<ButtonProps, 'env'> | void,
    meta? : $PropertyType<ButtonProps, 'meta'> | void,
    stage? : $PropertyType<ButtonProps, 'stage'> | void,
    stageUrl? : $PropertyType<ButtonProps, 'stageUrl'> | void,
    platform? : $PropertyType<ButtonProps, 'platform'> | void,
    fundingEligibility? : $PropertyType<ButtonProps, 'fundingEligibility'> | void,
    remembered? : $PropertyType<ButtonProps, 'remembered'> | void,
    remember? : $PropertyType<ButtonProps, 'remember'> | void,
    sessionID? : $PropertyType<ButtonProps, 'sessionID'> | void,
    buttonSessionID? : $PropertyType<ButtonProps, 'buttonSessionID'> | void,
    nonce? : string,
    components : $ReadOnlyArray<$Values<typeof COMPONENTS>>,
    onShippingChange : ?Function,
    personalization? : Personalization,
    clientAccessToken? : string,
    csp? : {
        nonce? : string
    },
    content? : { [string] : string }
|};

export const DEFAULT_STYLE = {
    LAYOUT: BUTTON_LAYOUT.VERTICAL,
    COLOR:  BUTTON_COLOR.GOLD,
    SHAPE:  BUTTON_SHAPE.RECT
};

export const DEFAULT_PROPS = {
    LOCALE:   {
        country: COUNTRY.US,
        lang:    LANG.EN
    },
    COMMIT:   COMMIT.TRUE,
    VAULT:    VAULT.FALSE,
    INTENT:   INTENT.CAPTURE,
    ENV:      ENV.PRODUCTION,
    PLATFORM: PLATFORM.DESKTOP
};

// $FlowFixMe
export function normalizeButtonStyle(props : ?ButtonPropsInputs, style : ButtonStyleInputs) : ButtonStyle {

    if (!style) {
        throw new Error(`Expected props.style to be set`);
    }

    const { fundingSource = FUNDING.PAYPAL } = props;

    const FUNDING_CONFIG = getFundingConfig();
    const fundingConfig = FUNDING_CONFIG[fundingSource];

    if (!fundingConfig) {
        throw new Error(`Expected ${ fundingSource } to be eligible`);
    }

    const {
        label,
        layout = fundingSource ? BUTTON_LAYOUT.HORIZONTAL : fundingConfig.layouts[0],
        color = fundingConfig.colors[0],
        shape = fundingConfig.shapes[0],
        tagline = (layout === BUTTON_LAYOUT.HORIZONTAL && !fundingSource),
        height,
        period
    } = style;

    if (values(BUTTON_LAYOUT).indexOf(layout) === -1) {
        throw new Error(`Invalid layout: ${ layout }`);
    }

    if (label && values(BUTTON_LABEL).indexOf(label) === -1) {
        throw new Error(`Invalid label: ${ label }`);
    }

    if (color && fundingConfig.colors.indexOf(color) === -1) {
        throw new Error(`Unexpected style.color for ${ FUNDING.PAYPAL } button: ${ color }, expected ${ fundingConfig.colors.join(', ') }`);
    }

    if (shape && fundingConfig.shapes.indexOf(shape) === -1) {
        throw new Error(`Unexpected style.shape for ${ FUNDING.PAYPAL } button: ${ shape }, expected ${ fundingConfig.shapes.join(', ') }`);
    }

    if (height !== undefined) {
        if (typeof height !== 'number') {
            throw new TypeError(`Expected style.height to be a number, got: ${ height }`);
        }

        const [ minHeight, maxHeight ] = [ BUTTON_SIZE_STYLE[BUTTON_SIZE.SMALL].minHeight, BUTTON_SIZE_STYLE[BUTTON_SIZE.HUGE].maxHeight ];

        if (height < minHeight || height > maxHeight) {
            throw new Error(`Expected style.height to be between ${ minHeight }px and ${ maxHeight }px - got ${ height }px`);
        }
    }

    if (layout === BUTTON_LAYOUT.VERTICAL) {
        if (tagline) {
            throw new Error(`style.tagline is not allowed for ${ BUTTON_LAYOUT.VERTICAL } layout`);
        }
    }

    return { label, layout, color, shape, tagline, height, period };
}

const COUNTRIES = values(COUNTRY);
const FUNDING_SOURCES = values(FUNDING);
const ENVS = values(ENV);
const PLATFORMS = values(PLATFORM);

export function normalizeButtonProps(props : ?ButtonPropsInputs) : RenderButtonProps {

    if (!props) {
        throw new Error(`Expected props`);
    }

    let {
        clientID,
        fundingSource,
        // $FlowFixMe
        style = {},
        remembered = [],
        locale = DEFAULT_PROPS.LOCALE,
        env = DEFAULT_PROPS.ENV,
        platform = DEFAULT_PROPS.PLATFORM,
        commit = DEFAULT_PROPS.COMMIT,
        fundingEligibility,
        sessionID = uniqueID(),
        buttonSessionID = uniqueID(),
        csp = {},
        components = [ COMPONENTS.BUTTONS ],
        nonce = '',
        onShippingChange,
        personalization,
        clientAccessToken,
        content = {}
    } = props;

    const { country, lang } = locale;

    if (!country || COUNTRIES.indexOf(country) === -1) {
        throw new Error(`Expected valid country, got ${ country || 'undefined' }`);
    }

    if (!lang || COUNTRY_LANGS[country].indexOf(lang) === -1) {
        throw new Error(`Expected valid lang, got ${ lang || 'undefined' }`);
    }

    if (remembered.some(source => FUNDING_SOURCES.indexOf(source) === -1)) {
        throw new Error(`Expected valid funding sources, got ${ JSON.stringify(remembered) }`);
    }

    if (ENVS.indexOf(env) === -1) {
        throw new Error(`Expected valid env, got ${ env || 'undefined' }`);
    }

    if (!fundingEligibility) {
        throw new Error(`Expected fundingEligibility`);
    }

    if (PLATFORMS.indexOf(platform) === -1) {
        throw new Error(`Expected valid platform, got ${ platform || 'undefined' }`);
    }

    if (csp && csp.nonce) {
        nonce = csp.nonce;
    }

    if (fundingSource) {
        if (SUPPORTED_FUNDING_SOURCES.indexOf(fundingSource) === -1) {
            throw new Error(`Invalid funding source: ${ fundingSource }`);
        }

        if (!isFundingEligible(fundingSource, { platform, fundingEligibility, components, onShippingChange })) {
            throw new Error(`Funding Source not eligible: ${ fundingSource }`);
        }
    }

    style = normalizeButtonStyle(props, style);

    return { clientID, fundingSource, style, locale, remembered, env, fundingEligibility, platform, clientAccessToken,
        buttonSessionID, commit, sessionID, nonce, components, onShippingChange, personalization, content };
}
<commit_msg>Fix tests<commit_after>/* @flow */

import { ZalgoPromise } from 'zalgo-promise/src';
import { values, uniqueID } from 'belter/src';
import { type OrderCreateRequest, type FundingEligibilityType,
    type OrderGetResponse, type OrderCaptureResponse, type OrderAuthorizeResponse } from '@paypal/sdk-client/src';
import { FUNDING, PLATFORM, INTENT, COMMIT, VAULT,
    ENV, COUNTRY, LANG, COUNTRY_LANGS, type LocaleType, CARD, COMPONENTS } from '@paypal/sdk-constants/src';
import { type CrossDomainWindowType } from 'cross-domain-utils/src';
import { LOGO_COLOR } from '@paypal/sdk-logos/src';
import { SUPPORTED_FUNDING_SOURCES } from '@paypal/funding-components/src';
import type { ComponentFunctionType } from 'jsx-pragmatic/src';

import { BUTTON_LABEL, BUTTON_COLOR, BUTTON_LAYOUT, BUTTON_SHAPE, BUTTON_SIZE } from '../../constants';
import { getFundingConfig, isFundingEligible } from '../../funding';

import { BUTTON_SIZE_STYLE } from './config';

export type CreateOrderData = {|

|} | {};

export type CreateOrderActions = {|
    order : {
        create : (OrderCreateRequest) => ZalgoPromise<string>
    }
|};

export type CreateOrder = (CreateOrderData, CreateOrderActions) => ZalgoPromise<string> | string;

export type OnApproveData = {|
    orderID : string,
    payerID : string,
    paymentID? : string
|};

export type CreateBillingAgreement = () => ZalgoPromise<string> | string;

export type CreateSubscriptionRequest = {||};
export type SubscriptionResponse = {||} | {};
export type CreateSubscriptionData = {|
|} | {};

export type CreateSubscriptionActions = {|
    subscription : {
        create : (CreateSubscriptionRequest) => ZalgoPromise<string>,
        revise : (CreateSubscriptionRequest) => ZalgoPromise<string>
    }
|};

export type CreateSubscription = (CreateSubscriptionData, CreateSubscriptionActions) => ZalgoPromise<string> | string;

export type OnApproveActions = {|
    redirect : (string, CrossDomainWindowType) => ZalgoPromise<void>,
    order : {
        capture : () => ZalgoPromise<OrderCaptureResponse>,
        get : () => ZalgoPromise<OrderGetResponse>,
        authorize : () => ZalgoPromise<OrderAuthorizeResponse>
    },
    subscription : {
        get : () => ZalgoPromise<SubscriptionResponse>,
        activate : () => ZalgoPromise<SubscriptionResponse>
    }
|};

export type OnApprove = (data : OnApproveData, actions : OnApproveActions) => ZalgoPromise<void> | void;

type OnShippingChangeAddress = {|
    city : string,
    state : string,
    country_code : string,
    postal_code : string
|};

type OnShippingChangeMethod = {|
    label : string,
    type : string,
    amount : {
        currency_code : string,
        value : string
    }
|};

export type OnShippingChangeData = {|
    orderID : string,
    payerID : string,
    paymentID? : string,
    shipping_address : OnShippingChangeAddress,
    selected_shipping_method : OnShippingChangeMethod
|};

export type OnShippingChangeActions = {|
    order : {
        patch : () => ZalgoPromise<OrderGetResponse>
    }
|};

export type OnShippingChange = (data : OnShippingChangeData, actions : OnShippingChangeActions) => ZalgoPromise<void> | void;

export type OnCancelData = {|
    orderID : string,
    paymentID? : string
|};

export type OnCancelActions = {|
    redirect : (string, CrossDomainWindowType) => ZalgoPromise<void>
|};

export type OnCancel = (OnCancelData, OnCancelActions) => ZalgoPromise<void> | void;

export type OnClickData = {|
    fundingSource : $Values<typeof FUNDING>,
    card? : $Values<typeof CARD>
|};

export type OnClickActions = {|
    resolve : () => ZalgoPromise<void>,
    reject : () => ZalgoPromise<void>
|};

export type OnClick = (OnClickData, OnClickActions) => void;

export type ButtonStyle = {|
    label : ?$Values<typeof BUTTON_LABEL>,
    color : $Values<typeof BUTTON_COLOR>,
    shape : $Values<typeof BUTTON_SHAPE>,
    tagline : boolean,
    layout : $Values<typeof BUTTON_LAYOUT>,
    period? : number,
    height? : number
|};

export type ButtonStyleInputs = {|
    label? : $PropertyType<ButtonStyle, 'label'> | void,
    color? : $PropertyType<ButtonStyle, 'color'> | void,
    shape? : $PropertyType<ButtonStyle, 'shape'> | void,
    tagline? : $PropertyType<ButtonStyle, 'tagline'> | void,
    layout? : $PropertyType<ButtonStyle, 'layout'> | void,
    period? : $PropertyType<ButtonStyle, 'period'> | void,
    height? : $PropertyType<ButtonStyle, 'height'> | void
|};

type PersonalizationComponentProps = {|
   logoColor : $Values<typeof LOGO_COLOR>,
   period : ?number
|};

export type Personalization = {|
    buttonText? : {|
        text : string,
        Component : ?ComponentFunctionType<PersonalizationComponentProps>,
        tracking : {|
            impression : string,
            click : string
        |}
    |},
    tagline? : {|
        text : string,
        Component : ?ComponentFunctionType<PersonalizationComponentProps>,
        tracking : {|
            impression : string,
            click : string
        |}
    |}
|};

export type RenderButtonProps = {|
    style : ButtonStyle,
    locale : LocaleType,
    commit : boolean,
    fundingSource : ?$Values<typeof FUNDING>,
    env : $Values<typeof ENV>,
    stage? : string,
    stageUrl? : string,
    platform : $Values<typeof PLATFORM>,
    fundingEligibility : FundingEligibilityType,
    remembered : $ReadOnlyArray<$Values<typeof FUNDING>>,
    clientID : string,
    sessionID : string,
    buttonSessionID : string,
    nonce : string,
    components : $ReadOnlyArray<$Values<typeof COMPONENTS>>,
    onShippingChange : ?OnShippingChange,
    personalization : ?Personalization,
    clientAccessToken : ?string,
    content : { [string] : string }
|};

export type PrerenderDetails = {|
    win : ?CrossDomainWindowType,
    fundingSource : $Values<typeof FUNDING>,
    card : ?$Values<typeof CARD>
|};

export type GetPrerenderDetails = () => PrerenderDetails | void;

export type ButtonProps = {|
    fundingSource : ?$Values<typeof FUNDING>,
    intent : $Values<typeof INTENT>,
    createOrder : CreateOrder,
    createBillingAgreement : CreateBillingAgreement,
    createSubscription : CreateSubscription,
    oncancel : OnCancel,
    onApprove : OnApprove,
    onClick : OnClick,
    getPrerenderDetails : GetPrerenderDetails,
    style : ButtonStyle,
    locale : LocaleType,
    commit : boolean,
    env : $Values<typeof ENV>,
    stage? : string,
    stageUrl? : string,
    platform : $Values<typeof PLATFORM>,
    fundingEligibility : FundingEligibilityType,
    remembered : $ReadOnlyArray<$Values<typeof FUNDING>>,
    remember : ($ReadOnlyArray<$Values<typeof FUNDING>>) => void,
    clientID : string,
    sessionID : string,
    buttonSessionID : string,
    onShippingChange : ?OnShippingChange,
    clientAccessToken : ?string,
    nonce : string
|};

export type ButtonPropsInputs = {|
    clientID : string,
    fundingSource? : $Values<typeof FUNDING>,
    style? : ButtonStyleInputs | void,
    locale? : $PropertyType<ButtonProps, 'locale'> | void,
    commit? : $PropertyType<ButtonProps, 'commit'> | void,
    env? : $PropertyType<ButtonProps, 'env'> | void,
    meta? : $PropertyType<ButtonProps, 'meta'> | void,
    stage? : $PropertyType<ButtonProps, 'stage'> | void,
    stageUrl? : $PropertyType<ButtonProps, 'stageUrl'> | void,
    platform? : $PropertyType<ButtonProps, 'platform'> | void,
    fundingEligibility? : $PropertyType<ButtonProps, 'fundingEligibility'> | void,
    remembered? : $PropertyType<ButtonProps, 'remembered'> | void,
    remember? : $PropertyType<ButtonProps, 'remember'> | void,
    sessionID? : $PropertyType<ButtonProps, 'sessionID'> | void,
    buttonSessionID? : $PropertyType<ButtonProps, 'buttonSessionID'> | void,
    nonce? : string,
    components : $ReadOnlyArray<$Values<typeof COMPONENTS>>,
    onShippingChange : ?Function,
    personalization? : Personalization,
    clientAccessToken? : string,
    csp? : {
        nonce? : string
    },
    content? : { [string] : string }
|};

export const DEFAULT_STYLE = {
    LAYOUT: BUTTON_LAYOUT.VERTICAL,
    COLOR:  BUTTON_COLOR.GOLD,
    SHAPE:  BUTTON_SHAPE.RECT
};

export const DEFAULT_PROPS = {
    LOCALE:   {
        country: COUNTRY.US,
        lang:    LANG.EN
    },
    COMMIT:   COMMIT.TRUE,
    VAULT:    VAULT.FALSE,
    INTENT:   INTENT.CAPTURE,
    ENV:      ENV.PRODUCTION,
    PLATFORM: PLATFORM.DESKTOP
};

// $FlowFixMe
export function normalizeButtonStyle(props : ?ButtonPropsInputs, style : ButtonStyleInputs) : ButtonStyle {

    if (!style) {
        throw new Error(`Expected props.style to be set`);
    }

    const { fundingSource } = props;

    const FUNDING_CONFIG = getFundingConfig();
    const fundingConfig = FUNDING_CONFIG[fundingSource || FUNDING.PAYPAL];

    if (!fundingConfig) {
        throw new Error(`Expected ${ fundingSource || FUNDING.PAYPAL } to be eligible`);
    }

    const {
        label,
        layout = fundingSource ? BUTTON_LAYOUT.HORIZONTAL : fundingConfig.layouts[0],
        color = fundingConfig.colors[0],
        shape = fundingConfig.shapes[0],
        tagline = (layout === BUTTON_LAYOUT.HORIZONTAL && !fundingSource),
        height,
        period
    } = style;

    if (values(BUTTON_LAYOUT).indexOf(layout) === -1) {
        throw new Error(`Invalid layout: ${ layout }`);
    }

    if (label && values(BUTTON_LABEL).indexOf(label) === -1) {
        throw new Error(`Invalid label: ${ label }`);
    }

    if (color && fundingConfig.colors.indexOf(color) === -1) {
        throw new Error(`Unexpected style.color for ${ FUNDING.PAYPAL } button: ${ color }, expected ${ fundingConfig.colors.join(', ') }`);
    }

    if (shape && fundingConfig.shapes.indexOf(shape) === -1) {
        throw new Error(`Unexpected style.shape for ${ FUNDING.PAYPAL } button: ${ shape }, expected ${ fundingConfig.shapes.join(', ') }`);
    }

    if (height !== undefined) {
        if (typeof height !== 'number') {
            throw new TypeError(`Expected style.height to be a number, got: ${ height }`);
        }

        const [ minHeight, maxHeight ] = [ BUTTON_SIZE_STYLE[BUTTON_SIZE.SMALL].minHeight, BUTTON_SIZE_STYLE[BUTTON_SIZE.HUGE].maxHeight ];

        if (height < minHeight || height > maxHeight) {
            throw new Error(`Expected style.height to be between ${ minHeight }px and ${ maxHeight }px - got ${ height }px`);
        }
    }

    if (layout === BUTTON_LAYOUT.VERTICAL) {
        if (tagline) {
            throw new Error(`style.tagline is not allowed for ${ BUTTON_LAYOUT.VERTICAL } layout`);
        }
    }

    return { label, layout, color, shape, tagline, height, period };
}

const COUNTRIES = values(COUNTRY);
const FUNDING_SOURCES = values(FUNDING);
const ENVS = values(ENV);
const PLATFORMS = values(PLATFORM);

export function normalizeButtonProps(props : ?ButtonPropsInputs) : RenderButtonProps {

    if (!props) {
        throw new Error(`Expected props`);
    }

    let {
        clientID,
        fundingSource,
        // $FlowFixMe
        style = {},
        remembered = [],
        locale = DEFAULT_PROPS.LOCALE,
        env = DEFAULT_PROPS.ENV,
        platform = DEFAULT_PROPS.PLATFORM,
        commit = DEFAULT_PROPS.COMMIT,
        fundingEligibility,
        sessionID = uniqueID(),
        buttonSessionID = uniqueID(),
        csp = {},
        components = [ COMPONENTS.BUTTONS ],
        nonce = '',
        onShippingChange,
        personalization,
        clientAccessToken,
        content = {}
    } = props;

    const { country, lang } = locale;

    if (!country || COUNTRIES.indexOf(country) === -1) {
        throw new Error(`Expected valid country, got ${ country || 'undefined' }`);
    }

    if (!lang || COUNTRY_LANGS[country].indexOf(lang) === -1) {
        throw new Error(`Expected valid lang, got ${ lang || 'undefined' }`);
    }

    if (remembered.some(source => FUNDING_SOURCES.indexOf(source) === -1)) {
        throw new Error(`Expected valid funding sources, got ${ JSON.stringify(remembered) }`);
    }

    if (ENVS.indexOf(env) === -1) {
        throw new Error(`Expected valid env, got ${ env || 'undefined' }`);
    }

    if (!fundingEligibility) {
        throw new Error(`Expected fundingEligibility`);
    }

    if (PLATFORMS.indexOf(platform) === -1) {
        throw new Error(`Expected valid platform, got ${ platform || 'undefined' }`);
    }

    if (csp && csp.nonce) {
        nonce = csp.nonce;
    }

    if (fundingSource) {
        if (SUPPORTED_FUNDING_SOURCES.indexOf(fundingSource) === -1) {
            throw new Error(`Invalid funding source: ${ fundingSource }`);
        }

        if (!isFundingEligible(fundingSource, { platform, fundingEligibility, components, onShippingChange })) {
            throw new Error(`Funding Source not eligible: ${ fundingSource }`);
        }
    }

    style = normalizeButtonStyle(props, style);

    return { clientID, fundingSource, style, locale, remembered, env, fundingEligibility, platform, clientAccessToken,
        buttonSessionID, commit, sessionID, nonce, components, onShippingChange, personalization, content };
}
<|endoftext|>


<commit_before>/*!
 * Dialogs Manager v2.1.1
 * https://github.com/cobicarmel/dialogs-manager/
 *
 * Copyright Kobi Zaltzberg
 * Released under the MIT license
 * https://github.com/cobicarmel/dialogs-manager/blob/master/LICENSE.txt
 */

(function ($, global) {
	'use strict';

	/*
	 * Dialog Manager
	 */
	var DialogsManager = {
		widgetsTypes: {},
		createWidgetType: function (typeName, properties, Parent) {
			if (!Parent) {
				Parent = this.Widget;
			}

			var WidgetType = function () {

				Parent.apply(this, arguments);
			};

			var prototype = WidgetType.prototype = new Parent(typeName);

			$.extend(prototype, properties);

			prototype.constructor = WidgetType;

			WidgetType.extend = function (typeName, properties) {

				return DialogsManager.createWidgetType(typeName, properties, WidgetType);
			};

			return WidgetType;
		},
		addWidgetType: function (typeName, properties, Parent) {

			if (properties && properties.prototype instanceof this.Widget) {
				return this.widgetsTypes[typeName] = properties;
			}

			return this.widgetsTypes[typeName] = this.createWidgetType(typeName, properties, Parent);
		},
		getWidgetType: function (widgetType) {

			return this.widgetsTypes[widgetType];
		}
	};

	/*
	 * Dialog Manager instances constructor
	 */
	DialogsManager.Instance = function () {

		var self = this,
			components = {},
			settings = {};

		var initComponents = function () {

			components.$body = $('body');
		};

		var initSettings = function (options) {

			var defaultSettings = {
				classPrefix: 'dialog',
				effects: {
					show: 'fadeIn',
					hide: 'fadeOut'
				}
			};

			settings = $.extend(defaultSettings, options);
		};

		this.createWidget = function (widgetType, properties) {

			var WidgetTypeConstructor = DialogsManager.getWidgetType(widgetType),
				widget = new WidgetTypeConstructor(widgetType);

			properties = properties || {};

			widget.init(self, properties);

			widget.setMessage(properties.message);

			if (properties.linkedElement) {
				widget.linkElement(properties.linkedElement, widget);
			}

			return widget;
		};

		this.getSettings = function (property) {

			if (property) {
				return settings[property];
			}

			return Object.create(settings);
		};

		this.init = function (settings) {

			initSettings(settings);

			initComponents();

			return self;
		};

		self.init();
	};

	/*
	 * Widget types constructor
	 */
	DialogsManager.Widget = function (widgetName) {

		var self = this,
			settings = {},
			events = {},
			components = {
				$element: 0
			};

		var callEffect = function (intent) {

			var effect = settings.effects[intent],
				$widget = components.$widget;

			if ($.isFunction(effect)) {
				effect.call($widget);
			}
			else {

				if ($widget[effect]) {
					$widget[effect]();
				}
				else {
					throw 'Reference Error: The effect ' + effect + ' not found';
				}
			}
		};

		var initComponents = function () {

			self.addComponent('widget');

			self.addComponent('message');

			var id = self.getSettings('id');

			if (id) {

				self.getComponents('widget').attr('id', id);
			}
		};

		var initSettings = function (parent, userSettings) {

			var parentSettings = parent.getSettings();

			settings = self.getDefaultSettings();

			settings.effects = parentSettings.effects;

			var prefix = parentSettings.classPrefix + '-' + widgetName;

			settings.classes = {
				globalPrefix: parentSettings.classPrefix,
				prefix: prefix,
				widget: 'dialog-widget',
				linkedActive: prefix + '-linked-active'
			};

			$.extend(true, settings, userSettings);

			initSettingsEvents();
		};

		var initSettingsEvents = function () {

			var settings = self.getSettings();

			$.each( settings, function (settingKey) {

				var eventName = settingKey.match(/^on([A-Z].*)/);

				if (!eventName) {
					return;
				}

				eventName = eventName[1].charAt(0).toLowerCase() + eventName[1].slice(1);

				self.on(eventName, this);
			} );
		};

		var normalizeClassName = function (name) {

			return name.replace(/([a-z])([A-Z])/g, function () {
				return arguments[1] + '-' + arguments[2].toLowerCase();
			});
		};

		this.addComponent = function (name, component, type) {

			var $newComponent = components['$' + name] = $(component || '<div>'),
				className = settings.classes.prefix + '-';

			name = normalizeClassName(name);

			className += name;

			if (!type) {
				type = name;
			}

			className += ' ' + settings.classes.globalPrefix + '-' + type;

			$newComponent.addClass(className);

			return $newComponent;
		};

		this.getSettings = function (setting) {

			var copy = Object.create(settings);

			if (setting) {
				return copy[setting];
			}

			return copy;
		};

		this.init = function (parent, properties) {

			if (!(parent instanceof DialogsManager.Instance)) {
				throw 'The ' + self.widgetName + ' must to be initialized from an instance of DialogsManager.Instance';
			}

			self.trigger('init', properties);

			initSettings(parent, properties);

			initComponents();

			self.buildWidget();

			if (self.attachEvents) {
				self.attachEvents();
			}

			self.trigger('ready');

			return self;
		};

		this.getComponents = function (item) {

			return item ? components['$' + item] : components;
		};

		this.hide = function () {

			callEffect('hide');

			if (components.$element.length) {
				components.$element.removeClass(settings.classes.linkedActive);
			}

			self.trigger('hide');

			return self;
		};

		this.linkElement = function (element) {

			this.addComponent('element', element);

			return self;
		};

		this.on = function (eventName, callback) {
			if (!events[eventName]) {
				events[eventName] = [];
			}

			events[eventName].push(callback);

			return self;
		};

		this.setMessage = function (message) {

			components.$message.html(message);

			return self;
		};

		this.show = function (e, userSettings) {

			if (e) {
				e.stopPropagation();
			}

			components.$widget.appendTo('body');

			callEffect('show');

			if (components.$element.length) {
				components.$element.addClass(settings.classes.linkedActive);
			}

			self.trigger('show', userSettings);

			return self;
		};

		this.trigger = function (eventName, params) {

			var methodName = 'on' + eventName[0].toUpperCase() + eventName.slice(1);

			if (self[methodName]) {
				self[methodName](params);
			}

			var callbacks = events[eventName];

			if (!callbacks) {
				return;
			}

			$.each(callbacks, function (index, callback) {
				callback.call(self, params);
			});
		};
	};

	// Inheritable widget methods
	DialogsManager.Widget.prototype.buildWidget = function () {

		var components = this.getComponents();

		components.$widget.html(components.$message);
	};

	DialogsManager.Widget.prototype.getDefaultSettings = function () {

		return {};
	};

	DialogsManager.Widget.prototype.onHide = function () {
	};

	DialogsManager.Widget.prototype.onShow = function () {
	};

	DialogsManager.Widget.prototype.onInit = function () {
	};

	DialogsManager.Widget.prototype.onReady = function () {
	};

	/*
	 * Default basic widget types
	 */
	DialogsManager.addWidgetType('tool-tip', {
		onShow: function () {

			var components = this.getComponents();

			if (components.$element.length) {

				components.$widget.position({
					at: 'left top-5',
					my: 'left+10 bottom',
					of: components.$element,
					collision: 'none none'
				});

				components.$element.focus();
			}

			setTimeout(this.hide, 5000);
		}
	});

	DialogsManager.addWidgetType('options', {
		activeKeyUp: function (event) {

			var TAB_KEY = 9;

			if (event.which === TAB_KEY) {

				event.preventDefault();
			}

			if (this.hotKeys[event.which]) {
				this.hotKeys[event.which](this);
			}
		},
		activeKeyDown: function (event) {

			var TAB_KEY = 9;

			if (event.which === TAB_KEY) {
				event.preventDefault();

				var currentButtonIndex = this.focusedButton.index(),
					nextButtonIndex;

				if (event.shiftKey) {

					nextButtonIndex = currentButtonIndex - 1;

					if (nextButtonIndex < 0) {
						nextButtonIndex = this.buttons.length - 1;
					}
				} else {

					nextButtonIndex = currentButtonIndex + 1;

					if (nextButtonIndex >= this.buttons.length) {
						nextButtonIndex = 0;
					}
				}

				this.focusedButton = this.buttons[nextButtonIndex].focus();
			}
		},
		addButton: function (options) {

			var self = this,
				$button = self.addComponent(options.name, $('<button>').text(options.text));

			self.buttons.push($button);

			var buttonFn = function () {

				if (self.getSettings('hideOnButtonClick')) {
					self.hide();
				}

				if ($.isFunction(options.callback)) {
					options.callback.call(this, self);
				}
			};

			$button.on('click', buttonFn);

			if (options.hotKey) {
				this.hotKeys[options.hotKey] = buttonFn;
			}

			this.getComponents('buttonsWrapper').append($button);

			if (options.focus) {
				this.focusedButton = $button;
			}

			return self;
		},
		bindHotKeys: function () {
			var self = this;

			self.bindKeyUpEvents = function (event) {

				self.activeKeyUp(event);
			};

			self.bindKeyDownEvents = function (event) {

				self.activeKeyDown(event);
			};

			self.getComponents( 'window' ).on({
				keyup: self.bindKeyUpEvents,
				keydown: self.bindKeyDownEvents
			});
		},
		buildWidget: function () {
			this.addComponent( 'window', window );

			var $widgetHeader = this.addComponent('widgetHeader'),
				$widgetContent = this.addComponent('widgetContent'),
				$buttonsWrapper = this.addComponent('buttonsWrapper');

			var components = this.getComponents();

			$widgetContent.append($widgetHeader, components.$message, $buttonsWrapper);

			components.$widget.html($widgetContent);
		},
		getDefaultSettings: function () {

			return {
				position: {
					my: 'center',
					at: 'center center-100'
				},
				headerMessage: '',
				hideOnButtonClick: true
			};
		},
		onHide: function () {

			this.unbindHotKeys();
		},
		onInit: function () {

			this.buttons = [];

			this.hotKeys = {};

			this.focusedButton = null;
		},
		onReady: function(){

			this.setHeaderMessage(this.getSettings('headerMessage'));
		},
		onShow: function (userSettings) {

			var components = this.getComponents(),
				position = this.getSettings('position');

			position.of = components.$widget;

			if (userSettings) {
				$.extend(position, userSettings);
			}

			components.$widgetContent.position(position);

			this.bindHotKeys();

			if (!this.focusedButton) {
				this.focusedButton = this.buttons[0];
			}

			if (this.focusedButton) {
				this.focusedButton.focus();
			}
		},
		setHeaderMessage: function (message) {

			this.getComponents('widgetHeader').html(message);

			return this;
		},
		unbindHotKeys: function () {

			this.getComponents( 'window' ).off({
				keyup: this.bindKeyUpEvents,
				keydown: this.bindKeyDownEvents
			});
		}
	});

	DialogsManager.addWidgetType('confirm', DialogsManager.getWidgetType('options').extend('confirm', {
		onReady: function () {

			DialogsManager.getWidgetType('options').prototype.onReady.apply(this, arguments);

			var strings = this.getSettings('strings'),
				ESC_KEY = 27,
				isDefaultCancel = this.getSettings('defaultOption') === 'cancel';

			this.addButton({
				name: 'cancel',
				text: strings.cancel,
				callback: function (widget) {

					widget.trigger('cancel');
				},
				hotKey: ESC_KEY,
				focus: isDefaultCancel
			});

			this.addButton({
				name: 'ok',
				text: strings.confirm,
				callback: function (widget) {

					widget.trigger('confirm');
				},
				focus: !isDefaultCancel
			});
		},
		getDefaultSettings: function () {

			var settings = DialogsManager.getWidgetType('options').prototype.getDefaultSettings.apply(this, arguments);

			settings.strings = {
				confirm: 'OK',
				cancel: 'Cancel'
			};

			settings.defaultOption = 'cancel';

			return settings;
		}
	}));

	DialogsManager.addWidgetType('alert', DialogsManager.getWidgetType('options').extend('alert', {
		onReady: function () {

			DialogsManager.getWidgetType('options').prototype.onReady.apply(this, arguments);

			var strings = this.getSettings('strings');

			this.addButton({
				name: 'ok',
				text: strings.confirm,
				callback: function (widget) {

					widget.trigger('confirm');
				}
			});
		},
		getDefaultSettings: function () {
			var settings = DialogsManager.getWidgetType('options').prototype.getDefaultSettings.apply(this, arguments);

			settings.strings = {
				confirm: 'OK'
			};

			return settings;
		}
	}));

	DialogsManager.addWidgetType('popup', {
		getDefaultSettings: function () {

			return {
				position: {
					my: 'center',
					at: 'center',
					of: window
				},
				hide: {
					delay: 5000
				}
			};
		},
		onShow: function () {

			this.getComponents('message').position(this.getSettings('position'));

			setTimeout(this.hide, this.getSettings('hide').delay);
		}
	});

	// Exporting the DialogsManager variable to global
	global.DialogsManager = DialogsManager;
})(typeof require === 'function' ? require( 'jquery' ) : jQuery, typeof module !== 'undefined' ? module.exports : window);<commit_msg>Added default option to automatically refresh the widget position on window resize<commit_after>/*!
 * Dialogs Manager v2.1.1
 * https://github.com/cobicarmel/dialogs-manager/
 *
 * Copyright Kobi Zaltzberg
 * Released under the MIT license
 * https://github.com/cobicarmel/dialogs-manager/blob/master/LICENSE.txt
 */

(function ($, global) {
	'use strict';

	/*
	 * Dialog Manager
	 */
	var DialogsManager = {
		widgetsTypes: {},
		createWidgetType: function (typeName, properties, Parent) {
			if (!Parent) {
				Parent = this.Widget;
			}

			var WidgetType = function () {

				Parent.apply(this, arguments);
			};

			var prototype = WidgetType.prototype = new Parent(typeName);

			$.extend(prototype, properties);

			prototype.constructor = WidgetType;

			WidgetType.extend = function (typeName, properties) {

				return DialogsManager.createWidgetType(typeName, properties, WidgetType);
			};

			return WidgetType;
		},
		addWidgetType: function (typeName, properties, Parent) {

			if (properties && properties.prototype instanceof this.Widget) {
				return this.widgetsTypes[typeName] = properties;
			}

			return this.widgetsTypes[typeName] = this.createWidgetType(typeName, properties, Parent);
		},
		getWidgetType: function (widgetType) {

			return this.widgetsTypes[widgetType];
		}
	};

	/*
	 * Dialog Manager instances constructor
	 */
	DialogsManager.Instance = function () {

		var self = this,
			components = {},
			settings = {};

		var initComponents = function () {

			components.$body = $('body');
		};

		var initSettings = function (options) {

			var defaultSettings = {
				classPrefix: 'dialog',
				effects: {
					show: 'fadeIn',
					hide: 'fadeOut'
				}
			};

			settings = $.extend(defaultSettings, options);
		};

		this.createWidget = function (widgetType, properties) {

			var WidgetTypeConstructor = DialogsManager.getWidgetType(widgetType),
				widget = new WidgetTypeConstructor(widgetType);

			properties = properties || {};

			widget.init(self, properties);

			widget.setMessage(properties.message);

			if (properties.linkedElement) {
				widget.linkElement(properties.linkedElement, widget);
			}

			return widget;
		};

		this.getSettings = function (property) {

			if (property) {
				return settings[property];
			}

			return Object.create(settings);
		};

		this.init = function (settings) {

			initSettings(settings);

			initComponents();

			return self;
		};

		self.init();
	};

	/*
	 * Widget types constructor
	 */
	DialogsManager.Widget = function (widgetName) {

		var self = this,
			settings = {},
			events = {},
			components = {
				$element: 0
			};

		var callEffect = function (intent) {

			var effect = settings.effects[intent],
				$widget = components.$widget;

			if ($.isFunction(effect)) {
				effect.call($widget);
			}
			else {

				if ($widget[effect]) {
					$widget[effect]();
				}
				else {
					throw 'Reference Error: The effect ' + effect + ' not found';
				}
			}
		};

		var initComponents = function () {

			self.addComponent('widget');

			self.addComponent('message');

			var id = self.getSettings('id');

			if (id) {

				self.getComponents('widget').attr('id', id);
			}
		};

		var initSettings = function (parent, userSettings) {

			var parentSettings = parent.getSettings();

			settings = self.getDefaultSettings();

			settings.effects = parentSettings.effects;

			var prefix = parentSettings.classPrefix + '-' + widgetName;

			settings.classes = {
				globalPrefix: parentSettings.classPrefix,
				prefix: prefix,
				widget: 'dialog-widget',
				linkedActive: prefix + '-linked-active'
			};

			$.extend(true, settings, userSettings);

			initSettingsEvents();
		};

		var initSettingsEvents = function () {

			var settings = self.getSettings();

			$.each( settings, function (settingKey) {

				var eventName = settingKey.match(/^on([A-Z].*)/);

				if (!eventName) {
					return;
				}

				eventName = eventName[1].charAt(0).toLowerCase() + eventName[1].slice(1);

				self.on(eventName, this);
			} );
		};

		var normalizeClassName = function (name) {

			return name.replace(/([a-z])([A-Z])/g, function () {
				return arguments[1] + '-' + arguments[2].toLowerCase();
			});
		};

		this.addComponent = function (name, component, type) {

			var $newComponent = components['$' + name] = $(component || '<div>'),
				className = settings.classes.prefix + '-';

			name = normalizeClassName(name);

			className += name;

			if (!type) {
				type = name;
			}

			className += ' ' + settings.classes.globalPrefix + '-' + type;

			$newComponent.addClass(className);

			return $newComponent;
		};

		this.getSettings = function (setting) {

			var copy = Object.create(settings);

			if (setting) {
				return copy[setting];
			}

			return copy;
		};

		this.init = function (parent, properties) {

			if (!(parent instanceof DialogsManager.Instance)) {
				throw 'The ' + self.widgetName + ' must to be initialized from an instance of DialogsManager.Instance';
			}

			self.trigger('init', properties);

			initSettings(parent, properties);

			initComponents();

			self.buildWidget();

			if (self.attachEvents) {
				self.attachEvents();
			}

			self.trigger('ready');

			return self;
		};

		this.getComponents = function (item) {

			return item ? components['$' + item] : components;
		};

		this.hide = function () {

			callEffect('hide');

			if (components.$element.length) {
				components.$element.removeClass(settings.classes.linkedActive);
			}

			self.trigger('hide');

			return self;
		};

		this.linkElement = function (element) {

			this.addComponent('element', element);

			return self;
		};

		this.on = function (eventName, callback) {
			if (!events[eventName]) {
				events[eventName] = [];
			}

			events[eventName].push(callback);

			return self;
		};

		this.setMessage = function (message) {

			components.$message.html(message);

			return self;
		};

		this.show = function (e, userSettings) {

			if (e) {
				e.stopPropagation();
			}

			components.$widget.appendTo('body');

			callEffect('show');

			if (components.$element.length) {
				components.$element.addClass(settings.classes.linkedActive);
			}

			self.trigger('show', userSettings);

			return self;
		};

		this.trigger = function (eventName, params) {

			var methodName = 'on' + eventName[0].toUpperCase() + eventName.slice(1);

			if (self[methodName]) {
				self[methodName](params);
			}

			var callbacks = events[eventName];

			if (!callbacks) {
				return;
			}

			$.each(callbacks, function (index, callback) {
				callback.call(self, params);
			});
		};
	};

	// Inheritable widget methods
	DialogsManager.Widget.prototype.buildWidget = function () {

		var components = this.getComponents();

		components.$widget.html(components.$message);
	};

	DialogsManager.Widget.prototype.getDefaultSettings = function () {

		return {};
	};

	DialogsManager.Widget.prototype.onHide = function () {
	};

	DialogsManager.Widget.prototype.onShow = function () {
	};

	DialogsManager.Widget.prototype.onInit = function () {
	};

	DialogsManager.Widget.prototype.onReady = function () {
	};

	/*
	 * Default basic widget types
	 */
	DialogsManager.addWidgetType('tool-tip', {
		onShow: function () {

			var components = this.getComponents();

			if (components.$element.length) {

				components.$widget.position({
					at: 'left top-5',
					my: 'left+10 bottom',
					of: components.$element,
					collision: 'none none'
				});

				components.$element.focus();
			}

			setTimeout(this.hide, 5000);
		}
	});

	DialogsManager.addWidgetType('options', {
		activeKeyUp: function (event) {

			var TAB_KEY = 9;

			if (event.which === TAB_KEY) {

				event.preventDefault();
			}

			if (this.hotKeys[event.which]) {
				this.hotKeys[event.which](this);
			}
		},
		activeKeyDown: function (event) {

			var TAB_KEY = 9;

			if (event.which === TAB_KEY) {
				event.preventDefault();

				var currentButtonIndex = this.focusedButton.index(),
					nextButtonIndex;

				if (event.shiftKey) {

					nextButtonIndex = currentButtonIndex - 1;

					if (nextButtonIndex < 0) {
						nextButtonIndex = this.buttons.length - 1;
					}
				} else {

					nextButtonIndex = currentButtonIndex + 1;

					if (nextButtonIndex >= this.buttons.length) {
						nextButtonIndex = 0;
					}
				}

				this.focusedButton = this.buttons[nextButtonIndex].focus();
			}
		},
		addButton: function (options) {

			var self = this,
				$button = self.addComponent(options.name, $('<button>').text(options.text));

			self.buttons.push($button);

			var buttonFn = function () {

				if (self.getSettings('hideOnButtonClick')) {
					self.hide();
				}

				if ($.isFunction(options.callback)) {
					options.callback.call(this, self);
				}
			};

			$button.on('click', buttonFn);

			if (options.hotKey) {
				this.hotKeys[options.hotKey] = buttonFn;
			}

			this.getComponents('buttonsWrapper').append($button);

			if (options.focus) {
				this.focusedButton = $button;
			}

			return self;
		},
		bindHotKeys: function () {
			var self = this;

			self.bindKeyUpEvents = function (event) {

				self.activeKeyUp(event);
			};

			self.bindKeyDownEvents = function (event) {

				self.activeKeyDown(event);
			};

			self.getComponents( 'window' ).on({
				keyup: self.bindKeyUpEvents,
				keydown: self.bindKeyDownEvents
			});
		},
		buildWidget: function () {
			this.addComponent( 'window', window );

			var $widgetHeader = this.addComponent('widgetHeader'),
				$widgetContent = this.addComponent('widgetContent'),
				$buttonsWrapper = this.addComponent('buttonsWrapper');

			var components = this.getComponents();

			$widgetContent.append($widgetHeader, components.$message, $buttonsWrapper);

			components.$widget.html($widgetContent);
		},
		getDefaultSettings: function () {

			return {
				position: {
					my: 'center',
					at: 'center center-100'
				},
				headerMessage: '',
				hideOnButtonClick: true,
				refreshPosition: true
			};
		},
		onHide: function () {

			this.unbindHotKeys();

			this.getComponents( 'window' ).off( 'resize', this.placeWidget );
		},
		onInit: function () {

			this.buttons = [];

			this.hotKeys = {};

			this.focusedButton = null;
		},
		onReady: function(){

			this.setHeaderMessage(this.getSettings('headerMessage'));
		},
		onShow: function (userSettings) {

			if (self.getSettings('refreshPosition')) {

				self.getComponents( 'window' ).on( 'resize', function () {

					self.placeWidget();
				} );
			}
			var components = this.getComponents(),
				position = this.getSettings('position');

			position.of = components.$widget;

			if (userSettings) {
				$.extend(position, userSettings);
			}

			components.$widgetContent.position(position);

			this.bindHotKeys();

			if (!this.focusedButton) {
				this.focusedButton = this.buttons[0];
			}

			if (this.focusedButton) {
				this.focusedButton.focus();
			}
		},
		setHeaderMessage: function (message) {

			this.getComponents('widgetHeader').html(message);

			return this;
		},
		unbindHotKeys: function () {

			this.getComponents( 'window' ).off({
				keyup: this.bindKeyUpEvents,
				keydown: this.bindKeyDownEvents
			});
		}
	});

	DialogsManager.addWidgetType('confirm', DialogsManager.getWidgetType('options').extend('confirm', {
		onReady: function () {

			DialogsManager.getWidgetType('options').prototype.onReady.apply(this, arguments);

			var strings = this.getSettings('strings'),
				ESC_KEY = 27,
				isDefaultCancel = this.getSettings('defaultOption') === 'cancel';

			this.addButton({
				name: 'cancel',
				text: strings.cancel,
				callback: function (widget) {

					widget.trigger('cancel');
				},
				hotKey: ESC_KEY,
				focus: isDefaultCancel
			});

			this.addButton({
				name: 'ok',
				text: strings.confirm,
				callback: function (widget) {

					widget.trigger('confirm');
				},
				focus: !isDefaultCancel
			});
		},
		getDefaultSettings: function () {

			var settings = DialogsManager.getWidgetType('options').prototype.getDefaultSettings.apply(this, arguments);

			settings.strings = {
				confirm: 'OK',
				cancel: 'Cancel'
			};

			settings.defaultOption = 'cancel';

			return settings;
		}
	}));

	DialogsManager.addWidgetType('alert', DialogsManager.getWidgetType('options').extend('alert', {
		onReady: function () {

			DialogsManager.getWidgetType('options').prototype.onReady.apply(this, arguments);

			var strings = this.getSettings('strings');

			this.addButton({
				name: 'ok',
				text: strings.confirm,
				callback: function (widget) {

					widget.trigger('confirm');
				}
			});
		},
		getDefaultSettings: function () {
			var settings = DialogsManager.getWidgetType('options').prototype.getDefaultSettings.apply(this, arguments);

			settings.strings = {
				confirm: 'OK'
			};

			return settings;
		}
	}));

	DialogsManager.addWidgetType('popup', {
		getDefaultSettings: function () {

			return {
				position: {
					my: 'center',
					at: 'center',
					of: window
				},
				hide: {
					delay: 5000
				}
			};
		},
		onShow: function () {

			this.getComponents('message').position(this.getSettings('position'));

			setTimeout(this.hide, this.getSettings('hide').delay);
		}
	});

	// Exporting the DialogsManager variable to global
	global.DialogsManager = DialogsManager;
})(typeof require === 'function' ? require( 'jquery' ) : jQuery, typeof module !== 'undefined' ? module.exports : window);<|endoftext|>


<commit_before>/*
 * grunt-shampoo
 * https://github.com/soapcreative/grunt-shampoo
 *
 * Copyright (c) 2014 Soap Creative
 * Licensed under the MIT license.
 */

'use strict';

var request = require("request"),
    fs = require("fs"),
    knox = require("knox"),
    rc = require('rc'),
    DecompressZip = require('decompress-zip'),
    fs = require('fs'),
    path = require('path'),
    util = require('util'),
    url = require('url'),
    _mkdirp = require('mkdirp'),
    _ = require('lodash'),

    shampooApi = require('./lib/shampoo-api'),
    shampooUtils = require('./lib/shampoo-utils'),
    createHandlerFilter = require('./lib/handler-filters'),
    tryHttpDownload = require('./lib/try-http-download');

var HTTP_STATUS_NOT_MODIFIED = 304;

var ZIP_FOLDER_NAME = "content-backups",
    ZIP_FILE_NAME_PREFIX = "content-dump-";

var DEFAULT_MAX_CONNECTIONS = 8;

module.exports = function( grunt ) {

  grunt.registerMultiTask( "shampoo", "Retrieve content from the Shampoo CMS API on shampoo.io.", function() {

    var thisTask = this,
        handlerFilter = createHandlerFilter(grunt),
        knoxClient = null;

    function mkdirp(dirPath, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = null;
      }
      _mkdirp(dirPath, options,
        handlerFilter.logErrors("Couldn't create " + dirPath, callback)
      );
    }

    function isMediaAssetUrl(assetUrl) {
      // a URL is a media asset URL if its scheme/protocol is http or https,
      // and its hostname is a subdomain of amazonaws.com.
      return Boolean(
        assetUrl &&
        /^https?:$/i .test(assetUrl.protocol) &&
        /\.amazonaws\.com$/i .test(assetUrl.hostname)
      );
    }

    function getMediaAssetPath(assetUrlString) {
      var assetUrl;
      try {
        assetUrl = url.parse(assetUrlString);
        if (isMediaAssetUrl(assetUrl)) {
          var pathname = assetUrl.pathname;
          if (pathname.charAt(0) === "/") {
            pathname = pathname.slice(1);
          }
          // url.parse automatically url-escapes characters, but we need to
          // reverse that for passing to the knox client, as well as for saving
          // locally
          return decodeURI(pathname);
        }
      } catch (error) { } // url.parse failed, so just fall through to return null
      return null;
    }

    function getMediaAssets( obj, mediaCwd ) {
      var remotePaths = { },
          newJson;

      newJson = shampooUtils.transformJson(obj,
        function (value, key, parentObject, keyPath) {
          if (typeof value === "string") {
            var assetPath = getMediaAssetPath(value);

            if (assetPath != null) {
              var localPath = path.join(mediaCwd, assetPath);
              remotePaths[assetPath] = true;

              grunt.log.debug(
                "Rewriting %s\n" +
                "  old: %j\n" +
                "  new: %j\n",
                keyPath.join("."), value, localPath
              );

              return localPath;
            }
          }
          return value;
        }
      );

      return {
        newJson:     newJson,
        remotePaths: Object.keys(remotePaths)
      };
    }

    function processJsonFiles(localPaths, options, callback) {
      var waiting = 0;

      function fileComplete() {
        if (--waiting <= 0) {
          callback();
        }
      }

      localPaths.forEach(function (localPath) {
        waiting++;
        fs.readFile(localPath, handlerFilter.expectJsonContents(localPath,
          function (error, jsonContent) {
            if (error) {
              fileComplete();
            } else {
              processJson(jsonContent, localPath, options, fileComplete);
            }
          }
        ));
      });
    }

    function processZipFile(zipPath, options, callback) {
      var unzipper = new DecompressZip(zipPath);

      unzipper
        .once("extract", function (log) {
          grunt.log.debug("%s extract log:\n%j", zipPath, log);
          if (options.mediaOut != null) {
            var extractedFiles = log.map(function (extractResult) {
                return extractResult.deflated || extractResult.stored;
              }).filter(function (extractedPath) {
                return Boolean(extractedPath);
              }).map(function (extractedPath) {
                return path.join(options.zipOut, extractedPath);
              });

            if (extractedFiles.length > 0) {
              processJsonFiles(extractedFiles, options, callback);
              return;
            }
          }
          // fell through to here because options.mediaOut was not set,
          // or the zip file contained no usable files
          callback();
        })
        .once("error", function(error) {
          grunt.log.error("Error unzipping file %j: %s", zipPath, error);
          callback();
        });

      unzipper.extract({ path: options.zipOut });
    }

    function generateZipFileName() {
      return ZIP_FILE_NAME_PREFIX + Date.now() + ".zip";
    }

    function requestZip(zipUrl, options, callback) {
      var zipPath = path.join(
        options.zipOut, ZIP_FOLDER_NAME, generateZipFileName());

      mkdirp(path.dirname(zipPath), null, function (mkdirError) {
        if (mkdirError) {
          callback();
          return;
        }

        grunt.verbose.writeln("Downloading zip");
        shampooUtils.downloadToFile(zipUrl, zipPath, handlerFilter.expectHttpOk(zipUrl,
          function (error) {
            if (error) {
              callback();
            } else {
              processZipFile(zipPath, options, callback);
            }
          }
        ));
      });
    }

    function saveMedia(options, mediaAssets, callback) {

      grunt.log.subhead( "Retrieving files..." );

      grunt.log.debug("Media queue is:");
      mediaAssets.forEach(function (p) {
        grunt.log.debug("  %j", p);
      });

      var loadCounter = 0;
      var next = function() {
        loadCounter--;
        fillQueue();
      };

      var fillQueue = function() {
        if (mediaAssets.length === 0 && loadCounter === 0) {
          callback();
        } else {
          while (mediaAssets.length > 0 && loadCounter < options.maxConnections) {
            loadCounter++;
            verifyDownload( mediaAssets.shift(), options.mediaOut, next );
          }
        }
      };

      fillQueue();
    }

    function verifyDownload( remotePath, mediaOut, callback ) {
      var localPath = path.join(mediaOut, remotePath),
          tryOptions = {
            logError:   grunt.log.error.bind(grunt.log),
            logVerbose: grunt.verbose.writeln.bind(grunt.verbose),
            logDebug:   grunt.log.debug.bind(grunt.log)
          };

      grunt.log.debug("Verifying %j -> %j", remotePath, localPath);

      shampooUtils.hashFile(localPath, "md5", function (error, hasher) {
        if (error) {
          grunt.log.debug("Etag calculation of local file failed: %s", error);
        } else {
          tryOptions.etag = hasher.digest("hex");
        }

        mkdirp(path.dirname(localPath), function (error) {
          if (error) {
            callback();
          } else {
            tryHttpDownload(
              function (headers, tryCallback) {
                grunt.log.debug("S3 GET %j", remotePath);
                knoxClient.getFile(remotePath, headers, tryCallback);
              },
              localPath,
              tryOptions,
              function (error, response) {
                grunt.log.write("%s ", localPath);
                if (error) {
                  grunt.log.error("%j", error);
                } else if (response.statusCode === HTTP_STATUS_NOT_MODIFIED) {
                  grunt.log.ok("up to date");
                } else {
                  grunt.log.ok("downloaded");
                }
                callback();
              }
            );
          }
        });
      });
    }

    function writeJsonFile(out, object) {
      grunt.log.write( out + " ");
      grunt.log.ok( "saved" );
      grunt.file.write(out, JSON.stringify(object, null, '\t'));
    }

    function processJson(jsonContent, outJsonFile, options, callback) {
      var result = getMediaAssets(jsonContent, options.mediaCwd);
      if (outJsonFile) {
        writeJsonFile(outJsonFile, result.newJson);
        if (options.mediaOut != null) {
          saveMedia(options, result.remotePaths, callback);
          return;
        }
      }
      callback();
    }

    function requestJson(jsonUrl, options, callback) {
      request(jsonUrl, handlerFilter.expectJsonResponse(jsonUrl,
        function (error, response, jsonContent) {
          if (error) {
            callback();
          } else {
            processJson(jsonContent, options.out, options, callback);
          }
        }
      ));
    }

    function requestFiles(options, callback) {
      grunt.log.subhead( "Retrieving content..." );

      var apiUrl = shampooApi.createApiUrl(options);
      grunt.verbose.writeln("Url is %j", apiUrl);

      if (shampooApi.isZipQuery(options.query)) {
        grunt.verbose.writeln("Zip job");
        requestZip(apiUrl, options, callback);
      } else {
        grunt.verbose.writeln("JSON job");
        requestJson(apiUrl, options, callback);
      }
    }

    function makeClient(options) {
      return knox.createClient( _.pick(options, [
        'region', 'endpoint', 'port', 'key', 'secret', 'access', 'bucket',
        'secure', 'headers', 'style'
      ]));
    }

    function getOptions() {
      // Mix in default options, .shampoorc file
      var options = rc("shampoo", thisTask.options({
        api: 1,
        query: "dump/json/single-file",
        out: "data/content.json",
        mediaOut: null,
        mediaCwd: null,
        maxConnections: DEFAULT_MAX_CONNECTIONS
      }));

      var messages = [ ];
      var missing = { };
      var required = [ "key", "secret", "domain", "query", "out" ];

      if (shampooApi.isZipQuery(options.query)) {
        required.push("zipOut");
      }

      required.forEach(function (optionName) {
        if (!options[optionName]) {
          missing[optionName] = true;
        }
      });

      var missingArray = Object.keys(missing);
      if (missingArray.length > 0) {
        messages.push("The following required options are not set: " +
          missingArray.join(", "));
      }

      if (missing.key || missing.secret) {
        messages.push("Values for 'key' and 'secret' are found in your Shampoo account under Settings.");
      }

      if (missing.zipOut) {
        messages.push(util.format(
          "The query %j returns a zip file. This requires the 'zipOut' option to be set.",
          options.query
        ));
      }

      if (options.mediaCwd == null) {
        options.mediaCwd = options.mediaOut;
      }

      var parsedMaxConnections = parseInt(options.maxConnections, 10);
      if (isNaN(options.maxConnections)) {
        messages.push(util.format(
          "Invalid value for maxConnections: %j. Assuming default of %d.",
          options.maxConnections, DEFAULT_MAX_CONNECTIONS
        ));
        parsedMaxConnections = DEFAULT_MAX_CONNECTIONS;
      } else if (parsedMaxConnections < 1) {
        messages.push(
          "Value for maxConnections must be 1 or greater. Assuming 1.");
        parsedMaxConnections = 1;
      }

      options.maxConnections = parsedMaxConnections;

      return {
        options: options,
        messages: messages,
        ok: messages.length === 0
      };
    }

    function main() {
      var optionResult = getOptions();
      var messagesString = optionResult.messages.join("\n");
      var callback = thisTask.async();

      if (optionResult.ok) {
        var options = optionResult.options;
        grunt.log.writeln(messagesString);
        if (options.mediaOut) {
          knoxClient = makeClient(options.aws || { });
        }
        requestFiles(optionResult.options, callback);
      } else {
        grunt.log.error(messagesString);
        callback(false);
      }

    }

    return main();
  });
};
<commit_msg>some of the messages in getOptions are just advisory, and don't indicate a failure<commit_after>/*
 * grunt-shampoo
 * https://github.com/soapcreative/grunt-shampoo
 *
 * Copyright (c) 2014 Soap Creative
 * Licensed under the MIT license.
 */

'use strict';

var request = require("request"),
    fs = require("fs"),
    knox = require("knox"),
    rc = require('rc'),
    DecompressZip = require('decompress-zip'),
    fs = require('fs'),
    path = require('path'),
    util = require('util'),
    url = require('url'),
    _mkdirp = require('mkdirp'),
    _ = require('lodash'),

    shampooApi = require('./lib/shampoo-api'),
    shampooUtils = require('./lib/shampoo-utils'),
    createHandlerFilter = require('./lib/handler-filters'),
    tryHttpDownload = require('./lib/try-http-download');

var HTTP_STATUS_NOT_MODIFIED = 304;

var ZIP_FOLDER_NAME = "content-backups",
    ZIP_FILE_NAME_PREFIX = "content-dump-";

var DEFAULT_MAX_CONNECTIONS = 8;

module.exports = function( grunt ) {

  grunt.registerMultiTask( "shampoo", "Retrieve content from the Shampoo CMS API on shampoo.io.", function() {

    var thisTask = this,
        handlerFilter = createHandlerFilter(grunt),
        knoxClient = null;

    function mkdirp(dirPath, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = null;
      }
      _mkdirp(dirPath, options,
        handlerFilter.logErrors("Couldn't create " + dirPath, callback)
      );
    }

    function isMediaAssetUrl(assetUrl) {
      // a URL is a media asset URL if its scheme/protocol is http or https,
      // and its hostname is a subdomain of amazonaws.com.
      return Boolean(
        assetUrl &&
        /^https?:$/i .test(assetUrl.protocol) &&
        /\.amazonaws\.com$/i .test(assetUrl.hostname)
      );
    }

    function getMediaAssetPath(assetUrlString) {
      var assetUrl;
      try {
        assetUrl = url.parse(assetUrlString);
        if (isMediaAssetUrl(assetUrl)) {
          var pathname = assetUrl.pathname;
          if (pathname.charAt(0) === "/") {
            pathname = pathname.slice(1);
          }
          // url.parse automatically url-escapes characters, but we need to
          // reverse that for passing to the knox client, as well as for saving
          // locally
          return decodeURI(pathname);
        }
      } catch (error) { } // url.parse failed, so just fall through to return null
      return null;
    }

    function getMediaAssets( obj, mediaCwd ) {
      var remotePaths = { },
          newJson;

      newJson = shampooUtils.transformJson(obj,
        function (value, key, parentObject, keyPath) {
          if (typeof value === "string") {
            var assetPath = getMediaAssetPath(value);

            if (assetPath != null) {
              var localPath = path.join(mediaCwd, assetPath);
              remotePaths[assetPath] = true;

              grunt.log.debug(
                "Rewriting %s\n" +
                "  old: %j\n" +
                "  new: %j\n",
                keyPath.join("."), value, localPath
              );

              return localPath;
            }
          }
          return value;
        }
      );

      return {
        newJson:     newJson,
        remotePaths: Object.keys(remotePaths)
      };
    }

    function processJsonFiles(localPaths, options, callback) {
      var waiting = 0;

      function fileComplete() {
        if (--waiting <= 0) {
          callback();
        }
      }

      localPaths.forEach(function (localPath) {
        waiting++;
        fs.readFile(localPath, handlerFilter.expectJsonContents(localPath,
          function (error, jsonContent) {
            if (error) {
              fileComplete();
            } else {
              processJson(jsonContent, localPath, options, fileComplete);
            }
          }
        ));
      });
    }

    function processZipFile(zipPath, options, callback) {
      var unzipper = new DecompressZip(zipPath);

      unzipper
        .once("extract", function (log) {
          grunt.log.debug("%s extract log:\n%j", zipPath, log);
          if (options.mediaOut != null) {
            var extractedFiles = log.map(function (extractResult) {
                return extractResult.deflated || extractResult.stored;
              }).filter(function (extractedPath) {
                return Boolean(extractedPath);
              }).map(function (extractedPath) {
                return path.join(options.zipOut, extractedPath);
              });

            if (extractedFiles.length > 0) {
              processJsonFiles(extractedFiles, options, callback);
              return;
            }
          }
          // fell through to here because options.mediaOut was not set,
          // or the zip file contained no usable files
          callback();
        })
        .once("error", function(error) {
          grunt.log.error("Error unzipping file %j: %s", zipPath, error);
          callback();
        });

      unzipper.extract({ path: options.zipOut });
    }

    function generateZipFileName() {
      return ZIP_FILE_NAME_PREFIX + Date.now() + ".zip";
    }

    function requestZip(zipUrl, options, callback) {
      var zipPath = path.join(
        options.zipOut, ZIP_FOLDER_NAME, generateZipFileName());

      mkdirp(path.dirname(zipPath), null, function (mkdirError) {
        if (mkdirError) {
          callback();
          return;
        }

        grunt.verbose.writeln("Downloading zip");
        shampooUtils.downloadToFile(zipUrl, zipPath, handlerFilter.expectHttpOk(zipUrl,
          function (error) {
            if (error) {
              callback();
            } else {
              processZipFile(zipPath, options, callback);
            }
          }
        ));
      });
    }

    function saveMedia(options, mediaAssets, callback) {

      grunt.log.subhead( "Retrieving files..." );

      grunt.log.debug("Media queue is:");
      mediaAssets.forEach(function (p) {
        grunt.log.debug("  %j", p);
      });

      var loadCounter = 0;
      var next = function() {
        loadCounter--;
        fillQueue();
      };

      var fillQueue = function() {
        if (mediaAssets.length === 0 && loadCounter === 0) {
          callback();
        } else {
          while (mediaAssets.length > 0 && loadCounter < options.maxConnections) {
            loadCounter++;
            verifyDownload( mediaAssets.shift(), options.mediaOut, next );
          }
        }
      };

      fillQueue();
    }

    function verifyDownload( remotePath, mediaOut, callback ) {
      var localPath = path.join(mediaOut, remotePath),
          tryOptions = {
            logError:   grunt.log.error.bind(grunt.log),
            logVerbose: grunt.verbose.writeln.bind(grunt.verbose),
            logDebug:   grunt.log.debug.bind(grunt.log)
          };

      grunt.log.debug("Verifying %j -> %j", remotePath, localPath);

      shampooUtils.hashFile(localPath, "md5", function (error, hasher) {
        if (error) {
          grunt.log.debug("Etag calculation of local file failed: %s", error);
        } else {
          tryOptions.etag = hasher.digest("hex");
        }

        mkdirp(path.dirname(localPath), function (error) {
          if (error) {
            callback();
          } else {
            tryHttpDownload(
              function (headers, tryCallback) {
                grunt.log.debug("S3 GET %j", remotePath);
                knoxClient.getFile(remotePath, headers, tryCallback);
              },
              localPath,
              tryOptions,
              function (error, response) {
                grunt.log.write("%s ", localPath);
                if (error) {
                  grunt.log.error("%j", error);
                } else if (response.statusCode === HTTP_STATUS_NOT_MODIFIED) {
                  grunt.log.ok("up to date");
                } else {
                  grunt.log.ok("downloaded");
                }
                callback();
              }
            );
          }
        });
      });
    }

    function writeJsonFile(out, object) {
      grunt.log.write( out + " ");
      grunt.log.ok( "saved" );
      grunt.file.write(out, JSON.stringify(object, null, '\t'));
    }

    function processJson(jsonContent, outJsonFile, options, callback) {
      var result = getMediaAssets(jsonContent, options.mediaCwd);
      if (outJsonFile) {
        writeJsonFile(outJsonFile, result.newJson);
        if (options.mediaOut != null) {
          saveMedia(options, result.remotePaths, callback);
          return;
        }
      }
      callback();
    }

    function requestJson(jsonUrl, options, callback) {
      request(jsonUrl, handlerFilter.expectJsonResponse(jsonUrl,
        function (error, response, jsonContent) {
          if (error) {
            callback();
          } else {
            processJson(jsonContent, options.out, options, callback);
          }
        }
      ));
    }

    function requestFiles(options, callback) {
      grunt.log.subhead( "Retrieving content..." );

      var apiUrl = shampooApi.createApiUrl(options);
      grunt.verbose.writeln("Url is %j", apiUrl);

      if (shampooApi.isZipQuery(options.query)) {
        grunt.verbose.writeln("Zip job");
        requestZip(apiUrl, options, callback);
      } else {
        grunt.verbose.writeln("JSON job");
        requestJson(apiUrl, options, callback);
      }
    }

    function makeClient(options) {
      return knox.createClient( _.pick(options, [
        'region', 'endpoint', 'port', 'key', 'secret', 'access', 'bucket',
        'secure', 'headers', 'style'
      ]));
    }

    function getOptions() {
      // Mix in default options, .shampoorc file
      var options = rc("shampoo", thisTask.options({
        api: 1,
        query: "dump/json/single-file",
        out: "data/content.json",
        mediaOut: null,
        mediaCwd: null,
        maxConnections: DEFAULT_MAX_CONNECTIONS
      }));

      var messages = [ ];
      var missing = { };
      var required = [ "key", "secret", "domain", "query", "out" ];
      var isOk = true;

      if (shampooApi.isZipQuery(options.query)) {
        required.push("zipOut");
      }

      required.forEach(function (optionName) {
        if (!options[optionName]) {
          missing[optionName] = true;
        }
      });

      var missingArray = Object.keys(missing);
      if (missingArray.length > 0) {
        messages.push("The following required options are not set: " +
          missingArray.join(", "));
        isOk = false;
      }

      if (missing.key || missing.secret) {
        messages.push("Values for 'key' and 'secret' are found in your Shampoo account under Settings.");
      }

      if (missing.zipOut) {
        messages.push(util.format(
          "The query %j returns a zip file. This requires the 'zipOut' option to be set.",
          options.query
        ));
      }

      if (options.mediaCwd == null) {
        options.mediaCwd = options.mediaOut;
      }

      var parsedMaxConnections = parseInt(options.maxConnections, 10);
      if (isNaN(options.maxConnections)) {
        messages.push(util.format(
          "Invalid value for maxConnections: %j. Assuming default of %d.",
          options.maxConnections, DEFAULT_MAX_CONNECTIONS
        ));
        parsedMaxConnections = DEFAULT_MAX_CONNECTIONS;
      } else if (parsedMaxConnections < 1) {
        messages.push(
          "Value for maxConnections must be 1 or greater. Assuming 1.");
        parsedMaxConnections = 1;
      }

      options.maxConnections = parsedMaxConnections;

      return {
        options: options,
        messages: messages,
        ok: isOk
      };
    }

    function main() {
      var optionResult = getOptions();
      var messagesString = optionResult.messages.join("\n");
      var callback = thisTask.async();

      if (optionResult.ok) {
        var options = optionResult.options;
        grunt.log.writeln(messagesString);
        if (options.mediaOut) {
          knoxClient = makeClient(options.aws || { });
        }
        requestFiles(optionResult.options, callback);
      } else {
        grunt.log.error(messagesString);
        callback(false);
      }

    }

    return main();
  });
};
<|endoftext|>


<commit_before>import htmlToElement from 'html-to-element';
import classNames from 'classnames';

let $imageBox;
let $imageShadow;
let $imageWrapper;
let imageList = [];
let currentIndex = 0;

export default class ImageWrapper {
    /**
     * creates an ImageWrapper and ImageShadow element on DOM with given images
     * @param {array} urls
     * @param {int} startIndex
     */
    static show = (urls, startIndex) => {
        ImageWrapper.hide();

        currentIndex = startIndex;

        $imageShadow = htmlToElement('

');
        $imageWrapper = htmlToElement('

');
        $imageBox = htmlToElement('

');

        const navPrev = htmlToElement('

');
        const navNext = htmlToElement('

');

        document.body.appendChild($imageShadow);
        document.body.appendChild($imageWrapper);

        urls.forEach((url) => {
            imageList.push({
                url
            });
        });

        if (imageList.length > 1) $imageWrapper.appendChild(navPrev);
        $imageWrapper.appendChild($imageBox);
        if (imageList.length > 1) $imageWrapper.appendChild(navNext);

        const prevImg = imageList.length > 1 ? getImage(getPreviousIndex(), false) : null;
        const nextImg = imageList.length > 1 ? getImage(getNextIndex(), false) : null;
        const curImg = getImage(currentIndex, true);

        if (prevImg) $imageBox.appendChild(prevImg);
        $imageBox.appendChild(curImg);
        if (nextImg) $imageBox.appendChild(nextImg);

        document.body.addEventListener('keydown', handleKeyDown);
        $imageWrapper.addEventListener('click', handleClick);
    };

    /**
     * hides ImageWrapper
     */
    static hide = () => {
        if ($imageWrapper) document.body.removeChild($imageWrapper);
        if ($imageShadow) document.body.removeChild($imageShadow);

        if (document.body) document.body.removeEventListener('keydown', handleKeyDown);
        if ($imageWrapper) $imageWrapper.removeEventListener('click', handleClick);

        $imageWrapper = null;
        $imageShadow = null;
        imageList = [];
    };

    /**
     * navigates to next image
     */
    static next = () => {
        const newIndex = getNextIndex();

        document.querySelector(`#imageContainer_${currentIndex}`).classList.add('image-wrapper__image-box__image-container--hide');

        currentIndex = newIndex;

        $imageBox.removeChild($imageBox.firstChild);

        const nextImg = getImage(getNextIndex(), false);

        if (nextImg) $imageBox.appendChild(nextImg);

        document.querySelector(`#imageContainer_${currentIndex}`).classList.remove('image-wrapper__image-box__image-container--hide');
    };

    /**
     * navigates to previous image
     */
    static prev = () => {
        const newIndex = getPreviousIndex();

        document.querySelector(`#imageContainer_${currentIndex}`).classList.add('image-wrapper__image-box__image-container--hide');

        currentIndex = newIndex;

        $imageBox.removeChild($imageBox.lastChild);

        const nextImg = getImage(getPreviousIndex(), false);

        if (nextImg) $imageBox.insertBefore(nextImg, $imageBox.firstChild);

        document.querySelector(`#imageContainer_${currentIndex}`).classList.remove('image-wrapper__image-box__image-container--hide');
    };
}

/**
 * handles key down events and their actions
 * @param event
 */
function handleKeyDown(event) {
    if (event.keyCode === 27) {
        ImageWrapper.hide();
    }

    if (event.keyCode === 37) {
        ImageWrapper.prev();
    }

    if (event.keyCode === 39) {
        ImageWrapper.next();
    }
}

/**
 * handles click events and their actions
 * @param event
 */
function handleClick(event) {
    const targetClassList = event.target.classList;

    if (targetClassList.contains('image-wrapper__nav-button') || targetClassList.contains('image-wrapper__image-box__image-container__img-button') || targetClassList.contains('image-wrapper__nav-button__icon')) {
        if (targetClassList.contains('image-wrapper__nav-button--next') || targetClassList.contains('image-wrapper__image-box__image-container__img-button--next') || targetClassList.contains('image-wrapper__nav-button__icon--next')) {
            ImageWrapper.next();
        } else {
            ImageWrapper.prev();
        }
    } else if (!targetClassList.contains('image-wrapper__image-box__image-container__image')) {
        ImageWrapper.hide();
    }
}

/**
 * returns a Image node
 * @param {int} index
 * @param {boolean} show
 * @returns {*}
 */
function getImage(index, show) {
    const containerClasses = classNames('image-wrapper__image-box__image-container', {
        'image-wrapper__image-box__image-container--hide': !show
    });

    const imgBtnPrevClasses = classNames('image-wrapper__image-box__image-container__img-button', 'image-wrapper__image-box__image-container__img-button--prev', {
        'image-wrapper__image-box__image-container__img-button--hide': imageList.length < 2
    });

    const imgBtnNextClasses = classNames('image-wrapper__image-box__image-container__img-button', 'image-wrapper__image-box__image-container__img-button--next', {
        'image-wrapper__image-box__image-container__img-button--hide': imageList.length < 2
    });

    const image = imageList[index];

    return htmlToElement(`

`);
}

/**
 * gives previous index
 */
function getPreviousIndex() {
    let index = currentIndex - 1;

    const imageCount = imageList.length;

    if (index < 0) index += imageCount;

    return index;
}

/**
 * gives next index
 */
function getNextIndex() {
    let index = currentIndex + 1;

    const imageCount = imageList.length;

    if (index > imageCount - 1) index -= imageCount;

    return index;
}
<commit_msg>adds close image-wrapper on shadow click<commit_after>import htmlToElement from 'html-to-element';
import classNames from 'classnames';

let $imageBox;
let $imageShadow;
let $imageWrapper;
let imageList = [];
let currentIndex = 0;

export default class ImageWrapper {
    /**
     * creates an ImageWrapper and ImageShadow element on DOM with given images
     * @param {array} urls
     * @param {int} startIndex
     */
    static show = (urls, startIndex) => {
        ImageWrapper.hide();

        currentIndex = startIndex;

        $imageShadow = htmlToElement('

');
        $imageWrapper = htmlToElement('

');
        $imageBox = htmlToElement('

');

        const navPrev = htmlToElement('

');
        const navNext = htmlToElement('

');

        document.body.appendChild($imageShadow);
        document.body.appendChild($imageWrapper);

        urls.forEach((url) => {
            imageList.push({
                url
            });
        });

        if (imageList.length > 1) $imageWrapper.appendChild(navPrev);
        $imageWrapper.appendChild($imageBox);
        if (imageList.length > 1) $imageWrapper.appendChild(navNext);

        const prevImg = imageList.length > 1 ? getImage(getPreviousIndex(), false) : null;
        const nextImg = imageList.length > 1 ? getImage(getNextIndex(), false) : null;
        const curImg = getImage(currentIndex, true);

        if (prevImg) $imageBox.appendChild(prevImg);
        $imageBox.appendChild(curImg);
        if (nextImg) $imageBox.appendChild(nextImg);

        $imageShadow.addEventListener('click', () => ImageWrapper.hide());
        document.body.addEventListener('keydown', handleKeyDown);
        $imageWrapper.addEventListener('click', handleClick);
    };

    /**
     * hides ImageWrapper
     */
    static hide = () => {
        if ($imageWrapper) document.body.removeChild($imageWrapper);
        if ($imageShadow) document.body.removeChild($imageShadow);

        if (document.body) document.body.removeEventListener('keydown', handleKeyDown);
        if ($imageWrapper) $imageWrapper.removeEventListener('click', handleClick);

        $imageWrapper = null;
        $imageShadow = null;
        imageList = [];
    };

    /**
     * navigates to next image
     */
    static next = () => {
        const newIndex = getNextIndex();

        document.querySelector(`#imageContainer_${currentIndex}`).classList.add('image-wrapper__image-box__image-container--hide');

        currentIndex = newIndex;

        $imageBox.removeChild($imageBox.firstChild);

        const nextImg = getImage(getNextIndex(), false);

        if (nextImg) $imageBox.appendChild(nextImg);

        document.querySelector(`#imageContainer_${currentIndex}`).classList.remove('image-wrapper__image-box__image-container--hide');
    };

    /**
     * navigates to previous image
     */
    static prev = () => {
        const newIndex = getPreviousIndex();

        document.querySelector(`#imageContainer_${currentIndex}`).classList.add('image-wrapper__image-box__image-container--hide');

        currentIndex = newIndex;

        $imageBox.removeChild($imageBox.lastChild);

        const nextImg = getImage(getPreviousIndex(), false);

        if (nextImg) $imageBox.insertBefore(nextImg, $imageBox.firstChild);

        document.querySelector(`#imageContainer_${currentIndex}`).classList.remove('image-wrapper__image-box__image-container--hide');
    };
}

/**
 * handles key down events and their actions
 * @param event
 */
function handleKeyDown(event) {
    if (event.keyCode === 27) {
        ImageWrapper.hide();
    }

    if (event.keyCode === 37) {
        ImageWrapper.prev();
    }

    if (event.keyCode === 39) {
        ImageWrapper.next();
    }
}

/**
 * handles click events and their actions
 * @param event
 */
function handleClick(event) {
    const targetClassList = event.target.classList;

    if (targetClassList.contains('image-wrapper__nav-button') || targetClassList.contains('image-wrapper__image-box__image-container__img-button') || targetClassList.contains('image-wrapper__nav-button__icon')) {
        if (targetClassList.contains('image-wrapper__nav-button--next') || targetClassList.contains('image-wrapper__image-box__image-container__img-button--next') || targetClassList.contains('image-wrapper__nav-button__icon--next')) {
            ImageWrapper.next();
        } else {
            ImageWrapper.prev();
        }
    } else if (!targetClassList.contains('image-wrapper__image-box__image-container__image')) {
        ImageWrapper.hide();
    }
}

/**
 * returns a Image node
 * @param {int} index
 * @param {boolean} show
 * @returns {*}
 */
function getImage(index, show) {
    const containerClasses = classNames('image-wrapper__image-box__image-container', {
        'image-wrapper__image-box__image-container--hide': !show
    });

    const imgBtnPrevClasses = classNames('image-wrapper__image-box__image-container__img-button', 'image-wrapper__image-box__image-container__img-button--prev', {
        'image-wrapper__image-box__image-container__img-button--hide': imageList.length < 2
    });

    const imgBtnNextClasses = classNames('image-wrapper__image-box__image-container__img-button', 'image-wrapper__image-box__image-container__img-button--next', {
        'image-wrapper__image-box__image-container__img-button--hide': imageList.length < 2
    });

    const image = imageList[index];

    return htmlToElement(`

`);
}

/**
 * gives previous index
 */
function getPreviousIndex() {
    let index = currentIndex - 1;

    const imageCount = imageList.length;

    if (index < 0) index += imageCount;

    return index;
}

/**
 * gives next index
 */
function getNextIndex() {
    let index = currentIndex + 1;

    const imageCount = imageList.length;

    if (index > imageCount - 1) index -= imageCount;

    return index;
}
<|endoftext|>