<commit_before>import Component from '@ember/component';
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
import {action} from '@ember/object';
import {computed} from '@ember/object';
import {or, reads} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';

const CONFIRM_EMAIL_POLL_LENGTH = 1000;
const CONFIRM_EMAIL_MAX_POLL_LENGTH = 15 * 1000;

export default Component.extend({
    clock: service(),
    feature: service(),
    settings: service(),
    config: service(),
    session: service(),
    store: service(),
    limit: service(),

    classNames: 'gh-publishmenu',
    displayState: 'draft',
    post: null,
    postStatus: 'draft',
    distributionAction: 'publish_send',
    runningText: null,
    saveTask: null,
    sendEmailWhenPublished: null,
    typedDateError: null,
    isSendingEmailLimited: false,
    sendingEmailLimitError: '',

    _publishedAtBlogTZ: null,
    _previousStatus: null,

    isClosing: null,

    onClose() {},

    forcePublishedMenu: reads('post.pastScheduledTime'),

    hasEmailPermission: or('session.user.isOwnerOnly', 'session.user.isAdminOnly', 'session.user.isEditor'),

    canSendEmail: computed('hasEmailPermission', 'post.{isPost,email}', 'settings.{editorDefaultEmailRecipients,membersSignupAccess,mailgunIsConfigured}', 'config.mailgunIsConfigured', function () {
        let isDisabled = this.settings.get('editorDefaultEmailRecipients') === 'disabled' || this.settings.get('membersSignupAccess') === 'none';
        let mailgunIsConfigured = this.settings.get('mailgunIsConfigured') || this.config.get('mailgunIsConfigured');
        let isPost = this.post.isPost;
        let hasSentEmail = !!this.post.email;

        return this.hasEmailPermission &&
            !isDisabled &&
            mailgunIsConfigured &&
            isPost &&
            !hasSentEmail;
    }),

    postState: computed('post.{isPublished,isScheduled}', 'forcePublishedMenu', function () {
        if (this.forcePublishedMenu || this.get('post.isPublished')) {
            return 'published';
        } else if (this.get('post.isScheduled')) {
            return 'scheduled';
        } else {
            return 'draft';
        }
    }),

    triggerText: computed('postState', function () {
        let state = this.postState;

        if (state === 'published') {
            return 'Update';
        } else if (state === 'scheduled') {
            return 'Scheduled';
        } else {
            return 'Publish';
        }
    }),

    _runningText: computed('postState', 'saveType', function () {
        let saveType = this.saveType;
        let postState = this.postState;
        let runningText;

        if (postState === 'draft') {
            runningText = saveType === 'publish' ? 'Publishing' : 'Scheduling';
        }

        if (postState === 'published') {
            runningText = saveType === 'publish' ? 'Updating' : 'Unpublishing';
        }

        if (postState === 'scheduled') {
            runningText = saveType === 'schedule' ? 'Rescheduling' : 'Unscheduling';
        }

        return runningText || 'Publishing';
    }),

    buttonText: computed('postState', 'saveType', 'distributionAction', function () {
        let saveType = this.saveType;
        let postState = this.postState;
        let distributionAction = this.get('distributionAction');
        let buttonText;

        if (postState === 'draft') {
            let publishText = this.feature.emailOnlyPosts ? 'Publish & send' : 'Publish';

            switch (distributionAction) {
            case 'publish_send':
                buttonText = (saveType === 'publish') ? publishText : 'Schedule';
                break;
            case 'publish':
                buttonText = (saveType === 'publish') ? 'Publish' : 'Schedule';
                break;
            case 'send':
                buttonText = saveType === 'publish' ? 'Send' : 'Schedule';
                break;
            }
        }

        if (postState === 'published') {
            buttonText = saveType === 'publish' ? 'Update' : 'Unpublish';
        }

        if (postState === 'scheduled') {
            buttonText = saveType === 'schedule' ? 'Reschedule' : 'Unschedule';
        }

        return buttonText || 'Publish';
    }),

    successText: computed('_previousStatus', 'postState', function () {
        let postState = this.postState;
        let previousStatus = this._previousStatus;
        let buttonText;

        if (previousStatus === 'draft') {
            buttonText = postState === 'published' ? 'Published' : 'Scheduled';
        }

        if (previousStatus === 'published') {
            buttonText = postState === 'draft' ? 'Unpublished' : 'Updated';
        }

        if (previousStatus === 'scheduled') {
            buttonText = postState === 'draft' ? 'Unscheduled' : 'Rescheduled';
        }

        return buttonText;
    }),

    defaultEmailRecipients: computed('settings.{editorDefaultEmailRecipients,editorDefaultEmailRecipientsFilter}', 'post.visibility', function () {
        const defaultEmailRecipients = this.settings.get('editorDefaultEmailRecipients');

        if (defaultEmailRecipients === 'disabled') {
            return null;
        }

        if (defaultEmailRecipients === 'visibility') {
            if (this.post.visibility === 'public') {
                return 'status:free,status:-free';
            }

            if (this.post.visibility === 'members') {
                return 'status:free,status:-free';
            }

            if (this.post.visibility === 'paid') {
                return 'status:-free';
            }

            return this.post.visibility;
        }

        return this.settings.get('editorDefaultEmailRecipientsFilter');
    }),

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

        // update the displayState based on the post status but only after a
        // save has finished to avoid swapping the menu prematurely and triggering
        // calls to `setSaveType` due to the component re-rendering
        // TODO: we should have a better way of dealing with this where we don't
        // rely on the side-effect of component rendering calling setSaveType
        let postStatus = this.postStatus;
        if (postStatus !== this._postStatus) {
            if (this.get('saveTask.isRunning')) {
                this.get('saveTask.last').then(() => {
                    this.set('displayState', postStatus);
                });
            } else {
                this.set('displayState', postStatus);
            }
        }

        this._postStatus = this.postStatus;
        this.setDefaultSendEmailWhenPublished();
        this.checkIsSendingEmailLimitedTask.perform();
    },

    actions: {
        setSaveType(saveType) {
            let post = this.post;

            this.set('saveType', saveType);

            if (saveType === 'draft') {
                post.set('statusScratch', 'draft');
            } else if (saveType === 'schedule') {
                post.set('statusScratch', 'scheduled');
            } else if (saveType === 'publish') {
                post.set('statusScratch', 'published');
            }
        },

        setSendEmailWhenPublished(sendEmailWhenPublished) {
            this.set('sendEmailWhenPublished', sendEmailWhenPublished);
        },

        setDistributionAction(distributionAction) {
            this.set('distributionAction', distributionAction);

            if (distributionAction === 'publish') {
                this.set('sendEmailWhenPublished', false);
                this.set('post.emailRecipientFilter', 'none');
            } else {
                this.set('sendEmailWhenPublished', this.defaultEmailRecipients);
                this.set('post.emailRecipientFilter', this.defaultEmailRecipients);
            }
        },

        open() {
            this._cachePublishedAtBlogTZ();
            this.set('isClosing', false);
            this.get('post.errors').clear();

            this.setDefaultSendEmailWhenPublished();

            if (this.onOpen) {
                this.onOpen();
            }
        },

        close(dropdown, e) {
            // don't close the menu if the datepicker popup or confirm modal is clicked
            if (e) {
                let onDatepicker = !!e.target.closest('.ember-power-datepicker-content');
                let onModal = !!e.target.closest('.fullscreen-modal-container');

                if (onDatepicker || onModal) {
                    return false;
                }
            }

            if (!this._skipDropdownCloseCleanup) {
                this._cleanup();
            }
            this._skipDropdownCloseCleanup = false;

            this.onClose();
            this.set('isClosing', true);

            return true;
        },

        updateMemberCount(count) {
            this.memberCount = count;
        }
    },

    setDefaultSendEmailWhenPublished() {
        if (this.get('isSendingEmailLimited')) {
            this.set('sendEmailWhenPublished', false);
        } else if (this.postStatus === 'draft' && this.canSendEmail) {
            // Set default newsletter recipients
            this.set('sendEmailWhenPublished', this.defaultEmailRecipients);
        } else {
            this.set('sendEmailWhenPublished', this.post.emailRecipientFilter);
        }
    },

    checkIsSendingEmailLimitedTask: task(function* () {
        try {
            yield this.reloadSettingsTask.perform();

            if (this.limit.limiter && this.limit.limiter.isLimited('emails')) {
                yield this.limit.limiter.errorIfWouldGoOverLimit('emails');
            } else if (this.settings.get('emailVerificationRequired')) {
                this.set('isSendingEmailLimited', true);
                this.set('sendingEmailLimitError', 'Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org.');
                this.set('sendEmailWhenPublished', 'none');
                return;
            }

            this.set('isSendingEmailLimited', false);
            this.set('sendingEmailLimitError', null);
        } catch (error) {
            this.set('isSendingEmailLimited', true);
            this.set('sendingEmailLimitError', error.message);
            this.set('sendEmailWhenPublished', 'none');
        }
    }),

    // action is required because <GhFullscreenModal> only uses actions
    confirmEmailSend: action(function () {
        return this._confirmEmailSend.perform();
    }),

    _confirmEmailSend: task(function* () {
        this.sendEmailConfirmed = true;
        let post = yield this.save.perform();

        // simulate a validation error if saving failed so that the confirm
        // modal can react accordingly
        if (!post || post.errors.length) {
            throw null;
        }

        let pollTimeout = 0;
        if (post.email && post.email.status !== 'submitted') {
            while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
                yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
                post = yield post.reload();

                if (post.email.status === 'submitted') {
                    break;
                }
                if (post.email.status === 'failed') {
                    throw new EmailFailedError(post.email.error);
                }
            }
        }

        return post;
    }),

    retryEmailSend: action(function () {
        return this._retryEmailSend.perform();
    }),

    _retryEmailSend: task(function* () {
        if (!this.post.email) {
            return;
        }

        let email = yield this.post.email.retry();

        let pollTimeout = 0;
        if (email && email.status !== 'submitted') {
            while (pollTimeout < CONFIRM_EMAIL_POLL_LENGTH) {
                yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
                email = yield email.reload();

                if (email.status === 'submitted') {
                    break;
                }
                if (email.status === 'failed') {
                    throw new EmailFailedError(email.error);
                }
            }
        }

        return email;
    }),

    openEmailConfirmationModal: action(function (dropdown) {
        if (dropdown) {
            this._skipDropdownCloseCleanup = true;
            dropdown.actions.close();
        }
        this.set('showEmailConfirmationModal', true);
    }),

    closeEmailConfirmationModal: action(function () {
        this.set('showEmailConfirmationModal', false);
        this._cleanup();
    }),

    reloadSettingsTask: task(function* () {
        yield this.settings.reload();
    }),

    save: task(function* ({dropdown} = {}) {
        let {
            post,
            sendEmailWhenPublished,
            sendEmailConfirmed,
            saveType,
            typedDateError,
            distributionAction
        } = this;

        // don't allow save if an invalid schedule date is present
        if (typedDateError) {
            return false;
        }

        // validate publishedAtBlog to avoid an alert when saving for already displayed errors
        // important to do this before opening email confirmation modal too
        try {
            yield post.validate({property: 'publishedAtBlog'});
        } catch (error) {
            // re-throw if we don't have a validation error
            if (error) {
                throw error;
            }
            return false;
        }

        if (
            post.status === 'draft' &&
            !post.email && // email sent previously
            sendEmailWhenPublished && sendEmailWhenPublished !== 'none' &&
            distributionAction !== 'publish' &&
            !sendEmailConfirmed // set once confirmed so normal save happens
        ) {
            this.openEmailConfirmationModal(dropdown);
            return;
        }

        this.sendEmailConfirmed = false;

        // runningText needs to be declared before the other states change during the
        // save action.
        this.set('runningText', this._runningText);
        this.set('_previousStatus', this.get('post.status'));
        this.setSaveType(saveType);

        try {
            // will show alert for non-date related failed validations
            post = yield this.saveTask.perform({sendEmailWhenPublished});

            this._cachePublishedAtBlogTZ();
            return post;
        } catch (error) {
            // re-throw if we don't have a validation error
            if (error) {
                throw error;
            }
        }
    }),

    _cachePublishedAtBlogTZ() {
        this._publishedAtBlogTZ = this.get('post.publishedAtBlogTZ');
    },

    _cleanup() {
        this.set('showConfirmEmailModal', false);

        // when closing the menu we reset the publishedAtBlogTZ date so that the
        // unsaved changes made to the scheduled date aren't reflected in the PSM
        this.post.set('publishedAtBlogTZ', this._publishedAtBlogTZ);

        this.post.set('statusScratch', null);
        this.post.validate();
        // TODO: confirm with Naz why this is needed and find a different solution
        //  - we shouldn't be saving post data every time the menu is closed,
        //    especially when the user has chosen to cancel/close the menu without saving
        // this.post.save();
    }
});
<commit_msg>Fixed email recipient list in publish menu not showing selected tiers<commit_after>import Component from '@ember/component';
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
import {action} from '@ember/object';
import {computed} from '@ember/object';
import {or, reads} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';

const CONFIRM_EMAIL_POLL_LENGTH = 1000;
const CONFIRM_EMAIL_MAX_POLL_LENGTH = 15 * 1000;

export default Component.extend({
    clock: service(),
    feature: service(),
    settings: service(),
    config: service(),
    session: service(),
    store: service(),
    limit: service(),

    classNames: 'gh-publishmenu',
    displayState: 'draft',
    post: null,
    postStatus: 'draft',
    distributionAction: 'publish_send',
    runningText: null,
    saveTask: null,
    sendEmailWhenPublished: null,
    typedDateError: null,
    isSendingEmailLimited: false,
    sendingEmailLimitError: '',

    _publishedAtBlogTZ: null,
    _previousStatus: null,

    isClosing: null,

    onClose() {},

    forcePublishedMenu: reads('post.pastScheduledTime'),

    hasEmailPermission: or('session.user.isOwnerOnly', 'session.user.isAdminOnly', 'session.user.isEditor'),

    canSendEmail: computed('hasEmailPermission', 'post.{isPost,email}', 'settings.{editorDefaultEmailRecipients,membersSignupAccess,mailgunIsConfigured}', 'config.mailgunIsConfigured', function () {
        let isDisabled = this.settings.get('editorDefaultEmailRecipients') === 'disabled' || this.settings.get('membersSignupAccess') === 'none';
        let mailgunIsConfigured = this.settings.get('mailgunIsConfigured') || this.config.get('mailgunIsConfigured');
        let isPost = this.post.isPost;
        let hasSentEmail = !!this.post.email;

        return this.hasEmailPermission &&
            !isDisabled &&
            mailgunIsConfigured &&
            isPost &&
            !hasSentEmail;
    }),

    postState: computed('post.{isPublished,isScheduled}', 'forcePublishedMenu', function () {
        if (this.forcePublishedMenu || this.get('post.isPublished')) {
            return 'published';
        } else if (this.get('post.isScheduled')) {
            return 'scheduled';
        } else {
            return 'draft';
        }
    }),

    triggerText: computed('postState', function () {
        let state = this.postState;

        if (state === 'published') {
            return 'Update';
        } else if (state === 'scheduled') {
            return 'Scheduled';
        } else {
            return 'Publish';
        }
    }),

    _runningText: computed('postState', 'saveType', function () {
        let saveType = this.saveType;
        let postState = this.postState;
        let runningText;

        if (postState === 'draft') {
            runningText = saveType === 'publish' ? 'Publishing' : 'Scheduling';
        }

        if (postState === 'published') {
            runningText = saveType === 'publish' ? 'Updating' : 'Unpublishing';
        }

        if (postState === 'scheduled') {
            runningText = saveType === 'schedule' ? 'Rescheduling' : 'Unscheduling';
        }

        return runningText || 'Publishing';
    }),

    buttonText: computed('postState', 'saveType', 'distributionAction', function () {
        let saveType = this.saveType;
        let postState = this.postState;
        let distributionAction = this.get('distributionAction');
        let buttonText;

        if (postState === 'draft') {
            let publishText = this.feature.emailOnlyPosts ? 'Publish & send' : 'Publish';

            switch (distributionAction) {
            case 'publish_send':
                buttonText = (saveType === 'publish') ? publishText : 'Schedule';
                break;
            case 'publish':
                buttonText = (saveType === 'publish') ? 'Publish' : 'Schedule';
                break;
            case 'send':
                buttonText = saveType === 'publish' ? 'Send' : 'Schedule';
                break;
            }
        }

        if (postState === 'published') {
            buttonText = saveType === 'publish' ? 'Update' : 'Unpublish';
        }

        if (postState === 'scheduled') {
            buttonText = saveType === 'schedule' ? 'Reschedule' : 'Unschedule';
        }

        return buttonText || 'Publish';
    }),

    successText: computed('_previousStatus', 'postState', function () {
        let postState = this.postState;
        let previousStatus = this._previousStatus;
        let buttonText;

        if (previousStatus === 'draft') {
            buttonText = postState === 'published' ? 'Published' : 'Scheduled';
        }

        if (previousStatus === 'published') {
            buttonText = postState === 'draft' ? 'Unpublished' : 'Updated';
        }

        if (previousStatus === 'scheduled') {
            buttonText = postState === 'draft' ? 'Unscheduled' : 'Rescheduled';
        }

        return buttonText;
    }),

    defaultEmailRecipients: computed('settings.{editorDefaultEmailRecipients,editorDefaultEmailRecipientsFilter}', 'post.visibility', function () {
        const defaultEmailRecipients = this.settings.get('editorDefaultEmailRecipients');

        if (defaultEmailRecipients === 'disabled') {
            return null;
        }

        if (defaultEmailRecipients === 'visibility') {
            if (this.post.visibility === 'public') {
                return 'status:free,status:-free';
            }

            if (this.post.visibility === 'members') {
                return 'status:free,status:-free';
            }

            if (this.post.visibility === 'paid') {
                return 'status:-free';
            }

            if (this.post.visibility === 'filter') {
                return this.post.visibilityFilter;
            }

            return this.post.visibility;
        }

        return this.settings.get('editorDefaultEmailRecipientsFilter');
    }),

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

        // update the displayState based on the post status but only after a
        // save has finished to avoid swapping the menu prematurely and triggering
        // calls to `setSaveType` due to the component re-rendering
        // TODO: we should have a better way of dealing with this where we don't
        // rely on the side-effect of component rendering calling setSaveType
        let postStatus = this.postStatus;
        if (postStatus !== this._postStatus) {
            if (this.get('saveTask.isRunning')) {
                this.get('saveTask.last').then(() => {
                    this.set('displayState', postStatus);
                });
            } else {
                this.set('displayState', postStatus);
            }
        }

        this._postStatus = this.postStatus;
        this.setDefaultSendEmailWhenPublished();
        this.checkIsSendingEmailLimitedTask.perform();
    },

    actions: {
        setSaveType(saveType) {
            let post = this.post;

            this.set('saveType', saveType);

            if (saveType === 'draft') {
                post.set('statusScratch', 'draft');
            } else if (saveType === 'schedule') {
                post.set('statusScratch', 'scheduled');
            } else if (saveType === 'publish') {
                post.set('statusScratch', 'published');
            }
        },

        setSendEmailWhenPublished(sendEmailWhenPublished) {
            this.set('sendEmailWhenPublished', sendEmailWhenPublished);
        },

        setDistributionAction(distributionAction) {
            this.set('distributionAction', distributionAction);

            if (distributionAction === 'publish') {
                this.set('sendEmailWhenPublished', false);
                this.set('post.emailRecipientFilter', 'none');
            } else {
                this.set('sendEmailWhenPublished', this.defaultEmailRecipients);
                this.set('post.emailRecipientFilter', this.defaultEmailRecipients);
            }
        },

        open() {
            this._cachePublishedAtBlogTZ();
            this.set('isClosing', false);
            this.get('post.errors').clear();

            this.setDefaultSendEmailWhenPublished();

            if (this.onOpen) {
                this.onOpen();
            }
        },

        close(dropdown, e) {
            // don't close the menu if the datepicker popup or confirm modal is clicked
            if (e) {
                let onDatepicker = !!e.target.closest('.ember-power-datepicker-content');
                let onModal = !!e.target.closest('.fullscreen-modal-container');

                if (onDatepicker || onModal) {
                    return false;
                }
            }

            if (!this._skipDropdownCloseCleanup) {
                this._cleanup();
            }
            this._skipDropdownCloseCleanup = false;

            this.onClose();
            this.set('isClosing', true);

            return true;
        },

        updateMemberCount(count) {
            this.memberCount = count;
        }
    },

    setDefaultSendEmailWhenPublished() {
        if (this.get('isSendingEmailLimited')) {
            this.set('sendEmailWhenPublished', false);
        } else if (this.postStatus === 'draft' && this.canSendEmail) {
            // Set default newsletter recipients
            this.set('sendEmailWhenPublished', this.defaultEmailRecipients);
        } else {
            this.set('sendEmailWhenPublished', this.post.emailRecipientFilter);
        }
    },

    checkIsSendingEmailLimitedTask: task(function* () {
        try {
            yield this.reloadSettingsTask.perform();

            if (this.limit.limiter && this.limit.limiter.isLimited('emails')) {
                yield this.limit.limiter.errorIfWouldGoOverLimit('emails');
            } else if (this.settings.get('emailVerificationRequired')) {
                this.set('isSendingEmailLimited', true);
                this.set('sendingEmailLimitError', 'Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org.');
                this.set('sendEmailWhenPublished', 'none');
                return;
            }

            this.set('isSendingEmailLimited', false);
            this.set('sendingEmailLimitError', null);
        } catch (error) {
            this.set('isSendingEmailLimited', true);
            this.set('sendingEmailLimitError', error.message);
            this.set('sendEmailWhenPublished', 'none');
        }
    }),

    // action is required because <GhFullscreenModal> only uses actions
    confirmEmailSend: action(function () {
        return this._confirmEmailSend.perform();
    }),

    _confirmEmailSend: task(function* () {
        this.sendEmailConfirmed = true;
        let post = yield this.save.perform();

        // simulate a validation error if saving failed so that the confirm
        // modal can react accordingly
        if (!post || post.errors.length) {
            throw null;
        }

        let pollTimeout = 0;
        if (post.email && post.email.status !== 'submitted') {
            while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
                yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
                post = yield post.reload();

                if (post.email.status === 'submitted') {
                    break;
                }
                if (post.email.status === 'failed') {
                    throw new EmailFailedError(post.email.error);
                }
            }
        }

        return post;
    }),

    retryEmailSend: action(function () {
        return this._retryEmailSend.perform();
    }),

    _retryEmailSend: task(function* () {
        if (!this.post.email) {
            return;
        }

        let email = yield this.post.email.retry();

        let pollTimeout = 0;
        if (email && email.status !== 'submitted') {
            while (pollTimeout < CONFIRM_EMAIL_POLL_LENGTH) {
                yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
                email = yield email.reload();

                if (email.status === 'submitted') {
                    break;
                }
                if (email.status === 'failed') {
                    throw new EmailFailedError(email.error);
                }
            }
        }

        return email;
    }),

    openEmailConfirmationModal: action(function (dropdown) {
        if (dropdown) {
            this._skipDropdownCloseCleanup = true;
            dropdown.actions.close();
        }
        this.set('showEmailConfirmationModal', true);
    }),

    closeEmailConfirmationModal: action(function () {
        this.set('showEmailConfirmationModal', false);
        this._cleanup();
    }),

    reloadSettingsTask: task(function* () {
        yield this.settings.reload();
    }),

    save: task(function* ({dropdown} = {}) {
        let {
            post,
            sendEmailWhenPublished,
            sendEmailConfirmed,
            saveType,
            typedDateError,
            distributionAction
        } = this;

        // don't allow save if an invalid schedule date is present
        if (typedDateError) {
            return false;
        }

        // validate publishedAtBlog to avoid an alert when saving for already displayed errors
        // important to do this before opening email confirmation modal too
        try {
            yield post.validate({property: 'publishedAtBlog'});
        } catch (error) {
            // re-throw if we don't have a validation error
            if (error) {
                throw error;
            }
            return false;
        }

        if (
            post.status === 'draft' &&
            !post.email && // email sent previously
            sendEmailWhenPublished && sendEmailWhenPublished !== 'none' &&
            distributionAction !== 'publish' &&
            !sendEmailConfirmed // set once confirmed so normal save happens
        ) {
            this.openEmailConfirmationModal(dropdown);
            return;
        }

        this.sendEmailConfirmed = false;

        // runningText needs to be declared before the other states change during the
        // save action.
        this.set('runningText', this._runningText);
        this.set('_previousStatus', this.get('post.status'));
        this.setSaveType(saveType);

        try {
            // will show alert for non-date related failed validations
            post = yield this.saveTask.perform({sendEmailWhenPublished});

            this._cachePublishedAtBlogTZ();
            return post;
        } catch (error) {
            // re-throw if we don't have a validation error
            if (error) {
                throw error;
            }
        }
    }),

    _cachePublishedAtBlogTZ() {
        this._publishedAtBlogTZ = this.get('post.publishedAtBlogTZ');
    },

    _cleanup() {
        this.set('showConfirmEmailModal', false);

        // when closing the menu we reset the publishedAtBlogTZ date so that the
        // unsaved changes made to the scheduled date aren't reflected in the PSM
        this.post.set('publishedAtBlogTZ', this._publishedAtBlogTZ);

        this.post.set('statusScratch', null);
        this.post.validate();
        // TODO: confirm with Naz why this is needed and find a different solution
        //  - we shouldn't be saving post data every time the menu is closed,
        //    especially when the user has chosen to cancel/close the menu without saving
        // this.post.save();
    }
});
<|endoftext|>


<commit_before>// page-level worker

var Ipc = require('ipc');
var Path, Gulp, es;

var importPath;

// 必须立刻监听 IPC，否则会漏接收消息
Ipc.on('app:init-build-worker', function (callback) {
    Path = require('path');
    Gulp = require('gulp');
    es = require('event-stream');

    Editor.require('app://engine-framework');
    Editor.require('packages://canvas-assets/asset');

    //var runtimeUrl = 'app://runtime/runtime-' + Editor.remote.projectInfo.runtime + '/index.html';
    //var link = document.createElement('link');
    //link.rel = 'import';
    //link.onload = function (event) {
    //    callback();
    //};
    //link.onerror = callback;
    //link.href = runtimeUrl;
    //document.body.appendChild(link);

    var Async = require('async');
    var runtimePath = Editor.runtimePath;
    importPath = Editor.importPath;

    var runtimePkg = require(Path.join(runtimePath, 'package.json'));
    var runtimeScripts = runtimePkg.build.scriptsDev;

    Async.waterfall([
        // load runtime
        function (next) {
            Async.waterfall(
                runtimeScripts.map(function (script) {
                    return function (next) {
                        var scriptEL = document.createElement('script');
                        scriptEL.onload = function (event) {
                            next();
                        };
                        scriptEL.onerror = function (event) {
                            next(new Error('Failed to load ' + scriptEL.src));
                        };
                        scriptEL.src = Path.resolve(runtimePath, script);;
                        document.head.appendChild(scriptEL);
                    };
                }),
                next
            );
        },
        // init engine
        function (next) {
            Fire.AssetLibrary.init(importPath);
            var canvas = document.createElement('canvas');
            document.body.appendChild(canvas);
            Fire.engine.init({
                width: 800,
                height: 600,
                canvas: canvas
            }, next);
        },
        // load compiled script
        function (next) {
            function doLoad (src, cb) {
                var script = document.createElement('script');
                script.onload = function () {
                    cb();
                };
                script.onerror = function () {
                    console.error('Failed to load %s', src);
                    cb(new Error('Failed to load ' + src));
                };
                script.setAttribute('type','text/javascript');
                script.setAttribute('charset', 'utf-8');
                script.setAttribute('src', src);
                document.head.appendChild(script);
            }
            var scriptPath = Path.join(Editor.remote.libraryPath, 'bundle.project.js');
            var Fs = require('fire-fs');
            Fs.exists(scriptPath, function (exists) {
                if (exists) {
                    doLoad(scriptPath, next);
                }
                else {
                    next();
                }
            });
        }],
        callback
    );

});

Ipc.on('app:build-assets', function (callback, proj, dest, debug) {
    var gulp = new Gulp.Gulp();
    var rawFiles = [Path.join(importPath, '*/*/*'), '!' + Path.join(importPath, '*/*/*.js')];
    var assets = [Path.join(importPath, '*/*.json')];

    // build assets
    var sharedTempInfo = new Fire._DeserializeInfo();   // use this obj to create asset refs
    var buildAssets = gulp.src(assets, { base: importPath })
        .pipe(es.map(function (file, callback) {
            // 去掉带有 editor only 标记的对象和属性，同时压缩 json 文件到最小
            sharedTempInfo.reset();
            var obj = Fire.deserialize(file.contents, null, {
                isEditor: false,
                createAssetRefs: true
            });
            if (obj instanceof Fire.Scene) {
                Fire.engine._initScene(obj.scene, function () {
                    file.contents = new Buffer(Editor.serialize(obj, {
                        exporting: true,
                        minify: !debug
                    }));
                    return callback(null, file);
                });
            }
            else {
                file.contents = new Buffer(Editor.serialize(obj, {
                    exporting: true,
                    minify: !debug
                }));
                return callback(null, file);
            }
        }))
        .pipe(gulp.dest(dest));

    // copy raw files
    var copyRawFiles = gulp.src(rawFiles, { base: importPath })
        .pipe(gulp.dest(dest));

    es.merge(buildAssets, copyRawFiles).on('end', callback);
});
<commit_msg>use remote path in builder<commit_after>// page-level worker

var Ipc = require('ipc');
var Path, Gulp, es;

var importPath;

// 必须立刻监听 IPC，否则会漏接收消息
Ipc.on('app:init-build-worker', function (callback) {
    Path = require('path');
    Gulp = require('gulp');
    es = require('event-stream');

    Editor.require('app://engine-framework');
    Editor.require('packages://canvas-assets/asset');

    //var runtimeUrl = 'app://runtime/runtime-' + Editor.remote.projectInfo.runtime + '/index.html';
    //var link = document.createElement('link');
    //link.rel = 'import';
    //link.onload = function (event) {
    //    callback();
    //};
    //link.onerror = callback;
    //link.href = runtimeUrl;
    //document.body.appendChild(link);

    var Async = require('async');
    var runtimePath = Editor.remote.runtimePath;
    importPath = Editor.remote.importPath;

    var runtimePkg = require(Path.join(runtimePath, 'package.json'));
    var runtimeScripts = runtimePkg.build.scriptsDev;

    Async.waterfall([
        // load runtime
        function (next) {
            Async.waterfall(
                runtimeScripts.map(function (script) {
                    return function (next) {
                        var scriptEL = document.createElement('script');
                        scriptEL.onload = function (event) {
                            next();
                        };
                        scriptEL.onerror = function (event) {
                            next(new Error('Failed to load ' + scriptEL.src));
                        };
                        scriptEL.src = Path.resolve(runtimePath, script);;
                        document.head.appendChild(scriptEL);
                    };
                }),
                next
            );
        },
        // init engine
        function (next) {
            Fire.AssetLibrary.init(importPath);
            var canvas = document.createElement('canvas');
            document.body.appendChild(canvas);
            Fire.engine.init({
                width: 800,
                height: 600,
                canvas: canvas
            }, next);
        },
        // load compiled script
        function (next) {
            function doLoad (src, cb) {
                var script = document.createElement('script');
                script.onload = function () {
                    cb();
                };
                script.onerror = function () {
                    console.error('Failed to load %s', src);
                    cb(new Error('Failed to load ' + src));
                };
                script.setAttribute('type','text/javascript');
                script.setAttribute('charset', 'utf-8');
                script.setAttribute('src', src);
                document.head.appendChild(script);
            }
            var scriptPath = Path.join(Editor.remote.libraryPath, 'bundle.project.js');
            var Fs = require('fire-fs');
            Fs.exists(scriptPath, function (exists) {
                if (exists) {
                    doLoad(scriptPath, next);
                }
                else {
                    next();
                }
            });
        }],
        callback
    );

});

Ipc.on('app:build-assets', function (callback, proj, dest, debug) {
    var gulp = new Gulp.Gulp();
    var rawFiles = [Path.join(importPath, '*/*/*'), '!' + Path.join(importPath, '*/*/*.js')];
    var assets = [Path.join(importPath, '*/*.json')];

    // build assets
    var sharedTempInfo = new Fire._DeserializeInfo();   // use this obj to create asset refs
    var buildAssets = gulp.src(assets, { base: importPath })
        .pipe(es.map(function (file, callback) {
            // 去掉带有 editor only 标记的对象和属性，同时压缩 json 文件到最小
            sharedTempInfo.reset();
            var obj = Fire.deserialize(file.contents, null, {
                isEditor: false,
                createAssetRefs: true
            });
            if (obj instanceof Fire.Scene) {
                Fire.engine._initScene(obj.scene, function () {
                    file.contents = new Buffer(Editor.serialize(obj, {
                        exporting: true,
                        minify: !debug
                    }));
                    return callback(null, file);
                });
            }
            else {
                file.contents = new Buffer(Editor.serialize(obj, {
                    exporting: true,
                    minify: !debug
                }));
                return callback(null, file);
            }
        }))
        .pipe(gulp.dest(dest));

    // copy raw files
    var copyRawFiles = gulp.src(rawFiles, { base: importPath })
        .pipe(gulp.dest(dest));

    es.merge(buildAssets, copyRawFiles).on('end', callback);
});
<|endoftext|>


<commit_before>/**
 * @fileoverview
 * these are lightweight helpers that should not be attached to the element more than once
 */

goog.provide('recoil.ui.HtmlHelper');

goog.require('goog.dom.classlist');

/**
 * @constructor
 * @param {!recoil.ui.WidgetScope} scope gui scope
 */
recoil.ui.HtmlHelper = function(scope) {
    this.scope_ = scope;
};

/**
 * @param {!Element} element
 * @param {!recoil.frp.Behaviour<string>} classesB
 * @return {!recoil.ui.WidgetHelper}
 */
recoil.ui.HtmlHelper.prototype.class = function(element, classesB) {
    var helper = new recoil.ui.WidgetHelper(this.scope_, element, null, function() {
        if (classesB.good()) {
            goog.dom.classlist.set(element, classesB.get());
        }
    });
    helper.attach(classesB);
    return helper;
};

/**
 * @private
 * @param {!number} skip number of args to skip
 * @param {!string} elType type of element to create
 * @param {!IArrayLike} args
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createDom_ = function(skip, elType, args) {
    var funcArgs = [elType];
    for (var i = skip; i < args.length; i++) {
        funcArgs.push(args[i]);
    }
    return goog.dom.createDom.apply(null, funcArgs);
};

/**
 * helper function to create make an append function from a create function
 * @param {function(!recoil.frp.Behaviour<string>,(Object|Array<string>|string)=,...(Object|string|Array|NodeList)):!Element} func
 * @param {!IArrayLike} args
 * @return {!Element}
 * @private
 */
recoil.ui.HtmlHelper.prototype.append_ = function(func, args) {
    var funcArgs = [];
    for (var i = 1; i < args.length; i++) {
        funcArgs.push(args[i]);
    }
    var el = func.apply(this, args);
    goog.dom.append(args[0], el);
    return el;
};

/**
 * @param {!recoil.frp.Behaviour<string>} classesB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createClassDiv = function(classesB, opt_options, var_args) {
    var div = this.createDom_(1, 'div', arguments);
    this.class(div, classesB);
    return div;
};

/**
 * @param {!Node} parent
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendDiv = function(parent, opt_options, var_args) {
    var res = this.createDom_(1, 'div', arguments);
    goog.dom.append(parent, res);
    return res;
};
/**
 * @param {!Node} parent
 * @param {!recoil.frp.Behaviour<string>} classesB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendClassDiv = function(parent, classesB, opt_options, var_args) {
    return this.append_(this.createClassDiv, arguments);
};
/**
 * @param {!Element} element
 * @param {!recoil.frp.Behaviour<string>} innerHtmlB
 * @return {!recoil.ui.WidgetHelper}
 */
recoil.ui.HtmlHelper.prototype.innerHtml = function(element, innerHtmlB) {
    var helper = new recoil.ui.WidgetHelper(this.scope_, element, null, function() {
        if (innerHtmlB.good()) {
            element.innerHTML = innerHtmlB.get();
        }
    });
    helper.attach(innerHtmlB);
    return helper;
};

/**
 * @param {!recoil.frp.Behaviour<string>} innerHtmlB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createInnerHtmlDiv = function(innerHtmlB, opt_options, var_args) {
    var div = this.createDom_(1, 'div', arguments);
    this.innerHtml(div, innerHtmlB);
    return div;
};

/**
 * @param {!Node} parent
 * @param {!recoil.frp.Behaviour<string>} innerHtmlB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendInnerHtmlDiv = function(parent, innerHtmlB, opt_options, var_args) {
    return this.append_(this.createInnerHtmlDiv, arguments);
};

/**
 * sets the display to none/undefined depending on {@code showB}
 * @param {!Element} element
 * @param {!recoil.frp.Behaviour<!boolean>} showB
 * @return {!recoil.ui.WidgetHelper}
 */
recoil.ui.HtmlHelper.prototype.show = function(element, showB) {
    var orig = element.style.display;
    orig = orig === 'none' ? undefined : orig;
    var helper = new recoil.ui.WidgetHelper(this.scope_, element, null, function() {
        var show = showB.good() && showB.get();
        element.style.display = show ? orig : 'none';
    });
    return helper;
};

/**
 * @param {!recoil.frp.Behaviour<!boolean>} showB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createShowDiv = function(showB, opt_options, var_args) {
    var div = this.createDom_(1, 'div', arguments);
    this.show(div, showB);
    return div;
};

/**
 * @param {!Node} parent
 * @param {!recoil.frp.Behaviour<!boolean>} showB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendShowDiv = function(parent, showB, opt_options, var_args) {
    return this.append_(this.createInnerHtmlDiv, arguments);
};
<commit_msg>append_ passing wrong args<commit_after>/**
 * @fileoverview
 * these are lightweight helpers that should not be attached to the element more than once
 */

goog.provide('recoil.ui.HtmlHelper');

goog.require('goog.dom.classlist');

/**
 * @constructor
 * @param {!recoil.ui.WidgetScope} scope gui scope
 */
recoil.ui.HtmlHelper = function(scope) {
    this.scope_ = scope;
};

/**
 * @param {!Element} element
 * @param {!recoil.frp.Behaviour<string>} classesB
 * @return {!recoil.ui.WidgetHelper}
 */
recoil.ui.HtmlHelper.prototype.class = function(element, classesB) {
    var helper = new recoil.ui.WidgetHelper(this.scope_, element, null, function() {
        if (classesB.good()) {
            goog.dom.classlist.set(element, classesB.get());
        }
    });
    helper.attach(classesB);
    return helper;
};

/**
 * @private
 * @param {!number} skip number of args to skip
 * @param {!string} elType type of element to create
 * @param {!IArrayLike} args
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createDom_ = function(skip, elType, args) {
    var funcArgs = [elType];
    for (var i = skip; i < args.length; i++) {
        funcArgs.push(args[i]);
    }
    return goog.dom.createDom.apply(null, funcArgs);
};

/**
 * helper function to create make an append function from a create function
 * @param {function(!recoil.frp.Behaviour<string>,(Object|Array<string>|string)=,...(Object|string|Array|NodeList)):!Element} func
 * @param {!IArrayLike} args
 * @return {!Element}
 * @private
 */
recoil.ui.HtmlHelper.prototype.append_ = function(func, args) {
    var funcArgs = [];
    for (var i = 1; i < args.length; i++) {
        funcArgs.push(args[i]);
    }
    var el = func.apply(this, funcArgs);
    goog.dom.append(args[0], el);
    return el;
};

/**
 * @param {!recoil.frp.Behaviour<string>} classesB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createClassDiv = function(classesB, opt_options, var_args) {
    var div = this.createDom_(1, 'div', arguments);
    this.class(div, classesB);
    return div;
};

/**
 * @param {!Node} parent
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendDiv = function(parent, opt_options, var_args) {
    var res = this.createDom_(1, 'div', arguments);
    goog.dom.append(parent, res);
    return res;
};
/**
 * @param {!Node} parent
 * @param {!recoil.frp.Behaviour<string>} classesB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendClassDiv = function(parent, classesB, opt_options, var_args) {
    return this.append_(this.createClassDiv, arguments);
};
/**
 * @param {!Element} element
 * @param {!recoil.frp.Behaviour<string>} innerHtmlB
 * @return {!recoil.ui.WidgetHelper}
 */
recoil.ui.HtmlHelper.prototype.innerHtml = function(element, innerHtmlB) {
    var helper = new recoil.ui.WidgetHelper(this.scope_, element, null, function() {
        if (innerHtmlB.good()) {
            element.innerHTML = innerHtmlB.get();
        }
    });
    helper.attach(innerHtmlB);
    return helper;
};

/**
 * @param {!recoil.frp.Behaviour<string>} innerHtmlB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createInnerHtmlDiv = function(innerHtmlB, opt_options, var_args) {
    var div = this.createDom_(1, 'div', arguments);
    this.innerHtml(div, innerHtmlB);
    return div;
};

/**
 * @param {!Node} parent
 * @param {!recoil.frp.Behaviour<string>} innerHtmlB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendInnerHtmlDiv = function(parent, innerHtmlB, opt_options, var_args) {
    return this.append_(this.createInnerHtmlDiv, arguments);
};

/**
 * sets the display to none/undefined depending on {@code showB}
 * @param {!Element} element
 * @param {!recoil.frp.Behaviour<!boolean>} showB
 * @return {!recoil.ui.WidgetHelper}
 */
recoil.ui.HtmlHelper.prototype.show = function(element, showB) {
    var orig = element.style.display;
    orig = orig === 'none' ? undefined : orig;
    var helper = new recoil.ui.WidgetHelper(this.scope_, element, null, function() {
        var show = showB.good() && showB.get();
        element.style.display = show ? orig : 'none';
    });
    return helper;
};

/**
 * @param {!recoil.frp.Behaviour<!boolean>} showB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.createShowDiv = function(showB, opt_options, var_args) {
    var div = this.createDom_(1, 'div', arguments);
    this.show(div, showB);
    return div;
};

/**
 * @param {!Node} parent
 * @param {!recoil.frp.Behaviour<!boolean>} showB
 * @param {(Object|Array<string>|string)=} opt_options
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element}
 */
recoil.ui.HtmlHelper.prototype.appendShowDiv = function(parent, showB, opt_options, var_args) {
    return this.append_(this.createInnerHtmlDiv, arguments);
};
<|endoftext|>


<commit_before>/**
 * @file This is the central application logic and window management src file.
 * This is the central render process main window JS, and has access to
 * the DOM and all node abilities.
 **/
"use strict";

// Libraries ==============================================---------------------
// Must use require syntax for including these libs because of node duality.
window.$ = window.jQuery = require('jquery');
window._ = require('underscore');

// Main Process ===========================================---------------------
// Include global main process connector objects for the renderer (this window).
var remote = require('remote');
var mainWindow = remote.getCurrentWindow();
$.i18n = window.i18n = remote.require('i18next');
var app = remote.require('app');
require('../menus/menu-init')(app); // Initialize the menus

var scale = {};
var flattenResolution = 25; // Flatten curve value (smaller value = more points)
var lineEndPreShutoff = 20; // Remaining line length threshold for pump shutoff
var printArea = { // Print area limitations (in MM)
  x: [42, 485],
  y: [0, 210]
};

// Page loaded
$(function(){
   // After page load, wait for the griddle image to finish before initializing.
  $('#griddle').load(initEditor);

  // Apply element translation
  $('[data-i18n=""]').each(function() {
    var $node = $(this);

    if ($node.text().indexOf('.') > -1 && $node.attr('data-i18n') == "") {
      var key = $node.text();
      $node.attr('data-i18n', key);
      $node.text(i18n.t(key));
    }
  });

});

function initEditor() {
  var $griddle = $('#editor-wrapper img');
  var $editor = $('#editor');

  $griddle.aspect = 0.5390;
  $editor.aspect = 0.4717;
  $editor.pos = {
    left: 27,
    top: 24,
    right: 48,
    bottom: 52
  };

  var margin = [160, 100];

  // Set maximum work area render size
  $(window).on('resize', function(e){
    var win = [$(window).width(), $(window).height()];

    // Assume size to width
    $griddle.width(win[0] - margin[0]);
    $griddle.height((win[0] - margin[0]) * $griddle.aspect);

    // If too large, size to height
    if ($griddle.height() > win[1] - margin[1]) {
      $griddle.width((win[1] - margin[1]) / $griddle.aspect);
      $griddle.height((win[1] - margin[1]));
    }

    scale = {};
    scale.x = ($griddle.width()) / $griddle[0].naturalWidth;
    scale.y = ($griddle.height()) / $griddle[0].naturalHeight;

    scale = (scale.x < scale.y ? scale.x : scale.y);

    var off = $griddle.offset();
    $editor.css({
      top: off.top + (scale * $editor.pos.top),
      left: off.left + (scale * $editor.pos.left),
      width: $griddle.width() - ($editor.pos.right * scale),
      height: $griddle.height() - ($editor.pos.bottom * scale)
    });

    editorLoad(); // Load the editor (if it hasn't already been loaded)
    // This must happen after the very first resize, otherwise the canvas doesn't
    // have the correct dimensions for Paper to size to.
    $(mainWindow).trigger('move');

    updateFrosted();
  }).resize();
}

// Load the actual editor PaperScript (only when the canvas is ready).
var editorLoaded = false;
function editorLoad() {
  if (!editorLoaded) {
    editorLoaded = true;
    paper.PaperScript.load($('<script>').attr({
      type:"text/paperscript",
      src: "editor.ps.js",
      canvas: "editor"
    })[0]);
  }
}

// Trigger load init resize only after editor has called this function.
function editorLoadedInit() {
  $(window).resize();
  buildToolbar();
  buildColorPicker();
  bindControls();
}

function buildToolbar() {
  var $t = $('<ul>').appendTo('#tools');

  _.each(paper.tools, function(tool, index){
    if (tool.key) {
      $t.append($('<li>').append(
        $('<img>').attr({
          src: 'images/icon-' + tool.key + '.png',
          title: i18n.t(tool.name),
          draggable: 'false'
        })
      ).click(function(){
        $('#tools li.active').removeClass('active');
        $(this).addClass('active');
        tool.activate();
        $('#editor').css('cursor', 'url("images/cursor-' + tool.key + '.png"), move');
      }));
    }
  });

  // Activate the first (default) tool.
  $t.find('li:first').click();
}

function buildColorPicker() {
  var $picker  = $('<div>').attr('id', 'picker');
  _.each(paper.pancakeShades, function(color, index) {
    $picker.append(
      $('<a>')
        .addClass('color' + index + (index === 0 ? ' active' : ''))
        .attr('href', '#')
        .attr('title', i18n.t('color.color' + index))
        .click(function(e){selectColor(index); e.preventDefault();})
        .css('background-color', color)
    );
  });

  var $color = $('<div>')
    .attr('id', 'color')
    .attr('title', i18n.t('color.title'))
    .css('background-color', paper.pancakeShades[0])
    .append($picker);

  $('#tools').append($color);
}

function selectColor(index) {
  paper.pancakeCurrentShade = index;
  $('#picker a.active').removeClass('active');
  $('#picker a.color' + index).addClass('active');
  $('#color').css('background-color', paper.pancakeShades[index]);

  if (paper.selectRect) {
    if (paper.selectRect.ppath) {
      paper.selectRect.ppath.strokeColor = paper.pancakeShades[index];
      paper.selectRect.ppath.data.color = index;
      paper.view.update();
    }
  }
}

function toggleExport(doShow) {
  if (typeof doShow === 'undefined') {
    doShow = !$('#overlay').is(':visible');
  }

  if (doShow) {
    $('#overlay').fadeIn('slow');
    paper.deselect();
    updateFrosted(function(){
      $('#export').fadeIn('slow');
    });
  } else {
    $('#overlay').fadeOut('slow');
    $('#export').fadeOut('slow');
  }
}

// When the page is done loading, all the controls in the page can be bound.
function bindControls() {
  // Export window
  $('#export button').click(function(e){
    switch ($(this).attr('class')) {
      case 'done':
        toggleExport();
        break;
      case 'start':
        generateGcode();
        break;
    }
  });

  // Catch all keystrokes
  $(document).keyup(function(e){
    if (e.keyCode === 27) {
      toggleExport(false);
    }
  });

  // Callback/event for when any menu item is clicked
  app.menuClick = function(menu) {
    switch (menu) {
      case 'file.export':
        toggleExport(true);
        break;
      default:
        console.log(menu);
    }
  };
}

function updateFrosted(callback) {
  if ($('#overlay').is(':visible')) {
    html2canvas($("#non-overlay-wrapper")).then(function(canvas) {
      $("#frosted").remove();
      $("#overlay").append(canvas);
      $("#overlay canvas").attr('id', 'frosted');
      stackBlurCanvasRGB('frosted', 0, 0, $("#frosted").width(), $("#frosted").height(), 20);
      if (callback) callback();
    });
  }
}

// Create gcode from current project
function generateGcode(callback) {
  var workLayer = paper.project.getActiveLayer().clone();
  var out = getCodeHeader();

  _.each(workLayer.children, function(path){
    if (!path.data.isPolygonal) {
      path.flatten(flattenResolution);
    }

    // Turn on pump and wait for batter to flow for start of path
    out+= [gc('pumpon'),gc('wait', {t: 2}), ''].join("\n");

    // Render segment points to Gcode movements
    _.each(path.segments, function(segment){

      // If the remaining length of the line is less than the shutoff value,
      // throw in the shut off early.
      if (path.length - path.getOffsetOf(segment.point) <= lineEndPreShutoff) {
        // Add an artificial move to the exact point where the pump should turn
        // off, before the next move occurs to ensure correct drip timing.
        out+= gc('move', reMap(path.getPointAt(path.length - lineEndPreShutoff))) + "\n";
        out+= gc('pumpoff') + "\n"
      }

      out+= gc('move', reMap(segment.point)) + "\n";
    })

    // Ready move to next position
  });

  out += getCodeFooter();

  $('#export textarea').val(out);
  workLayer.remove();
  if (callback) callback();
}

function getCodeHeader() {
  return [
    gc('units'),
    gc('feed', {f: 6600}),
    gc('pumpoff'),
    gc('wait', {t: 1}),
    gc('off'),
    gc('home'),
    ''
  ].join("\n");
}

function getCodeFooter() {
  return [
    gc('wait', {t: 1}),
    gc('home'),
    gc('off')
  ].join("\n");
}

/**
 * Create a serial command string from a key:value object
 *
 * @param {string} name
 *   Key in cmds object to find the command string
 * @param {object} values
 *   Object containing the keys of placeholders to find in command string, with
 *   value to replace placeholder
 * @returns {string}
 *   Serial command string intended to be outputted directly, empty string
 *   if error.
 */
function gc(name, values) {
  var cmds = {
    units: 'G21 ;Set units to MM',
    abs: 'G90 ;Use Absolute units',
    home: 'G28 ;Home All Axis',
    move: 'G1 X%x Y%y',
    rate: 'G1 F%f',
    pumpon: 'M106 ;Pump on',
    pumpoff: 'M107 ;Pump off',
    wait: 'M84 S%t ;Pause for %t second(s)',
    off: 'M84 ;Motors off'
  };
  if (!name || !cmds[name]) return ''; // Sanity check
  var out = cmds[name];

  if (values) {
    for(var v in values) {
      out = out.replace(new RegExp('%' + v, 'g'), values[v]);
    }
  }

  return out;
}

// Convert an input Paper.js coordinate to an output bot mapped coordinate
function reMap(p) {
  var b = paper.view.bounds;
  return {
    x: Math.round(map(b.width - (p.x - b.x), 0, b.width, printArea.x[0], printArea.x[1]) * 1000) / 1000,
    y: Math.round(map(p.y - b.y, 0, b.height, printArea.y[0], printArea.y[1]) * 1000) / 1000
  };
}

// Map a value in a given range to a new range
function map(x, inMin, inMax, outMin, outMax) {
  return (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
<commit_msg>Improve feedrate set gcode<commit_after>/**
 * @file This is the central application logic and window management src file.
 * This is the central render process main window JS, and has access to
 * the DOM and all node abilities.
 **/
"use strict";

// Libraries ==============================================---------------------
// Must use require syntax for including these libs because of node duality.
window.$ = window.jQuery = require('jquery');
window._ = require('underscore');

// Main Process ===========================================---------------------
// Include global main process connector objects for the renderer (this window).
var remote = require('remote');
var mainWindow = remote.getCurrentWindow();
$.i18n = window.i18n = remote.require('i18next');
var app = remote.require('app');
require('../menus/menu-init')(app); // Initialize the menus

var scale = {};
var flattenResolution = 25; // Flatten curve value (smaller value = more points)
var lineEndPreShutoff = 20; // Remaining line length threshold for pump shutoff
var printArea = { // Print area limitations (in MM)
  x: [42, 485],
  y: [0, 210]
};

// Page loaded
$(function(){
   // After page load, wait for the griddle image to finish before initializing.
  $('#griddle').load(initEditor);

  // Apply element translation
  $('[data-i18n=""]').each(function() {
    var $node = $(this);

    if ($node.text().indexOf('.') > -1 && $node.attr('data-i18n') == "") {
      var key = $node.text();
      $node.attr('data-i18n', key);
      $node.text(i18n.t(key));
    }
  });

});

function initEditor() {
  var $griddle = $('#editor-wrapper img');
  var $editor = $('#editor');

  $griddle.aspect = 0.5390;
  $editor.aspect = 0.4717;
  $editor.pos = {
    left: 27,
    top: 24,
    right: 48,
    bottom: 52
  };

  var margin = [160, 100];

  // Set maximum work area render size
  $(window).on('resize', function(e){
    var win = [$(window).width(), $(window).height()];

    // Assume size to width
    $griddle.width(win[0] - margin[0]);
    $griddle.height((win[0] - margin[0]) * $griddle.aspect);

    // If too large, size to height
    if ($griddle.height() > win[1] - margin[1]) {
      $griddle.width((win[1] - margin[1]) / $griddle.aspect);
      $griddle.height((win[1] - margin[1]));
    }

    scale = {};
    scale.x = ($griddle.width()) / $griddle[0].naturalWidth;
    scale.y = ($griddle.height()) / $griddle[0].naturalHeight;

    scale = (scale.x < scale.y ? scale.x : scale.y);

    var off = $griddle.offset();
    $editor.css({
      top: off.top + (scale * $editor.pos.top),
      left: off.left + (scale * $editor.pos.left),
      width: $griddle.width() - ($editor.pos.right * scale),
      height: $griddle.height() - ($editor.pos.bottom * scale)
    });

    editorLoad(); // Load the editor (if it hasn't already been loaded)
    // This must happen after the very first resize, otherwise the canvas doesn't
    // have the correct dimensions for Paper to size to.
    $(mainWindow).trigger('move');

    updateFrosted();
  }).resize();
}

// Load the actual editor PaperScript (only when the canvas is ready).
var editorLoaded = false;
function editorLoad() {
  if (!editorLoaded) {
    editorLoaded = true;
    paper.PaperScript.load($('<script>').attr({
      type:"text/paperscript",
      src: "editor.ps.js",
      canvas: "editor"
    })[0]);
  }
}

// Trigger load init resize only after editor has called this function.
function editorLoadedInit() {
  $(window).resize();
  buildToolbar();
  buildColorPicker();
  bindControls();
}

function buildToolbar() {
  var $t = $('<ul>').appendTo('#tools');

  _.each(paper.tools, function(tool, index){
    if (tool.key) {
      $t.append($('<li>').append(
        $('<img>').attr({
          src: 'images/icon-' + tool.key + '.png',
          title: i18n.t(tool.name),
          draggable: 'false'
        })
      ).click(function(){
        $('#tools li.active').removeClass('active');
        $(this).addClass('active');
        tool.activate();
        $('#editor').css('cursor', 'url("images/cursor-' + tool.key + '.png"), move');
      }));
    }
  });

  // Activate the first (default) tool.
  $t.find('li:first').click();
}

function buildColorPicker() {
  var $picker  = $('<div>').attr('id', 'picker');
  _.each(paper.pancakeShades, function(color, index) {
    $picker.append(
      $('<a>')
        .addClass('color' + index + (index === 0 ? ' active' : ''))
        .attr('href', '#')
        .attr('title', i18n.t('color.color' + index))
        .click(function(e){selectColor(index); e.preventDefault();})
        .css('background-color', color)
    );
  });

  var $color = $('<div>')
    .attr('id', 'color')
    .attr('title', i18n.t('color.title'))
    .css('background-color', paper.pancakeShades[0])
    .append($picker);

  $('#tools').append($color);
}

function selectColor(index) {
  paper.pancakeCurrentShade = index;
  $('#picker a.active').removeClass('active');
  $('#picker a.color' + index).addClass('active');
  $('#color').css('background-color', paper.pancakeShades[index]);

  if (paper.selectRect) {
    if (paper.selectRect.ppath) {
      paper.selectRect.ppath.strokeColor = paper.pancakeShades[index];
      paper.selectRect.ppath.data.color = index;
      paper.view.update();
    }
  }
}

function toggleExport(doShow) {
  if (typeof doShow === 'undefined') {
    doShow = !$('#overlay').is(':visible');
  }

  if (doShow) {
    $('#overlay').fadeIn('slow');
    paper.deselect();
    updateFrosted(function(){
      $('#export').fadeIn('slow');
    });
  } else {
    $('#overlay').fadeOut('slow');
    $('#export').fadeOut('slow');
  }
}

// When the page is done loading, all the controls in the page can be bound.
function bindControls() {
  // Export window
  $('#export button').click(function(e){
    switch ($(this).attr('class')) {
      case 'done':
        toggleExport();
        break;
      case 'start':
        generateGcode();
        break;
    }
  });

  // Catch all keystrokes
  $(document).keyup(function(e){
    if (e.keyCode === 27) {
      toggleExport(false);
    }
  });

  // Callback/event for when any menu item is clicked
  app.menuClick = function(menu) {
    switch (menu) {
      case 'file.export':
        toggleExport(true);
        break;
      default:
        console.log(menu);
    }
  };
}

function updateFrosted(callback) {
  if ($('#overlay').is(':visible')) {
    html2canvas($("#non-overlay-wrapper")).then(function(canvas) {
      $("#frosted").remove();
      $("#overlay").append(canvas);
      $("#overlay canvas").attr('id', 'frosted');
      stackBlurCanvasRGB('frosted', 0, 0, $("#frosted").width(), $("#frosted").height(), 20);
      if (callback) callback();
    });
  }
}

// Create gcode from current project
function generateGcode(callback) {
  var workLayer = paper.project.getActiveLayer().clone();
  var out = getCodeHeader();

  _.each(workLayer.children, function(path){
    if (!path.data.isPolygonal) {
      path.flatten(flattenResolution);
    }

    // Turn on pump and wait for batter to flow for start of path
    out+= [gc('pumpon'),gc('wait', {t: 2}), ''].join("\n");

    // Render segment points to Gcode movements
    _.each(path.segments, function(segment){

      // If the remaining length of the line is less than the shutoff value,
      // throw in the shut off early.
      if (path.length - path.getOffsetOf(segment.point) <= lineEndPreShutoff) {
        // Add an artificial move to the exact point where the pump should turn
        // off, before the next move occurs to ensure correct drip timing.
        out+= gc('move', reMap(path.getPointAt(path.length - lineEndPreShutoff))) + "\n";
        out+= gc('pumpoff') + "\n"
      }

      out+= gc('move', reMap(segment.point)) + "\n";
    })

    // Ready move to next position
  });

  out += getCodeFooter();

  $('#export textarea').val(out);
  workLayer.remove();
  if (callback) callback();
}

function getCodeHeader() {
  return [
    gc('units'),
    gc('rate', {f: 6600}),
    gc('pumpoff'),
    gc('wait', {t: 1}),
    gc('off'),
    gc('home'),
    ''
  ].join("\n");
}

function getCodeFooter() {
  return [
    gc('wait', {t: 1}),
    gc('home'),
    gc('off')
  ].join("\n");
}

/**
 * Create a serial command string from a key:value object
 *
 * @param {string} name
 *   Key in cmds object to find the command string
 * @param {object} values
 *   Object containing the keys of placeholders to find in command string, with
 *   value to replace placeholder
 * @returns {string}
 *   Serial command string intended to be outputted directly, empty string
 *   if error.
 */
function gc(name, values) {
  var cmds = {
    units: 'G21 ;Set units to MM',
    abs: 'G90 ;Use Absolute units',
    home: 'G28 ;Home All Axis',
    move: 'G1 X%x Y%y',
    rate: 'G1 F%f ; Set Feedrate',
    pumpon: 'M106 ;Pump on',
    pumpoff: 'M107 ;Pump off',
    wait: 'M84 S%t ;Pause for %t second(s)',
    off: 'M84 ;Motors off'
  };
  if (!name || !cmds[name]) return ''; // Sanity check
  var out = cmds[name];

  if (values) {
    for(var v in values) {
      out = out.replace(new RegExp('%' + v, 'g'), values[v]);
    }
  }

  return out;
}

// Convert an input Paper.js coordinate to an output bot mapped coordinate
function reMap(p) {
  var b = paper.view.bounds;
  return {
    x: Math.round(map(b.width - (p.x - b.x), 0, b.width, printArea.x[0], printArea.x[1]) * 1000) / 1000,
    y: Math.round(map(p.y - b.y, 0, b.height, printArea.y[0], printArea.y[1]) * 1000) / 1000
  };
}

// Map a value in a given range to a new range
function map(x, inMin, inMax, outMin, outMax) {
  return (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
<|endoftext|>


<commit_before>var assert = require('chai').assert;
var Scope = require('../lib/scope');

describe('Scope', function() {
    var rootScope;

    beforeEach(function() {
        rootScope = new Scope();
    });

    it('building scope tree', function() {
        var childScope1 = new Scope();
        var childScope2 = new Scope();
        var result;

        result = rootScope.addScope(childScope1);
        assert.equal(result, true);
        result = rootScope.addScope(childScope1);
        assert.equal(result, false);
        assert.sameMembers(rootScope.scopes, [childScope1]);
        assert.isTrue(rootScope.hasScope(childScope1));

        result = rootScope.addScope(childScope2);
        assert.equal(result, true);
        assert.sameMembers(rootScope.scopes, [childScope1, childScope2]);
        assert.isTrue(rootScope.hasScope(childScope1));
        assert.isTrue(rootScope.hasScope(childScope2));
        assert.equal(childScope1.parent, rootScope);
        assert.equal(childScope2.parent, rootScope);

        result = childScope1.addScope(childScope2);
        assert.equal(result, true);
        assert.sameMembers(rootScope.scopes, [childScope1]);
        assert.isTrue(rootScope.hasScope(childScope1));
        assert.isFalse(rootScope.hasScope(childScope2));
        assert.sameMembers(childScope1.scopes, [childScope2]);
        assert.isTrue(childScope1.hasScope(childScope2));
        assert.equal(childScope2.parent, childScope1);
    });

    describe('references', function() {
        it('working with references', function() {
            var childScope1 = new Scope(rootScope);
            var childScope2 = new Scope(childScope1);

            rootScope.setReference('a', 'someValue');
            assert.isTrue(rootScope.hasReference('a'));
            assert.isTrue(rootScope.hasOwnReference('a'));
            assert.equal(rootScope.getReference('a'), 'someValue');
            assert.equal(rootScope.getOwnReference('a'), 'someValue');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(rootScope.getReferenceNames(), ['a']);
            assert.sameMembers(rootScope.getOwnReferenceNames(), ['a']);

            childScope1.setReference('a', 'someValue2');
            assert.isTrue(childScope1.hasReference('a'));
            assert.isFalse(childScope1.hasOwnReference('a'));
            assert.equal(childScope1.getReference('a'), 'someValue2');
            assert.isNull(childScope1.getOwnReference('a'));
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.lengthOf(childScope1.getOwnReferenceNames(), 0);
            assert.equal(rootScope.getReference('a'), 'someValue2');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(rootScope.getReferenceNames(), ['a']);
            assert.sameMembers(rootScope.getOwnReferenceNames(), ['a']);

            childScope1.setOwnReference('a', 'someValue3');
            assert.isTrue(childScope1.hasReference('a'));
            assert.isTrue(childScope1.hasOwnReference('a'));
            assert.equal(childScope1.getReference('a'), 'someValue3');
            assert.equal(childScope1.getOwnReference('a'), 'someValue3');
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.sameMembers(childScope1.getOwnReferenceNames(), ['a']);
            assert.equal(rootScope.getReference('a'), 'someValue2');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.sameMembers(childScope1.getOwnReferenceNames(), ['a']);

            childScope2.setReference('a', 'someValue4');
            assert.isTrue(childScope2.hasReference('a'));
            assert.isFalse(childScope2.hasOwnReference('a'));
            assert.equal(childScope2.getReference('a'), 'someValue4');
            assert.isNull(childScope2.getOwnReference('a'));
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.sameMembers(childScope2.getReferenceNames(), ['a']);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.getReference('a'), 'someValue4');
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.sameMembers(childScope1.getOwnReferenceNames(), ['a']);
            assert.equal(rootScope.getReference('a'), 'someValue2');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope2.setOwnReference('b', 'someOtherValue');
            assert.isTrue(childScope2.hasReference('b'));
            assert.isTrue(childScope2.hasOwnReference('b'));
            assert.equal(childScope2.getReference('b'), 'someOtherValue');
            assert.equal(childScope2.getOwnReference('b'), 'someOtherValue');
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.sameMembers(childScope2.getReferenceNames(), ['a', 'b']);
            assert.sameMembers(childScope2.getOwnReferenceNames(), ['b']);
            assert.isNull(childScope1.getReference('b'));
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.isNull(rootScope.getReference('b'));
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope2.removeOwnReference('b');
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.sameMembers(childScope2.getReferenceNames(), ['a']);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope1.removeOwnReference('a');
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.sameMembers(childScope2.getReferenceNames(), ['a']);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.lengthOf(childScope1.getOwnReferenceNames(), 0);
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(rootScope.getReferenceNames(), ['a']);
            assert.sameMembers(rootScope.getOwnReferenceNames(), ['a']);

            rootScope.removeOwnReference('a');
            assert.equal(childScope2.countReferences(), 0);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.lengthOf(childScope2.getReferenceNames(), 0);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.countReferences(), 0);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.lengthOf(childScope2.getReferenceNames(), 0);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(rootScope.countReferences(), 0);
            assert.equal(rootScope.countOwnReferences(), 0);
            assert.lengthOf(childScope2.getReferenceNames(), 0);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);

            rootScope.setOwnReference('a', 'someValue');
            rootScope.setOwnReference('b', 'someValue');
            childScope1.setOwnReference('a', 'someValue2');
            childScope1.setOwnReference('b', 'someValue3');
            childScope2.setOwnReference('a', 'someValue4');
            childScope2.setOwnReference('b', 'someValue4');
            childScope2.removeOwnReference('a');
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.equal(childScope1.countReferences(), 2);
            assert.equal(childScope1.countOwnReferences(), 2);
            assert.equal(rootScope.countReferences(), 2);
            assert.equal(rootScope.countOwnReferences(), 2);

            childScope2.removeNearReference('a');
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.equal(childScope1.countReferences(), 2);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.equal(rootScope.countReferences(), 2);
            assert.equal(rootScope.countOwnReferences(), 2);

            childScope2.removeNearReference('a');
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope2.removeReference('b');
            assert.equal(childScope2.countReferences(), 0);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.equal(childScope1.countReferences(), 0);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.equal(rootScope.countReferences(), 0);
            assert.equal(rootScope.countOwnReferences(), 0);

            rootScope.setOwnReference('a', 'someValue');
            rootScope.setOwnReference('b', 'someValue');
            rootScope.setOwnReference('c', 'someValue');
            childScope1.setOwnReference('a', 'someValue2');
            childScope1.setOwnReference('b', 'someValue3');
            childScope1.setOwnReference('d', 'someValue3');
            childScope2.setOwnReference('a', 'someValue4');
            childScope2.setOwnReference('b', 'someValue4');

            assert.isFalse(rootScope.removeScope({}));
            assert.equal(childScope2.countReferences(), 4);
            assert.equal(childScope2.countOwnReferences(), 2);
            assert.equal(childScope1.countReferences(), 4);
            assert.equal(childScope1.countOwnReferences(), 3);
            assert.equal(rootScope.countReferences(), 3);
            assert.equal(rootScope.countOwnReferences(), 3);

            rootScope.removeScope(childScope1);
            assert.equal(childScope2.countReferences(), 3);
            assert.equal(childScope2.countOwnReferences(), 2);
            assert.equal(childScope1.countReferences(), 3);
            assert.equal(childScope1.countOwnReferences(), 3);
            assert.equal(rootScope.countReferences(), 3);
            assert.equal(rootScope.countOwnReferences(), 3);

            childScope1.removeScope(childScope2);
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 2);
            assert.equal(childScope1.countReferences(), 3);
            assert.equal(childScope1.countOwnReferences(), 3);
            assert.equal(rootScope.countReferences(), 3);
            assert.equal(rootScope.countOwnReferences(), 3);
        });
    });
});
<commit_msg>improve Scope tests<commit_after>var assert = require('chai').assert;
var Scope = require('../lib/scope');

describe('Scope', function() {
    var rootScope;

    beforeEach(function() {
        rootScope = new Scope();
    });

    it('building scope tree', function() {
        var childScope1 = new Scope();
        var childScope2 = new Scope();
        var result;

        result = rootScope.addScope(childScope1);
        assert.equal(result, true);
        result = rootScope.addScope(childScope1);
        assert.equal(result, false);
        assert.sameMembers(rootScope.scopes, [childScope1]);
        assert.isTrue(rootScope.hasScope(childScope1));

        result = rootScope.addScope(childScope2);
        assert.equal(result, true);
        assert.sameMembers(rootScope.scopes, [childScope1, childScope2]);
        assert.isTrue(rootScope.hasScope(childScope1));
        assert.isTrue(rootScope.hasScope(childScope2));
        assert.equal(childScope1.parent, rootScope);
        assert.equal(childScope2.parent, rootScope);

        result = childScope1.addScope(childScope2);
        assert.equal(result, true);
        assert.sameMembers(rootScope.scopes, [childScope1]);
        assert.isTrue(rootScope.hasScope(childScope1));
        assert.isFalse(rootScope.hasScope(childScope2));
        assert.sameMembers(childScope1.scopes, [childScope2]);
        assert.isTrue(childScope1.hasScope(childScope2));
        assert.equal(childScope2.parent, childScope1);
    });

    describe('references', function() {
        it('working with references', function() {
            var childScope1 = new Scope(rootScope);
            var childScope2 = new Scope(childScope1);

            var reference = rootScope.setReference('a', 'someValue');

            assert.strictEqual(reference, 'someValue');
            assert.isTrue(rootScope.hasReference('a'));
            assert.isTrue(rootScope.hasOwnReference('a'));
            assert.equal(rootScope.getReference('a'), 'someValue');
            assert.equal(rootScope.getOwnReference('a'), 'someValue');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(rootScope.getReferenceNames(), ['a']);
            assert.sameMembers(rootScope.getOwnReferenceNames(), ['a']);

            childScope1.setReference('a', 'someValue2');
            assert.isTrue(childScope1.hasReference('a'));
            assert.isFalse(childScope1.hasOwnReference('a'));
            assert.equal(childScope1.getReference('a'), 'someValue2');
            assert.isNull(childScope1.getOwnReference('a'));
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.lengthOf(childScope1.getOwnReferenceNames(), 0);
            assert.equal(rootScope.getReference('a'), 'someValue2');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(rootScope.getReferenceNames(), ['a']);
            assert.sameMembers(rootScope.getOwnReferenceNames(), ['a']);

            childScope1.setOwnReference('a', 'someValue3');
            assert.isTrue(childScope1.hasReference('a'));
            assert.isTrue(childScope1.hasOwnReference('a'));
            assert.equal(childScope1.getReference('a'), 'someValue3');
            assert.equal(childScope1.getOwnReference('a'), 'someValue3');
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.sameMembers(childScope1.getOwnReferenceNames(), ['a']);
            assert.equal(rootScope.getReference('a'), 'someValue2');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.sameMembers(childScope1.getOwnReferenceNames(), ['a']);

            childScope2.setReference('a', 'someValue4');
            assert.isTrue(childScope2.hasReference('a'));
            assert.isFalse(childScope2.hasOwnReference('a'));
            assert.equal(childScope2.getReference('a'), 'someValue4');
            assert.isNull(childScope2.getOwnReference('a'));
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.sameMembers(childScope2.getReferenceNames(), ['a']);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.getReference('a'), 'someValue4');
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.sameMembers(childScope1.getOwnReferenceNames(), ['a']);
            assert.equal(rootScope.getReference('a'), 'someValue2');
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope2.setOwnReference('b', 'someOtherValue');
            assert.isTrue(childScope2.hasReference('b'));
            assert.isTrue(childScope2.hasOwnReference('b'));
            assert.equal(childScope2.getReference('b'), 'someOtherValue');
            assert.equal(childScope2.getOwnReference('b'), 'someOtherValue');
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.sameMembers(childScope2.getReferenceNames(), ['a', 'b']);
            assert.sameMembers(childScope2.getOwnReferenceNames(), ['b']);
            assert.isNull(childScope1.getReference('b'));
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.isNull(rootScope.getReference('b'));
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope2.removeOwnReference('a');
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.sameMembers(childScope2.getReferenceNames(), ['a', 'b']);
            assert.sameMembers(childScope2.getOwnReferenceNames(), ['b']);
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.sameMembers(childScope1.getOwnReferenceNames(), ['a']);
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(rootScope.getReferenceNames(), ['a']);
            assert.sameMembers(rootScope.getOwnReferenceNames(), ['a']);

            childScope2.removeOwnReference('b');
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.sameMembers(childScope2.getReferenceNames(), ['a']);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope1.removeOwnReference('a');
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.sameMembers(childScope2.getReferenceNames(), ['a']);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.sameMembers(childScope1.getReferenceNames(), ['a']);
            assert.lengthOf(childScope1.getOwnReferenceNames(), 0);
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);
            assert.sameMembers(rootScope.getReferenceNames(), ['a']);
            assert.sameMembers(rootScope.getOwnReferenceNames(), ['a']);

            rootScope.removeOwnReference('a');
            assert.equal(childScope2.countReferences(), 0);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.lengthOf(childScope2.getReferenceNames(), 0);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(childScope1.countReferences(), 0);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.lengthOf(childScope2.getReferenceNames(), 0);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);
            assert.equal(rootScope.countReferences(), 0);
            assert.equal(rootScope.countOwnReferences(), 0);
            assert.lengthOf(childScope2.getReferenceNames(), 0);
            assert.lengthOf(childScope2.getOwnReferenceNames(), 0);

            rootScope.setOwnReference('a', 'someValue');
            rootScope.setOwnReference('b', 'someValue');
            childScope1.setOwnReference('a', 'someValue2');
            childScope1.setOwnReference('b', 'someValue3');
            childScope2.setOwnReference('a', 'someValue4');
            childScope2.setOwnReference('b', 'someValue4');
            childScope2.removeOwnReference('a');
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.equal(childScope1.countReferences(), 2);
            assert.equal(childScope1.countOwnReferences(), 2);
            assert.equal(rootScope.countReferences(), 2);
            assert.equal(rootScope.countOwnReferences(), 2);

            childScope2.removeNearReference('a');
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.equal(childScope1.countReferences(), 2);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.equal(rootScope.countReferences(), 2);
            assert.equal(rootScope.countOwnReferences(), 2);

            childScope2.removeNearReference('a');
            assert.equal(childScope2.countReferences(), 1);
            assert.equal(childScope2.countOwnReferences(), 1);
            assert.equal(childScope1.countReferences(), 1);
            assert.equal(childScope1.countOwnReferences(), 1);
            assert.equal(rootScope.countReferences(), 1);
            assert.equal(rootScope.countOwnReferences(), 1);

            childScope2.removeReference('b');
            assert.equal(childScope2.countReferences(), 0);
            assert.equal(childScope2.countOwnReferences(), 0);
            assert.equal(childScope1.countReferences(), 0);
            assert.equal(childScope1.countOwnReferences(), 0);
            assert.equal(rootScope.countReferences(), 0);
            assert.equal(rootScope.countOwnReferences(), 0);

            rootScope.setOwnReference('a', 'someValue');
            rootScope.setOwnReference('b', 'someValue');
            rootScope.setOwnReference('c', 'someValue');
            childScope1.setOwnReference('a', 'someValue2');
            childScope1.setOwnReference('b', 'someValue3');
            childScope1.setOwnReference('d', 'someValue3');
            childScope2.setOwnReference('a', 'someValue4');
            childScope2.setOwnReference('b', 'someValue4');

            assert.isFalse(rootScope.removeScope({}));
            assert.equal(childScope2.countReferences(), 4);
            assert.equal(childScope2.countOwnReferences(), 2);
            assert.equal(childScope1.countReferences(), 4);
            assert.equal(childScope1.countOwnReferences(), 3);
            assert.equal(rootScope.countReferences(), 3);
            assert.equal(rootScope.countOwnReferences(), 3);

            rootScope.removeScope(childScope1);
            assert.equal(childScope2.countReferences(), 3);
            assert.equal(childScope2.countOwnReferences(), 2);
            assert.equal(childScope1.countReferences(), 3);
            assert.equal(childScope1.countOwnReferences(), 3);
            assert.equal(rootScope.countReferences(), 3);
            assert.equal(rootScope.countOwnReferences(), 3);

            childScope1.removeScope(childScope2);
            assert.equal(childScope2.countReferences(), 2);
            assert.equal(childScope2.countOwnReferences(), 2);
            assert.equal(childScope1.countReferences(), 3);
            assert.equal(childScope1.countOwnReferences(), 3);
            assert.equal(rootScope.countReferences(), 3);
            assert.equal(rootScope.countOwnReferences(), 3);
        });
    });
});
<|endoftext|>