<commit_before>import Component from '@ember/component';
import SettingsMenuMixin from 'ghost-admin/mixins/settings-menu-component';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import formatMarkdown from 'ghost-admin/utils/format-markdown';
import moment from 'moment';
import {alias, or} from '@ember/object/computed';
import {computed} from '@ember/object';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';

const PSM_ANIMATION_LENGTH = 400;

export default Component.extend(SettingsMenuMixin, {
    store: service(),
    config: service(),
    ghostPaths: service(),
    notifications: service(),
    slugGenerator: service(),
    session: service(),
    settings: service(),
    ui: service(),

    post: null,

    _showSettingsMenu: false,
    _showThrobbers: false,

    customExcerptScratch: alias('post.customExcerptScratch'),
    codeinjectionFootScratch: alias('post.codeinjectionFootScratch'),
    codeinjectionHeadScratch: alias('post.codeinjectionHeadScratch'),
    metaDescriptionScratch: alias('post.metaDescriptionScratch'),
    metaTitleScratch: alias('post.metaTitleScratch'),
    ogDescriptionScratch: alias('post.ogDescriptionScratch'),
    ogTitleScratch: alias('post.ogTitleScratch'),
    twitterDescriptionScratch: alias('post.twitterDescriptionScratch'),
    twitterTitleScratch: alias('post.twitterTitleScratch'),
    slugValue: boundOneWay('post.slug'),

    facebookDescription: or('ogDescriptionScratch', 'customExcerptScratch', 'seoDescription'),
    facebookImage: or('post.ogImage', 'post.featureImage'),
    facebookTitle: or('ogTitleScratch', 'seoTitle'),
    seoTitle: or('metaTitleScratch', 'post.titleScratch'),
    twitterDescription: or('twitterDescriptionScratch', 'customExcerptScratch', 'seoDescription'),
    twitterImage: or('post.twitterImage', 'post.featureImage'),
    twitterTitle: or('twitterTitleScratch', 'seoTitle'),

    seoDescription: computed('post.scratch', 'metaDescriptionScratch', function () {
        let metaDescription = this.get('metaDescriptionScratch') || '';
        let mobiledoc = this.get('post.scratch');
        let markdown = mobiledoc.cards && mobiledoc.cards[0][1].markdown;
        let placeholder;

        if (metaDescription) {
            placeholder = metaDescription;
        } else {
            let div = document.createElement('div');
            div.innerHTML = formatMarkdown(markdown, false);

            // Strip HTML
            placeholder = div.textContent;
            // Replace new lines and trim
            placeholder = placeholder.replace(/\n+/g, ' ').trim();
        }

        return placeholder;
    }),

    seoURL: computed('post.slug', 'config.blogUrl', function () {
        let blogUrl = this.get('config.blogUrl');
        let seoSlug = this.get('post.slug') ? this.get('post.slug') : '';
        let seoURL = `${blogUrl}/${seoSlug}`;

        // only append a slash to the URL if the slug exists
        if (seoSlug) {
            seoURL += '/';
        }

        return seoURL;
    }),

    didReceiveAttrs() {
        this._super(...arguments);

        // HACK: ugly method of working around the CSS animations so that we
        // can add throbbers only when the animation has finished
        // TODO: use liquid-fire to handle PSM slide-in and replace tabs manager
        // with something more ember-like
        if (this.get('showSettingsMenu') && !this._showSettingsMenu) {
            this.get('showThrobbers').perform();
        }

        // fired when menu is closed
        if (!this.get('showSettingsMenu') && this._showSettingsMenu) {
            let post = this.get('post');
            let errors = post.get('errors');

            // reset the publish date if it has an error
            if (errors.has('publishedAtBlogDate') || errors.has('publishedAtBlogTime')) {
                post.set('publishedAtBlogTZ', post.get('publishedAtUTC'));
                post.validate({attribute: 'publishedAtBlog'});
            }

            // remove throbbers
            this.set('_showThrobbers', false);
        }

        this._showSettingsMenu = this.get('showSettingsMenu');
    },

    actions: {
        showSubview(subview) {
            this._super(...arguments);

            this.set('subview', subview);

            // Chrome appears to have an animation bug that cancels the slide
            // transition unless there's a delay between the animation starting
            // and the throbbers being removed
            run.later(this, function () {
                this.set('_showThrobbers', false);
            }, 50);
        },

        closeSubview() {
            this._super(...arguments);

            this.set('subview', null);
            this.get('showThrobbers').perform();
        },

        discardEnter() {
            return false;
        },

        togglePage() {
            this.toggleProperty('post.page');

            // If this is a new post.  Don't save the post.  Defer the save
            // to the user pressing the save button
            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        toggleFeatured() {
            this.toggleProperty('post.featured');

            // If this is a new post.  Don't save the post.  Defer the save
            // to the user pressing the save button
            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        /**
         * triggered by user manually changing slug
         */
        updateSlug(newSlug) {
            return this.get('updateSlug')
                .perform(newSlug)
                .catch((error) => {
                    this.showError(error);
                    this.get('post').rollbackAttributes();
                });
        },

        setPublishedAtBlogDate(date) {
            let post = this.get('post');
            let dateString = moment(date).format('YYYY-MM-DD');

            post.get('errors').remove('publishedAtBlogDate');

            if (post.get('isNew') || date === post.get('publishedAtBlogDate')) {
                post.validate({property: 'publishedAtBlog'});
            } else {
                post.set('publishedAtBlogDate', dateString);
                return this.get('savePost').perform();
            }
        },

        setPublishedAtBlogTime(time) {
            let post = this.get('post');

            post.get('errors').remove('publishedAtBlogDate');

            if (post.get('isNew') || time === post.get('publishedAtBlogTime')) {
                post.validate({property: 'publishedAtBlog'});
            } else {
                post.set('publishedAtBlogTime', time);
                return this.get('savePost').perform();
            }
        },

        setCustomExcerpt(excerpt) {
            let post = this.get('post');
            let currentExcerpt = post.get('customExcerpt');

            if (excerpt === currentExcerpt) {
                return;
            }

            post.set('customExcerpt', excerpt);

            return post.validate({property: 'customExcerpt'}).then(() => this.get('savePost').perform());
        },

        setHeaderInjection(code) {
            let post = this.get('post');
            let currentCode = post.get('codeinjectionHead');

            if (code === currentCode) {
                return;
            }

            post.set('codeinjectionHead', code);

            return post.validate({property: 'codeinjectionHead'}).then(() => this.get('savePost').perform());
        },

        setFooterInjection(code) {
            let post = this.get('post');
            let currentCode = post.get('codeinjectionFoot');

            if (code === currentCode) {
                return;
            }

            post.set('codeinjectionFoot', code);

            return post.validate({property: 'codeinjectionFoot'}).then(() => this.get('savePost').perform());
        },

        setMetaTitle(metaTitle) {
            // Grab the post and current stored meta title
            let post = this.get('post');
            let currentTitle = post.get('metaTitle');

            // If the title entered matches the stored meta title, do nothing
            if (currentTitle === metaTitle) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            post.set('metaTitle', metaTitle);

            // Make sure the meta title is valid and if so, save it into the post
            return post.validate({property: 'metaTitle'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setMetaDescription(metaDescription) {
            // Grab the post and current stored meta description
            let post = this.get('post');
            let currentDescription = post.get('metaDescription');

            // If the title entered matches the stored meta title, do nothing
            if (currentDescription === metaDescription) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            post.set('metaDescription', metaDescription);

            // Make sure the meta title is valid and if so, save it into the post
            return post.validate({property: 'metaDescription'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setOgTitle(ogTitle) {
            // Grab the post and current stored facebook title
            let post = this.get('post');
            let currentTitle = post.get('ogTitle');

            // If the title entered matches the stored facebook title, do nothing
            if (currentTitle === ogTitle) {
                return;
            }

            // If the title entered is different, set it as the new facebook title
            post.set('ogTitle', ogTitle);

            // Make sure the facebook title is valid and if so, save it into the post
            return post.validate({property: 'ogTitle'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setOgDescription(ogDescription) {
            // Grab the post and current stored facebook description
            let post = this.get('post');
            let currentDescription = post.get('ogDescription');

            // If the title entered matches the stored facebook description, do nothing
            if (currentDescription === ogDescription) {
                return;
            }

            // If the description entered is different, set it as the new facebook description
            post.set('ogDescription', ogDescription);

            // Make sure the facebook description is valid and if so, save it into the post
            return post.validate({property: 'ogDescription'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setTwitterTitle(twitterTitle) {
            // Grab the post and current stored twitter title
            let post = this.get('post');
            let currentTitle = post.get('twitterTitle');

            // If the title entered matches the stored twitter title, do nothing
            if (currentTitle === twitterTitle) {
                return;
            }

            // If the title entered is different, set it as the new twitter title
            post.set('twitterTitle', twitterTitle);

            // Make sure the twitter title is valid and if so, save it into the post
            return post.validate({property: 'twitterTitle'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setTwitterDescription(twitterDescription) {
            // Grab the post and current stored twitter description
            let post = this.get('post');
            let currentDescription = post.get('twitterDescription');

            // If the description entered matches the stored twitter description, do nothing
            if (currentDescription === twitterDescription) {
                return;
            }

            // If the description entered is different, set it as the new twitter description
            post.set('twitterDescription', twitterDescription);

            // Make sure the twitter description is valid and if so, save it into the post
            return post.validate({property: 'twitterDescription'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setCoverImage(image) {
            this.set('post.featureImage', image);

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        clearCoverImage() {
            this.set('post.featureImage', '');

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        setOgImage(image) {
            this.set('post.ogImage', image);

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        clearOgImage() {
            this.set('post.ogImage', '');

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        setTwitterImage(image) {
            this.set('post.twitterImage', image);

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        clearTwitterImage() {
            this.set('post.twitterImage', '');

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        changeAuthors(newAuthors) {
            let post = this.get('post');

            // return if nothing changed
            if (newAuthors.mapBy('id').join() === post.get('authors').mapBy('id').join()) {
                return;
            }

            post.set('authors', newAuthors);
            post.validate({property: 'authors'});

            // if this is a new post (never been saved before), don't try to save it
            if (post.get('isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                post.rollbackAttributes();
            });
        },

        deletePost() {
            if (this.get('deletePost')) {
                this.get('deletePost')();
            }
        }
    },

    showThrobbers: task(function* () {
        yield timeout(PSM_ANIMATION_LENGTH);
        this.set('_showThrobbers', true);
    }).restartable(),

    showError(error) {
        // TODO: remove null check once ValidationEngine has been removed
        if (error) {
            this.get('notifications').showAPIError(error);
        }
    }
});
<commit_msg>Koenig - Fix PSM meta/twitter/facebook pane errors on new posts<commit_after>import Component from '@ember/component';
import SettingsMenuMixin from 'ghost-admin/mixins/settings-menu-component';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import formatMarkdown from 'ghost-admin/utils/format-markdown';
import moment from 'moment';
import {alias, or} from '@ember/object/computed';
import {computed} from '@ember/object';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';

const PSM_ANIMATION_LENGTH = 400;

export default Component.extend(SettingsMenuMixin, {
    store: service(),
    config: service(),
    ghostPaths: service(),
    notifications: service(),
    slugGenerator: service(),
    session: service(),
    settings: service(),
    ui: service(),

    post: null,

    _showSettingsMenu: false,
    _showThrobbers: false,

    customExcerptScratch: alias('post.customExcerptScratch'),
    codeinjectionFootScratch: alias('post.codeinjectionFootScratch'),
    codeinjectionHeadScratch: alias('post.codeinjectionHeadScratch'),
    metaDescriptionScratch: alias('post.metaDescriptionScratch'),
    metaTitleScratch: alias('post.metaTitleScratch'),
    ogDescriptionScratch: alias('post.ogDescriptionScratch'),
    ogTitleScratch: alias('post.ogTitleScratch'),
    twitterDescriptionScratch: alias('post.twitterDescriptionScratch'),
    twitterTitleScratch: alias('post.twitterTitleScratch'),
    slugValue: boundOneWay('post.slug'),

    facebookDescription: or('ogDescriptionScratch', 'customExcerptScratch', 'seoDescription'),
    facebookImage: or('post.ogImage', 'post.featureImage'),
    facebookTitle: or('ogTitleScratch', 'seoTitle'),
    twitterDescription: or('twitterDescriptionScratch', 'customExcerptScratch', 'seoDescription'),
    twitterImage: or('post.twitterImage', 'post.featureImage'),
    twitterTitle: or('twitterTitleScratch', 'seoTitle'),

    seoTitle: computed('metaTitleScratch', 'post.titleScratch', function () {
        return this.metaTitleScratch || this.post.titleScratch || '(Untitled)';
    }),

    seoDescription: computed('post.scratch', 'metaDescriptionScratch', function () {
        let metaDescription = this.get('metaDescriptionScratch') || '';
        let mobiledoc = this.get('post.scratch');
        let [markdownCard] = mobiledoc.cards;
        let markdown = markdownCard && markdownCard[1] && markdownCard[1].markdown;
        let placeholder;

        if (metaDescription) {
            placeholder = metaDescription;
        } else {
            let div = document.createElement('div');
            div.innerHTML = formatMarkdown(markdown, false);

            // Strip HTML
            placeholder = div.textContent;
            // Replace new lines and trim
            placeholder = placeholder.replace(/\n+/g, ' ').trim();
        }

        return placeholder;
    }),

    seoURL: computed('post.slug', 'config.blogUrl', function () {
        let blogUrl = this.get('config.blogUrl');
        let seoSlug = this.get('post.slug') ? this.get('post.slug') : '';
        let seoURL = `${blogUrl}/${seoSlug}`;

        // only append a slash to the URL if the slug exists
        if (seoSlug) {
            seoURL += '/';
        }

        return seoURL;
    }),

    didReceiveAttrs() {
        this._super(...arguments);

        // HACK: ugly method of working around the CSS animations so that we
        // can add throbbers only when the animation has finished
        // TODO: use liquid-fire to handle PSM slide-in and replace tabs manager
        // with something more ember-like
        if (this.get('showSettingsMenu') && !this._showSettingsMenu) {
            this.get('showThrobbers').perform();
        }

        // fired when menu is closed
        if (!this.get('showSettingsMenu') && this._showSettingsMenu) {
            let post = this.get('post');
            let errors = post.get('errors');

            // reset the publish date if it has an error
            if (errors.has('publishedAtBlogDate') || errors.has('publishedAtBlogTime')) {
                post.set('publishedAtBlogTZ', post.get('publishedAtUTC'));
                post.validate({attribute: 'publishedAtBlog'});
            }

            // remove throbbers
            this.set('_showThrobbers', false);
        }

        this._showSettingsMenu = this.get('showSettingsMenu');
    },

    actions: {
        showSubview(subview) {
            this._super(...arguments);

            this.set('subview', subview);

            // Chrome appears to have an animation bug that cancels the slide
            // transition unless there's a delay between the animation starting
            // and the throbbers being removed
            run.later(this, function () {
                this.set('_showThrobbers', false);
            }, 50);
        },

        closeSubview() {
            this._super(...arguments);

            this.set('subview', null);
            this.get('showThrobbers').perform();
        },

        discardEnter() {
            return false;
        },

        togglePage() {
            this.toggleProperty('post.page');

            // If this is a new post.  Don't save the post.  Defer the save
            // to the user pressing the save button
            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        toggleFeatured() {
            this.toggleProperty('post.featured');

            // If this is a new post.  Don't save the post.  Defer the save
            // to the user pressing the save button
            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        /**
         * triggered by user manually changing slug
         */
        updateSlug(newSlug) {
            return this.get('updateSlug')
                .perform(newSlug)
                .catch((error) => {
                    this.showError(error);
                    this.get('post').rollbackAttributes();
                });
        },

        setPublishedAtBlogDate(date) {
            let post = this.get('post');
            let dateString = moment(date).format('YYYY-MM-DD');

            post.get('errors').remove('publishedAtBlogDate');

            if (post.get('isNew') || date === post.get('publishedAtBlogDate')) {
                post.validate({property: 'publishedAtBlog'});
            } else {
                post.set('publishedAtBlogDate', dateString);
                return this.get('savePost').perform();
            }
        },

        setPublishedAtBlogTime(time) {
            let post = this.get('post');

            post.get('errors').remove('publishedAtBlogDate');

            if (post.get('isNew') || time === post.get('publishedAtBlogTime')) {
                post.validate({property: 'publishedAtBlog'});
            } else {
                post.set('publishedAtBlogTime', time);
                return this.get('savePost').perform();
            }
        },

        setCustomExcerpt(excerpt) {
            let post = this.get('post');
            let currentExcerpt = post.get('customExcerpt');

            if (excerpt === currentExcerpt) {
                return;
            }

            post.set('customExcerpt', excerpt);

            return post.validate({property: 'customExcerpt'}).then(() => this.get('savePost').perform());
        },

        setHeaderInjection(code) {
            let post = this.get('post');
            let currentCode = post.get('codeinjectionHead');

            if (code === currentCode) {
                return;
            }

            post.set('codeinjectionHead', code);

            return post.validate({property: 'codeinjectionHead'}).then(() => this.get('savePost').perform());
        },

        setFooterInjection(code) {
            let post = this.get('post');
            let currentCode = post.get('codeinjectionFoot');

            if (code === currentCode) {
                return;
            }

            post.set('codeinjectionFoot', code);

            return post.validate({property: 'codeinjectionFoot'}).then(() => this.get('savePost').perform());
        },

        setMetaTitle(metaTitle) {
            // Grab the post and current stored meta title
            let post = this.get('post');
            let currentTitle = post.get('metaTitle');

            // If the title entered matches the stored meta title, do nothing
            if (currentTitle === metaTitle) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            post.set('metaTitle', metaTitle);

            // Make sure the meta title is valid and if so, save it into the post
            return post.validate({property: 'metaTitle'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setMetaDescription(metaDescription) {
            // Grab the post and current stored meta description
            let post = this.get('post');
            let currentDescription = post.get('metaDescription');

            // If the title entered matches the stored meta title, do nothing
            if (currentDescription === metaDescription) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            post.set('metaDescription', metaDescription);

            // Make sure the meta title is valid and if so, save it into the post
            return post.validate({property: 'metaDescription'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setOgTitle(ogTitle) {
            // Grab the post and current stored facebook title
            let post = this.get('post');
            let currentTitle = post.get('ogTitle');

            // If the title entered matches the stored facebook title, do nothing
            if (currentTitle === ogTitle) {
                return;
            }

            // If the title entered is different, set it as the new facebook title
            post.set('ogTitle', ogTitle);

            // Make sure the facebook title is valid and if so, save it into the post
            return post.validate({property: 'ogTitle'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setOgDescription(ogDescription) {
            // Grab the post and current stored facebook description
            let post = this.get('post');
            let currentDescription = post.get('ogDescription');

            // If the title entered matches the stored facebook description, do nothing
            if (currentDescription === ogDescription) {
                return;
            }

            // If the description entered is different, set it as the new facebook description
            post.set('ogDescription', ogDescription);

            // Make sure the facebook description is valid and if so, save it into the post
            return post.validate({property: 'ogDescription'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setTwitterTitle(twitterTitle) {
            // Grab the post and current stored twitter title
            let post = this.get('post');
            let currentTitle = post.get('twitterTitle');

            // If the title entered matches the stored twitter title, do nothing
            if (currentTitle === twitterTitle) {
                return;
            }

            // If the title entered is different, set it as the new twitter title
            post.set('twitterTitle', twitterTitle);

            // Make sure the twitter title is valid and if so, save it into the post
            return post.validate({property: 'twitterTitle'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setTwitterDescription(twitterDescription) {
            // Grab the post and current stored twitter description
            let post = this.get('post');
            let currentDescription = post.get('twitterDescription');

            // If the description entered matches the stored twitter description, do nothing
            if (currentDescription === twitterDescription) {
                return;
            }

            // If the description entered is different, set it as the new twitter description
            post.set('twitterDescription', twitterDescription);

            // Make sure the twitter description is valid and if so, save it into the post
            return post.validate({property: 'twitterDescription'}).then(() => {
                if (post.get('isNew')) {
                    return;
                }

                return this.get('savePost').perform();
            });
        },

        setCoverImage(image) {
            this.set('post.featureImage', image);

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        clearCoverImage() {
            this.set('post.featureImage', '');

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        setOgImage(image) {
            this.set('post.ogImage', image);

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        clearOgImage() {
            this.set('post.ogImage', '');

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        setTwitterImage(image) {
            this.set('post.twitterImage', image);

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        clearTwitterImage() {
            this.set('post.twitterImage', '');

            if (this.get('post.isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                this.get('post').rollbackAttributes();
            });
        },

        changeAuthors(newAuthors) {
            let post = this.get('post');

            // return if nothing changed
            if (newAuthors.mapBy('id').join() === post.get('authors').mapBy('id').join()) {
                return;
            }

            post.set('authors', newAuthors);
            post.validate({property: 'authors'});

            // if this is a new post (never been saved before), don't try to save it
            if (post.get('isNew')) {
                return;
            }

            this.get('savePost').perform().catch((error) => {
                this.showError(error);
                post.rollbackAttributes();
            });
        },

        deletePost() {
            if (this.get('deletePost')) {
                this.get('deletePost')();
            }
        }
    },

    showThrobbers: task(function* () {
        yield timeout(PSM_ANIMATION_LENGTH);
        this.set('_showThrobbers', true);
    }).restartable(),

    showError(error) {
        // TODO: remove null check once ValidationEngine has been removed
        if (error) {
            this.get('notifications').showAPIError(error);
        }
    }
});
<|endoftext|>


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

'use strict'

const pkg = require('./package.json')
const Configstore = require('configstore')
const defaultConfig = require('./defaultConfig')
const Vorpal = require('vorpal')
const chalk = Vorpal().chalk

const updateNotifier = require('update-notifier')
let notifier = updateNotifier({
  pkg,
  updateCheckInterval: 1000 * 60 * 60 * 24 // 1 day
})

const winston = require('winston')

let networkConnection = true

winston
  .add(winston.transports.File, {
    filename: 'trinity.log',
    level: 'debug'
  })
  .remove(winston.transports.Console)

const wallet = require('./lib/wallet')
const network = require('./lib/network')
const contacts = require('./lib/contacts')

const conf = new Configstore(pkg.name, defaultConfig)

const trinity = Vorpal()
  .delimiter(chalk.dim.green('[' + network.get() + '] ') + chalk.green('trinity') + chalk.green(' >'))
  .history('trinity-command-history')
  .show()

trinity.help(() => {
  let result = ''
  let width = 0

  result += "\n"
  result += chalk.green(' Commands:') + "\n"

  for (let command in trinity.commands) {
    let cmd = trinity.find(trinity.commands[command]._name)
    if (cmd._name.length > width) {
      width = cmd._name.length
    }
  }

  for (let command in trinity.commands) {
    let cmd = trinity.find(trinity.commands[command]._name)
    result += chalk.green("\n" + '    ') + chalk.bold.green(trinity.util.pad(cmd._name, width)) + '    ' + chalk.green(cmd._description)
  }

  result += "\n"

  return result
})

trinity.log(chalk.bold.green("\n" + ' Wake up, Neo… ' + "\n"))

updateCheck(trinity, notifier)

trinity
  .command('send neo', 'Send NEO from one of your addresses to one of your contacts.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.sendNeo(self, args, cb)
  })

trinity
  .command('send gas', 'Send GAS from one of your addresses to one of your contacts.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.sendGas(self, args, cb)
  })

trinity
  .command('wallet list', 'List available wallets.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.list(self, args, cb)
  })

trinity
  .command('wallet show', 'Select an address to show its balances, transactions, claimables, etc.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.show(self, args, cb)
  })

trinity
  .command('wallet create', 'Creates a new wallet address.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.create(self, args, cb)
  })

trinity
  .command('wallet import', 'Import an existing private key in WIF format.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.import(self, args, cb)
  })

trinity
  .command('wallet remove', 'Select an address to remove from local storage.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.remove(self, args, cb)
  })

trinity
  .command('wallet clear', 'Purge all wallet information from local storage.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.clear(self, args, cb)
  })

trinity
  .command('contact list', 'List your contacts.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.list(self, args, cb)
  })

trinity
  .command('contact add', 'Add a new contact.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.add(self, args, cb)
  })

trinity
  .command('contact remove', 'Remove an existing contact.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.remove(self, args, cb)
  })

trinity
  .command('contact clear', 'Purge all contact information from local storage.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.clear(self, args, cb)
  })

trinity
  .command('claim gas', 'Claim all available and unavailable gas.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.claimAllGas(self, args, cb)
  })

trinity
  .command('network', 'Switch to a different network.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    network.set(self, args, cb)
  })

trinity
  .command('version', 'Show Trinity version information.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this

    let output = "\n"
    output += chalk.bold.green('Trinity v' + pkg.version) + "\n"
    output += chalk.green(pkg.description) + "\n"
    output += chalk.green(pkg.homepage) + "\n"
    output += "\n"
    output += chalk.green('Written by ' + pkg.author + ' and released under the ' + pkg.license + ' license.') + "\n"
    output += chalk.green('Please report any bugs you discover to ' + pkg.bugs.url) + "\n"
    output += "\n"
    output += chalk.green('Uses the Neon JS library and Neon API by Ethan Fast and City of Zion (http://cityofzion.io/)') + "\n"

    self.log(output)

    cb()
  })

function commandHelp(args, cb) {
  let cmd = trinity.find(args)

  let result = ''
  let width = 0

  result += "\n"
  result += chalk.green(' Usage: ' + cmd._name) + "\n"
  result += "\n"
  result += chalk.green(' ' + cmd._description) + "\n"

  cb(result)
}

function updateCheck(trinity, notifier) {
  let update = notifier.update

  if (update != undefined) {
    if (update.latest > update.current) {
      trinity.log(chalk.bold.green(' Update available ' + update.current + ' → ' + update.latest))
      trinity.log(chalk.green(' Run "npm install -g trinity-cli" to update'))
      trinity.log('')
    }
  }
}

wallet.updateBalances(trinity)
var trinityLoop = setInterval(() => {
  wallet.updateBalances(trinity)
  wallet.updateClaimables(trinity)
}, 5000)
<commit_msg>Put logs somewhere more sensible<commit_after>#!/usr/bin/env node

'use strict'

const pkg = require('./package.json')
const Configstore = require('configstore')
const defaultConfig = require('./defaultConfig')
const Vorpal = require('vorpal')
const chalk = Vorpal().chalk
const os = require('os')
const fs = require('fs')

const logdir = os.homedir() + '/.trinity/'

if (!fs.existsSync(logdir)) {
  fs.mkdirSync(logdir)
}

const updateNotifier = require('update-notifier')
let notifier = updateNotifier({
  pkg,
  updateCheckInterval: 1000 * 60 * 60 * 24 // 1 day
})

const winston = require('winston')

let networkConnection = true

winston
  .add(winston.transports.File, {
    filename: logdir + '/trinity.log',
    level: 'debug'
  })
  .remove(winston.transports.Console)

const wallet = require('./lib/wallet')
const network = require('./lib/network')
const contacts = require('./lib/contacts')

const conf = new Configstore(pkg.name, defaultConfig)

const trinity = Vorpal()
  .delimiter(chalk.dim.green('[' + network.get() + '] ') + chalk.green('trinity') + chalk.green(' >'))
  .history('trinity-command-history')
  .show()

trinity.help(() => {
  let result = ''
  let width = 0

  result += "\n"
  result += chalk.green(' Commands:') + "\n"

  for (let command in trinity.commands) {
    let cmd = trinity.find(trinity.commands[command]._name)
    if (cmd._name.length > width) {
      width = cmd._name.length
    }
  }

  for (let command in trinity.commands) {
    let cmd = trinity.find(trinity.commands[command]._name)
    result += chalk.green("\n" + '    ') + chalk.bold.green(trinity.util.pad(cmd._name, width)) + '    ' + chalk.green(cmd._description)
  }

  result += "\n"

  return result
})

trinity.log(chalk.bold.green("\n" + ' Wake up, Neo… ' + "\n"))

updateCheck(trinity, notifier)

trinity
  .command('send neo', 'Send NEO from one of your addresses to one of your contacts.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.sendNeo(self, args, cb)
  })

trinity
  .command('send gas', 'Send GAS from one of your addresses to one of your contacts.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.sendGas(self, args, cb)
  })

trinity
  .command('wallet list', 'List available wallets.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.list(self, args, cb)
  })

trinity
  .command('wallet show', 'Select an address to show its balances, transactions, claimables, etc.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.show(self, args, cb)
  })

trinity
  .command('wallet create', 'Creates a new wallet address.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.create(self, args, cb)
  })

trinity
  .command('wallet import', 'Import an existing private key in WIF format.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.import(self, args, cb)
  })

trinity
  .command('wallet remove', 'Select an address to remove from local storage.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.remove(self, args, cb)
  })

trinity
  .command('wallet clear', 'Purge all wallet information from local storage.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.clear(self, args, cb)
  })

trinity
  .command('contact list', 'List your contacts.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.list(self, args, cb)
  })

trinity
  .command('contact add', 'Add a new contact.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.add(self, args, cb)
  })

trinity
  .command('contact remove', 'Remove an existing contact.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.remove(self, args, cb)
  })

trinity
  .command('contact clear', 'Purge all contact information from local storage.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    contacts.clear(self, args, cb)
  })

trinity
  .command('claim gas', 'Claim all available and unavailable gas.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    wallet.claimAllGas(self, args, cb)
  })

trinity
  .command('network', 'Switch to a different network.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this
    network.set(self, args, cb)
  })

trinity
  .command('version', 'Show Trinity version information.')
  .help(commandHelp)
  .action(function (args, cb) {
    let self = this

    let output = "\n"
    output += chalk.bold.green('Trinity v' + pkg.version) + "\n"
    output += chalk.green(pkg.description) + "\n"
    output += chalk.green(pkg.homepage) + "\n"
    output += "\n"
    output += chalk.green('Written by ' + pkg.author + ' and released under the ' + pkg.license + ' license.') + "\n"
    output += chalk.green('Please report any bugs you discover to ' + pkg.bugs.url) + "\n"
    output += "\n"
    output += chalk.green('Uses the Neon JS library and Neon API by Ethan Fast and City of Zion (http://cityofzion.io/)') + "\n"

    self.log(output)

    cb()
  })

function commandHelp(args, cb) {
  let cmd = trinity.find(args)

  let result = ''
  let width = 0

  result += "\n"
  result += chalk.green(' Usage: ' + cmd._name) + "\n"
  result += "\n"
  result += chalk.green(' ' + cmd._description) + "\n"

  cb(result)
}

function updateCheck(trinity, notifier) {
  let update = notifier.update

  if (update != undefined) {
    if (update.latest > update.current) {
      trinity.log(chalk.bold.green(' Update available ' + update.current + ' → ' + update.latest))
      trinity.log(chalk.green(' Run "npm install -g trinity-cli" to update'))
      trinity.log('')
    }
  }
}

wallet.updateBalances(trinity)
var trinityLoop = setInterval(() => {
  wallet.updateBalances(trinity)
  wallet.updateClaimables(trinity)
}, 5000)
<|endoftext|>


<commit_before>var images = [];

//function ensureGalleryInitialized() {
document.addEventListener('dynload', function(event) {
    if (images.length > 0) return;

    var thumbnails = getByClassName("thumbnail");

    for (var i = 0; i < thumbnails.length; ++i) {
        var image = thumbnails[i].getElementsByTagName("IMG")[0];
        images.push(image.src);
    }

    if (window.location.hash) {
        var imageFull = getByClassName("image-full")[0];
        imageFull.src = "static/gallery/" + location.hash.substr(1);
        setElement("show", "hidden", "visible");
    }

}, false);

function currentImage() {
    var imageFull = getByClassName("image-full")[0];
    return images.indexOf(imageFull.src);
}

function showImage(event) {
    var imageFull = getByClassName("image-full")[0];
    imageFull.src = event.target.src;
    var path = imageFull.src.replace("/static", "");
    pushHistory(document.title, path);
    setElement("show", "hidden", "visible");
}

function hideImage(event) {
    if (event.target !== event.currentTarget)
    {
        stop(event);
        return false;
    }
    pushHistory(document.title, "/gallery");

    setElement("show", "visible", "hidden");
}

window.onhelp = function(ev) {
    switchElement("help", "hidden", "visible");
    stop(ev);
    return false;
}

window.onkeypress = function(event) {
    var imageFull = getByClassName("image-full")[0];

    if (event.key === "Escape" || event.key === "Esc") {
        setElement("show", "visible", "hidden");
        pushHistory(document.title, "/gallery");
    }

    if (event.key === "ArrowRight" || event.key.toLowerCase() === "d") {
        imageFull.src = images[(currentImage() + 1) % images.length];
        var path = imageFull.src.replace("/static", "");
        pushHistory(document.title, path);
    }

    if (event.key === "ArrowLeft" || event.key.toLowerCase() === "a") {
        var nextImage = currentImage() - 1;
        if (nextImage === -1)
            nextImage += images.length;

        imageFull.src = images[nextImage];
        var path = imageFull.src.replace("/static", "");
        pushHistory(document.title, path);
    }

    if (event.key === "F1") {
        switchElement("help", "hidden", "visible");
    }
}
<commit_msg>Chrome support<commit_after>var images = [];

//function ensureGalleryInitialized() {
document.addEventListener('dynload', function(event) {
    if (images.length > 0) return;

    var thumbnails = getByClassName("thumbnail");

    for (var i = 0; i < thumbnails.length; ++i) {
        var image = thumbnails[i].getElementsByTagName("IMG")[0];
        images.push(image.src);
    }
}, false);

function currentImage() {
    var imageFull = getByClassName("image-full")[0];
    return images.indexOf(imageFull.src);
}

function showImage(event) {
    var imageFull = getByClassName("image-full")[0];
    imageFull.src = event.target.src;
    var path = imageFull.src.replace("/static", "");
    pushHistory(document.title, path);
    setElement("show", "hidden", "visible");
}

function hideImage(event) {
    if (event.target !== event.currentTarget)
    {
        stop(event);
        return false;
    }
    pushHistory(document.title, "/gallery");

    setElement("show", "visible", "hidden");
}

window.onhelp = function(ev) {
    switchElement("help", "hidden", "visible");
    stop(ev);
    return false;
}

window.addEventListener("keydown", function(event) {
    var imageFull = getByClassName("image-full")[0];

    var showVisible = getByClassName("show-visible").length > 0;

    stop(event);

    if (!showVisible)
        return false;

    if (event.key === "Escape" || event.key === "Esc") {
        setElement("show", "visible", "hidden");
        pushHistory(document.title, "/gallery");
    }

    if (event.key === "ArrowRight" || event.key.toLowerCase() === "d") {
        imageFull.src = images[(currentImage() + 1) % images.length];
        var path = imageFull.src.replace("/static", "");
        pushHistory(document.title, path);
    }

    if (event.key === "ArrowLeft" || event.key.toLowerCase() === "a") {
        var nextImage = currentImage() - 1;
        if (nextImage === -1)
            nextImage += images.length;

        imageFull.src = images[nextImage];
        var path = imageFull.src.replace("/static", "");
        pushHistory(document.title, path);
    }

    if (event.key === "F1") {
        switchElement("help", "hidden", "visible");
    }

    return false;
});
<|endoftext|>


<commit_before>#!/usr/bin/env node
var assert = require('assert');

var async = require('async');
var express = require('express');
var request = require('request');
var sugar = require('object-sugar');

var rest = require('../lib/rest-sugar');
var models = require('./models');

main();

function main() {
    var host = 'http://localhost';
    var port = 3000;
    var prefix = '/api/';
    var resource = host + ':' + port + prefix + 'authors';
    var app = express();

    app.configure(function() {
        app.use(express.methodOverride()); // handles PUT
        app.use(express.bodyParser()); // handles POST
        app.use(app.router);
    });

    rest.init(app, prefix, {
        authors: models.Author
    }, sugar, function(err, ok) {
        // TODO: perhaps treat this as a middleware instead and provide "next"?
        return function(req, res) {
            ok(req, res);
        };
    }, function(req, res, body) {

    });

    start();
    app.listen(port, function(err) {
        if(err) return console.error(err);

        // TODO: empty db before each so these can be treated as idempotent funcs
        async.series([
            getResource(resource),
            postResource(resource),
            postResourceViaGet(resource)
        ], finish);
    });
}

function getResource(r) {
    return function(cb) {
        request.get({url: r, json: true}, function(err, d, body) {
            if(err) return console.error(err);

            assert.equal(body.length, 0);

            cb();
        });
    };
}

function postResource(r) {
    return function(cb) {
        var name = 'Joe';

        request.post({url: r, json: {name: name}}, function(err, r, body) {
            if(err) return console.error(err);

            assert.equal(body.name, name);

            cb();
        });
    };
}

function postResourceViaGet(r) {
    return function(cb) {
        var name = 'Jack';

        request.get({url: r, qs: {name: name, method: 'post'}, json: true}, function(err, r, body) {
            if(err) return console.error(err);

            assert.equal(body.name, name);

            cb();
        });
    };
}

function start() {
    console.log('Running tests!');
}

function finish() {
    console.log('Tests finished!');
    process.exit();
}
<commit_msg>Remove model data between test executions<commit_after>#!/usr/bin/env node
var assert = require('assert');

var async = require('async');
var express = require('express');
var request = require('request');
var sugar = require('object-sugar');

var rest = require('../lib/rest-sugar');
var models = require('./models');

main();

function main() {
    var host = 'http://localhost';
    var port = 3000;
    var prefix = '/api/';
    var resource = host + ':' + port + prefix + 'authors';
    var app = express();

    app.configure(function() {
        app.use(express.methodOverride()); // handles PUT
        app.use(express.bodyParser()); // handles POST
        app.use(app.router);
    });

    rest.init(app, prefix, {
        authors: models.Author
    }, sugar, function(err, ok) {
        // TODO: perhaps treat this as a middleware instead and provide "next"?
        return function(req, res) {
            ok(req, res);
        };
    }, function(req, res, body) {

    });

    start();
    app.listen(port, function(err) {
        if(err) return console.error(err);

        async.series(setup([
            getResource(resource),
            postResource(resource),
            postResourceViaGet(resource)
        ], function(t) {
            return function(cb) {
                async.series([
                    removeAuthors
                ], t.bind(undefined, cb));
            };
        }), finish);
    });
}

function removeAuthors(cb) {
    sugar.removeAll(models.Author, cb);
}

function setup(tests, fn) {
    return tests.map(function(t) {
        return fn(t);
    });
}

function getResource(r) {
    return function(cb) {
        request.get({url: r, json: true}, function(err, d, body) {
            if(err) return console.error(err);

            assert.equal(body.length, 0);

            cb();
        });
    };
}

function postResource(r) {
    return function(cb) {
        var name = 'Joe';

        request.post({url: r, json: {name: name}}, function(err, r, body) {
            if(err) return console.error(err);

            assert.equal(body.name, name);

            cb();
        });
    };
}

function postResourceViaGet(r) {
    return function(cb) {
        var name = 'Jack';

        request.get({url: r, qs: {name: name, method: 'post'}, json: true}, function(err, r, body) {
            if(err) return console.error(err);

            assert.equal(body.name, name);

            cb();
        });
    };
}

function start() {
    console.log('Running tests!');
}

function finish() {
    console.log('Tests finished!');
    process.exit();
}
<|endoftext|>


<commit_before>/**
 Copyright 2015 Rick Wargo. All Rights Reserved.

 Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at

 http://aws.amazon.com/apache2.0/

 or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */

'use strict';

// Util functions for generating valid SSML from plain text
// ========================================================

var ssml = {
    fromStr: function (str, current_ssml) {
        // remove any <speak> tags from the input string, if they exist. There can only be one set of <speak> tags.
        str = str || '';
        str = str.replace(/<speak>/gi, ' ').replace(/<\/speak>/gi, ' ').trim();

        // and remove them from the concatenated string, if exists
        current_ssml = current_ssml || '';
        current_ssml = current_ssml.replace(/<speak>/gi, ' ').replace(/<\/speak>/gi, ' ').trim();

        //TODO: Need a library with how to easily construct these statements with appropriate spacing, etc.
        //TODO: make sure all attribute values are surrounded by "..."
        var ssml_str = '<speak>' + current_ssml + (current_ssml === '' ? '' : ' ') + str + '</speak>';

        return ssml_str.replace(/  +/, ' ');
    }
    ,
    cleanse: function (str) {
        // <p> is left in place to support intended HTML output
        return str.replace(/<\/?(speak|break|phoneme|audio|say-as|s\b|w\b)[^>]*>/gi, ' ')
            .replace(/\s*\n\s*/g, "\n")
            .replace(/  +/g, ' ')
            .replace(/ ([.,!?;:])/g, "$1")
            .trim();
    }
};

module.change_code = 1;
module.exports = ssml;
<commit_msg>apply consistent formatting<commit_after>/**
 Copyright 2015 Rick Wargo. All Rights Reserved.

 Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at

 http://aws.amazon.com/apache2.0/

 or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */

"use strict";

// Util functions for generating valid SSML from plain text
// ========================================================

var ssml = {
  fromStr: function(str, current_ssml) {
    // remove any <speak> tags from the input string, if they exist. There can only be one set of <speak> tags.
    str = str || "";
    str = str.replace(/<speak>/gi, " ").replace(/<\/speak>/gi, " ").trim();

    // and remove them from the concatenated string, if exists
    current_ssml = current_ssml || "";
    current_ssml = current_ssml.replace(/<speak>/gi, " ").replace(/<\/speak>/gi, " ").trim();

    //TODO: Need a library with how to easily construct these statements with appropriate spacing, etc.
    //TODO: make sure all attribute values are surrounded by "..."
    var ssml_str = "<speak>" + current_ssml + (current_ssml === "" ? "" : " ") + str + "</speak>";

    return ssml_str.replace(/  +/, " ");
  },
  cleanse: function(str) {
    // <p> is left in place to support intended HTML output
    return str.replace(/<\/?(speak|break|phoneme|audio|say-as|s\b|w\b)[^>]*>/gi, " ")
      .replace(/\s*\n\s*/g, "\n")
      .replace(/  +/g, " ")
      .replace(/ ([.,!?;:])/g, "$1")
      .trim();
  }
};

module.change_code = 1;
module.exports = ssml;
<|endoftext|>


<commit_before>/**
 * @ngdoc directive
 * @memberof ngmap
 * @name custom-marker
 * @param Attr2Options {service} convert html attribute to Gogole map api options
 * @param $compile {service} AngularJS $compile service
 * @param $timeout {service} AngularJS $timeout
 * @description
 *   Marker with html
 *   Requires:  map directive
 *   Restrict To:  Element
 *
 * @attr {String} position required, position on map
 * @attr {Number} z-index optional
 * @attr {Boolean} visible optional
 * @example
 *
 * Example:
 *   <map center="41.850033,-87.6500523" zoom="3">
 *     <custom-marker position="41.850033,-87.6500523">
 *

 *         Home
 *

 *     </custom-marker>
 *   </map>
 *
 */
(function() {
  'use strict';
  var parser, $compile, $timeout;

  var cAbortEvent = function (e) {
    e.preventDefault && e.preventDefault();
    e.cancelBubble = true;
    e.stopPropagation && e.stopPropagation();
  };

  var CustomMarker = function(options) {
    options = options || {};

    this.el = document.createElement('div');
    this.el.style.display = 'inline-block';
    Nthis.visible = true; for (var key in options) {
     this[key] = options[key];
    }
  };

  CustomMarker.prototype = new google.maps.OverlayView();

  CustomMarker.prototype.setContent = function(html, scope) {
    this.html = html;
    if (scope) {
      var compiledEl = $compile(html)(scope);
      var customMarkerEl = compiledEl[0];
      var me = this;
      $timeout(function() {
        me.content = customMarkerEl.innerHTML;
        me.el.innerHTML = me.content;
      });
    } else {
      this.content = html;
      this.el.innerHTML = this.content;
    }
    this.el.style.position = 'relative';
    this.el.className = 'custom-marker';
  };

  CustomMarker.prototype.setPosition = function(position) {
    position && (this.position = position);
    if (this.getProjection() && typeof this.position.lng == 'function') {
      var posPixel = this.getProjection().fromLatLngToDivPixel(this.position);
      var x = Math.round(posPixel.x - (this.el.offsetWidth/2));
      var y = Math.round(posPixel.y - this.el.offsetHeight - 10); // 10px for anchor
      this.el.style.left = x + "px";
      this.el.style.top = y + "px";
    }
  };

  CustomMarker.prototype.setZIndex = function(zIndex) {
    zIndex && (this.zIndex = zIndex);
    this.el.style.zIndex = this.zIndex;
  };

  CustomMarker.prototype.setVisible = function(visible) {
    this.el.style.display = visible ? 'inline-block' : 'none';
    this.visible = visible;
  };

  CustomMarker.prototype.addClass = function(className) {
    var classNames = this.el.className.split(' ');
    (classNames.indexOf(className) == -1) && classNames.push(className);
    this.el.className = classNames.join(' ');
  };

  CustomMarker.prototype.removeClass = function(className) {
    var classNames = this.el.className.split(' ');
    var index = classNames.indexOf(className);
    (index > -1) && classNames.splice(index, 1);
    this.el.className = classNames.join(' ');
  };

  CustomMarker.prototype.onAdd = function() {
    this.getPanes().overlayMouseTarget.appendChild(this.el);
  };

  CustomMarker.prototype.draw = function() {
    this.setPosition();
    this.setZIndex(this.zIndex);
    this.setVisible(this.visible);
  };

  CustomMarker.prototype.onRemove = function() {
    this.el.parentNode.removeChild(this.el);
    this.el = null;
  };

  var customMarkerDirective = function(Attr2Options, _$compile_, _$timeout_)  {
    parser = Attr2Options;
    $compile = _$compile_;
    $timeout = _$timeout_;

    return {
      restrict: 'E',
      require: '^map',
      link: function(scope, element, attrs, mapController) {
        var orgAttrs = parser.orgAttributes(element);
        var filtered = parser.filter(attrs);
        var options = parser.getOptions(filtered, scope);
        var events = parser.getEvents(scope, filtered);

        /**
         * build a custom marker element
         */
        var removedEl = element[0].parentElement.removeChild(element[0]);
        console.log("custom-marker options", options);
        var customMarker = new CustomMarker(options);
        customMarker.setContent(removedEl.innerHTML, scope);
        console.log('customMarker', customMarker);

        console.log("custom-marker events", "events");
        for (var eventName in events) {
          google.maps.event.addDomListener(
            customMarker.el, eventName, events[eventName]);
        }
        mapController.addObject('customMarkers', customMarker);

        if (!(options.position instanceof google.maps.LatLng)) {
          mapController.getGeoLocation(options.position).then(
            function(latlng) {
              customMarker.setPosition(latlng);
            }
          );
        }

        element.bind('$destroy', function() {
          //Is it required to remove event listeners when DOM is removed?
          mapController.deleteObject('customMarkers', marker);
        });

      } //link
    }; // return
  };// function
  customMarkerDirective.$inject = ['Attr2Options', '$compile', '$timeout'];

  angular.module('ngMap').directive('customMarker', customMarkerDirective);
})();
<commit_msg>Update custom-marker.js<commit_after>/**
 * @ngdoc directive
 * @memberof ngmap
 * @name custom-marker
 * @param Attr2Options {service} convert html attribute to Gogole map api options
 * @param $compile {service} AngularJS $compile service
 * @param $timeout {service} AngularJS $timeout
 * @description
 *   Marker with html
 *   Requires:  map directive
 *   Restrict To:  Element
 *
 * @attr {String} position required, position on map
 * @attr {Number} z-index optional
 * @attr {Boolean} visible optional
 * @example
 *
 * Example:
 *   <map center="41.850033,-87.6500523" zoom="3">
 *     <custom-marker position="41.850033,-87.6500523">
 *

 *         Home
 *

 *     </custom-marker>
 *   </map>
 *
 */
(function() {
  'use strict';
  var parser, $compile, $timeout;

  var cAbortEvent = function (e) {
    e.preventDefault && e.preventDefault();
    e.cancelBubble = true;
    e.stopPropagation && e.stopPropagation();
  };

  var CustomMarker = function(options) {
    options = options || {};

    this.el = document.createElement('div');
    this.el.style.display = 'inline-block';
    this.visible = true; for (var key in options) {
     this[key] = options[key];
    }
  };

  CustomMarker.prototype = new google.maps.OverlayView();

  CustomMarker.prototype.setContent = function(html, scope) {
    this.html = html;
    if (scope) {
      var compiledEl = $compile(html)(scope);
      var customMarkerEl = compiledEl[0];
      var me = this;
      $timeout(function() {
        me.content = customMarkerEl.innerHTML;
        me.el.innerHTML = me.content;
      });
    } else {
      this.content = html;
      this.el.innerHTML = this.content;
    }
    this.el.style.position = 'relative';
    this.el.className = 'custom-marker';
  };

  CustomMarker.prototype.setPosition = function(position) {
    position && (this.position = position);
    if (this.getProjection() && typeof this.position.lng == 'function') {
      var posPixel = this.getProjection().fromLatLngToDivPixel(this.position);
      var x = Math.round(posPixel.x - (this.el.offsetWidth/2));
      var y = Math.round(posPixel.y - this.el.offsetHeight - 10); // 10px for anchor
      this.el.style.left = x + "px";
      this.el.style.top = y + "px";
    }
  };

  CustomMarker.prototype.setZIndex = function(zIndex) {
    zIndex && (this.zIndex = zIndex);
    this.el.style.zIndex = this.zIndex;
  };

  CustomMarker.prototype.setVisible = function(visible) {
    this.el.style.display = visible ? 'inline-block' : 'none';
    this.visible = visible;
  };

  CustomMarker.prototype.addClass = function(className) {
    var classNames = this.el.className.split(' ');
    (classNames.indexOf(className) == -1) && classNames.push(className);
    this.el.className = classNames.join(' ');
  };

  CustomMarker.prototype.removeClass = function(className) {
    var classNames = this.el.className.split(' ');
    var index = classNames.indexOf(className);
    (index > -1) && classNames.splice(index, 1);
    this.el.className = classNames.join(' ');
  };

  CustomMarker.prototype.onAdd = function() {
    this.getPanes().overlayMouseTarget.appendChild(this.el);
  };

  CustomMarker.prototype.draw = function() {
    this.setPosition();
    this.setZIndex(this.zIndex);
    this.setVisible(this.visible);
  };

  CustomMarker.prototype.onRemove = function() {
    this.el.parentNode.removeChild(this.el);
    this.el = null;
  };

  var customMarkerDirective = function(Attr2Options, _$compile_, _$timeout_)  {
    parser = Attr2Options;
    $compile = _$compile_;
    $timeout = _$timeout_;

    return {
      restrict: 'E',
      require: '^map',
      link: function(scope, element, attrs, mapController) {
        var orgAttrs = parser.orgAttributes(element);
        var filtered = parser.filter(attrs);
        var options = parser.getOptions(filtered, scope);
        var events = parser.getEvents(scope, filtered);

        /**
         * build a custom marker element
         */
        var removedEl = element[0].parentElement.removeChild(element[0]);
        console.log("custom-marker options", options);
        var customMarker = new CustomMarker(options);
        customMarker.setContent(removedEl.innerHTML, scope);
        console.log('customMarker', customMarker);

        console.log("custom-marker events", "events");
        for (var eventName in events) {
          google.maps.event.addDomListener(
            customMarker.el, eventName, events[eventName]);
        }
        mapController.addObject('customMarkers', customMarker);

        if (!(options.position instanceof google.maps.LatLng)) {
          mapController.getGeoLocation(options.position).then(
            function(latlng) {
              customMarker.setPosition(latlng);
            }
          );
        }

        element.bind('$destroy', function() {
          //Is it required to remove event listeners when DOM is removed?
          mapController.deleteObject('customMarkers', marker);
        });

      } //link
    }; // return
  };// function
  customMarkerDirective.$inject = ['Attr2Options', '$compile', '$timeout'];

  angular.module('ngMap').directive('customMarker', customMarkerDirective);
})();
<|endoftext|>


<commit_before>
angular.module('bkRulesLab', [])
.controller('bkLabController', function($scope) {
  $scope.rule = theRule();
})
.directive('constraintPaymentVolume', function (/*Range*/) {
  return {
    restrict: 'E',
    scope: { key : '=' },
    controller: function ($scope) {
      if (angular.isString($scope.rule.constraints.payment_volume)) {
        $scope.rule.constraints.payment_volume = Range.fromString($scope.rule.constraints.payment_volume);
      }
    },
    templateUrl: '../panel/bk/views/constraint/payment_volume.html'
  }
});

function theRule() {
  return {
/*  "priority": 0,
    "toDelete": false,*/
    "key": "MERCHANT_REVENUE_SHARE",
/*    "value": {
      "revenue-share": [
        0,
        "%"
      ],
      "applied-to": null
    },
    "constraintsKeys": [
      "payment_method",
      "country",
      "credit_provider",
      "merchant",
      "payment_volume",
      "amount",
      "container_amount",
      "connectivity",
      "route",
      "container_kind",
      "business_model",
      "service",
      "category",
      "context"
    ],*/
    "constraints": {
      "payment_method": "any",
      "country": "any",
      "credit_provider": "any",
      "merchant": "any",
      "payment_volume": {
        "from": {
          "operator": "",
          "value": ""
        },
        "to": {
          "operator": "",
          "value": ""
        }
      },
      "amount": "any",
      "container_amount": "any",
      "connectivity": "any",
      "route": "any",
      "container_kind": "any",
      "business_model": "any",
      "service": "any",
      "category": "any",
      "context": "any"
    }/*,
    "_id": {
      "$id": "new-0"
    }*/
  }
}<commit_msg>placeholder for country directive<commit_after>
angular.module('bkRulesLab', [])
.controller('bkLabController', function($scope) {
  $scope.rule = theRule();
})
.directive('constraintPaymentVolume', function (/*Range*/) {
  return {
    restrict: 'E',
    scope: { key : '=' },
    controller: function ($scope) {
      if (angular.isString($scope.rule.constraints.payment_volume)) {
        $scope.rule.constraints.payment_volume = Range.fromString($scope.rule.constraints.payment_volume);
      }
    },
    templateUrl: '../panel/bk/views/constraint/payment_volume.html'
  }
})
.directive('constraintCountry', function ($http) {
  return {
    restrict: 'E',
    replace: true,
    controller: function ($scope, $attrs) {
    },
    templateUrl: '../panel/bk/views/constraint/select.html',
    link: function(scope, elem, attrs) {
      // need an operator that reads 'any' in the model
      // and puts it as the selected option
    }
  }
});

function theRule() {
  return {
/*  "priority": 0,
    "toDelete": false,*/
    "key": "MERCHANT_REVENUE_SHARE",
/*    "value": {
      "revenue-share": [
        0,
        "%"
      ],
      "applied-to": null
    },
    "constraintsKeys": [
      "payment_method",
      "country",
      "credit_provider",
      "merchant",
      "payment_volume",
      "amount",
      "container_amount",
      "connectivity",
      "route",
      "container_kind",
      "business_model",
      "service",
      "category",
      "context"
    ],*/
    "constraints": {
      "payment_method": "any",
      "country": "any",
      "credit_provider": "any",
      "merchant": "any",
      "payment_volume": {
        "from": {
          "operator": "",
          "value": ""
        },
        "to": {
          "operator": "",
          "value": ""
        }
      },
      "amount": "any",
      "container_amount": "any",
      "connectivity": "any",
      "route": "any",
      "container_kind": "any",
      "business_model": "any",
      "service": "any",
      "category": "any",
      "context": "any"
    }/*,
    "_id": {
      "$id": "new-0"
    }*/
  }
}<|endoftext|>


<commit_before>'use strict';

/*
 * AngularJS Toaster
 * Version: 0.4.5
 *
 * Copyright 2013 Jiri Kavulak.
 * All Rights Reserved.
 * Use, reproduction, distribution, and modification of this code is subject to the terms and
 * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
 *
 * Author: Jiri Kavulak
 * Related to project of John Papa and Hans Fjällemark
 */

angular.module('toaster', ['ngAnimate'])
.service('toaster', ['$rootScope', function ($rootScope) {
    this.pop = function (type, title, body, timeout, bodyOutputType) {
        this.toast = {
            type: type,
            title: title,
            body: body,
            timeout: timeout,
            bodyOutputType: bodyOutputType
        };
        $rootScope.$broadcast('toaster-newToast');
    };

    this.clear = function () {
        $rootScope.$broadcast('toaster-clearToasts');
    };
}])
.constant('toasterConfig', {
    'limit': 0,                   // limits max number of toasts
    'tap-to-dismiss': true,
    'newest-on-top': true,
    //'fade-in': 1000,            // done in css
    //'on-fade-in': undefined,    // not implemented
    //'fade-out': 1000,           // done in css
    // 'on-fade-out': undefined,  // not implemented
    //'extended-time-out': 1000,    // not implemented
    'time-out': 5000, // Set timeOut and extendedTimeout to 0 to make it sticky
    'icon-classes': {
        error: 'toast-error',
        info: 'toast-info',
        success: 'toast-success',
        warning: 'toast-warning'
    },
    'body-output-type': '', // Options: '', 'trustedHtml', 'template'
    'body-template': 'toasterBodyTmpl.html',
    'icon-class': 'toast-info',
    'position-class': 'toast-top-right',
    'title-class': 'toast-title',
    'message-class': 'toast-message'
})
.directive('toasterContainer', ['$compile', '$timeout', '$sce', 'toasterConfig', 'toaster',
function ($compile, $timeout, $sce, toasterConfig, toaster) {
    return {
        replace: true,
        restrict: 'EA',
        scope: true, // creates an internal scope for this directive
        link: function (scope, elm, attrs) {

            var id = 0,
                mergedConfig;

            mergedConfig = angular.extend({}, toasterConfig, scope.$eval(attrs.toasterOptions));

            scope.config = {
                position: mergedConfig['position-class'],
                title: mergedConfig['title-class'],
                message: mergedConfig['message-class'],
                tap: mergedConfig['tap-to-dismiss']
            };

            scope.configureTimer = function configureTimer(toast) {
                var timeout = typeof (toast.timeout) == "number" ? toast.timeout : mergedConfig['time-out'];
                if (timeout > 0)
                    setTimeout(toast, timeout);
            };

            function addToast(toast) {
                toast.type = mergedConfig['icon-classes'][toast.type];
                if (!toast.type)
                    toast.type = mergedConfig['icon-class'];

                id++;
                angular.extend(toast, { id: id });

                // Set the toast.bodyOutputType to the default if it isn't set
                toast.bodyOutputType = toast.bodyOutputType || mergedConfig['body-output-type'];
                switch (toast.bodyOutputType) {
                    case 'trustedHtml':
                        toast.html = $sce.trustAsHtml(toast.body);
                        break;
                    case 'template':
                        toast.bodyTemplate = toast.body || mergedConfig['body-template'];
                        break;
                }

                scope.configureTimer(toast);

                if (mergedConfig['newest-on-top'] === true) {
                    scope.toasters.unshift(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.pop();
                    }
                } else {
                    scope.toasters.push(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.shift();
                    }
                }
            }

            function setTimeout(toast, time) {
                toast.timeout = $timeout(function () {
                    scope.removeToast(toast.id);
                }, time);
            }

            scope.toasters = [];
            scope.$on('toaster-newToast', function () {
                addToast(toaster.toast);
            });

            scope.$on('toaster-clearToasts', function () {
                scope.toasters.splice(0, scope.toasters.length);
            });
        },
        controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {

            $scope.stopTimer = function (toast) {
                if (toast.timeout) {
                    $timeout.cancel(toast.timeout);
                    toast.timeout = null;
                }
            };

            $scope.restartTimer = function (toast) {
                if (!toast.timeout)
                    $scope.configureTimer(toast);
            };

            $scope.removeToast = function (id) {
                var i = 0;
                for (i; i < $scope.toasters.length; i++) {
                    if ($scope.toasters[i].id === id)
                        break;
                }
                $scope.toasters.splice(i, 1);
            };

            $scope.remove = function (id) {
                if ($scope.config.tap === true) {
                    $scope.removeToast(id);
                }
            };
        }],
        template:
        '
' +
            '
' +
              '
{{toaster.title}}
' +
              '
' +
                '

' +
                '

' +
                '
{{toaster.body}}
' +
              '
' +
            '
' +
        '
'
    };
}]);
<commit_msg>Added click handler<commit_after>'use strict';

/*
 * AngularJS Toaster
 * Version: 0.4.5
 *
 * Copyright 2013 Jiri Kavulak.
 * All Rights Reserved.
 * Use, reproduction, distribution, and modification of this code is subject to the terms and
 * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
 *
 * Author: Jiri Kavulak
 * Related to project of John Papa and Hans Fjällemark
 */

angular.module('toaster', ['ngAnimate'])
.service('toaster', ['$rootScope', function ($rootScope) {
    this.pop = function (type, title, body, timeout, bodyOutputType, clickHandler) {
        this.toast = {
            type: type,
            title: title,
            body: body,
            timeout: timeout,
            bodyOutputType: bodyOutputType,
            clickHandler: clickHandler
        };
        $rootScope.$broadcast('toaster-newToast');
    };

    this.clear = function () {
        $rootScope.$broadcast('toaster-clearToasts');
    };
}])
.constant('toasterConfig', {
    'limit': 0,                   // limits max number of toasts
    'tap-to-dismiss': true,
    'newest-on-top': true,
    //'fade-in': 1000,            // done in css
    //'on-fade-in': undefined,    // not implemented
    //'fade-out': 1000,           // done in css
    // 'on-fade-out': undefined,  // not implemented
    //'extended-time-out': 1000,    // not implemented
    'time-out': 5000, // Set timeOut and extendedTimeout to 0 to make it sticky
    'icon-classes': {
        error: 'toast-error',
        info: 'toast-info',
        success: 'toast-success',
        warning: 'toast-warning'
    },
    'body-output-type': '', // Options: '', 'trustedHtml', 'template'
    'body-template': 'toasterBodyTmpl.html',
    'icon-class': 'toast-info',
    'position-class': 'toast-top-right',
    'title-class': 'toast-title',
    'message-class': 'toast-message'
})
.directive('toasterContainer', ['$compile', '$timeout', '$sce', 'toasterConfig', 'toaster',
function ($compile, $timeout, $sce, toasterConfig, toaster) {
    return {
        replace: true,
        restrict: 'EA',
        scope: true, // creates an internal scope for this directive
        link: function (scope, elm, attrs) {

            var id = 0,
                mergedConfig;

            mergedConfig = angular.extend({}, toasterConfig, scope.$eval(attrs.toasterOptions));

            scope.config = {
                position: mergedConfig['position-class'],
                title: mergedConfig['title-class'],
                message: mergedConfig['message-class'],
                tap: mergedConfig['tap-to-dismiss']
            };

            scope.configureTimer = function configureTimer(toast) {
                var timeout = typeof (toast.timeout) == "number" ? toast.timeout : mergedConfig['time-out'];
                if (timeout > 0)
                    setTimeout(toast, timeout);
            };

            function addToast(toast) {
                toast.type = mergedConfig['icon-classes'][toast.type];
                if (!toast.type)
                    toast.type = mergedConfig['icon-class'];

                id++;
                angular.extend(toast, { id: id });

                // Set the toast.bodyOutputType to the default if it isn't set
                toast.bodyOutputType = toast.bodyOutputType || mergedConfig['body-output-type'];
                switch (toast.bodyOutputType) {
                    case 'trustedHtml':
                        toast.html = $sce.trustAsHtml(toast.body);
                        break;
                    case 'template':
                        toast.bodyTemplate = toast.body || mergedConfig['body-template'];
                        break;
                }

                scope.configureTimer(toast);

                if (mergedConfig['newest-on-top'] === true) {
                    scope.toasters.unshift(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.pop();
                    }
                } else {
                    scope.toasters.push(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.shift();
                    }
                }
            }

            function setTimeout(toast, time) {
                toast.timeout = $timeout(function () {
                    scope.removeToast(toast.id);
                }, time);
            }

            scope.toasters = [];
            scope.$on('toaster-newToast', function () {
                addToast(toaster.toast);
            });

            scope.$on('toaster-clearToasts', function () {
                scope.toasters.splice(0, scope.toasters.length);
            });
        },
        controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {

            $scope.stopTimer = function (toast) {
                if (toast.timeout) {
                    $timeout.cancel(toast.timeout);
                    toast.timeout = null;
                }
            };

            $scope.restartTimer = function (toast) {
                if (!toast.timeout)
                    $scope.configureTimer(toast);
            };

            $scope.removeToast = function (id) {
                var i = 0;
                for (i; i < $scope.toasters.length; i++) {
                    if ($scope.toasters[i].id === id)
                        break;
                }
                $scope.toasters.splice(i, 1);
            };

            $scope.click = function (toaster) {
                if ($scope.config.tap === true) {
                    if (toaster.clickHandler) {
                        $scope.$parent.$eval(toaster.clickHandler)(toaster);
                    } else {
                        $scope.removeToast(toaster.id);
                    }
                }
            };
        }],
        template:
        '
' +
            '
' +
              '
{{toaster.title}}
' +
              '
' +
                '

' +
                '

' +
                '
{{toaster.body}}
' +
              '
' +
            '
' +
        '
'
    };
}]);
<|endoftext|>


<commit_before>'use strict';

/*
 * AngularJS Toaster
 * Version: 0.4.5
 *
 * Copyright 2013 Jiri Kavulak.
 * All Rights Reserved.
 * Use, reproduction, distribution, and modification of this code is subject to the terms and
 * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
 *
 * Author: Jiri Kavulak
 * Related to project of John Papa and Hans Fjällemark
 */

angular.module('toaster', ['ngAnimate'])
.service('toaster', ['$rootScope', function ($rootScope) {
    this.pop = function (type, title, body, timeout, bodyOutputType) {
        var toast = {
            type: type,
            title: title,
            body: body,
            timeout: timeout,
            bodyOutputType: bodyOutputType
        };
        $rootScope.$broadcast('toaster-newToast', toast);
    };

    this.clear = function () {
        $rootScope.$broadcast('toaster-clearToasts');
    };
}])
.constant('toasterConfig', {
    'limit': 0,                   // limits max number of toasts
    'tap-to-dismiss': true,
    'newest-on-top': true,
    //'fade-in': 1000,            // done in css
    //'on-fade-in': undefined,    // not implemented
    //'fade-out': 1000,           // done in css
    // 'on-fade-out': undefined,  // not implemented
    //'extended-time-out': 1000,    // not implemented
    'time-out': 5000, // Set timeOut and extendedTimeout to 0 to make it sticky
    'icon-classes': {
        error: 'toast-error',
        info: 'toast-info',
        success: 'toast-success',
        warning: 'toast-warning'
    },
    'body-output-type': '', // Options: '', 'trustedHtml', 'template'
    'body-template': 'toasterBodyTmpl.html',
    'icon-class': 'toast-info',
    'position-class': 'toast-top-right',
    'title-class': 'toast-title',
    'message-class': 'toast-message'
})
.directive('toasterContainer', ['$compile', '$timeout', '$sce', 'toasterConfig', 'toaster',
function ($compile, $timeout, $sce, toasterConfig, toaster) {
    return {
        replace: true,
        restrict: 'EA',
        scope: true, // creates an internal scope for this directive
        link: function (scope, elm, attrs) {

            var id = 0,
                mergedConfig;

            if (attrs.toasterOptions) {
                mergedConfig = angular.extend({}, toasterConfig, scope.$eval(attrs.toasterOptions));
            }

            scope.config = {
                position: mergedConfig['position-class'],
                title: mergedConfig['title-class'],
                message: mergedConfig['message-class'],
                tap: mergedConfig['tap-to-dismiss']
            };

            scope.configureTimer = function configureTimer(toast) {
                var timeout = typeof (toast.timeout) == "number" ? toast.timeout : mergedConfig['time-out'];
                if (timeout > 0)
                    setTimeout(toast, timeout);
            };

            function addToast(toast) {
                toast.type = mergedConfig['icon-classes'][toast.type];
                if (!toast.type)
                    toast.type = mergedConfig['icon-class'];

                toast.id = ++id;

                // Set the toast.bodyOutputType to the default if it isn't set
                toast.bodyOutputType = toast.bodyOutputType || mergedConfig['body-output-type']
                switch (toast.bodyOutputType) {
                    case 'trustedHtml':
                        toast.html = $sce.trustAsHtml(toast.body);
                        break;
                    case 'template':
                        toast.bodyTemplate = toast.body || mergedConfig['body-template'];
                        break;
                }

                scope.configureTimer(toast);

                if (mergedConfig['newest-on-top'] === true) {
                    scope.toasters.unshift(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.pop();
                    }
                } else {
                    scope.toasters.push(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.shift();
                    }
                }
            }

            function setTimeout(toast, time) {
                toast.timeout = $timeout(function () {
                    scope.removeToast(toast.id);
                }, time);
            }

            scope.toasters = [];
            scope.$on('toaster-newToast', function (event, toast) {
                addToast(toast);
            });

            scope.$on('toaster-clearToasts', function () {
                scope.toasters.splice(0, scope.toasters.length);
            });
        },
        controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {

            $scope.stopTimer = function (toast) {
                if (toast.timeout) {
                    $timeout.cancel(toast.timeout);
                    toast.timeout = null;
                }
            };

            $scope.restartTimer = function (toast) {
                if (!toast.timeout)
                    $scope.configureTimer(toast);
            };

            $scope.removeToast = function (id) {
                var i = 0;
                for (i; i < $scope.toasters.length; i++) {
                    if ($scope.toasters[i].id === id)
                        break;
                }
                $scope.toasters.splice(i, 1);
            };

            $scope.remove = function (id) {
                if ($scope.config.tap === true) {
                    $scope.removeToast(id);
                }
            };
        }],
        template:
        '
' +
            '
' +
              '
{{toaster.title}}
' +
              '
' +
                '

' +
                '

' +
                '
{{toaster.body}}
' +
              '
' +
            '
' +
        '
'
    };
}]);
<commit_msg>Rename the toast "id" concept to "index" because it's mostly used as a literal index (for ordering of the toasts in GUI)  And I need to recycle the "id" keyword to be used as unique string identifiers Rename "id" variable to toastCounter<commit_after>'use strict';

/*
 * AngularJS Toaster
 * Version: 0.4.5
 *
 * Copyright 2013 Jiri Kavulak.
 * All Rights Reserved.
 * Use, reproduction, distribution, and modification of this code is subject to the terms and
 * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
 *
 * Author: Jiri Kavulak
 * Related to project of John Papa and Hans Fjällemark
 */

angular.module('toaster', ['ngAnimate'])
.service('toaster', ['$rootScope', function ($rootScope) {
    this.pop = function (type, title, body, timeout, bodyOutputType) {
        var toast = {
            type: type,
            title: title,
            body: body,
            timeout: timeout,
            bodyOutputType: bodyOutputType
        };
        $rootScope.$broadcast('toaster-newToast', toast);
    };

    this.clear = function () {
        $rootScope.$broadcast('toaster-clearToasts');
    };
}])
.constant('toasterConfig', {
    'limit': 0,                   // limits max number of toasts
    'tap-to-dismiss': true,
    'newest-on-top': true,
    //'fade-in': 1000,            // done in css
    //'on-fade-in': undefined,    // not implemented
    //'fade-out': 1000,           // done in css
    // 'on-fade-out': undefined,  // not implemented
    //'extended-time-out': 1000,    // not implemented
    'time-out': 5000, // Set timeOut and extendedTimeout to 0 to make it sticky
    'icon-classes': {
        error: 'toast-error',
        info: 'toast-info',
        success: 'toast-success',
        warning: 'toast-warning'
    },
    'body-output-type': '', // Options: '', 'trustedHtml', 'template'
    'body-template': 'toasterBodyTmpl.html',
    'icon-class': 'toast-info',
    'position-class': 'toast-top-right',
    'title-class': 'toast-title',
    'message-class': 'toast-message'
})
.directive('toasterContainer', ['$compile', '$timeout', '$sce', 'toasterConfig', 'toaster',
function ($compile, $timeout, $sce, toasterConfig, toaster) {
    return {
        replace: true,
        restrict: 'EA',
        scope: true, // creates an internal scope for this directive
        link: function (scope, elm, attrs) {

            var toastCounter = 0,
                mergedConfig;
            scope.toasters = [];

            if (attrs.toasterOptions) {
                mergedConfig = angular.extend({}, toasterConfig, scope.$eval(attrs.toasterOptions));
            }

            scope.config = {
                position: mergedConfig['position-class'],
                title: mergedConfig['title-class'],
                message: mergedConfig['message-class'],
                tap: mergedConfig['tap-to-dismiss']
            };

            scope.configureTimer = function configureTimer(toast) {
                var timeout = typeof (toast.timeout) == "number" ? toast.timeout : mergedConfig['time-out'];
                if (timeout > 0)
                    setTimeout(toast, timeout);
            };

            function addToast(toast) {
                toast.type = mergedConfig['icon-classes'][toast.type];
                if (!toast.type)
                    toast.type = mergedConfig['icon-class'];

                toast.index = ++toastCounter;

                // Set the toast.bodyOutputType to the default if it isn't set
                toast.bodyOutputType = toast.bodyOutputType || mergedConfig['body-output-type']
                switch (toast.bodyOutputType) {
                    case 'trustedHtml':
                        toast.html = $sce.trustAsHtml(toast.body);
                        break;
                    case 'template':
                        toast.bodyTemplate = toast.body || mergedConfig['body-template'];
                        break;
                }

                scope.configureTimer(toast);

                if (mergedConfig['newest-on-top'] === true) {
                    scope.toasters.unshift(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.pop();
                    }
                } else {
                    scope.toasters.push(toast);
                    if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
                        scope.toasters.shift();
                    }
                }
            }

            function setTimeout(toast, time) {
                toast.timeout = $timeout(function () {
                    scope.removeToast(toast.index);
                }, time);
            }

            scope.$on('toaster-newToast', function (event, toast) {
                addToast(toast);
            });

            scope.$on('toaster-clearToasts', function () {
                scope.toasters.splice(0, scope.toasters.length);
            });
        },
        controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {

            $scope.stopTimer = function (toast) {
                if (toast.timeout) {
                    $timeout.cancel(toast.timeout);
                    toast.timeout = null;
                }
            };

            $scope.restartTimer = function (toast) {
                if (!toast.timeout)
                    $scope.configureTimer(toast);
            };

            $scope.removeToast = function (toastIndex) {
                var i = 0;
                for (i; i < $scope.toasters.length; i++) {
                    if ($scope.toasters[i].index === toastIndex)
                        break;
                }
                $scope.toasters.splice(i, 1);
            };

            $scope.remove = function (toastIndex) {
                if ($scope.config.tap === true) {
                    $scope.removeToast(toastIndex);
                }
            };
        }],
        template:
        '
' +
            '
' +
              '
{{toaster.title}}
' +
              '
' +
                '

' +
                '

' +
                '
{{toaster.body}}
' +
              '
' +
            '
' +
        '
'
    };
}]);
<|endoftext|>