<commit_before>const _ = require('lodash');
module.exports = class MemberRepository {
    /**
     * @param {object} deps
     * @param {any} deps.Member
     * @param {any} deps.StripeCustomer
     * @param {any} deps.StripeCustomerSubscription
     * @param {import('../../services/stripe-api')} deps.stripeAPIService
     * @param {import('../../services/stripe-plans')} deps.stripePlansService
     * @param {any} deps.logger
     */
    constructor({
        Member,
        StripeCustomer,
        StripeCustomerSubscription,
        stripeAPIService,
        stripePlansService,
        logger
    }) {
        this._Member = Member;
        this._StripeCustomer = StripeCustomer;
        this._StripeCustomerSubscription = StripeCustomerSubscription;
        this._stripeAPIService = stripeAPIService;
        this._stripePlansService = stripePlansService;
        this._logging = logger;
    }

    async get(data, options) {
        if (data.customer_id) {
            const customer = await this._StripeCustomer.findOne({
                customer_id: data.customer_id
            }, {
                withRelated: ['member']
            });
            if (customer) {
                return customer.related('member');
            }
            return null;
        }
        return this._Member.findOne(data, options);
    }

    async create(data, options) {
        const {labels} = data;

        if (labels) {
            labels.forEach((label, index) => {
                if (typeof label === 'string') {
                    labels[index] = {name: label};
                }
            });
        }

        const memberData = _.pick(data, ['email', 'name', 'note', 'subscribed', 'geolocation', 'created_at']);

        return this._Member.add({
            ...memberData,
            labels
        }, options);
    }

    async update(data, options) {
        const member = await this._Member.edit(_.pick(data, [
            'email',
            'name',
            'note',
            'subscribed',
            'labels',
            'geolocation'
        ]), options);

        if (this._stripeAPIService && member._changed.email) {
            await member.related('stripeCustomers').fetch();
            const customers = member.related('stripeCustomers');
            for (const customer of customers.models) {
                await this._stripeAPIService.updateCustomerEmail(
                    customer.get('customer_id'),
                    member.get('email')
                );
            }
        }

        return member;
    }

    async list(options) {
        return this._Member.findPage(options);
    }

    async destroy(data, options) {
        const member = await this._Member.findOne(data, options);

        if (!member) {
            // throw error?
            return;
        }

        if (this._stripeAPIService && options.cancelStripeSubscriptions) {
            await member.related('stripeSubscriptions');
            const subscriptions = member.related('stripeSubscriptions');
            for (const subscription of subscriptions.models) {
                if (subscription.get('status') !== 'canceled') {
                    const updatedSubscription = await this._stripeAPIService.cancelSubscription(
                        subscription.get('subscription_id')
                    );
                    await this._StripeCustomerSubscription.update({
                        status: updatedSubscription.status
                    });
                }
            }
        }

        return this._Member.destroy({
            id: data.id
        }, options);
    }

    async upsertCustomer(data) {
        return await this._StripeCustomer.upsert({
            customer_id: data.customer_id,
            member_id: data.member_id,
            name: data.name,
            email: data.email
        });
    }

    async linkStripeCustomer(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot link Stripe Customer with no Stripe Connection');
        }
        const customer = await this._stripeAPIService.getCustomer(data.customer_id);

        if (!customer) {
            return;
        }

        // Add instead of upsert ensures that we do not link existing customer
        await this._StripeCustomer.add({
            customer_id: data.customer_id,
            member_id: data.member_id,
            name: customer.name,
            email: customer.email
        }, options);

        for (const subscription of customer.subscriptions.data) {
            await this.linkSubscription({
                id: data.member_id,
                subscription
            }, options);
        }
    }

    async linkSubscription(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot link Stripe Subscription with no Stripe Connection');
        }
        const member = await this._Member.findOne({
            id: data.id
        }, options);

        const customer = await member.related('stripeCustomers').query({
            where: {
                customer_id: data.subscription.customer
            }
        }).fetchOne(options);

        if (!customer) {
            // Maybe just link the customer?
            throw new Error('Subscription is not associated with a customer for the member');
        }

        const subscription = await this._stripeAPIService.getSubscription(data.subscription.id);
        let paymentMethodId;
        if (!subscription.default_payment_method) {
            paymentMethodId = null;
        } else if (typeof subscription.default_payment_method === 'string') {
            paymentMethodId = subscription.default_payment_method;
        } else {
            paymentMethodId = subscription.default_payment_method.id;
        }
        const paymentMethod = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null;
        await this._StripeCustomerSubscription.upsert({
            customer_id: subscription.customer,
            subscription_id: subscription.id,
            status: subscription.status,
            cancel_at_period_end: subscription.cancel_at_period_end,
            cancellation_reason: subscription.metadata && subscription.metadata.cancellation_reason || null,
            current_period_end: new Date(subscription.current_period_end * 1000),
            start_date: new Date(subscription.start_date * 1000),
            default_payment_card_last4: paymentMethod && paymentMethod.card && paymentMethod.card.last4 || null,

            plan_id: subscription.plan.id,
            // NOTE: Defaulting to interval as migration to nullable field
            // turned out to be much bigger problem.
            // Ideally, would need nickname field to be nullable on the DB level
            // condition can be simplified once this is done
            plan_nickname: subscription.plan.nickname || subscription.plan.interval,
            plan_interval: subscription.plan.interval,
            plan_amount: subscription.plan.amount,
            plan_currency: subscription.plan.currency
        }, {
            ...options,
            subscription_id: subscription.id
        });
    }

    async updateSubscription(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot update Stripe Subscription with no Stripe Connection');
        }
        const member = await this._Member.findOne({
            id: data.id
        });

        const subscription = await member.related('stripeSubscriptions').query({
            where: {
                subscription_id: data.subscription.subscription_id
            }
        }).fetchOne(options);

        if (!subscription) {
            throw new Error('Subscription not found');
        }

        if (data.subscription.cancel_at_period_end === undefined) {
            throw new Error('Incorrect usage');
        }

        if (data.subscription.cancel_at_period_end) {
            await this._stripeAPIService.cancelSubscriptionAtPeriodEnd(data.subscription.subscription_id);
        } else {
            await this._stripeAPIService.continueSubscriptionAtPeriodEnd(data.subscription.subscription_id);
        }

        await this._StripeCustomerSubscription.edit({
            subscription_id: data.subscription.subscription_id,
            cancel_at_period_end: data.subscription.cancel_at_period_end
        }, {
            id: subscription.id
        });
    }

    async setComplimentarySubscription(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot update Stripe Subscription with no Stripe Connection');
        }
        const member = await this._Member.findOne({
            id: data.id
        }, options);

        const subscriptions = await member.related('stripeSubscriptions').fetch(options);

        const activeSubscriptions = subscriptions.models.filter((subscription) => {
            return ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.get('status'));
        });

        // NOTE: Because we allow for multiple Complimentary plans, need to take into account currently availalbe
        //       plan currencies so that we don't end up giving a member complimentary subscription in wrong currency.
        //       Giving member a subscription in different currency would prevent them from resubscribing with a regular
        //       plan if Complimentary is cancelled (ref. https://stripe.com/docs/billing/customer#currency)
        let complimentaryCurrency = this._stripePlansService.getPlans().find(plan => plan.interval === 'month').currency.toLowerCase();

        if (activeSubscriptions.length) {
            complimentaryCurrency = activeSubscriptions[0].get('plan_currency').toLowerCase();
        }

        const complimentaryPlan = this._stripePlansService.getComplimentaryPlan(complimentaryCurrency);

        if (!complimentaryPlan) {
            throw new Error('Could not find Complimentary plan');
        }

        let stripeCustomer;

        await member.related('stripeCustomers').fetch(options);

        for (const customer of member.related('stripeCustomers').models) {
            try {
                const fetchedCustomer = await this._stripeAPIService.getCustomer(customer.get('customer_id'));
                if (!fetchedCustomer.deleted) {
                    stripeCustomer = fetchedCustomer;
                    break;
                }
            } catch (err) {
                console.log('Ignoring error for fetching customer for checkout');
            }
        }

        if (!stripeCustomer) {
            stripeCustomer = await this._stripeAPIService.createCustomer({
                email: member.get('email')
            });

            await this._StripeCustomer.upsert({
                customer_id: stripeCustomer.id,
                member_id: data.id,
                email: stripeCustomer.email,
                name: stripeCustomer.name
            }, options);
        }

        if (!activeSubscriptions.length) {
            const subscription = await this._stripeAPIService.createSubscription(stripeCustomer.id, complimentaryPlan.id);

            await this.linkSubscription({
                id: member.id,
                subscription
            }, options);
        } else {
            // NOTE: we should only ever have 1 active subscription, but just in case there is more update is done on all of them
            for (const subscription of activeSubscriptions) {
                const updatedSubscription = await this._stripeAPIService.changeSubscriptionPlan(
                    subscription.id,
                    complimentaryPlan.id
                );

                await this.linkSubscription({
                    id: member.id,
                    subscription: updatedSubscription
                }, options);
            }
        }
    }

    async cancelComplimentarySubscription(data) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot cancel Complimentary Subscription with no Stripe Connection');
        }

        const member = await this._Member.findOne({
            id: data.id
        });

        const subscriptions = await member.related('stripeSubscriptions').fetch();

        for (const subscription of subscriptions.models) {
            if (subscription.get('status') !== 'canceled') {
                try {
                    const updatedSubscription = await this._stripeAPIService.cancelSubscription(
                        subscription.get('subscription_id')
                    );
                    // Only needs to update `status`
                    await this.linkSubscription({
                        id: data.id,
                        subscription: updatedSubscription
                    });
                } catch (err) {
                    this._logging.error(`There was an error cancelling subscription ${subscription.get('subscription_id')}`);
                    this._logging.error(err);
                }
            }
        }
        return true;
    }
};
<commit_msg>🐛 Fixed comp plans for members w/ active subscriptions<commit_after>const _ = require('lodash');
module.exports = class MemberRepository {
    /**
     * @param {object} deps
     * @param {any} deps.Member
     * @param {any} deps.StripeCustomer
     * @param {any} deps.StripeCustomerSubscription
     * @param {import('../../services/stripe-api')} deps.stripeAPIService
     * @param {import('../../services/stripe-plans')} deps.stripePlansService
     * @param {any} deps.logger
     */
    constructor({
        Member,
        StripeCustomer,
        StripeCustomerSubscription,
        stripeAPIService,
        stripePlansService,
        logger
    }) {
        this._Member = Member;
        this._StripeCustomer = StripeCustomer;
        this._StripeCustomerSubscription = StripeCustomerSubscription;
        this._stripeAPIService = stripeAPIService;
        this._stripePlansService = stripePlansService;
        this._logging = logger;
    }

    async get(data, options) {
        if (data.customer_id) {
            const customer = await this._StripeCustomer.findOne({
                customer_id: data.customer_id
            }, {
                withRelated: ['member']
            });
            if (customer) {
                return customer.related('member');
            }
            return null;
        }
        return this._Member.findOne(data, options);
    }

    async create(data, options) {
        const {labels} = data;

        if (labels) {
            labels.forEach((label, index) => {
                if (typeof label === 'string') {
                    labels[index] = {name: label};
                }
            });
        }

        const memberData = _.pick(data, ['email', 'name', 'note', 'subscribed', 'geolocation', 'created_at']);

        return this._Member.add({
            ...memberData,
            labels
        }, options);
    }

    async update(data, options) {
        const member = await this._Member.edit(_.pick(data, [
            'email',
            'name',
            'note',
            'subscribed',
            'labels',
            'geolocation'
        ]), options);

        if (this._stripeAPIService && member._changed.email) {
            await member.related('stripeCustomers').fetch();
            const customers = member.related('stripeCustomers');
            for (const customer of customers.models) {
                await this._stripeAPIService.updateCustomerEmail(
                    customer.get('customer_id'),
                    member.get('email')
                );
            }
        }

        return member;
    }

    async list(options) {
        return this._Member.findPage(options);
    }

    async destroy(data, options) {
        const member = await this._Member.findOne(data, options);

        if (!member) {
            // throw error?
            return;
        }

        if (this._stripeAPIService && options.cancelStripeSubscriptions) {
            await member.related('stripeSubscriptions');
            const subscriptions = member.related('stripeSubscriptions');
            for (const subscription of subscriptions.models) {
                if (subscription.get('status') !== 'canceled') {
                    const updatedSubscription = await this._stripeAPIService.cancelSubscription(
                        subscription.get('subscription_id')
                    );
                    await this._StripeCustomerSubscription.update({
                        status: updatedSubscription.status
                    });
                }
            }
        }

        return this._Member.destroy({
            id: data.id
        }, options);
    }

    async upsertCustomer(data) {
        return await this._StripeCustomer.upsert({
            customer_id: data.customer_id,
            member_id: data.member_id,
            name: data.name,
            email: data.email
        });
    }

    async linkStripeCustomer(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot link Stripe Customer with no Stripe Connection');
        }
        const customer = await this._stripeAPIService.getCustomer(data.customer_id);

        if (!customer) {
            return;
        }

        // Add instead of upsert ensures that we do not link existing customer
        await this._StripeCustomer.add({
            customer_id: data.customer_id,
            member_id: data.member_id,
            name: customer.name,
            email: customer.email
        }, options);

        for (const subscription of customer.subscriptions.data) {
            await this.linkSubscription({
                id: data.member_id,
                subscription
            }, options);
        }
    }

    async linkSubscription(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot link Stripe Subscription with no Stripe Connection');
        }
        const member = await this._Member.findOne({
            id: data.id
        }, options);

        const customer = await member.related('stripeCustomers').query({
            where: {
                customer_id: data.subscription.customer
            }
        }).fetchOne(options);

        if (!customer) {
            // Maybe just link the customer?
            throw new Error('Subscription is not associated with a customer for the member');
        }

        const subscription = await this._stripeAPIService.getSubscription(data.subscription.id);
        let paymentMethodId;
        if (!subscription.default_payment_method) {
            paymentMethodId = null;
        } else if (typeof subscription.default_payment_method === 'string') {
            paymentMethodId = subscription.default_payment_method;
        } else {
            paymentMethodId = subscription.default_payment_method.id;
        }
        const paymentMethod = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null;
        await this._StripeCustomerSubscription.upsert({
            customer_id: subscription.customer,
            subscription_id: subscription.id,
            status: subscription.status,
            cancel_at_period_end: subscription.cancel_at_period_end,
            cancellation_reason: subscription.metadata && subscription.metadata.cancellation_reason || null,
            current_period_end: new Date(subscription.current_period_end * 1000),
            start_date: new Date(subscription.start_date * 1000),
            default_payment_card_last4: paymentMethod && paymentMethod.card && paymentMethod.card.last4 || null,

            plan_id: subscription.plan.id,
            // NOTE: Defaulting to interval as migration to nullable field
            // turned out to be much bigger problem.
            // Ideally, would need nickname field to be nullable on the DB level
            // condition can be simplified once this is done
            plan_nickname: subscription.plan.nickname || subscription.plan.interval,
            plan_interval: subscription.plan.interval,
            plan_amount: subscription.plan.amount,
            plan_currency: subscription.plan.currency
        }, {
            ...options,
            subscription_id: subscription.id
        });
    }

    async updateSubscription(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot update Stripe Subscription with no Stripe Connection');
        }
        const member = await this._Member.findOne({
            id: data.id
        });

        const subscription = await member.related('stripeSubscriptions').query({
            where: {
                subscription_id: data.subscription.subscription_id
            }
        }).fetchOne(options);

        if (!subscription) {
            throw new Error('Subscription not found');
        }

        if (data.subscription.cancel_at_period_end === undefined) {
            throw new Error('Incorrect usage');
        }

        if (data.subscription.cancel_at_period_end) {
            await this._stripeAPIService.cancelSubscriptionAtPeriodEnd(data.subscription.subscription_id);
        } else {
            await this._stripeAPIService.continueSubscriptionAtPeriodEnd(data.subscription.subscription_id);
        }

        await this._StripeCustomerSubscription.edit({
            subscription_id: data.subscription.subscription_id,
            cancel_at_period_end: data.subscription.cancel_at_period_end
        }, {
            id: subscription.id
        });
    }

    async setComplimentarySubscription(data, options) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot update Stripe Subscription with no Stripe Connection');
        }
        const member = await this._Member.findOne({
            id: data.id
        }, options);

        const subscriptions = await member.related('stripeSubscriptions').fetch(options);

        const activeSubscriptions = subscriptions.models.filter((subscription) => {
            return ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.get('status'));
        });

        // NOTE: Because we allow for multiple Complimentary plans, need to take into account currently availalbe
        //       plan currencies so that we don't end up giving a member complimentary subscription in wrong currency.
        //       Giving member a subscription in different currency would prevent them from resubscribing with a regular
        //       plan if Complimentary is cancelled (ref. https://stripe.com/docs/billing/customer#currency)
        let complimentaryCurrency = this._stripePlansService.getPlans().find(plan => plan.interval === 'month').currency.toLowerCase();

        if (activeSubscriptions.length) {
            complimentaryCurrency = activeSubscriptions[0].get('plan_currency').toLowerCase();
        }

        const complimentaryPlan = this._stripePlansService.getComplimentaryPlan(complimentaryCurrency);

        if (!complimentaryPlan) {
            throw new Error('Could not find Complimentary plan');
        }

        let stripeCustomer;

        await member.related('stripeCustomers').fetch(options);

        for (const customer of member.related('stripeCustomers').models) {
            try {
                const fetchedCustomer = await this._stripeAPIService.getCustomer(customer.get('customer_id'));
                if (!fetchedCustomer.deleted) {
                    stripeCustomer = fetchedCustomer;
                    break;
                }
            } catch (err) {
                console.log('Ignoring error for fetching customer for checkout');
            }
        }

        if (!stripeCustomer) {
            stripeCustomer = await this._stripeAPIService.createCustomer({
                email: member.get('email')
            });

            await this._StripeCustomer.upsert({
                customer_id: stripeCustomer.id,
                member_id: data.id,
                email: stripeCustomer.email,
                name: stripeCustomer.name
            }, options);
        }

        if (!activeSubscriptions.length) {
            const subscription = await this._stripeAPIService.createSubscription(stripeCustomer.id, complimentaryPlan.id);

            await this.linkSubscription({
                id: member.id,
                subscription
            }, options);
        } else {
            // NOTE: we should only ever have 1 active subscription, but just in case there is more update is done on all of them
            for (const subscription of activeSubscriptions) {
                const updatedSubscription = await this._stripeAPIService.changeSubscriptionPlan(
                    subscription.get('subscription_id'),
                    complimentaryPlan.id
                );

                await this.linkSubscription({
                    id: member.id,
                    subscription: updatedSubscription
                }, options);
            }
        }
    }

    async cancelComplimentarySubscription(data) {
        if (!this._stripeAPIService.configured) {
            throw new Error('Cannot cancel Complimentary Subscription with no Stripe Connection');
        }

        const member = await this._Member.findOne({
            id: data.id
        });

        const subscriptions = await member.related('stripeSubscriptions').fetch();

        for (const subscription of subscriptions.models) {
            if (subscription.get('status') !== 'canceled') {
                try {
                    const updatedSubscription = await this._stripeAPIService.cancelSubscription(
                        subscription.get('subscription_id')
                    );
                    // Only needs to update `status`
                    await this.linkSubscription({
                        id: data.id,
                        subscription: updatedSubscription
                    });
                } catch (err) {
                    this._logging.error(`There was an error cancelling subscription ${subscription.get('subscription_id')}`);
                    this._logging.error(err);
                }
            }
        }
        return true;
    }
};
<|endoftext|>


<commit_before>(function () {

  "use strict";

  describe('jQuery NeoTabs TestSuite', function () {

    var fixtureBasic = '
' +
      '
' +
        '
Tabbody Heading
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 2
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 3
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 4
' +
        '
Lorem Ipsum
' +
      '
' +
    '
';

    var fixtureDropdown = '
' +
      '
' +
        '
Tabbody Heading
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 2
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 3
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 4
' +
        '
Lorem Ipsum
' +
      '
' +
    '
';

    var defaultOptions = {
      wrapperClass: 'content',
      activeClass: 'active',
      tabHeadClass: 'tabhead',
      tabBodyClass: 'tabbody',
      firstTabClass: 'first',
      lastTabClass: 'last',
      clearfixClass: 'group',
      tabsListClass: 'tabs-list',
      tabHeadElement: 'h4',
      tabsPosition: 'top',
      cssClassAvailable: true,
      fx: 'show',
      fxSpeed: 0,
      autoAnchor: true,
      wrapInnerTabs: '',
      dropdownTabLabel: '▾',
      dropdownTabClass: 'dropdown',
      dropdownTabsListClass: 'tabs-list',
      dropdownTabsClearfixClass: 'group'
    };

    var options;

    beforeEach(function () {
      options = defaultOptions;
    });

    describe('Setup', function () {

      it('tests if jQuery is loaded', function () {
        expect(!!$).toBe(true);
        expect(typeof($)).toBe('function');
      });

      it('tests if NeoTabs is a jQuery plugin', function () {
        expect(typeof($.fn.neoTabs)).toBe('function');
      });

      it('tests if NeoTabs constructor is avaible in global scope', function () {
        expect(!!NeoTabs).toBe(true);
        expect(typeof(NeoTabs)).toBe('function');
      });
    });

    describe('Public API', function () {
      var $el = $(fixtureBasic),
          $neoTabs = new NeoTabs($el, options);

      it('should have a method to activate tabs', function () {
        var methodExists = (typeof($neoTabs.activateTab) === 'function');
        expect(methodExists).toBe(true);
      });

      it('should have a method to open a dropdown', function () {
        var methodExists = (typeof($neoTabs.openDropdown) === 'function');
        expect(methodExists).toBe(true);
      });
    });

    describe('Initialization', function () {
      var $el = $(fixtureBasic),
          $neoTabs = new NeoTabs($el, options);

      it('should assign an element', function () {
        expect($neoTabs.$el).toBe($el);
      });

      it('should inject a content wrapper', function () {
        expect($neoTabs.$el).toContain('.' + options.wrapperClass);
      });

      it('should inject a tabs list', function () {
        expect($neoTabs.$el).toContain('.' + options.tabsListClass);
        expect($neoTabs.$el.find('.' + options.tabsListClass)).toBe('ul');
        expect($neoTabs.$el.find('.' + options.tabsListClass)).not.toBe('ol');
      });

      it('should give the tabs list a cleafix class', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).hasClass(options.clearfixClass)).toBe(true);
      });

      it('should set the first tab active', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).children(0).hasClass(options.activeClass)).toBe(true);
      });

      it('should give the first tab the firstTabClass', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).children(0).hasClass(options.firstTabClass)).toBe(true);
      });

      it('should give each tab a tabhead class', function () {
        $neoTabs.$el.find('.' + options.tabsListClass).children().each(function (i, tab) {
          expect($(tab).hasClass(options.tabHeadClass)).toBe(true);
        });
      });

      it('should give the last tab the lastTabClass', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).children(':last').hasClass(options.lastTabClass)).toBe(true);
      });

      it('should prepend tabs list in tabs by default', function () {
        expect($neoTabs.$el.children(0)).toBe('ul');
      });

      it('should apply pre-existing class of tabhead by default', function () {
        expect($neoTabs.$el).toContain('.foo');
        expect($neoTabs.$el.find('.foo')).toBe('li');
      });
    });

    describe('Dropdown initialization', function () {
      var $el = $(fixtureDropdown),
          $neoTabs = new NeoTabs($el, options);

      it('should have a dropdown tab', function () {
        expect($neoTabs.$el).toContain('.' + options.dropdownTabClass);
      });

      it('should have a dropdownTabsList of two tabs', function () {
        expect(typeof($neoTabs.dropdownTabsList)).toBe('object');
        expect(typeof($neoTabs.dropdownTabsList.tabs)).toBe('object');
        expect($neoTabs.dropdownTabsList.tabs.length).toBe(2);
      });
    });
  });
}());
<commit_msg>Added option tests<commit_after>(function () {

  "use strict";

  describe('jQuery NeoTabs TestSuite', function () {

    var fixtureBasic = '
' +
      '
' +
        '
Tabbody Heading
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 2
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 3
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 4
' +
        '
Lorem Ipsum
' +
      '
' +
    '
';

    var fixtureDropdown = '
' +
      '
' +
        '
Tabbody Heading
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 2
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 3
' +
        '
Lorem Ipsum
' +
      '
' +
      '
' +
        '
Tabbody Heading 4
' +
        '
Lorem Ipsum
' +
      '
' +
    '
';

    var defaultOptions = {
      wrapperClass: 'content',
      activeClass: 'active',
      tabHeadClass: 'tabhead',
      tabBodyClass: 'tabbody',
      firstTabClass: 'first',
      lastTabClass: 'last',
      clearfixClass: 'group',
      tabsListClass: 'tabs-list',
      tabHeadElement: 'h4',
      tabsPosition: 'top',
      cssClassAvailable: true,
      fx: 'show',
      fxSpeed: 0,
      autoAnchor: true,
      wrapInnerTabs: '',
      dropdownTabLabel: '▾',
      dropdownTabClass: 'dropdown',
      dropdownTabsListClass: 'tabs-list',
      dropdownTabsClearfixClass: 'group'
    };

    var options;

    beforeEach(function () {
      options = defaultOptions;
    });

    describe('Setup', function () {

      it('tests if jQuery is loaded', function () {
        expect(!!$).toBe(true);
        expect(typeof($)).toBe('function');
      });

      it('tests if NeoTabs is a jQuery plugin', function () {
        expect(typeof($.fn.neoTabs)).toBe('function');
      });

      it('tests if NeoTabs constructor is avaible in global scope', function () {
        expect(!!NeoTabs).toBe(true);
        expect(typeof(NeoTabs)).toBe('function');
      });
    });

    describe('Public API', function () {
      var $el = $(fixtureBasic),
          $neoTabs = new NeoTabs($el, options);

      it('should have a method to activate tabs', function () {
        var methodExists = (typeof($neoTabs.activateTab) === 'function');
        expect(methodExists).toBe(true);
      });

      it('should have a method to open a dropdown', function () {
        var methodExists = (typeof($neoTabs.openDropdown) === 'function');
        expect(methodExists).toBe(true);
      });
    });

    describe('Initialization', function () {
      var $el = $(fixtureBasic),
          $neoTabs = new NeoTabs($el, options);

      it('should assign an element', function () {
        expect($neoTabs.$el).toBe($el);
      });

      describe('Options initialization', function () {
        it('should have the right wrapperClass option', function () {
          expect($neoTabs.opts.wrapperClass).toBe(options.wrapperClass);
        });

        it('should have the right activeClass option', function () {
          expect($neoTabs.opts.activeClass).toBe(options.activeClass);
        });

        it('should have the right tabHeadClass option', function () {
          expect($neoTabs.opts.tabHeadClass).toBe(options.tabHeadClass);
        });

        it('should have the right tabBodyClass option', function () {
          expect($neoTabs.opts.tabBodyClass).toBe(options.tabBodyClass);
        });

        it('should have the right firstTabClass option', function () {
          expect($neoTabs.opts.firstTabClass).toBe(options.firstTabClass);
        });

        it('should have the right lastTabClass option', function () {
          expect($neoTabs.opts.lastTabClass).toBe(options.lastTabClass);
        });

        it('should have the right clearfixClass option', function () {
          expect($neoTabs.opts.clearfixClass).toBe(options.clearfixClass);
        });

        it('should have the right tabsListClass option', function () {
          expect($neoTabs.opts.tabsListClass).toBe(options.tabsListClass);
        });

        it('should have the right tabHeadElement option', function () {
          expect($neoTabs.opts.tabHeadElement).toBe(options.tabHeadElement);
        });

        it('should have the right tabsPosition option', function () {
          expect($neoTabs.opts.tabsPosition).toBe(options.tabsPosition);
        });

        it('should have the right cssClassAvailable option', function () {
          expect($neoTabs.opts.cssClassAvailable).toBe(options.cssClassAvailable);
        });
      });

      it('should inject a content wrapper', function () {
        expect($neoTabs.$el).toContain('.' + options.wrapperClass);
      });

      it('should inject a tabs list', function () {
        expect($neoTabs.$el).toContain('.' + options.tabsListClass);
        expect($neoTabs.$el.find('.' + options.tabsListClass)).toBe('ul');
        expect($neoTabs.$el.find('.' + options.tabsListClass)).not.toBe('ol');
      });

      it('should give the tabs list a cleafix class', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).hasClass(options.clearfixClass)).toBe(true);
      });

      it('should set the first tab active', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).children(0).hasClass(options.activeClass)).toBe(true);
      });

      it('should give the first tab the firstTabClass', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).children(0).hasClass(options.firstTabClass)).toBe(true);
      });

      it('should give each tab a tabhead class', function () {
        $neoTabs.$el.find('.' + options.tabsListClass).children().each(function (i, tab) {
          expect($(tab).hasClass(options.tabHeadClass)).toBe(true);
        });
      });

      it('should give the last tab the lastTabClass', function () {
        expect($neoTabs.$el.find('.' + options.tabsListClass).children(':last').hasClass(options.lastTabClass)).toBe(true);
      });

      it('should prepend tabs list in tabs by default', function () {
        expect($neoTabs.$el.children(0)).toBe('ul');
      });

      it('should apply pre-existing class of tabhead by default', function () {
        expect($neoTabs.$el).toContain('.foo');
        expect($neoTabs.$el.find('.foo')).toBe('li');
      });
    });

    describe('Dropdown initialization', function () {
      var $el = $(fixtureDropdown),
          $neoTabs = new NeoTabs($el, options);

      it('should have a dropdown tab', function () {
        expect($neoTabs.$el).toContain('.' + options.dropdownTabClass);
      });

      it('should have a dropdown list', function () {
        expect($neoTabs.$el.find('.'+options.dropdownTabClass).children('ul').length).toBe(1);
        expect($neoTabs.$el.find('.' + options.dropdownTabClass).children('ul').hasClass(options.dropdownTabsListClass)).toBe(true);
      });

      it('should have a dropdownTabsList of two tabs', function () {
        expect(typeof($neoTabs.dropdownTabsList)).toBe('object');
        expect(typeof($neoTabs.dropdownTabsList.tabs)).toBe('object');
        expect($neoTabs.dropdownTabsList.tabs.length).toBe(2);
      });
    });
  });
}());
<|endoftext|>


<commit_before><commit_msg>アラーム鳴動処理を実装<commit_after><|endoftext|>


<commit_before>jQuery(function($, undefined) {
	$.fn.extend({
		autocompleteSelMul: function() {
			return this.not('.autocompleteSelMulInited').each(function() {
				var me = $(this).hide().addClass('autocompleteSelMulInited'),
					meCont = me.parent().is('.selectCont') ? me.parent().hide() : me,
					val = me.val(),
					options = me.children('option'),
                    keepValue = me.data('keepvalue'),
					input = $('<input type="text" name="'+(keepValue && keepValue.length > 2 ? keepValue : me.attr('id')+'_new')+'" />').insertBefore(meCont),
					list = [],
					cur = [],
					sep = me.data('sep') || ',',
					sepJoin = me.data('sepjoin') || sep+' ',
					split = function (val) {
						return val.split(new RegExp(sep+'\s*'));
					},
					extractLast = function (term) {
						return split(term).pop();
					},
					writeForm = function() {
						var tmp = split(input.val()),
							val = [],
                            others = [];
						$.each(tmp, function() {
							var cur = $.trim(this),
                                curOpt = options.filter('[rel="'+cur+'"]');
							if (curOpt.length) {
								val.push(curOpt.attr('value'));
                            } else {
                                others.push(cur);
                            }
						});
						me.val(val);
                        if (keepValue) {
                            input.val(others.join(sepJoin));
                        } else {
                            input.attr('disabled', 'disabled');
                        }
					},
					reenable = function() {
						input.removeAttr('disabled');
					};

				me.removeProp('required').removeAttr('required');

				if (me.attr('placeholder'))
					input.attr('placeholder', me.attr('placeholder'));

				options.each(function() {
					var opt = $(this),
						txt = $.trim(opt.text());
					opt.attr('rel', txt);
					list.push({
						value: opt.attr('value'),
						label: txt
					});
				});
				if (val && val.length) {
					$.each(val, function(k, v) {
						cur.push(options.filter('[value="'+v+'"]').text());
					});
					cur.push('');
					input.val(cur.join(sepJoin));
				}

				input.autocomplete({
					minLength: 1,
					source: function(request, response) {
						// delegate back to autocomplete, but extract the last term
						response($.ui.autocomplete.filter(list, $.trim(extractLast(request.term))));
					},
					focus: function() {
						return false;
					},
                    search: function(e, ui) {
                        if ($.ui.version.indexOf('1.12') === 0) {
                            // Fix bug in jQuery.ui somewhere where menu.bindings just grows and grows
                            // See https://bugs.jqueryui.com/ticket/10050
                            $(this).data('ui-autocomplete').menu.bindings = $();
                        }
                    },
					select: function(event, ui) {
						var terms = split(this.value);
						// remove the current input
						terms.pop();
						// add the selected item
						terms.push(ui.item.label);
						// add placeholder to get the comma-and-space at the end
						terms.push('');
						this.value = terms.join(sepJoin);
													$(this).trigger('change');
						return false;
					}
				});

				me
					.on('autocompleteWriteForm', writeForm)
					.on('autocompleteReenable', reenable)
					.closest('form').on('submit', function(e) {
						if (!e.isDefaultPrevented())
							writeForm();
					});
			}).end();
		},
		autocompleteSel: function() {
			return this.not('.autocompleteSelInited').each(function() {
				var me = $(this).hide().addClass('autocompleteSelInited'),
					meCont = me.parent().is('.selectCont') ? me.parent().hide() : me,
					val,
					options,
					input = $('<input type="text" name="'+me.attr('id')+'_new" '+(me.attr('required') ? 'required="required"' : '')+'/>').insertBefore(meCont),
					list,
					writeForm = function() {
						var val = input.val(),
							curOpt = options.filter('[rel="'+$.trim(val)+'"]');
						if (curOpt.length) {
							me.val(curOpt.attr('value'));
                        }
						input.attr('disabled', 'disabled');
					},
					reenable = function() {
						input.removeAttr('disabled');
					},
                    init = function() {
                        options = me.children('option');
                        list = [];
                        options.each(function() {
                            var opt = $(this),
                                txt = $.trim(opt.text());
                            opt.attr('rel', txt);
                            list.push({
                                value: opt.attr('value'),
                                label: txt
                            });
                        });
                        val = me.val();
                        if (val && val.length) {
                            input.val(options.filter('[value="'+val+'"]').text());
                        } else {
                            input.val('');
                        }
                    };

				me.removeProp('required').removeAttr('required');
				if (me.attr('placeholder')) {
					input.attr('placeholder', me.attr('placeholder'));
                }

                init();

				input.autocomplete({
					minLength: 1,
					source: function(request, response) {
						// delegate back to autocomplete, but extract the last term
						response($.ui.autocomplete.filter(list, $.trim(request.term)));
					},
					focus: function() {
						return false;
					},
					select: function(event, ui) {
						this.value = ui.item.label;
						$(this).trigger('change');
						me.val(ui.item.value).trigger('change');
						return false;
					}
				});

				me
					.on('autocompleteWriteForm', writeForm)
					.on('autocompleteReenable', reenable)
					.on('autocompleteReinit', init)
					.closest('form').on('submit', function(e) {
						if (!e.isDefaultPrevented()) {
							writeForm();
                        }
					});
			}).end();
		}
	});

	$('select.autocompleteSelMul').autocompleteSelMul();
	$('select.autocompleteSel').autocompleteSel();
});
<commit_msg>Fix autocomplete juqeryUI for unique value too<commit_after>jQuery(function($, undefined) {
	$.fn.extend({
		autocompleteSelMul: function() {
			return this.not('.autocompleteSelMulInited').each(function() {
				var me = $(this).hide().addClass('autocompleteSelMulInited'),
					meCont = me.parent().is('.selectCont') ? me.parent().hide() : me,
					val = me.val(),
					options = me.children('option'),
                    keepValue = me.data('keepvalue'),
					input = $('<input type="text" name="'+(keepValue && keepValue.length > 2 ? keepValue : me.attr('id')+'_new')+'" />').insertBefore(meCont),
					list = [],
					cur = [],
					sep = me.data('sep') || ',',
					sepJoin = me.data('sepjoin') || sep+' ',
					split = function (val) {
						return val.split(new RegExp(sep+'\s*'));
					},
					extractLast = function (term) {
						return split(term).pop();
					},
					writeForm = function() {
						var tmp = split(input.val()),
							val = [],
                            others = [];
						$.each(tmp, function() {
							var cur = $.trim(this),
                                curOpt = options.filter('[rel="'+cur+'"]');
							if (curOpt.length) {
								val.push(curOpt.attr('value'));
                            } else {
                                others.push(cur);
                            }
						});
						me.val(val);
                        if (keepValue) {
                            input.val(others.join(sepJoin));
                        } else {
                            input.attr('disabled', 'disabled');
                        }
					},
					reenable = function() {
						input.removeAttr('disabled');
					};

				me.removeProp('required').removeAttr('required');

				if (me.attr('placeholder'))
					input.attr('placeholder', me.attr('placeholder'));

				options.each(function() {
					var opt = $(this),
						txt = $.trim(opt.text());
					opt.attr('rel', txt);
					list.push({
						value: opt.attr('value'),
						label: txt
					});
				});
				if (val && val.length) {
					$.each(val, function(k, v) {
						cur.push(options.filter('[value="'+v+'"]').text());
					});
					cur.push('');
					input.val(cur.join(sepJoin));
				}

				input.autocomplete({
					minLength: 1,
					source: function(request, response) {
						// delegate back to autocomplete, but extract the last term
						response($.ui.autocomplete.filter(list, $.trim(extractLast(request.term))));
					},
					focus: function() {
						return false;
					},
                    search: function(e, ui) {
                        if ($.ui.version.indexOf('1.12') === 0) {
                            // Fix bug in jQuery.ui somewhere where menu.bindings just grows and grows
                            // See https://bugs.jqueryui.com/ticket/10050
                            $(this).data('ui-autocomplete').menu.bindings = $();
                        }
                    },
					select: function(event, ui) {
						var terms = split(this.value);
						// remove the current input
						terms.pop();
						// add the selected item
						terms.push(ui.item.label);
						// add placeholder to get the comma-and-space at the end
						terms.push('');
						this.value = terms.join(sepJoin);
													$(this).trigger('change');
						return false;
					}
				});

				me
					.on('autocompleteWriteForm', writeForm)
					.on('autocompleteReenable', reenable)
					.closest('form').on('submit', function(e) {
						if (!e.isDefaultPrevented())
							writeForm();
					});
			}).end();
		},
		autocompleteSel: function() {
			return this.not('.autocompleteSelInited').each(function() {
				var me = $(this).hide().addClass('autocompleteSelInited'),
					meCont = me.parent().is('.selectCont') ? me.parent().hide() : me,
					val,
					options,
					input = $('<input type="text" name="'+me.attr('id')+'_new" '+(me.attr('required') ? 'required="required"' : '')+'/>').insertBefore(meCont),
					list,
					writeForm = function() {
						var val = input.val(),
							curOpt = options.filter('[rel="'+$.trim(val)+'"]');
						if (curOpt.length) {
							me.val(curOpt.attr('value'));
                        }
						input.attr('disabled', 'disabled');
					},
					reenable = function() {
						input.removeAttr('disabled');
					},
                    init = function() {
                        options = me.children('option');
                        list = [];
                        options.each(function() {
                            var opt = $(this),
                                txt = $.trim(opt.text());
                            opt.attr('rel', txt);
                            list.push({
                                value: opt.attr('value'),
                                label: txt
                            });
                        });
                        val = me.val();
                        if (val && val.length) {
                            input.val(options.filter('[value="'+val+'"]').text());
                        } else {
                            input.val('');
                        }
                    };

				me.removeProp('required').removeAttr('required');
				if (me.attr('placeholder')) {
					input.attr('placeholder', me.attr('placeholder'));
                }

                init();

				input.autocomplete({
					minLength: 1,
					source: function(request, response) {
						// delegate back to autocomplete, but extract the last term
						response($.ui.autocomplete.filter(list, $.trim(request.term)));
					},
					focus: function() {
						return false;
					},
                    search: function(e, ui) {
                        if ($.ui.version.indexOf('1.12') === 0) {
                            // Fix bug in jQuery.ui somewhere where menu.bindings just grows and grows
                            // See https://bugs.jqueryui.com/ticket/10050
                            $(this).data('ui-autocomplete').menu.bindings = $();
                        }
                    },
					select: function(event, ui) {
						this.value = ui.item.label;
						$(this).trigger('change');
						me.val(ui.item.value).trigger('change');
						return false;
					}
				});

				me
					.on('autocompleteWriteForm', writeForm)
					.on('autocompleteReenable', reenable)
					.on('autocompleteReinit', init)
					.closest('form').on('submit', function(e) {
						if (!e.isDefaultPrevented()) {
							writeForm();
                        }
					});
			}).end();
		}
	});

	$('select.autocompleteSelMul').autocompleteSelMul();
	$('select.autocompleteSel').autocompleteSel();
});
<|endoftext|>


<commit_before>'use strict';

const fs = require('fs');
const path = require('path');
const VersionChecker = require('ember-cli-version-checker');

module.exports = function (blueprint) {
  blueprint.supportsAddon = function () {
    return false;
  };

  blueprint.filesPath = function () {
    let type;

    let dependencies = this.project.dependencies();
    if ('ember-qunit' in dependencies) {
      type = 'qunit-rfc-232';
    } else if ('ember-cli-qunit' in dependencies) {
      let checker = new VersionChecker(this.project);
      if (
        fs.existsSync(this.path + '/qunit-rfc-232-files') &&
        checker.for('ember-cli-qunit', 'npm').gte('4.2.0')
      ) {
        type = 'qunit-rfc-232';
      } else {
        type = 'qunit';
      }
    } else if ('ember-mocha' in dependencies) {
      let checker = new VersionChecker(this.project);
      if (
        fs.existsSync(this.path + '/mocha-rfc-232-files') &&
        checker.for('ember-mocha', 'npm').gte('0.14.0')
      ) {
        type = 'mocha-rfc-232';
      } else {
        type = 'mocha-0.12';
      }
    } else if ('ember-cli-mocha' in dependencies) {
      let checker = new VersionChecker(this.project);
      if (
        fs.existsSync(this.path + '/mocha-0.12-files') &&
        checker.for('ember-cli-mocha', 'npm').gte('0.12.0')
      ) {
        type = 'mocha-0.12';
      } else {
        type = 'mocha';
      }
    } else {
      this.ui.writeLine("Couldn't determine test style - using QUnit");
      type = 'qunit';
    }

    return path.join(this.path, type + '-files');
  };

  return blueprint;
};
<commit_msg>add constants to test-framework-detector<commit_after>'use strict';

const fs = require('fs');
const path = require('path');
const VersionChecker = require('ember-cli-version-checker');

module.exports = function (blueprint) {
  blueprint.supportsAddon = function () {
    return false;
  };

  blueprint.filesPath = function () {
    let type;
    const qunitRfcVersion = 'qunit-rfc-232';
    const mochaRfcVersion = 'mocha-rfc-232';
    const mochaVersion = 'mocha-0.12';

    let dependencies = this.project.dependencies();
    if ('ember-qunit' in dependencies) {
      type = qunitRfcVersion;
    } else if ('ember-cli-qunit' in dependencies) {
      let checker = new VersionChecker(this.project);
      if (
        fs.existsSync(`${this.path}/${qunitRfcVersion}-files`) &&
        checker.for('ember-cli-qunit', 'npm').gte('4.2.0')
      ) {
        type = qunitRfcVersion;
      } else {
        type = 'qunit';
      }
    } else if ('ember-mocha' in dependencies) {
      let checker = new VersionChecker(this.project);
      if (
        fs.existsSync(`${this.path}/${mochaRfcVersion}-files`) &&
        checker.for('ember-mocha', 'npm').gte('0.14.0')
      ) {
        type = mochaRfcVersion;
      } else {
        type = mochaVersion;
      }
    } else if ('ember-cli-mocha' in dependencies) {
      let checker = new VersionChecker(this.project);
      if (
        fs.existsSync(`${this.path}/${mochaVersion}-files`) &&
        checker.for('ember-cli-mocha', 'npm').gte('0.12.0')
      ) {
        type = mochaVersion;
      } else {
        type = 'mocha';
      }
    } else {
      this.ui.writeLine("Couldn't determine test style - using QUnit");
      type = 'qunit';
    }

    return path.join(this.path, type + '-files');
  };

  return blueprint;
};
<|endoftext|>


<commit_before>import { call, put } from 'redux-saga/effects'
import AccountActions from '../Redux/AccountRedux'
import { callApi } from './CallApiSaga'

// attempts to account
export function * getAccount (api) {
  const apiCall = call(api.getAccount)
  const response = yield call(callApi, apiCall)

  // success?
  if (response.ok) {
    console.tron.log('Account - OK')
    yield put(AccountActions.accountSuccess(response.data))
  } else {
    console.tron.log('Account - FAIL')
    yield put(AccountActions.accountFailure('WRONG'))
  }
}

// attempts to update account settings
export function * updateAccount (api, action) {
  const { account } = action
  const apiCall = call(api.updateAccount, account)
  const response = yield call(callApi, apiCall)

  // success?
  if (response.ok) {
    console.tron.log('AccountUpdate - OK')
    yield put(AccountActions.accountUpdateSuccess(account))
  } else {
    console.tron.log('AccountUpdate - FAIL')
    yield put(AccountActions.accountFailure('WRONG'))
  }
}
<commit_msg>don't show LoginScreen for 401 on account<commit_after>import { call, put } from 'redux-saga/effects'
import AccountActions from '../Redux/AccountRedux'
import LoginActions from '../Redux/LoginRedux'
import { callApi } from './CallApiSaga'

// attempts to account
export function * getAccount (api) {
  const response = yield call(api.getAccount)

  // success?
  if (response.ok) {
    console.tron.log('Account - OK')
    yield put(AccountActions.accountSuccess(response.data))
  } else {
    console.tron.log('Account - FAIL')
    yield put(AccountActions.accountFailure('WRONG'))
    yield put(LoginActions.logoutRequest())
  }
}

// attempts to update account settings
export function * updateAccount (api, action) {
  const { account } = action
  const apiCall = call(api.updateAccount, account)
  const response = yield call(callApi, apiCall)

  // success?
  if (response.ok) {
    console.tron.log('AccountUpdate - OK')
    yield put(AccountActions.accountUpdateSuccess(account))
  } else {
    console.tron.log('AccountUpdate - FAIL')
    yield put(AccountActions.accountFailure('WRONG'))
  }
}
<|endoftext|>


<commit_before>alert("Hola Mundo");
<commit_msg>Modificación del archivo<commit_after>alert("Editado");
<|endoftext|>


<commit_before>/**
 * Chris Troutner 4/22/16
 * ConnextCMS.com
 * This plugin allows linking to locally hosted files.
 *
 * The code here was originally copied from the 'link' TinyMCE plugin. The code was very sophisicated and I left whatever code that didn't
 * seem to be in my way. It's a bit of a hack. Consider yourself warned.
 */

/**
 * Example plugin that adds a toolbar button and menu item.
 */
tinymce.PluginManager.add('file_link', function(editor, url) {

  //Easier to just store the selected filename into a global variable.
  var selectedFileName = '';

  // Add a button that opens a window
	editor.addButton('file_link', {
		icon: 'link',
    tooltip: 'Insert/edit file',
		onclick: createLinkList(showDialog),
    stateSelector: 'a[href]'
	});

  function createLinkList(callback) {
    //debugger;
		return function() {
			var linkList = editor.settings.link_list;

			if (typeof linkList == "string") {
				tinymce.util.XHR.send({
					url: linkList,
					success: function(text) {
						callback(tinymce.util.JSON.parse(text));
					}
				});
			} else if (typeof linkList == "function") {
				linkList(callback);
			} else {
				callback(linkList);
			}
		};
	}

  function buildListItems(inputList, itemCallback, startItems) {
    //debugger;
		function appendItems(values, output) {
			output = output || [];

			tinymce.each(values, function(item) {
				var menuItem = {text: item.text || item.title};

				if (item.menu) {
					menuItem.menu = appendItems(item.menu);
				} else {
					menuItem.value = item.value;

					if (itemCallback) {
						itemCallback(menuItem);
					}
				}

				output.push(menuItem);
			});

			return output;
		}

		return appendItems(inputList, startItems || []);
	}

  function showDialog(linkList) {
    //debugger;
		var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText;
		var win, onlyText, textListCtrl, linkListCtrl, relListCtrl, targetListCtrl, fileListCtrl, classListCtrl, linkTitleCtrl, value;

		function linkListChangeHandler(e) {
			var textCtrl = win.find('#text');

			if (!textCtrl.value() || (e.lastControl && textCtrl.value() == e.lastControl.text())) {
				textCtrl.value(e.control.text());
			}

			win.find('#href').value(e.control.value());
		}

		function buildAnchorListControl(url) {
      //debugger;
			var anchorList = [];

			tinymce.each(editor.dom.select('a:not([href])'), function(anchor) {
        //debugger;
				var id = anchor.name || anchor.id;

				if (id) {
					anchorList.push({
						text: id,
						value: '#' + id,
						selected: url.indexOf('#' + id) != -1
					});
				}
			});

			if (anchorList.length) {
				anchorList.unshift({text: 'None', value: ''});

				return {
					name: 'anchor',
					type: 'listbox',
					label: 'Anchors',
					values: anchorList,
					onselect: linkListChangeHandler
				};
			}
		}

		function updateText() {
      //debugger;
			if (!initialText && data.text.length === 0 && onlyText) {
				this.parent().parent().find('#text')[0].value(this.value());
			}
		}

		function urlChange(e) {
			var meta = e.meta || {};

			if (linkListCtrl) {
				linkListCtrl.value(editor.convertURL(this.value(), 'href'));
			}

			tinymce.each(e.meta, function(value, key) {
				win.find('#' + key).value(value);
			});

			if (!meta.text) {
				updateText.call(this);
			}
		}

		function isOnlyTextSelected(anchorElm) {
      //debugger;
			var html = selection.getContent();

			// Partial html and not a fully selected anchor element
			if (/</.test(html) && (!/^<a [^>]+>[^<]+<\/a>$/.test(html) || html.indexOf('href=') == -1)) {
				return false;
			}

			if (anchorElm) {
				var nodes = anchorElm.childNodes, i;

				if (nodes.length === 0) {
					return false;
				}

				for (i = nodes.length - 1; i >= 0; i--) {
					if (nodes[i].nodeType != 3) {
						return false;
					}
				}
			}

			return true;
		}

		selectedElm = selection.getNode();
		anchorElm = dom.getParent(selectedElm, 'a[href]');
		onlyText = isOnlyTextSelected();

		data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'});
		data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : '';

		if (anchorElm) {
			data.target = dom.getAttrib(anchorElm, 'target');
		} else if (editor.settings.default_link_target) {
			data.target = editor.settings.default_link_target;
		}

		if ((value = dom.getAttrib(anchorElm, 'rel'))) {
			data.rel = value;
		}

		if ((value = dom.getAttrib(anchorElm, 'class'))) {
			data['class'] = value;
		}

		if ((value = dom.getAttrib(anchorElm, 'title'))) {
			data.title = value;
		}

		if (onlyText) {
			textListCtrl = {
				name: 'text',
				type: 'textbox',
				size: 40,
				label: 'Text to display',
				onchange: function() {
					data.text = this.value();
				}
			};
		}

		if (linkList) {
			linkListCtrl = {
				type: 'listbox',
				label: 'Link list',
				values: buildListItems(
					linkList,
					function(item) {
						item.value = editor.convertURL(item.value || item.url, 'href');
					},
					[{text: 'None', value: ''}]
				),
				onselect: linkListChangeHandler,
				value: editor.convertURL(data.href, 'href'),
				onPostRender: function() {
					/*eslint consistent-this:0*/
					linkListCtrl = this;
				}
			};
		}

		if (editor.settings.target_list !== false) {
			if (!editor.settings.target_list) {
				editor.settings.target_list = [
					{text: 'None', value: ''},
					{text: 'New window', value: '_blank'}
				];
			}

			targetListCtrl = {
				name: 'target',
				type: 'listbox',
				label: 'Target',
				values: buildListItems(editor.settings.target_list)
			};
		}

    //CT
    //Create the drop-down list of files in CMS
    if (editor.settings.file_list !== false) {
			if (!editor.settings.file_list) {
        //debugger;

        editor.settings.file_list = [];
        for( var i=0; i < global.fileUploadCollection.length; i++ ) {
          editor.settings.file_list.push(
            //{text: global.fileUploadCollection.models[i].get('fileName'), value: global.fileUploadCollection.models[i].get('url')}
            {text: global.fileUploadCollection.models[i].get('fileName'), value: i}
          );
        }

				//editor.settings.file_list = [
				//	{text: 'None', value: ''},
				//	{text: 'New window', value: '_blank'}
				//];
			}

			fileListCtrl = {
				name: 'file',
				type: 'listbox',
				label: 'File',
				values: buildListItems(editor.settings.file_list)
			};
		}

		if (editor.settings.rel_list) {
			relListCtrl = {
				name: 'rel',
				type: 'listbox',
				label: 'Rel',
				values: buildListItems(editor.settings.rel_list)
			};
		}

		if (editor.settings.link_class_list) {
			classListCtrl = {
				name: 'class',
				type: 'listbox',
				label: 'Class',
				values: buildListItems(
					editor.settings.link_class_list,
					function(item) {
						if (item.value) {
							item.textStyle = function() {
								return editor.formatter.getCssText({inline: 'a', classes: [item.value]});
							};
						}
					}
				)
			};
		}

		if (editor.settings.link_title !== false) {
			linkTitleCtrl = {
				name: 'title',
				type: 'textbox',
				label: 'Title',
				value: data.title
			};
		}

		win = editor.windowManager.open({
			title: 'Insert link',
			data: data,
			body: [
				fileListCtrl,
        /*
        {
					name: 'href',
					type: 'filepicker',
					filetype: 'file',
					size: 40,
					autofocus: true,
					label: 'Url',
					onchange: urlChange,
					//onkeyup: updateText
				},
        */
				textListCtrl,
				linkTitleCtrl,
				buildAnchorListControl(data.href),
				linkListCtrl,
				relListCtrl
				//targetListCtrl,
				//classListCtrl
			],
			onSubmit: function(e) {
        //debugger;
				/*eslint dot-notation: 0*/
				var href;

				data = tinymce.extend(data, e.data);
				//href = data.href;
        href = global.fileUploadCollection.models[data.file].get('url');

				// Delay confirm since onSubmit will move focus
				function delayedConfirm(message, callback) {
					var rng = editor.selection.getRng();

					tinymce.util.Delay.setEditorTimeout(editor, function() {
						editor.windowManager.confirm(message, function(state) {
							editor.selection.setRng(rng);
							callback(state);
						});
					});
				}

				function insertLink() {
          //debugger;
					var linkAttrs = {
						href: href,
						target: data.target ? data.target : null,
						rel: data.rel ? data.rel : null,
						"class": data["class"] ? data["class"] : null,
						title: data.title ? data.title : null,
            download: global.fileUploadCollection.models[data.file].get('fileName')
					};

					if (anchorElm) {
						editor.focus();

						if (onlyText && data.text != initialText) {
							if ("innerText" in anchorElm) {
								anchorElm.innerText = data.text;
							} else {
								anchorElm.textContent = data.text;
							}
						}

						dom.setAttribs(anchorElm, linkAttrs);

						selection.select(anchorElm);
						editor.undoManager.add();
					} else {
						if (onlyText) {
							editor.insertContent(dom.createHTML('a', linkAttrs, dom.encode(data.text)));
						} else {
							editor.execCommand('mceInsertLink', false, linkAttrs);
						}
					}
				}

				if (!href) {
					editor.execCommand('unlink');
					return;
				}

				// Is email and not //user@domain.com
				if (href.indexOf('@') > 0 && href.indexOf('//') == -1 && href.indexOf('mailto:') == -1) {
					delayedConfirm(
						'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?',
						function(state) {
							if (state) {
								href = 'mailto:' + href;
							}

							insertLink();
						}
					);

					return;
				}

				// Is not protocol prefixed
				if ((editor.settings.link_assume_external_targets && !/^\w+:/i.test(href)) ||
					(!editor.settings.link_assume_external_targets && /^\s*www[\.|\d\.]/i.test(href))) {
					delayedConfirm(
						'The URL you entered seems to be an external link. Do you want to add the required http:// prefix?',
						function(state) {
							if (state) {
								href = 'http://' + href;
							}

							insertLink();
						}
					);

					return;
				}

				insertLink();
			}
		});
	}

});<commit_msg>fixing broken file_link tinymce plugin<commit_after>/**
 * Chris Troutner 4/22/16
 * ConnextCMS.com
 * This plugin allows linking to locally hosted files.
 *
 * The code here was originally copied from the 'link' TinyMCE plugin. The code was very sophisicated and I left whatever code that didn't
 * seem to be in my way. It's a bit of a hack. Consider yourself warned.
 */

/**
 * Example plugin that adds a toolbar button and menu item.
 */
tinymce.PluginManager.add('file_link', function(editor, url) {

  //Easier to just store the selected filename into a global variable.
  var selectedFileName = '';

  // Add a button that opens a window
	editor.addButton('file_link', {
		icon: 'link',
    tooltip: 'Insert/edit file',
		onclick: createLinkList(showDialog),
    stateSelector: 'a[href]'
	});

  function createLinkList(callback) {
    //debugger;
		return function() {
			var linkList = editor.settings.link_list;

			if (typeof linkList == "string") {
				tinymce.util.XHR.send({
					url: linkList,
					success: function(text) {
						callback(tinymce.util.JSON.parse(text));
					}
				});
			} else if (typeof linkList == "function") {
				linkList(callback);
			} else {
				callback(linkList);
			}
		};
	}

  function buildListItems(inputList, itemCallback, startItems) {
    //debugger;
		function appendItems(values, output) {
			output = output || [];

			tinymce.each(values, function(item) {
				var menuItem = {text: item.text || item.title};

				if (item.menu) {
					menuItem.menu = appendItems(item.menu);
				} else {
					menuItem.value = item.value;

					if (itemCallback) {
						itemCallback(menuItem);
					}
				}

				output.push(menuItem);
			});

			return output;
		}

		return appendItems(inputList, startItems || []);
	}

  function showDialog(linkList) {
    //debugger;
		var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText;
		var win, onlyText, textListCtrl, linkListCtrl, relListCtrl, targetListCtrl, fileListCtrl, classListCtrl, linkTitleCtrl, value;

		function linkListChangeHandler(e) {
			var textCtrl = win.find('#text');

			if (!textCtrl.value() || (e.lastControl && textCtrl.value() == e.lastControl.text())) {
				textCtrl.value(e.control.text());
			}

			win.find('#href').value(e.control.value());
		}

		function buildAnchorListControl(url) {
      //debugger;
			var anchorList = [];

			tinymce.each(editor.dom.select('a:not([href])'), function(anchor) {
        //debugger;
				var id = anchor.name || anchor.id;

				if (id) {
					anchorList.push({
						text: id,
						value: '#' + id,
						selected: url.indexOf('#' + id) != -1
					});
				}
			});

			if (anchorList.length) {
				anchorList.unshift({text: 'None', value: ''});

				return {
					name: 'anchor',
					type: 'listbox',
					label: 'Anchors',
					values: anchorList,
					onselect: linkListChangeHandler
				};
			}
		}

		function updateText() {
      //debugger;
			if (!initialText && data.text.length === 0 && onlyText) {
				this.parent().parent().find('#text')[0].value(this.value());
			}
		}

		function urlChange(e) {
			var meta = e.meta || {};

			if (linkListCtrl) {
				linkListCtrl.value(editor.convertURL(this.value(), 'href'));
			}

			tinymce.each(e.meta, function(value, key) {
				win.find('#' + key).value(value);
			});

			if (!meta.text) {
				updateText.call(this);
			}
		}

		function isOnlyTextSelected(anchorElm) {
      //debugger;
			var html = selection.getContent();

			// Partial html and not a fully selected anchor element
			if (/</.test(html) && (!/^<a [^>]+>[^<]+<\/a>$/.test(html) || html.indexOf('href=') == -1)) {
				return false;
			}

			if (anchorElm) {
				var nodes = anchorElm.childNodes, i;

				if (nodes.length === 0) {
					return false;
				}

				for (i = nodes.length - 1; i >= 0; i--) {
					if (nodes[i].nodeType != 3) {
						return false;
					}
				}
			}

			return true;
		}

		selectedElm = selection.getNode();
		anchorElm = dom.getParent(selectedElm, 'a[href]');
		onlyText = isOnlyTextSelected();

		data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'});
		data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : '';

		if (anchorElm) {
			data.target = dom.getAttrib(anchorElm, 'target');
		} else if (editor.settings.default_link_target) {
			data.target = editor.settings.default_link_target;
		}

		if ((value = dom.getAttrib(anchorElm, 'rel'))) {
			data.rel = value;
		}

		if ((value = dom.getAttrib(anchorElm, 'class'))) {
			data['class'] = value;
		}

		if ((value = dom.getAttrib(anchorElm, 'title'))) {
			data.title = value;
		}

		if (onlyText) {
			textListCtrl = {
				name: 'text',
				type: 'textbox',
				size: 40,
				label: 'Text to display',
				onchange: function() {
					data.text = this.value();
				}
			};
		}

		if (linkList) {
			linkListCtrl = {
				type: 'listbox',
				label: 'Link list',
				values: buildListItems(
					linkList,
					function(item) {
						item.value = editor.convertURL(item.value || item.url, 'href');
					},
					[{text: 'None', value: ''}]
				),
				onselect: linkListChangeHandler,
				value: editor.convertURL(data.href, 'href'),
				onPostRender: function() {
					/*eslint consistent-this:0*/
					linkListCtrl = this;
				}
			};
		}

		if (editor.settings.target_list !== false) {
			if (!editor.settings.target_list) {
				editor.settings.target_list = [
					{text: 'None', value: ''},
					{text: 'New window', value: '_blank'}
				];
			}

			targetListCtrl = {
				name: 'target',
				type: 'listbox',
				label: 'Target',
				values: buildListItems(editor.settings.target_list)
			};
		}

    //CT
    //Create the drop-down list of files in CMS
    if (editor.settings.file_list !== false) {
			if (!editor.settings.file_list) {
        //debugger;

        editor.settings.file_list = [];
        for( var i=0; i < global.fileUploadCollection.length; i++ ) {
          editor.settings.file_list.push(
            //{text: global.fileUploadCollection.models[i].get('fileName'), value: global.fileUploadCollection.models[i].get('url')}
            {text: global.fileUploadCollection.models[i].get('name'), value: i}
          );
        }

				//editor.settings.file_list = [
				//	{text: 'None', value: ''},
				//	{text: 'New window', value: '_blank'}
				//];
			}

			fileListCtrl = {
				name: 'file',
				type: 'listbox',
				label: 'File',
				values: buildListItems(editor.settings.file_list)
			};
		}

		if (editor.settings.rel_list) {
			relListCtrl = {
				name: 'rel',
				type: 'listbox',
				label: 'Rel',
				values: buildListItems(editor.settings.rel_list)
			};
		}

		if (editor.settings.link_class_list) {
			classListCtrl = {
				name: 'class',
				type: 'listbox',
				label: 'Class',
				values: buildListItems(
					editor.settings.link_class_list,
					function(item) {
						if (item.value) {
							item.textStyle = function() {
								return editor.formatter.getCssText({inline: 'a', classes: [item.value]});
							};
						}
					}
				)
			};
		}

		if (editor.settings.link_title !== false) {
			linkTitleCtrl = {
				name: 'title',
				type: 'textbox',
				label: 'Title',
				value: data.title
			};
		}

		win = editor.windowManager.open({
			title: 'Insert link',
			data: data,
			body: [
				fileListCtrl,
        /*
        {
					name: 'href',
					type: 'filepicker',
					filetype: 'file',
					size: 40,
					autofocus: true,
					label: 'Url',
					onchange: urlChange,
					//onkeyup: updateText
				},
        */
				textListCtrl,
				linkTitleCtrl,
				buildAnchorListControl(data.href),
				linkListCtrl,
				relListCtrl
				//targetListCtrl,
				//classListCtrl
			],
			onSubmit: function(e) {
        //debugger;
				/*eslint dot-notation: 0*/
				var href;

				data = tinymce.extend(data, e.data);
				//href = data.href;
        href = global.fileUploadCollection.models[data.file].get('url');

				// Delay confirm since onSubmit will move focus
				function delayedConfirm(message, callback) {
					var rng = editor.selection.getRng();

					tinymce.util.Delay.setEditorTimeout(editor, function() {
						editor.windowManager.confirm(message, function(state) {
							editor.selection.setRng(rng);
							callback(state);
						});
					});
				}

				function insertLink() {
          //debugger;
					var linkAttrs = {
						href: href,
						target: data.target ? data.target : null,
						rel: data.rel ? data.rel : null,
						"class": data["class"] ? data["class"] : null,
						title: data.title ? data.title : null,
            download: global.fileUploadCollection.models[data.file].get('fileName')
					};

					if (anchorElm) {
						editor.focus();

						if (onlyText && data.text != initialText) {
							if ("innerText" in anchorElm) {
								anchorElm.innerText = data.text;
							} else {
								anchorElm.textContent = data.text;
							}
						}

						dom.setAttribs(anchorElm, linkAttrs);

						selection.select(anchorElm);
						editor.undoManager.add();
					} else {
						if (onlyText) {
							editor.insertContent(dom.createHTML('a', linkAttrs, dom.encode(data.text)));
						} else {
							editor.execCommand('mceInsertLink', false, linkAttrs);
						}
					}
				}

				if (!href) {
					editor.execCommand('unlink');
					return;
				}

				// Is email and not //user@domain.com
				if (href.indexOf('@') > 0 && href.indexOf('//') == -1 && href.indexOf('mailto:') == -1) {
					delayedConfirm(
						'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?',
						function(state) {
							if (state) {
								href = 'mailto:' + href;
							}

							insertLink();
						}
					);

					return;
				}

				// Is not protocol prefixed
				if ((editor.settings.link_assume_external_targets && !/^\w+:/i.test(href)) ||
					(!editor.settings.link_assume_external_targets && /^\s*www[\.|\d\.]/i.test(href))) {
					delayedConfirm(
						'The URL you entered seems to be an external link. Do you want to add the required http:// prefix?',
						function(state) {
							if (state) {
								href = 'http://' + href;
							}

							insertLink();
						}
					);

					return;
				}

				insertLink();
			}
		});
	}

});<|endoftext|>


<commit_before>VISH.Editor.Flashcard.Creator = (function(V,$,undefined){

	//Point to current flashcard.
	//Singleton: Only one flashcard can be edited in a Vish Editor instance.
	var flashcardId;
	//Point to the current pois
	var currentPois = undefined;

	//Internal
	var trustOrgPois = false;

	var init = function(){
		flashcardId = null;
	};

	var getId = function(){
		return flashcardId;
	}

	/*
	 * Switch to Flashcard creator in order to allow the creation of a new flashcard
	 * using the current slides
	 */
	var onLoadMode = function(){
		loadSlideset();
		//change thumbnail onclick event (preview slide instead of go to edit it)
		//it will change itself depending on presentationType, also remove drag and drop to order slides
		//also a _redrawPois functions is passed to show the pois, do them draggables, etc
		V.Editor.Thumbnails.redrawThumbnails();
		V.Editor.Tools.init();
	};

	/*
	 * Method to call before leave the flashcard mode
	 * Store the current pois.
	 */
	var onLeaveMode = function(){
		currentPois = _getCurrentPois();
		$("#flashcard-background").hide();
	}

	/*
	 * Load a flashcard in the creator mode
	 * @presentation must be undefined for new flashcard, or a previous flashcard
	 * (presentation[type='flashcard']) if we are editing an existing flashcard
	 */
	var loadSlideset = function(presentation){
		V.Editor.setPresentationType(V.Constant.FLASHCARD);

		V.Editor.Slides.hideSlides();

		//Show flashcard background
		$("#flashcard-background").show();

		if(presentation){
			//If we are editing an existing flashcard
			var flashcard = presentation.slides[0];
			flashcardId = flashcard.id;
			$("#flashcard-background").attr("flashcard_id", flashcardId);
			$("#flashcard-background").css("background-image", flashcard.background);
			$("#fc_change_bg_big").hide();
			//When we load a flashcard, we can trust in the json pois
			//Otherwise, this pois may be belong to another slideset (e.g virtual tour)
			trustOrgPois = true;
		} else {
			//Create new flashcard
			if(!flashcardId){
				flashcardId = V.Utils.getId("article");
			}
			$("#flashcard-background").attr("flashcard_id", flashcardId);
		}
		$("#flashcard-background").droppable();  //to accept the pois
	};

	/*
	 * Callback from the V.Editor.Image module to add the background
	 */
	var onBackgroundSelected = function(contentToAdd){
		$("#flashcard-background").css("background-image", "url("+contentToAdd+")");
		$("#fc_change_bg_big").hide();
		$.fancybox.close();
	}

	/*
	 * Redraw the pois of the flashcard
	 * This actions must be called after thumbnails have been rewritten
	 */
	var redrawPois = function(){
		//Show draggable items to create the flashcard
		$(".draggable_arrow_div").show();
		//Apply them the style to get the previous position
		_applyStyleToPois();

		$(".draggable_arrow_div").draggable({
			revert: "invalid",   //poi will return to original position if not dropped on the background
			stop: function(event, ui) {
				//change the moved attribute of the poi, and change it to position absolute
				//check if inside background
				if($(event.target).offset().top > 50 && $(event.target).offset().top < 600 && $(event.target).offset().left > 55 && $(event.target).offset().left < 805){
					$(event.target).attr("moved", "true");
					//change to position absolute
					var old_pos = $(event.target).offset();
					$(event.target).css("position", "fixed");
					$(event.target).css("top", (old_pos.top +30) + "px");
					$(event.target).css("left", (old_pos.left -16) + "px");
				} else {
					$(event.target).attr("moved", "false");
					//change to position relative so it moves with the carrousel
					var old_pos = $(event.target).offset();
					$(event.target).css("position", "relative");
					$(event.target).css("top", "auto");
					$(event.target).css("left", "auto");
				}
			}
		});
		$(".carrousel_element_single_row_slides").droppable();
		$(".image_carousel").css("overflow", "visible");
		$("#menubar").css("z-index", "1075");
		$(".draggable_arrow_div").css("z-index", "1075");
	};

	var _applyStyleToPois = function(){
		if((typeof currentPois === "undefined")&&(trustOrgPois)){
			//We are loading a flascard, get the pois from the json
			var presentation = V.Editor.getPresentation();
			if(presentation && presentation.slides && presentation.slides[0] && presentation.slides[0].pois){
				currentPois = presentation.slides[0].pois;
			} else {
				return;
			}
		}

		if(typeof currentPois !== "undefined"){
			$.each(currentPois, function(index, val) {
				$("#" + val.id).css("position", "fixed");
				$("#" + val.id).offset({ top: 600*parseInt(val.y)/100 + 75, left: 800*parseInt(val.x)/100 + 55});
				$("#" + val.id).attr("moved", "true");
			});
		}
	};

	/*
	 * Returns the array of pois to be stored in the JSON
	 * Pois are nested!
	 */
	var getPois = function(){
		var pois = [];
		$(".draggable_arrow_div[moved='true']").each(function(index,s){
			pois[index]= {};
			pois[index].id = V.Utils.getId(flashcardId+"_"+s.id,true);
			pois[index].x = (100*($(s).offset().left - 48)/800).toString(); //to be relative to his parent, the flashcard-background
			pois[index].y = (100*($(s).offset().top - 38)/600).toString(); //to be relative to his parent, the flashcard-background
			pois[index].slide_id = V.Utils.getId(flashcardId+"_"+$(s).attr('slide_id'),true);
		});
		return pois;
	};

	/*
	 * Return the current array of pois, without nesting.
	 * The id matches the id of the DOM element.
	 */
	var _getCurrentPois = function(){
		var pois = [];
		$(".draggable_arrow_div[moved='true']").each(function(index,s){
			pois[index]= {};
			pois[index].id = s.id;
			pois[index].x = (100*($(s).offset().left - 48)/800).toString(); //to be relative to his parent, the flashcard-background
			pois[index].y = (100*($(s).offset().top - 38)/600).toString(); //to be relative to his parent, the flashcard-background
			pois[index].slide_id = $(s).attr('slide_id');
		});
		return pois;
	};

	var hidePois = function(){
		$(".draggable_arrow_div").hide();
	};

	var hasPoiInBackground = function(){
		return $(".draggable_arrow_div[moved='true']").length > 0;
	};

	var hasChangedBackground = function(){
		if($("#flashcard-background").css("background-image").indexOf("flashcard_initial_background.jpg") != -1){
			return false;
		}else{
			return true;
		}
	};

	/*
	 * Carrousel onClick callback
	 */
	var onClickCarrouselElement = function(event){
		switch($(event.target).attr("action")){
			case "plus":
				V.Debugging.log("Show message warning that we are changing to presentation and change");
					$.fancybox(
						$("#message2_form").html(),
						{
							'autoDimensions'	: false,
							'scrolling'			: 'no',
							'width'         	: 550,
							'height'        	: 200,
							'showCloseButton'	: false,
							'padding' 			: 5
						}
					);
			  break;
			case "goToSlide":
			  	V.Editor.Preview.preview({forcePresentation: true, slideNumberToPreview: parseInt($(event.target).attr("slideNumber"))});
			  break;
			default:
			  break;
		}
	 }

	////////////////////
	// Validate
	////////////////////

	/*
	 * OnValidationError: Return the id of the form to be show
	 * OnValidationSuccess:Return true
	 */
	var validateOnSave = function(){
		if(!hasPoiInBackground()){
			return "message3_form";
		}
		if(!hasChangedBackground()){
			return "message4_form";
		}
		return true;
	}

	////////////////////
	// JSON Manipulation
	////////////////////

	/*
	 * Used for VISH.Editor module to save the flashcard in a JSON file
	 */
	var getSlideHeader = function(){
		var slide = {};
		slide.id = flashcardId;
		slide.type = V.Constant.FLASHCARD;
		slide.background = $("#flashcard-background").css("background-image");
		slide.pois = getPois();
		slide.slides = [];
		return slide;
	}

	return {
		init 				 		: init,
		getId						: getId,
		onLoadMode			 		: onLoadMode,
		onLeaveMode 				: onLeaveMode,
		loadSlideset		 		: loadSlideset,
		onBackgroundSelected		: onBackgroundSelected,
		redrawPois 			 		: redrawPois,
		getPois			 			: getPois,
		hidePois			 		: hidePois,
		hasChangedBackground 		: hasChangedBackground,
		hasPoiInBackground	 		: hasPoiInBackground,
		onClickCarrouselElement 	: onClickCarrouselElement,
		getSlideHeader				: getSlideHeader,
		validateOnSave				: validateOnSave
	};

}) (VISH, jQuery);<commit_msg>Flashcard arrow fixed<commit_after>VISH.Editor.Flashcard.Creator = (function(V,$,undefined){

	//Point to current flashcard.
	//Singleton: Only one flashcard can be edited in a Vish Editor instance.
	var flashcardId;
	//Point to the current pois
	var currentPois = undefined;

	//Internal
	var trustOrgPois = false;

	var init = function(){
		flashcardId = null;
	};

	var getId = function(){
		return flashcardId;
	}

	/*
	 * Switch to Flashcard creator in order to allow the creation of a new flashcard
	 * using the current slides
	 */
	var onLoadMode = function(){
		loadSlideset();
		//change thumbnail onclick event (preview slide instead of go to edit it)
		//it will change itself depending on presentationType, also remove drag and drop to order slides
		//also a _redrawPois functions is passed to show the pois, do them draggables, etc
		V.Editor.Thumbnails.redrawThumbnails();
		V.Editor.Tools.init();
	};

	/*
	 * Method to call before leave the flashcard mode
	 * Store the current pois.
	 */
	var onLeaveMode = function(){
		currentPois = _getCurrentPois();
		$("#flashcard-background").hide();
	}

	/*
	 * Load a flashcard in the creator mode
	 * @presentation must be undefined for new flashcard, or a previous flashcard
	 * (presentation[type='flashcard']) if we are editing an existing flashcard
	 */
	var loadSlideset = function(presentation){
		V.Editor.setPresentationType(V.Constant.FLASHCARD);

		V.Editor.Slides.hideSlides();

		//Show flashcard background
		$("#flashcard-background").show();

		if(presentation){
			//If we are editing an existing flashcard
			var flashcard = presentation.slides[0];
			flashcardId = flashcard.id;
			$("#flashcard-background").attr("flashcard_id", flashcardId);
			$("#flashcard-background").css("background-image", flashcard.background);
			$("#fc_change_bg_big").hide();
			//When we load a flashcard, we can trust in the json pois
			//Otherwise, this pois may be belong to another slideset (e.g virtual tour)
			trustOrgPois = true;
		} else {
			//Create new flashcard
			if(!flashcardId){
				flashcardId = V.Utils.getId("article");
			}
			$("#flashcard-background").attr("flashcard_id", flashcardId);
		}
		$("#flashcard-background").droppable();  //to accept the pois
	};

	/*
	 * Callback from the V.Editor.Image module to add the background
	 */
	var onBackgroundSelected = function(contentToAdd){
		$("#flashcard-background").css("background-image", "url("+contentToAdd+")");
		$("#fc_change_bg_big").hide();
		$.fancybox.close();
	}

	/*
	 * Redraw the pois of the flashcard
	 * This actions must be called after thumbnails have been rewritten
	 */
	var redrawPois = function(){
		//Show draggable items to create the flashcard
		$(".draggable_arrow_div").show();
		//Apply them the style to get the previous position
		_applyStyleToPois();

		$(".draggable_arrow_div").draggable({
			revert: "invalid",   //poi will return to original position if not dropped on the background
			stop: function(event, ui) {
				//change the moved attribute of the poi, and change it to position absolute
				//check if inside background
				if($(event.target).offset().top > 50 && $(event.target).offset().top < 600 && $(event.target).offset().left > 55 && $(event.target).offset().left < 805){
					$(event.target).attr("moved", "true");
					//change to position absolute
					var old_pos = $(event.target).offset();
					$(event.target).css("position", "fixed");
					$(event.target).css("top", (old_pos.top +30) + "px");
					$(event.target).css("left", (old_pos.left -16) + "px");
				} else {
					$(event.target).attr("moved", "false");
					//change to position relative so it moves with the carrousel
					var old_pos = $(event.target).offset();
					$(event.target).css("position", "relative");
					$(event.target).css("top", "auto");
					$(event.target).css("left", "auto");
				}
			}
		});
		$(".carrousel_element_single_row_slides").droppable();
		$(".image_carousel").css("overflow", "visible");
		$("#menubar").css("z-index", "1075");
		$(".draggable_arrow_div").css("z-index", "1075");
	};

	var _applyStyleToPois = function(){
		if((typeof currentPois === "undefined")&&(trustOrgPois)){
			//We are loading a flascard, get the pois from the json
			var presentation = V.Editor.getPresentation();
			if(presentation && presentation.slides && presentation.slides[0] && presentation.slides[0].pois){
				currentPois = presentation.slides[0].pois;
			} else {
				return;
			}
		}

		if(typeof currentPois !== "undefined"){
			$.each(currentPois, function(index, val) {
				$("#" + val.id).css("position", "fixed");
				$("#" + val.id).offset({ top: 600*parseInt(val.y)/100 + 38, left: 800*parseInt(val.x)/100 + 48});
				$("#" + val.id).attr("moved", "true");
			});
		}
	};

	/*
	 * Returns the array of pois to be stored in the JSON
	 * Pois are nested!
	 */
	var getPois = function(){
		var pois = [];
		$(".draggable_arrow_div[moved='true']").each(function(index,s){
			pois[index]= {};
			pois[index].id = V.Utils.getId(flashcardId+"_"+s.id,true);
			pois[index].x = (100*($(s).offset().left - 48)/800).toString(); //to be relative to his parent, the flashcard-background
			pois[index].y = (100*($(s).offset().top - 38)/600).toString(); //to be relative to his parent, the flashcard-background
			pois[index].slide_id = V.Utils.getId(flashcardId+"_"+$(s).attr('slide_id'),true);
		});
		return pois;
	};

	/*
	 * Return the current array of pois, without nesting.
	 * The id matches the id of the DOM element.
	 */
	var _getCurrentPois = function(){
		var pois = [];
		$(".draggable_arrow_div[moved='true']").each(function(index,s){
			pois[index]= {};
			pois[index].id = s.id;
			pois[index].x = (100*($(s).offset().left - 48)/800).toString(); //to be relative to his parent, the flashcard-background
			pois[index].y = (100*($(s).offset().top - 38)/600).toString(); //to be relative to his parent, the flashcard-background
			pois[index].slide_id = $(s).attr('slide_id');
		});
		return pois;
	};

	var hidePois = function(){
		$(".draggable_arrow_div").hide();
	};

	var hasPoiInBackground = function(){
		return $(".draggable_arrow_div[moved='true']").length > 0;
	};

	var hasChangedBackground = function(){
		if($("#flashcard-background").css("background-image").indexOf("flashcard_initial_background.jpg") != -1){
			return false;
		}else{
			return true;
		}
	};

	/*
	 * Carrousel onClick callback
	 */
	var onClickCarrouselElement = function(event){
		switch($(event.target).attr("action")){
			case "plus":
					$.fancybox(
						$("#message2_form").html(),
						{
							'autoDimensions'	: false,
							'scrolling'			: 'no',
							'width'         	: 550,
							'height'        	: 200,
							'showCloseButton'	: false,
							'padding' 			: 5
						}
					);
			  break;
			case "goToSlide":
			  	V.Editor.Preview.preview({forcePresentation: true, slideNumberToPreview: parseInt($(event.target).attr("slideNumber"))});
			  break;
			default:
			  break;
		}
	 }

	////////////////////
	// Validate
	////////////////////

	/*
	 * OnValidationError: Return the id of the form to be show
	 * OnValidationSuccess:Return true
	 */
	var validateOnSave = function(){
		if(!hasPoiInBackground()){
			return "message3_form";
		}
		if(!hasChangedBackground()){
			return "message4_form";
		}
		return true;
	}

	////////////////////
	// JSON Manipulation
	////////////////////

	/*
	 * Used for VISH.Editor module to save the flashcard in a JSON file
	 */
	var getSlideHeader = function(){
		var slide = {};
		slide.id = flashcardId;
		slide.type = V.Constant.FLASHCARD;
		slide.background = $("#flashcard-background").css("background-image");
		slide.pois = getPois();
		slide.slides = [];
		return slide;
	}

	return {
		init 				 		: init,
		getId						: getId,
		onLoadMode			 		: onLoadMode,
		onLeaveMode 				: onLeaveMode,
		loadSlideset		 		: loadSlideset,
		onBackgroundSelected		: onBackgroundSelected,
		redrawPois 			 		: redrawPois,
		getPois			 			: getPois,
		hidePois			 		: hidePois,
		hasChangedBackground 		: hasChangedBackground,
		hasPoiInBackground	 		: hasPoiInBackground,
		onClickCarrouselElement 	: onClickCarrouselElement,
		getSlideHeader				: getSlideHeader,
		validateOnSave				: validateOnSave
	};

}) (VISH, jQuery);<|endoftext|>