<commit_before>// @flow
import { helpers } from 'utils';
import WindowService from '../../services/WindowService';
import GlobalEventService from '../../services/GlobalEventService';

const THROTTLE_DELAY = 100;

const classes = {
    OPEN: 'open',
    DROPDOWN_DOWN: 'dropdown__wrp_down',
    DROPDOWN_WRP_OVER: 'dropdown__wrp_down-over',
    DROPDOWN_UP: 'dropdown__wrp_up',
    DROPDOWN_UP_OVER: 'dropdown__wrp_up-over',
    VISIBLE_COLLECTION: 'visible-collection'
};

const WINDOW_BORDER_OFFSET = 10;
const MAX_DROPDOWN_PANEL_WIDTH = 200;

const panelPosition = {
    DOWN: 'down',
    UP: 'up'
};

const panelMinWidth = {
    NONE: 'none',
    BUTTON_WIDTH: 'button-width'
};

const defaultOptions = {
    autoOpen: true,
    renderAfterClose: true,
    panelPosition: panelPosition.DOWN,
    panelMinWidth: panelMinWidth.BUTTON_WIDTH,
    allowNestedFocus: true,
    externalBlurHandler: () => false
};

/**
 * @name DropdownView
 * @memberof module:core.dropdown.views
 * @class Composite View that implements dropdown logic similar to SELECT HTML-element.
 * Unlike {@link module:core.dropdown.views.PopoutView PopoutView}, a panel doesn't have a speech bubble triangle and
 * it's min-width is always determined and equal to the width of a button view.
 *
 * A dropdown view contains button and panel regions that can be fully customizable by the properties buttonView and panelView.
 *

 *
Button View is used for displaying a button. Click on that button trigger a panel to open.

 *
Panel View is used to display a panel that drops down.

 *

 *
 * Panel width is determined by its layout but it cannot be less than the button's width. Panel height is fully determined by its layout.
 * A place where the panel appears depends on the panelPosition option.

 * Possible events:

 *
'open' (dropdownView) - fires after the panel has opened.

 *
'close' (dropdownView, ...) - fires after the panel has closed.
 * If the panel was closed via close(...) method, the arguments of this method are transferred into this event.

 *
'button:\*'  - all events the buttonView triggers are repeated by this view with 'button:' prefix.

 *
'panel:\*'  - all events the panelView triggers are repeated by this view with 'panel:' prefix.

 *

 * @constructor
 * @param {Object} options Options object.
 * @param {Marionette.View} options.buttonView View class for displaying the button.
 * @param {(Object|Function)} [options.buttonViewOptions] Options passed into the view on its creation.
 * @param {Marionette.View} options.panelView View class for displaying the panel. The view is created every time the panel is triggered to open.
 * @param {(Object|Function)} [options.panelViewOptions] Options passed into the view on its creation.
 * @param {Boolean} [options.autoOpen=true] Whether click on the button should trigger the panel to open.
 * @param {String} [options.panelPosition='down'] Opening direction:
 *

'down' - opens down.

 *
'down-over' - opens down and the panel is located above the button overlapping it.

 *
'up' - opens up.

 *
'up-over' - opens up and the panel is located above the button overlapping it.

 * @param {Boolean} [options.renderAfterClose=true] Whether to trigger button render when the panel has closed.
 * */

export default class DropdownView {
    constructor(options) {
        this.options = _.defaults({}, Object.assign({}, defaultOptions), options);

        this.__observedEntities = [];
        this.maxWidth = options.panelViewOptions && options.panelViewOptions.maxWidth ? options.panelViewOptions.maxWidth : 0;
        this.__checkElements = _.throttle(this.__checkElements.bind(this), THROTTLE_DELAY);

        this.button = new this.options.buttonView(_.extend({ parent: this }, this.options.buttonViewOptions));
        this.button.close = this.close.bind(this);
        this.button.open = this.open.bind(this);
        this.button.adjustPosition = this.adjustPosition.bind(this);

        this.button.once('render', () => {
            this.isShown = true;
            this.button.on('change:content', () => this.__adjustPosition(true));
            if (options.autoOpen !== false) {
                this.button.el.addEventListener('click', this.__handleClick.bind(this));
            }
            this.button.el.addEventListener('blur', this.__onBlur.bind(this));

            if (!this.options.customAnchor && this.options.showDropdownAnchor) {
                //todo add cutom anchor
                //this.$el.append(`<i class="js-default-anchor ${classes.DEFAULT_ANCHOR}"></i>`);
            }
        });

        return this.button;
    }

    className() {
        return `dropdown ${this.options.class || ''}`;
    }

    ui: {
        button: '.js-button-region'
    };

    events: {
        blur: '__onBlur',
        click: '__triggerClick'
    };

    onDestroy() {
        if (this.button) {
            this.button.destroy();
        }
        if (this.button.isOpen) {
            WindowService.closePopup(this.popupId);
        }
    }

    adjustPosition(isNeedToRefreshAnchorPosition) {
        this.__adjustPosition(isNeedToRefreshAnchorPosition);
    }

    __adjustPosition(isNeedToRefreshAnchorPosition) {
        if (!this.button.isOpen || !this.panelEl) {
            return;
        }
        this.panelEl.style.height = ''; //resetting custom height

        const viewportHeight = window.innerHeight;
        const dropDownRoot = this.button.$el.closest('.dropdown_root')[0];
        const dropDownRootPositionUp = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_up');
        const dropDownRootPositionDown = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_down');
        const buttonEl = dropDownRoot || this.button.el;
        const buttonRect = buttonEl.getBoundingClientRect();

        const bottom = viewportHeight - buttonRect.top - buttonRect.height;

        const computedWidth = Math.max(MAX_DROPDOWN_PANEL_WIDTH, buttonRect.width || 0);

        const panelWidth = this.maxWidth ? Math.min(computedWidth, this.maxWidth) : computedWidth;

        if (this.options.panelMinWidth === panelMinWidth.BUTTON_WIDTH) {
            this.panelEl.style.width = `${panelWidth}px`;
        }

        if (this.panelEl.clientWidth < MAX_DROPDOWN_PANEL_WIDTH) {
            this.panelEl.style.minWidth = `${panelWidth}px`;
        }

        let leftOffset = buttonRect.left;
        let offsetHeight = this.panelEl.offsetHeight;

        let position = this.options.panelPosition;

        if (dropDownRoot && dropDownRootPositionUp) {
            position = panelPosition.UP;
        } else if (dropDownRoot && dropDownRootPositionDown) {
            position = panelPosition.DOWN;
        } else if (position === panelPosition.DOWN && ((bottom < offsetHeight && buttonRect.top > bottom) || bottom < this.options.minAvailableHeight + offsetHeight)) {
            position = panelPosition.UP;
        } else if (position === panelPosition.UP && buttonRect.top < offsetHeight && bottom > buttonRect.top) {
            position = panelPosition.DOWN;
        }

        // class adjustments
        if (position === panelPosition.DOWN) {
            this.button.el.classList.add(classes.DROPDOWN_DOWN);
            this.panelEl.classList.add(classes.DROPDOWN_DOWN);

            this.button.el.classList.remove(classes.DROPDOWN_UP);
            this.panelEl.classList.remove(classes.DROPDOWN_UP);
        } else if (position === panelPosition.UP) {
            this.button.el.classList.add(classes.DROPDOWN_UP);
            this.panelEl.classList.add(classes.DROPDOWN_UP);

            this.button.el.classList.remove(classes.DROPDOWN_DOWN);
            this.panelEl.classList.remove(classes.DROPDOWN_DOWN);
        }
        this.panelEl.classList.add('dropdown__wrp');

        offsetHeight = this.panelEl.offsetHeight;

        // panel positioning
        let top: number = 0;
        switch (position) {
            case panelPosition.UP:
                top = buttonRect.top - offsetHeight;
                break;
            case panelPosition.DOWN:
                top = buttonRect.top + buttonRect.height;
                break;
            default:
                break;
        }

        // trying to fit into viewport
        if (top + offsetHeight > viewportHeight - WINDOW_BORDER_OFFSET) {
            top = viewportHeight - WINDOW_BORDER_OFFSET - offsetHeight; // todo add border offset
            if (offsetHeight + WINDOW_BORDER_OFFSET > bottom) {
                const diff = offsetHeight + WINDOW_BORDER_OFFSET - bottom;
                top += diff;
                this.panelEl.style.height = `${offsetHeight + WINDOW_BORDER_OFFSET - diff}px`;
            }
        }
        if (top <= WINDOW_BORDER_OFFSET) {
            top = WINDOW_BORDER_OFFSET;

            if (offsetHeight > buttonRect.top) {
                const diff = offsetHeight - buttonRect.top;
                this.panelEl.style.height = `${offsetHeight - diff}px`;
            }
        }

        this.panelEl.style.top = `${top}px`;

        if (panelWidth > buttonRect.width) {
            leftOffset -= panelWidth - buttonRect.width;
        }
        this.panelEl.style.left = `${leftOffset}px`;

        if (isNeedToRefreshAnchorPosition) {
            this.__updateAnchorPosition(this.button.el);
        }
    }

    open() {
        if (this.button.isOpen) {
            return;
        }
        this.button.trigger('before:open', this);

        const panelViewOptions = _.extend(this.options.panelViewOptions || {}, {
            parent: this
        });
        this.button.el.classList.add(classes.OPEN);
        this.panelView = new this.options.panelView(panelViewOptions);
        this.button.panelView = this.panelView;
        this.panelView.on('all', (...args) => {
            args[0] = `panel:${args[0]}`;
            this.button.trigger(...args);
        });

        this.popupId = WindowService.showTransientPopup(this.panelView, {
            hostEl: this.button.el
        });
        this.panelEl = this.panelView.el;

        this.button.isOpen = true;
        this.__adjustPosition();
        //const buttonWidth = this.button.el.getBoundingClientRect().width;

        //this.panelView.el.getElementsByClassName(classes.VISIBLE_COLLECTION)[0].style.width = `${panelWidth}`;

        this.panelView.on('change:content', () => this.__adjustPosition());
        this.__listenToElementMoveOnce(this.button.el, this.close);

        GlobalEventService.on('window:keydown:captured', (document, event) => this.__keyAction(event));
        GlobalEventService.on('window:mousedown:captured', this.__handleGlobalMousedown.bind(this));
        WindowService.on('popup:close', this.__onWindowServicePopupClose.bind(this));

        const activeElement = document.activeElement;
        if (!this.__isNestedInButton(activeElement) && !this.__isNestedInPanel(activeElement)) {
            //this.panelView.$el.focus(); todo
        } else {
            this.__focus(activeElement);
        }
        this.__suppressHandlingBlur = false;
        this.button.trigger('open', this);
    }

    /**
     * Closes the dropdown panel.
     * @param {...*} arguments Arguments transferred into the 'close' event.
     * */
    close(...args) {
        if (!this.button.isOpen || !document.body.contains(this.button.el)) {
            return;
        }
        this.button.trigger('before:close', this);

        this.button.el.classList.remove(classes.OPEN);

        GlobalEventService.off('window:keydown:captured', (document, event) => this.__keyAction(event));
        GlobalEventService.off('window:mousedown:captured', this.__handleGlobalMousedown);
        GlobalEventService.off('window:wheel:captured', this.__checkElements);
        GlobalEventService.off('window:mouseup:captured', this.__checkElements);
        GlobalEventService.off('window:keydown:captured', this.__checkElements);

        WindowService.off('popup:close', this.__onWindowServicePopupClose.bind(this));
        this.panelView.off();

        WindowService.closePopup(this.popupId);

        this.__stopListeningToElementMove();

        this.button.$el.focus();
        this.button.isOpen = false;

        this.button.trigger('close', this, ...args);
    }

    __keyAction(event) {
        if (event.keyCode === 27) {
            this.close();
        }
    }

    __handleClick() {
        if (this.options.autoOpen) {
            this.open();
        }
    }

    __isNestedInButton(testedEl) {
        return this.button.el === testedEl || this.button.el.contains(testedEl);
    }

    __isNestedInPanel(testedEl) {
        const palet = document.getElementsByClassName('sp-container')[0]; //Color picker custom el container;

        return WindowService.get(this.popupId).some(x => x.el.contains(testedEl) || this.button.el.contains(testedEl)) || (palet && palet.contains(testedEl));
    }

    __handleBlur() {
        if (
            !this.options.externalBlurHandler(document.activeElement) &&
            !this.__suppressHandlingBlur &&
            !this.__isNestedInButton(document.activeElement) &&
            !this.__isNestedInPanel(document.activeElement)
        ) {
            this.close();
        }
    }

    __handleGlobalMousedown(target) {
        if (this.__isNestedInPanel(target) || this.options.externalBlurHandler(target)) {
            this.__suppressHandlingBlur = true;
        } else if (!this.__isNestedInButton(target)) {
            this.close();
        }
    }

    __onWindowServicePopupClose(popupId) {
        if (this.button.isOpen && this.popupId === popupId) {
            this.close();
        }
    }

    __focus(focusedEl) {
        if (!focusedEl) {
            this.__getFocusableEl().focus();
        } else if (document.activeElement) {
            document.activeElement.addEventListener('blur', this.__onBlur.bind(this));
        }
        this.isFocused = true;
    }

    __onBlur() {
        _.defer(() => {
            this.isFocused = false;
            this.__handleBlur();
        });
        if (document.activeElement) {
            document.activeElement.removeEventListener('blur', this.__onBlur.bind(this));
        }
    }

    __listenToElementMoveOnce(el, callback) {
        if (this.__observedEntities.length === 0) {
            GlobalEventService.on('window:wheel:captured', this.__checkElements);
            GlobalEventService.on('window:mouseup:captured', this.__checkElements);
            GlobalEventService.on('window:keydown:captured', this.__checkElements);
        }

        // saving el position relative to the viewport for further check
        const { left, top } = el.getBoundingClientRect();

        this.__observedEntities.push({
            anchorViewportPos: {
                left: Math.floor(left),
                top: Math.floor(top)
            },
            el,
            callback
        });
    }

    __stopListeningToElementMove(el = null) {
        if (!el) {
            this.__observedEntities = [];
        } else {
            this.__observedEntities.splice(this.__observedEntities.findIndex(x => x.el === el), 1);
        }
    }

    __checkElements(target) {
        if (!this.__isNestedInButton(target) && !this.__isNestedInPanel(target)) {
            setTimeout(() => {
                if (this.button.isDestroyed()) {
                    return;
                }
                this.__observedEntities.forEach(x => {
                    const { left, top } = x.el.getBoundingClientRect();
                    if (Math.floor(left) !== x.anchorViewportPos.left || Math.floor(top) !== x.anchorViewportPos.top) {
                        x.callback.call(this);
                    }
                });
            }, 50);
        }
    }

    __updateAnchorPosition(el) {
        const observable = this.__observedEntities.find(entrie => entrie.el === el);

        if (observable) {
            const { left, top } = el.getBoundingClientRect();

            observable.anchorViewportPos = {
                left: Math.floor(left),
                top: Math.floor(top)
            };
        }
    }

    __triggerClick() {
        this.button.trigger('container:click');
    }
}
<commit_msg>Dropdown: toggle on button click.<commit_after>// @flow
import { helpers } from 'utils';
import WindowService from '../../services/WindowService';
import GlobalEventService from '../../services/GlobalEventService';

const THROTTLE_DELAY = 100;

const classes = {
    OPEN: 'open',
    DROPDOWN_DOWN: 'dropdown__wrp_down',
    DROPDOWN_WRP_OVER: 'dropdown__wrp_down-over',
    DROPDOWN_UP: 'dropdown__wrp_up',
    DROPDOWN_UP_OVER: 'dropdown__wrp_up-over',
    VISIBLE_COLLECTION: 'visible-collection'
};

const WINDOW_BORDER_OFFSET = 10;
const MAX_DROPDOWN_PANEL_WIDTH = 200;

const panelPosition = {
    DOWN: 'down',
    UP: 'up'
};

const panelMinWidth = {
    NONE: 'none',
    BUTTON_WIDTH: 'button-width'
};

const defaultOptions = {
    autoOpen: true,
    renderAfterClose: true,
    panelPosition: panelPosition.DOWN,
    panelMinWidth: panelMinWidth.BUTTON_WIDTH,
    allowNestedFocus: true,
    externalBlurHandler: () => false
};

/**
 * @name DropdownView
 * @memberof module:core.dropdown.views
 * @class Composite View that implements dropdown logic similar to SELECT HTML-element.
 * Unlike {@link module:core.dropdown.views.PopoutView PopoutView}, a panel doesn't have a speech bubble triangle and
 * it's min-width is always determined and equal to the width of a button view.
 *
 * A dropdown view contains button and panel regions that can be fully customizable by the properties buttonView and panelView.
 *

 *
Button View is used for displaying a button. Click on that button trigger a panel to open.

 *
Panel View is used to display a panel that drops down.

 *

 *
 * Panel width is determined by its layout but it cannot be less than the button's width. Panel height is fully determined by its layout.
 * A place where the panel appears depends on the panelPosition option.

 * Possible events:

 *
'open' (dropdownView) - fires after the panel has opened.

 *
'close' (dropdownView, ...) - fires after the panel has closed.
 * If the panel was closed via close(...) method, the arguments of this method are transferred into this event.

 *
'button:\*'  - all events the buttonView triggers are repeated by this view with 'button:' prefix.

 *
'panel:\*'  - all events the panelView triggers are repeated by this view with 'panel:' prefix.

 *

 * @constructor
 * @param {Object} options Options object.
 * @param {Marionette.View} options.buttonView View class for displaying the button.
 * @param {(Object|Function)} [options.buttonViewOptions] Options passed into the view on its creation.
 * @param {Marionette.View} options.panelView View class for displaying the panel. The view is created every time the panel is triggered to open.
 * @param {(Object|Function)} [options.panelViewOptions] Options passed into the view on its creation.
 * @param {Boolean} [options.autoOpen=true] Whether click on the button should trigger the panel to open.
 * @param {String} [options.panelPosition='down'] Opening direction:
 *

'down' - opens down.

 *
'down-over' - opens down and the panel is located above the button overlapping it.

 *
'up' - opens up.

 *
'up-over' - opens up and the panel is located above the button overlapping it.

 * @param {Boolean} [options.renderAfterClose=true] Whether to trigger button render when the panel has closed.
 * */

export default class DropdownView {
    constructor(options) {
        this.options = _.defaults({}, Object.assign({}, defaultOptions), options);

        this.__observedEntities = [];
        this.maxWidth = options.panelViewOptions && options.panelViewOptions.maxWidth ? options.panelViewOptions.maxWidth : 0;
        this.__checkElements = _.throttle(this.__checkElements.bind(this), THROTTLE_DELAY);

        this.button = new this.options.buttonView(_.extend({ parent: this }, this.options.buttonViewOptions));
        this.button.close = this.close.bind(this);
        this.button.open = this.open.bind(this);
        this.button.adjustPosition = this.adjustPosition.bind(this);

        this.button.once('render', () => {
            this.isShown = true;
            this.button.on('change:content', () => this.__adjustPosition(true));
            if (options.autoOpen !== false) {
                this.button.el.addEventListener('click', this.__handleClick.bind(this));
            }
            this.button.el.addEventListener('blur', this.__onBlur.bind(this));

            if (!this.options.customAnchor && this.options.showDropdownAnchor) {
                //todo add cutom anchor
                //this.$el.append(`<i class="js-default-anchor ${classes.DEFAULT_ANCHOR}"></i>`);
            }
        });

        return this.button;
    }

    className() {
        return `dropdown ${this.options.class || ''}`;
    }

    ui: {
        button: '.js-button-region'
    };

    events: {
        blur: '__onBlur',
        click: '__triggerClick'
    };

    onDestroy() {
        if (this.button) {
            this.button.destroy();
        }
        if (this.button.isOpen) {
            WindowService.closePopup(this.popupId);
        }
    }

    adjustPosition(isNeedToRefreshAnchorPosition) {
        this.__adjustPosition(isNeedToRefreshAnchorPosition);
    }

    __adjustPosition(isNeedToRefreshAnchorPosition) {
        if (!this.button.isOpen || !this.panelEl) {
            return;
        }
        this.panelEl.style.height = ''; //resetting custom height

        const viewportHeight = window.innerHeight;
        const dropDownRoot = this.button.$el.closest('.dropdown_root')[0];
        const dropDownRootPositionUp = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_up');
        const dropDownRootPositionDown = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_down');
        const buttonEl = dropDownRoot || this.button.el;
        const buttonRect = buttonEl.getBoundingClientRect();

        const bottom = viewportHeight - buttonRect.top - buttonRect.height;

        const computedWidth = Math.max(MAX_DROPDOWN_PANEL_WIDTH, buttonRect.width || 0);

        const panelWidth = this.maxWidth ? Math.min(computedWidth, this.maxWidth) : computedWidth;

        if (this.options.panelMinWidth === panelMinWidth.BUTTON_WIDTH) {
            this.panelEl.style.width = `${panelWidth}px`;
        }

        if (this.panelEl.clientWidth < MAX_DROPDOWN_PANEL_WIDTH) {
            this.panelEl.style.minWidth = `${panelWidth}px`;
        }

        let leftOffset = buttonRect.left;
        let offsetHeight = this.panelEl.offsetHeight;

        let position = this.options.panelPosition;

        if (dropDownRoot && dropDownRootPositionUp) {
            position = panelPosition.UP;
        } else if (dropDownRoot && dropDownRootPositionDown) {
            position = panelPosition.DOWN;
        } else if (position === panelPosition.DOWN && ((bottom < offsetHeight && buttonRect.top > bottom) || bottom < this.options.minAvailableHeight + offsetHeight)) {
            position = panelPosition.UP;
        } else if (position === panelPosition.UP && buttonRect.top < offsetHeight && bottom > buttonRect.top) {
            position = panelPosition.DOWN;
        }

        // class adjustments
        if (position === panelPosition.DOWN) {
            this.button.el.classList.add(classes.DROPDOWN_DOWN);
            this.panelEl.classList.add(classes.DROPDOWN_DOWN);

            this.button.el.classList.remove(classes.DROPDOWN_UP);
            this.panelEl.classList.remove(classes.DROPDOWN_UP);
        } else if (position === panelPosition.UP) {
            this.button.el.classList.add(classes.DROPDOWN_UP);
            this.panelEl.classList.add(classes.DROPDOWN_UP);

            this.button.el.classList.remove(classes.DROPDOWN_DOWN);
            this.panelEl.classList.remove(classes.DROPDOWN_DOWN);
        }
        this.panelEl.classList.add('dropdown__wrp');

        offsetHeight = this.panelEl.offsetHeight;

        // panel positioning
        let top: number = 0;
        switch (position) {
            case panelPosition.UP:
                top = buttonRect.top - offsetHeight;
                break;
            case panelPosition.DOWN:
                top = buttonRect.top + buttonRect.height;
                break;
            default:
                break;
        }

        // trying to fit into viewport
        if (top + offsetHeight > viewportHeight - WINDOW_BORDER_OFFSET) {
            top = viewportHeight - WINDOW_BORDER_OFFSET - offsetHeight; // todo add border offset
            if (offsetHeight + WINDOW_BORDER_OFFSET > bottom) {
                const diff = offsetHeight + WINDOW_BORDER_OFFSET - bottom;
                top += diff;
                this.panelEl.style.height = `${offsetHeight + WINDOW_BORDER_OFFSET - diff}px`;
            }
        }
        if (top <= WINDOW_BORDER_OFFSET) {
            top = WINDOW_BORDER_OFFSET;

            if (offsetHeight > buttonRect.top) {
                const diff = offsetHeight - buttonRect.top;
                this.panelEl.style.height = `${offsetHeight - diff}px`;
            }
        }

        this.panelEl.style.top = `${top}px`;

        if (panelWidth > buttonRect.width) {
            leftOffset -= panelWidth - buttonRect.width;
        }
        this.panelEl.style.left = `${leftOffset}px`;

        if (isNeedToRefreshAnchorPosition) {
            this.__updateAnchorPosition(this.button.el);
        }
    }

    open() {
        if (this.button.isOpen) {
            return;
        }
        this.button.trigger('before:open', this);

        const panelViewOptions = _.extend(this.options.panelViewOptions || {}, {
            parent: this
        });
        this.button.el.classList.add(classes.OPEN);
        this.panelView = new this.options.panelView(panelViewOptions);
        this.button.panelView = this.panelView;
        this.panelView.on('all', (...args) => {
            args[0] = `panel:${args[0]}`;
            this.button.trigger(...args);
        });

        this.popupId = WindowService.showTransientPopup(this.panelView, {
            hostEl: this.button.el
        });
        this.panelEl = this.panelView.el;

        this.button.isOpen = true;
        this.__adjustPosition();
        //const buttonWidth = this.button.el.getBoundingClientRect().width;

        //this.panelView.el.getElementsByClassName(classes.VISIBLE_COLLECTION)[0].style.width = `${panelWidth}`;

        this.panelView.on('change:content', () => this.__adjustPosition());
        this.__listenToElementMoveOnce(this.button.el, this.close);

        GlobalEventService.on('window:keydown:captured', (document, event) => this.__keyAction(event));
        GlobalEventService.on('window:mousedown:captured', this.__handleGlobalMousedown.bind(this));
        WindowService.on('popup:close', this.__onWindowServicePopupClose.bind(this));

        const activeElement = document.activeElement;
        if (!this.__isNestedInButton(activeElement) && !this.__isNestedInPanel(activeElement)) {
            //this.panelView.$el.focus(); todo
        } else {
            this.__focus(activeElement);
        }
        this.__suppressHandlingBlur = false;
        this.button.trigger('open', this);
    }

    /**
     * Closes the dropdown panel.
     * @param {...*} arguments Arguments transferred into the 'close' event.
     * */
    close(...args) {
        if (!this.button.isOpen || !document.body.contains(this.button.el)) {
            return;
        }
        this.button.trigger('before:close', this);

        this.button.el.classList.remove(classes.OPEN);

        GlobalEventService.off('window:keydown:captured', (document, event) => this.__keyAction(event));
        GlobalEventService.off('window:mousedown:captured', this.__handleGlobalMousedown);
        GlobalEventService.off('window:wheel:captured', this.__checkElements);
        GlobalEventService.off('window:mouseup:captured', this.__checkElements);
        GlobalEventService.off('window:keydown:captured', this.__checkElements);

        WindowService.off('popup:close', this.__onWindowServicePopupClose.bind(this));
        this.panelView.off();

        WindowService.closePopup(this.popupId);

        this.__stopListeningToElementMove();

        this.button.$el.focus();
        this.button.isOpen = false;

        this.button.trigger('close', this, ...args);
    }

    toggle(...args) {
        if (this.button.isOpen) {
            this.close(...args);
        } else {
            this.open(...args);
        }
    }

    __keyAction(event) {
        if (event.keyCode === 27) {
            this.close();
        }
    }

    __handleClick() {
        if (this.options.autoOpen) {
            this.toggle();
        }
    }

    __isNestedInButton(testedEl) {
        return this.button.el === testedEl || this.button.el.contains(testedEl);
    }

    __isNestedInPanel(testedEl) {
        const palet = document.getElementsByClassName('sp-container')[0]; //Color picker custom el container;

        return WindowService.get(this.popupId).some(x => x.el.contains(testedEl) || this.button.el.contains(testedEl)) || (palet && palet.contains(testedEl));
    }

    __handleBlur() {
        if (
            !this.options.externalBlurHandler(document.activeElement) &&
            !this.__suppressHandlingBlur &&
            !this.__isNestedInButton(document.activeElement) &&
            !this.__isNestedInPanel(document.activeElement)
        ) {
            this.close();
        }
    }

    __handleGlobalMousedown(target) {
        if (this.__isNestedInPanel(target) || this.options.externalBlurHandler(target)) {
            this.__suppressHandlingBlur = true;
        } else if (!this.__isNestedInButton(target)) {
            this.close();
        }
    }

    __onWindowServicePopupClose(popupId) {
        if (this.button.isOpen && this.popupId === popupId) {
            this.close();
        }
    }

    __focus(focusedEl) {
        if (!focusedEl) {
            this.__getFocusableEl().focus();
        } else if (document.activeElement) {
            document.activeElement.addEventListener('blur', this.__onBlur.bind(this));
        }
        this.isFocused = true;
    }

    __onBlur() {
        _.defer(() => {
            this.isFocused = false;
            this.__handleBlur();
        });
        if (document.activeElement) {
            document.activeElement.removeEventListener('blur', this.__onBlur.bind(this));
        }
    }

    __listenToElementMoveOnce(el, callback) {
        if (this.__observedEntities.length === 0) {
            GlobalEventService.on('window:wheel:captured', this.__checkElements);
            GlobalEventService.on('window:mouseup:captured', this.__checkElements);
            GlobalEventService.on('window:keydown:captured', this.__checkElements);
        }

        // saving el position relative to the viewport for further check
        const { left, top } = el.getBoundingClientRect();

        this.__observedEntities.push({
            anchorViewportPos: {
                left: Math.floor(left),
                top: Math.floor(top)
            },
            el,
            callback
        });
    }

    __stopListeningToElementMove(el = null) {
        if (!el) {
            this.__observedEntities = [];
        } else {
            this.__observedEntities.splice(this.__observedEntities.findIndex(x => x.el === el), 1);
        }
    }

    __checkElements(target) {
        if (!this.__isNestedInButton(target) && !this.__isNestedInPanel(target)) {
            setTimeout(() => {
                if (this.button.isDestroyed()) {
                    return;
                }
                this.__observedEntities.forEach(x => {
                    const { left, top } = x.el.getBoundingClientRect();
                    if (Math.floor(left) !== x.anchorViewportPos.left || Math.floor(top) !== x.anchorViewportPos.top) {
                        x.callback.call(this);
                    }
                });
            }, 50);
        }
    }

    __updateAnchorPosition(el) {
        const observable = this.__observedEntities.find(entrie => entrie.el === el);

        if (observable) {
            const { left, top } = el.getBoundingClientRect();

            observable.anchorViewportPos = {
                left: Math.floor(left),
                top: Math.floor(top)
            };
        }
    }

    __triggerClick() {
        this.button.trigger('container:click');
    }
}
<|endoftext|>


<commit_before>import { asArray, defaults } from './utils';
import { DEFAULT_SETTINGS, DOM_ATTRIBUTES, toResultsWithRegExp } from './settings';

function writeGridPlacement(row, placement) {
    row.setAttribute(DOM_ATTRIBUTES.PLAYER_PLACEMENT, placement);
}

/**
 * Returns array of columns (array of cell values in the column) from given list of rows.
 *
 * @param {Array.<Element|HTMLElement>} rows
 * @param {string} cellSelector
 * @returns {Array.<Array.<string>>}
 */
function getColumnsFromRows(rows, cellSelector) {
    return rows.reduce((columns, row) => {
        asArray(row.querySelectorAll(cellSelector))
            .forEach((cell, index) => {
                let column = columns[index];

                if (!column) {
                    column = [];
                    columns[index] = column;
                }

                column.push(cell.textContent);
            });

        return columns;
    }, []);
}

/**
 * From given set of strings it returns the ones that look like Go results.
 *
 * @param {Array.<string>} items
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {Array.<string>}
 */
function getItemsWithGoResults(items, resultsMap) {
    return items.filter(cell => resultsMap.some(result => cell.match(result.regexp)));
}

/**
 * Checks if at least 40% of strings from given set look like Go results.
 * Why 40%? This value allowed to eliminate some sneaky columns from OpenGotha results where "=" is
 * used to show halves and is interpreted as jigo result by Highlighter. If the value is too high
 * then it is likely that large tournament results (like congress) will be not parsed correctly
 * as many people drop/skip rounds.
 *
 * @param {Array.<string>} items
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {boolean}
 */
function checkItemsForResults(items, resultsMap) {
    const count = items.length;
    const itemsWithResultsCount = getItemsWithGoResults(items, resultsMap).length;

    return itemsWithResultsCount / count >= 0.4;
}

/**
 * Returns the array of indexes of columns that look like keeping Go results.
 *
 * @param {Array.<Element|HTMLElement>} rows
 * @param {string} cellSelector
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {Array.<number>}
 */
function getIndexesOfColumnsWithResultsFromRows(rows, cellSelector, resultsMap) {
    return getColumnsFromRows(rows, cellSelector)
        .reduce((indexes, column, index) => {
            if (checkItemsForResults(column, resultsMap)) {
                indexes.push(index);
            }

            return indexes;
        }, []);
}

/**
 * Creates filter function which returns items from provided list of indexes.
 *
 * @param {Array.<number>} columnsIndexes
 * @returns {function(*, *=): boolean}
 */
function createCellFromColumnsFilter(columnsIndexes) {
    return (cell, index) => columnsIndexes.indexOf(index) !== -1;
}

/**
 * Returns the array of indexes of columns with Go results based on settings.
 *
 * @param {Array.<Element|HTMLElement>} rows
 * @param {HighlighterSettings} settings
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {function(*, *=): boolean}
 */
function getFilterForColumnsWithResults(rows, settings, resultsMap) {
    if (typeof settings.roundsColumns === 'string') {
        const  indexes = settings.roundsColumns.split(',').map(Number);

        return createCellFromColumnsFilter(indexes);
    }

    // check is disabled - return all columns
    if (!settings.checkColumnsForResults) {
        return () => true;
    }

    const  indexes = getIndexesOfColumnsWithResultsFromRows(rows, settings.cellTags, resultsMap);

    return createCellFromColumnsFilter(indexes);
}

/**
 * Traverses provided table and creates results map.
 *
 * @param {Element|HTMLElement} table - table results container
 * @param {HighlighterSettings} [config] - settings for parser
 * @returns {object}
 */
export default function parse(table, config) {
    const settings = defaults(DEFAULT_SETTINGS, config);
    const rows = asArray(table.querySelectorAll(settings.rowTags));
    const resultsMap = toResultsWithRegExp(settings.results);
    const resultsMapCount = resultsMap.length;
    const columnsWithResultsFilter = getFilterForColumnsWithResults(rows, settings, resultsMap);
    const results = {};

    function parseGames(player, cells) {
        cells.forEach((cell) => {
            let opponentPlace;
            let resultCls;

            if (cell.hasAttribute(DOM_ATTRIBUTES.GAME_RESULT) && cell.hasAttribute(DOM_ATTRIBUTES.OPPONENT_PLACEMENT)) {
                opponentPlace = Number(cell.getAttribute(DOM_ATTRIBUTES.OPPONENT_PLACEMENT));
                resultCls = cell.getAttribute(DOM_ATTRIBUTES.GAME_RESULT);

            } else {
                for (let i = 0; i < resultsMapCount; i++) {
                    let match = cell.textContent.match(resultsMap[i].regexp);

                    if (!match) {
                        continue;
                    }

                    opponentPlace = Number(match[1]);
                    resultCls = resultsMap[i].cls;

                    // opponent row doesn't exist
                    if (opponentPlace <= 0 || (!settings.ignoreOutOfBoundsRows && opponentPlace > rows.length)) {
                        return;
                    }

                    cell.setAttribute(DOM_ATTRIBUTES.OPPONENT_PLACEMENT, opponentPlace);
                    cell.setAttribute(DOM_ATTRIBUTES.GAME_RESULT, resultsMap[i].cls);
                }

                if (!opponentPlace) {
                    return;
                }
            }

            player.games[opponentPlace] = {
                cell,
                cls: resultCls
            };

            player.opponents.push(opponentPlace);
        });
    }

    let lastTournamentPlacement;
    let lastGridPlacement;

    rows.forEach((row, index) => {
        if (index < settings.startingRow) {
            return;
        }

        const cells = asArray(row.querySelectorAll(settings.cellTags));
        const cellsWithResults = cells.filter(columnsWithResultsFilter);

        // assign default place
        let gridPlacement = -1;

        // no cells? unlikely to be a result row
        if (!cells.length || !cells[settings.placeColumn]) {
            writeGridPlacement(row, gridPlacement);
            return;
        }

        let tournamentPlacement = parseInt(cells[settings.placeColumn].textContent, 10);

        const player = {
            tournamentPlace: -1,
            row,
            games: {},
            opponents: []
        };

        if (row.hasAttribute(DOM_ATTRIBUTES.PLAYER_PLACEMENT)) {
            gridPlacement = Number(row.getAttribute(DOM_ATTRIBUTES.PLAYER_PLACEMENT));

        } else {

            // if no player has been mapped
            if (!lastGridPlacement) {

                // most probably not a result row
                if (isNaN(tournamentPlacement)) {
                    writeGridPlacement(row, gridPlacement);
                    return;
                }

                // assign tournament if defined (possibly showing an extract from greater table)
                gridPlacement = tournamentPlacement || 1;
            } else {
                gridPlacement = lastGridPlacement + 1;
            }

            // assumption: if place is not provided then it's an ex aequo case but
            // we need to set a lower place nonetheless
            if (!tournamentPlacement) {
                tournamentPlacement = lastTournamentPlacement ? lastTournamentPlacement : 1;

            } else if (tournamentPlacement <= lastTournamentPlacement) {
                tournamentPlacement = lastTournamentPlacement;
            }

            writeGridPlacement(row, gridPlacement);
        }

        if (gridPlacement == -1) {
            return;
        }

        parseGames(player, cellsWithResults);

        player.tournamentPlace = tournamentPlacement;
        player.opponents.sort((a, b) => a > b ? 1 : -1);

        results[gridPlacement] = player;

        lastTournamentPlacement = tournamentPlacement;
        lastGridPlacement = gridPlacement;
    });

    return results;
}<commit_msg>Improved performance of parsing big tables<commit_after>import { asArray, defaults } from './utils';
import { DEFAULT_SETTINGS, DOM_ATTRIBUTES, toResultsWithRegExp } from './settings';

function writeGridPlacement(row, placement) {
    row.setAttribute(DOM_ATTRIBUTES.PLAYER_PLACEMENT, placement);
}

/**
 * Returns array of columns (array of cell values in the column) from given list of rows.
 *
 * @param {Array.<CachedRow>} cachedTable
 * @returns {Array.<Array.<string>>}
 */
function getColumnsFromRows(cachedTable) {
    const columns = [];

    for (let i = 0; i < cachedTable.length; i++) {
        for (let j = 0; j < cachedTable[i].cells.length; j++) {
            if (!columns[j]) {
                columns[j] = [];
            }

            columns[j][i] = cachedTable[i].cells[j].textContent;
        }
    }

    return columns;
}

/**
 * From given set of strings it returns the ones that look like Go results.
 *
 * @param {Array.<string>} items
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {Array.<string>}
 */
function getItemsWithGoResults(items, resultsMap) {
    return items.filter(cell => resultsMap.some(result => cell.match(result.regexp)));
}

/**
 * Checks if at least 40% of strings from given set look like Go results.
 * Why 40%? This value allowed to eliminate some sneaky columns from OpenGotha results where "=" is
 * used to show halves and is interpreted as jigo result by Highlighter. If the value is too high
 * then it is likely that large tournament results (like congress) will be not parsed correctly
 * as many people drop/skip rounds.
 *
 * @param {Array.<string>} items
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {boolean}
 */
function checkItemsForResults(items, resultsMap) {
    const count = items.length;
    const itemsWithResultsCount = getItemsWithGoResults(items, resultsMap).length;

    return itemsWithResultsCount / count >= 0.4;
}

/**
 * Returns the array of indexes of columns that look like keeping Go results.
 *
 * @param {Array.<CachedRow>} cachedTable
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {Array.<number>}
 */
function getIndexesOfColumnsWithResultsFromRows(cachedTable, resultsMap) {
    return getColumnsFromRows(cachedTable)
        .reduce((indexes, column, index) => {
            if (checkItemsForResults(column, resultsMap)) {
                indexes.push(index);
            }

            return indexes;
        }, []);
}

/**
 * Creates filter function which returns items from provided list of indexes.
 *
 * @param {Array.<number>} columnsIndexes
 * @returns {function(*, *=): boolean}
 */
function createCellFromColumnsFilter(columnsIndexes) {
    return (cell, index) => columnsIndexes.indexOf(index) !== -1;
}

/**
 * Returns the array of indexes of columns with Go results based on settings.
 *
 * @param {Array.<CachedRow>} cachedTable
 * @param {HighlighterSettings} settings
 * @param {Array.<ResultMapping>} resultsMap
 * @returns {function(*, *=): boolean}
 */
function getFilterForColumnsWithResults(cachedTable, settings, resultsMap) {
    if (typeof settings.roundsColumns === 'string') {
        const  indexes = settings.roundsColumns.split(',').map(Number);

        return createCellFromColumnsFilter(indexes);
    }

    // check is disabled - return all columns
    if (!settings.checkColumnsForResults) {
        return () => true;
    }

    const  indexes = getIndexesOfColumnsWithResultsFromRows(cachedTable, resultsMap);

    return createCellFromColumnsFilter(indexes);
}

/**
 * Creates cached table
 * @param {HTMLElement} table
 * @param {HighlighterSettings} settings
 * @returns {Array.<CachedRow>}
 */
function cacheTable(table, settings) {
    const rows = table.querySelectorAll(settings.rowTags);
    const result = new Array(rows.length);

    for (let i = 0; i < rows.length; i++) {
        const row = rows[i];
        const cells = row.querySelectorAll(settings.cellTags);

        result[i] = {
            row,
            cells: asArray(cells)
        };
    }

    return result;
}

/**
 * Traverses provided table and creates results map.
 *
 * @param {Element|HTMLElement} table - table results container
 * @param {HighlighterSettings} [config] - settings for parser
 * @returns {object}
 */
export default function parse(table, config) {
    const settings = defaults(DEFAULT_SETTINGS, config);
    const resultsMap = toResultsWithRegExp(settings.results);
    const resultsMapCount = resultsMap.length;

    const cachedTable = cacheTable(table, settings);
    const columnsWithResultsFilter = getFilterForColumnsWithResults(cachedTable, settings, resultsMap);

    const results = {};
    let lastTournamentPlacement;
    let lastGridPlacement;

    cachedTable.forEach(parseRow);

    return results;

    function parseRow({ row, cells }, index) {
        if (index < settings.startingRow) {
            return;
        }

        // assign default place
        let gridPlacement = -1;

        // no cells? unlikely to be a result row
        if (!cells.length || !cells[settings.placeColumn]) {
            writeGridPlacement(row, gridPlacement);
            return;
        }

        let tournamentPlacement = parseInt(cells[settings.placeColumn].textContent, 10);

        const player = {
            tournamentPlace: -1,
            row,
            games: {},
            opponents: []
        };

        if (row.hasAttribute(DOM_ATTRIBUTES.PLAYER_PLACEMENT)) {
            gridPlacement = Number(row.getAttribute(DOM_ATTRIBUTES.PLAYER_PLACEMENT));

        } else {

            // if no player has been mapped
            if (!lastGridPlacement) {

                // most probably not a result row
                if (isNaN(tournamentPlacement)) {
                    writeGridPlacement(row, gridPlacement);
                    return;
                }

                // assign tournament if defined (possibly showing an extract from greater table)
                gridPlacement = tournamentPlacement || 1;
            } else {
                gridPlacement = lastGridPlacement + 1;
            }

            // assumption: if place is not provided then it's an ex aequo case but
            // we need to set a lower place nonetheless
            if (!tournamentPlacement) {
                tournamentPlacement = lastTournamentPlacement ? lastTournamentPlacement : 1;

            } else if (tournamentPlacement <= lastTournamentPlacement) {
                tournamentPlacement = lastTournamentPlacement;
            }

            writeGridPlacement(row, gridPlacement);
        }

        if (Number(gridPlacement) === -1) {
            return;
        }

        cells.forEach((cell, index) => {
             if (columnsWithResultsFilter(cell, index)) {
                 parseGame(player, cell);
             }
        });

        player.tournamentPlace = tournamentPlacement;
        player.opponents.sort((a, b) => a > b ? 1 : -1);

        results[gridPlacement] = player;

        lastTournamentPlacement = tournamentPlacement;
        lastGridPlacement = gridPlacement;
    }

    function parseGame(player, cell) {
        let opponentPlace;
        let resultCls;

        if (cell.hasAttribute(DOM_ATTRIBUTES.GAME_RESULT) && cell.hasAttribute(DOM_ATTRIBUTES.OPPONENT_PLACEMENT)) {
            opponentPlace = Number(cell.getAttribute(DOM_ATTRIBUTES.OPPONENT_PLACEMENT));
            resultCls = cell.getAttribute(DOM_ATTRIBUTES.GAME_RESULT);

        } else {
            for (let i = 0; i < resultsMapCount; i++) {
                let match = cell.textContent.match(resultsMap[i].regexp);

                if (!match) {
                    continue;
                }

                opponentPlace = Number(match[1]);
                resultCls = resultsMap[i].cls;

                // opponent row doesn't exist
                if (opponentPlace <= 0 || (!settings.ignoreOutOfBoundsRows && opponentPlace > cachedTable.length)) {
                    return;
                }

                cell.setAttribute(DOM_ATTRIBUTES.OPPONENT_PLACEMENT, opponentPlace);
                cell.setAttribute(DOM_ATTRIBUTES.GAME_RESULT, resultsMap[i].cls);
            }

            if (!opponentPlace) {
                return;
            }
        }

        player.games[opponentPlace] = {
            cell,
            cls: resultCls
        };

        player.opponents.push(opponentPlace);
    }
}

/**
 * @typedef {object} CachedRow
 * @property {HTMLElement} row - reference to row node
 * @property {Array.<HTMLElement>} cells - list of references to cell nodes
 */<|endoftext|>


<commit_before>(function ynab_transfer_jump() {
	if (typeof Em !== 'undefined' && typeof Ember !== 'undefined' && typeof $ !== 'undefined') {
		(function($) {
			$.event.special.destroyed = {
				remove: function(o) {
					if (o.handler) {
						o.handler();
					}
				},
			};
		})(jQuery);

		DEBUG = true;

		TransactionAmountTypes = {
			INFLOW: 0,
			OUTFLOW: 1
		}

		function get_jump_transaction(entry_date, amount) {
			if (DEBUG) {
				console.log('Entering get_jump_transaction()...');
				console.log('var dump: entry_date');
				console.log(entry_date);
				console.log('var dump: amount');
				console.log(amount);
			}

			var transaction = null;
			if (amount['type'] === TransactionAmountTypes.INFLOW) {
				transaction = $('div.ynab-grid-body-row').
					has("div.ynab-grid-cell-date:contains('" + entry_date + "')").
					has("div.ynab-grid-cell-outflow:contains('" + amount['amount'] + "')")
			}
			else {
				transaction = $('div.ynab-grid-body-row').
					has("div.ynab-grid-cell-date:contains('" + entry_date + "')").
					has("div.ynab-grid-cell-inflow:contains('" + amount['amount'] + "')")
			}

			if (DEBUG) {
				console.log('var dump: transaction');
				console.log(transaction);
				console.log('Exiting get_jump_transaction()...');
			}

			return transaction;
		}

		function get_transaction_data(clicked_jump_element) {
			if (DEBUG) {
				console.log('Entering get_jump_transaction()...');
				console.log('var dump: clicked_jump_element');
				console.log(clicked_jump_element);
			}

			var entry = clicked_jump_element.closest('div.ynab-grid-cell-payeeName');
			var account_name = entry.attr('title').split(': ')[1];
			var account_selector_id = $("div.nav-account-name[title='" + account_name + "']").parent().attr('id');
			var entry_date = entry.siblings('div.ynab-grid-cell-date').text();
			var entry_inflow = entry.siblings('div.ynab-grid-cell-inflow').text();
			var entry_outflow = entry.siblings('div.ynab-grid-cell-outflow').text();
			var transaction = {
				'account_name': account_name,
				'account_selector_id': account_selector_id,
				'date': entry_date,
				'inflow_amount': $.trim(entry_inflow),
				'outflow_amount': $.trim(entry_outflow)
			};

			if (DEBUG) {
				console.log('var dump: transaction');
				console.log(transaction);
				console.log('Exiting get_jump_transaction()...');
			}

			return transaction;
		}

		$('.transfer-jump').off().on('click', function(){
			if (DEBUG) {
				console.log('Entering click event...');
			}

			var entry = get_transaction_data($(this));

			var transaction_amount = {};
			if (entry['inflow_amount'] !== '$0.00') {
				transaction_amount['amount'] = entry['inflow_amount'];
				transaction_amount['type'] = TransactionAmountTypes.INFLOW;
			}
			else {
				transaction_amount['amount'] = entry['outflow_amount'];
				transaction_amount['type'] = TransactionAmountTypes.OUTFLOW;
			}

			if (DEBUG) {
				console.log('var dump: transaction_amount');
				console.log(transaction_amount);
			}

			// Simulate a click on the target account (i.e., the "other side")
			// of the transfer:
			$('div#' + entry['account_selector_id']).trigger('click');

			setTimeout(function() {
				var jump_transaction = get_jump_transaction(entry['date'], transaction_amount);
				if (jump_transaction) {
					var grid = jump_transaction.parent();
					grid.scrollTop(jump_transaction.offset().top);
					jump_transaction.addClass('is-checked');
				}
			}, 100);
			return false;
		});

		var transfer_cells = $("div.ynab-grid-cell-payeeName[title*='Transfer']");
		if (transfer_cells.length) {
			transfer_cells.each(function() {
				var _this = $(this);
				var id = _this.parent().attr('id') + '_jump';
				var jump_icon = '<button id="' + id + '" class="transfer-jump">⇆</button>';

				if (!$('button#' + id).length) {
					_this.prepend(jump_icon);
				}
			});
		}
	}

	setTimeout(ynab_transfer_jump, 10);
})()

<commit_msg>Turn off debugging.<commit_after>(function ynab_transfer_jump() {
	if (typeof Em !== 'undefined' && typeof Ember !== 'undefined' && typeof $ !== 'undefined') {
		(function($) {
			$.event.special.destroyed = {
				remove: function(o) {
					if (o.handler) {
						o.handler();
					}
				},
			};
		})(jQuery);

		DEBUG = false;

		TransactionAmountTypes = {
			INFLOW: 0,
			OUTFLOW: 1
		}

		function get_jump_transaction(entry_date, amount) {
			if (DEBUG) {
				console.log('Entering get_jump_transaction()...');
				console.log('var dump: entry_date');
				console.log(entry_date);
				console.log('var dump: amount');
				console.log(amount);
			}

			var transaction = null;
			if (amount['type'] === TransactionAmountTypes.INFLOW) {
				transaction = $('div.ynab-grid-body-row').
					has("div.ynab-grid-cell-date:contains('" + entry_date + "')").
					has("div.ynab-grid-cell-outflow:contains('" + amount['amount'] + "')")
			}
			else {
				transaction = $('div.ynab-grid-body-row').
					has("div.ynab-grid-cell-date:contains('" + entry_date + "')").
					has("div.ynab-grid-cell-inflow:contains('" + amount['amount'] + "')")
			}

			if (DEBUG) {
				console.log('var dump: transaction');
				console.log(transaction);
				console.log('Exiting get_jump_transaction()...');
			}

			return transaction;
		}

		function get_transaction_data(clicked_jump_element) {
			if (DEBUG) {
				console.log('Entering get_jump_transaction()...');
				console.log('var dump: clicked_jump_element');
				console.log(clicked_jump_element);
			}

			var entry = clicked_jump_element.closest('div.ynab-grid-cell-payeeName');
			var account_name = entry.attr('title').split(': ')[1];
			var account_selector_id = $("div.nav-account-name[title='" + account_name + "']").parent().attr('id');
			var entry_date = entry.siblings('div.ynab-grid-cell-date').text();
			var entry_inflow = entry.siblings('div.ynab-grid-cell-inflow').text();
			var entry_outflow = entry.siblings('div.ynab-grid-cell-outflow').text();
			var transaction = {
				'account_name': account_name,
				'account_selector_id': account_selector_id,
				'date': entry_date,
				'inflow_amount': $.trim(entry_inflow),
				'outflow_amount': $.trim(entry_outflow)
			};

			if (DEBUG) {
				console.log('var dump: transaction');
				console.log(transaction);
				console.log('Exiting get_jump_transaction()...');
			}

			return transaction;
		}

		$('.transfer-jump').off().on('click', function(){
			if (DEBUG) {
				console.log('Entering click event...');
			}

			var entry = get_transaction_data($(this));

			var transaction_amount = {};
			if (entry['inflow_amount'] !== '$0.00') {
				transaction_amount['amount'] = entry['inflow_amount'];
				transaction_amount['type'] = TransactionAmountTypes.INFLOW;
			}
			else {
				transaction_amount['amount'] = entry['outflow_amount'];
				transaction_amount['type'] = TransactionAmountTypes.OUTFLOW;
			}

			if (DEBUG) {
				console.log('var dump: transaction_amount');
				console.log(transaction_amount);
			}

			// Simulate a click on the target account (i.e., the "other side")
			// of the transfer:
			$('div#' + entry['account_selector_id']).trigger('click');

			setTimeout(function() {
				var jump_transaction = get_jump_transaction(entry['date'], transaction_amount);
				if (jump_transaction) {
					var grid = jump_transaction.parent();
					grid.scrollTop(jump_transaction.offset().top);
					jump_transaction.addClass('is-checked');
				}
			}, 100);
			return false;
		});

		var transfer_cells = $("div.ynab-grid-cell-payeeName[title*='Transfer']");
		if (transfer_cells.length) {
			transfer_cells.each(function() {
				var _this = $(this);
				var id = _this.parent().attr('id') + '_jump';
				var jump_icon = '<button id="' + id + '" class="transfer-jump">⇆</button>';

				if (!$('button#' + id).length) {
					_this.prepend(jump_icon);
				}
			});
		}
	}

	setTimeout(ynab_transfer_jump, 10);
})()

<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.build',
  name: 'DirCrawlModelDAO',
  extends: 'foam.dao.PromisedDAO',
  requires: [
    'foam.build.Lib',
    'foam.core.Script',
    'foam.dao.EasyDAO',
    'foam.dao.Relationship',
  ],
  imports: [
    'classloader',
  ],
  properties: [
    {
      name: 'unwrappedScripts',
      value: [
        'src/foam/core/stdlib\\.js',
        'src/foam/core/events\\.js',
        'src/foam/core/Boot\\.js',
        'src/foam/core/FObject\\.js',
        'src/foam/core/Model\\.js',
        'src/foam/core/Boolean\\.js',
        'src/foam/core/AxiomArray\\.js',
        'src/foam/core/EndBoot\\.js',
      ],
    },
    {
      name: 'blacklist',
      value: [
        // Boot files that cannot be wrapped in foam.SCRIPT
        'src/foam\\.js',
        'src/foam/core/poly\\.js',
        'src/foam/core/lib\\.js',

        // Not models.
        'src/foam/nanos/nanos\\.js',
        'src/files\\.js',

        // Dirs we don't care about.
        'src/com/*',
        'src/apps/*',

        // Test files.
        'src/lib/dao_test\\.js',
        'src/foam/box/node/forkScript\\.js',
      ],
    },
    {
      name: 'delegate',
      factory: function() { return this.EasyDAO.create({daoType: 'MDAO', of: 'foam.core.Model'}); },
    },
    {
      name: 'promise',
      factory: function() {
        var self = this;
        return self.fillDAO(self.delegate).then(function() {
          return self.delegate;
        });
      },
    },
    {
      name: 'srcDir',
      adapt: function(_, n) {
        return n.replace(/\/$/, '');
      },
      factory: function() {
        return global.FOAM_ROOT;
      },
    },
  ],
  methods: [
    function fillDAO(dao) {
      var self = this;

      var promises = [];

      var context = {
        foam: Object.create(foam)
      };

      context.foam.LIB = function(m) {
        if ( ! m.id ) {
          m.id = 'lib.Lib' + Date.now()
        }
        promises.push(dao.put(self.Lib.create({
          id: m.id,
          json: m,
        })));
      };

      context.foam.SCRIPT = function(m) {
        promises.push(dao.put(self.Script.create(m)));
      };

      context.foam.ENUM = function(m) {
        m.class = m.class || 'foam.core.EnumModel',
        context.foam.CLASS(m);
      };

      context.foam.INTERFACE = function(m) {
        m.class = m.class || 'foam.core.InterfaceModel',
        context.foam.CLASS(m);
      };

      context.foam.CLASS = function(m) {
        promises.push(Promise.all(foam.json.references(self.__context__, m)).then(function(p) {
          var cls = self.lookup(m.class || 'foam.core.Model')
          return dao.put(cls.create(m));
        }))
      };

      context.foam.RELATIONSHIP = function(m) {
        var r = self.Relationship.create(m);
        promises.push(Promise.all([
          self.classloader.load(r.sourceModel),
          self.classloader.load(r.targetModel)
        ]).then(function() {
          return dao.put(r);
        }))
      };

      var self = this;
      var scriptFilesExp = new RegExp(self.unwrappedScripts.join('|'));
      var blacklistExp = new RegExp(self.blacklist.join('|'));

      var files = [];
      self.ftw(self.srcDir, function(path, lstat) {
        if ( ! lstat.isFile() ) return;
        files.push(path);
      });

      var jsFiles = files
        .filter(function(o) {
          return o.endsWith('.js');
        })

      var evalFiles = jsFiles
        .filter(function(o) {
          return !scriptFilesExp.exec(o) && !blacklistExp.exec(o)
        })

      var scriptFiles = jsFiles
        .filter(function(o) {
          return scriptFilesExp.exec(o) && !blacklistExp.exec(o)
        })

      // TODO: Add a JavaClass model for manually written java classes.
      // Parse the package/name from the .java and then store them in the DAO as
      // well. Also do the same for swift files.
      var javaFiles = files
        .filter(function(o) {
          return o.endsWith('.java');
        })

      var fs = require('fs');
      var sep = require('path').sep;
      evalFiles.forEach(function(f) {
        var o = fs.readFileSync(f, 'utf-8');
        try {
          with ( context ) { eval(o) };
        } catch(e) {
          console.log(e);
        }
      });

      scriptFiles.forEach(function(f) {
        var o = fs.readFileSync(f, 'utf-8');

        // Remove extension and append 'Script' to name.
        var n = f.split(sep).pop().replace(/\.js$/, '') + 'Script';

        with ( context ) {
          foam.SCRIPT({
            package: 'foam.core',
            name: n,
            code: new Function(o),
          });
        }
      });

      return Promise.all(promises);
    },
    function ftw(dir, fn) {
      var fs = require('fs');
      var sep = require('path').sep;
      var dirs = [this.srcDir];
      while ( dirs.length ) {
        var dir = dirs.pop();
        fs.readdirSync(dir).forEach(function(f) {
          var path = dir + sep + f;
          var lstat = fs.lstatSync(path);
          if ( lstat.isDirectory() ) dirs.push(path)
          fn(path, lstat)
        })
      }
    },
    function execute() {
      this.select().then(function(a) {
        console.log(a.array.map(function(o) { return o.id }).join('\n'));
      });
    },
  ],
});
<commit_msg>Restructure code to use ftw a little nicer.<commit_after>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.build',
  name: 'DirCrawlModelDAO',
  extends: 'foam.dao.PromisedDAO',
  requires: [
    'foam.build.Lib',
    'foam.core.Script',
    'foam.dao.EasyDAO',
    'foam.dao.Relationship',
  ],
  imports: [
    'classloader',
  ],
  properties: [
    {
      name: 'unwrappedScripts',
      value: [
        'src/foam/core/stdlib\\.js',
        'src/foam/core/events\\.js',
        'src/foam/core/Boot\\.js',
        'src/foam/core/FObject\\.js',
        'src/foam/core/Model\\.js',
        'src/foam/core/Boolean\\.js',
        'src/foam/core/AxiomArray\\.js',
        'src/foam/core/EndBoot\\.js',
      ],
    },
    {
      name: 'blacklist',
      value: [
        // Boot files that cannot be wrapped in foam.SCRIPT
        'src/foam\\.js',
        'src/foam/core/poly\\.js',
        'src/foam/core/lib\\.js',

        // Not models.
        'src/foam/nanos/nanos\\.js',
        'src/files\\.js',

        // Dirs we don't care about.
        'src/com/*',
        'src/apps/*',

        // Test files.
        'src/lib/dao_test\\.js',
        'src/foam/box/node/forkScript\\.js',
      ],
    },
    {
      name: 'delegate',
      factory: function() { return this.EasyDAO.create({daoType: 'MDAO', of: 'foam.core.Model'}); },
    },
    {
      name: 'promise',
      factory: function() {
        var self = this;
        return self.fillDAO(self.delegate).then(function() {
          return self.delegate;
        });
      },
    },
    {
      name: 'srcDir',
      adapt: function(_, n) {
        return n.replace(/\/$/, '');
      },
      factory: function() {
        return global.FOAM_ROOT;
      },
    },
  ],
  methods: [
    function fillDAO(dao) {
      var self = this;

      var promises = [];

      var context = {
        foam: Object.create(foam)
      };

      context.foam.LIB = function(m) {
        if ( ! m.id ) {
          m.id = 'lib.Lib' + Date.now()
        }
        promises.push(dao.put(self.Lib.create({
          id: m.id,
          json: m,
        })));
      };

      context.foam.SCRIPT = function(m) {
        promises.push(dao.put(self.Script.create(m)));
      };

      context.foam.ENUM = function(m) {
        m.class = m.class || 'foam.core.EnumModel',
        context.foam.CLASS(m);
      };

      context.foam.INTERFACE = function(m) {
        m.class = m.class || 'foam.core.InterfaceModel',
        context.foam.CLASS(m);
      };

      context.foam.CLASS = function(m) {
        promises.push(Promise.all(foam.json.references(self.__context__, m)).then(function(p) {
          var cls = self.lookup(m.class || 'foam.core.Model')
          return dao.put(cls.create(m));
        }))
      };

      context.foam.RELATIONSHIP = function(m) {
        var r = self.Relationship.create(m);
        promises.push(Promise.all([
          self.classloader.load(r.sourceModel),
          self.classloader.load(r.targetModel)
        ]).then(function() {
          return dao.put(r);
        }))
      };

      var self = this;
      var scriptFilesExp = new RegExp(self.unwrappedScripts.join('|'));
      var blacklistExp = new RegExp(self.blacklist.join('|'));

      var fs = require('fs');
      var sep = require('path').sep;
      self.ftw(self.srcDir, function(path, lstat) {
        if ( ! lstat.isFile() ) return;
        if ( blacklistExp.exec(path) ) return;

        var o = fs.readFileSync(path, 'utf-8');

        if ( scriptFilesExp.exec(path) ) {
          // Remove extension and append 'Script' to name.
          var n = path.split(sep).pop().replace(/\.js$/, '') + 'Script';
          with ( context ) {
            foam.SCRIPT({
              package: 'foam.core',
              name: n,
              code: new Function(o),
            });
          }
        } else if ( path.endsWith('.js') ) {
          try {
            with ( context ) { eval(o) };
          } catch(e) {
            console.log(e);
          }
        } else if ( path.endsWith('.java') ) {
          // TODO: Add a JavaClass model for manually written java classes.
          // Parse the package/name from the .java and then store them in the DAO as
          // well. Also do the same for swift files.
        }
      });

      return Promise.all(promises);
    },
    function ftw(dir, fn) {
      var fs = require('fs');
      var sep = require('path').sep;
      var dirs = [this.srcDir];
      while ( dirs.length ) {
        var dir = dirs.pop();
        fs.readdirSync(dir).forEach(function(f) {
          var path = dir + sep + f;
          var lstat = fs.lstatSync(path);
          if ( lstat.isDirectory() ) dirs.push(path)
          fn(path, lstat)
        })
      }
    },
    function execute() {
      this.select().then(function(a) {
        console.log(a.array.map(function(o) { return o.id }).join('\n'));
      });
    },
  ],
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.build',
  name: 'DirCrawlModelDAO',
  extends: 'foam.dao.PromisedDAO',
  requires: [
    'foam.build.Lib',
    'foam.core.Script',
    'foam.dao.EasyDAO',
    'foam.dao.Relationship',
  ],
  properties: [
    {
      name: 'unwrappedScripts',
      value: [
        'src/foam/core/stdlib\\.js',
        'src/foam/core/events\\.js',
        'src/foam/core/Boot\\.js',
        'src/foam/core/FObject\\.js',
        'src/foam/core/Model\\.js',
        'src/foam/core/Boolean\\.js',
        'src/foam/core/AxiomArray\\.js',
        'src/foam/core/EndBoot\\.js',
      ],
    },
    {
      name: 'blacklist',
      value: [
        // Boot files that cannot be wrapped in foam.SCRIPT
        'src/foam\\.js',
        'src/foam/core/poly\\.js',
        'src/foam/core/lib\\.js',

        // Not models.
        'src/foam/nanos/nanos\\.js',
        'src/files\\.js',

        // Dirs we don't care about.
        'src/com/*',
        'src/apps/*',

        // Test files.
        'src/lib/dao_test\\.js',
        'src/foam/box/node/forkScript\\.js',
      ],
    },
    {
      name: 'delegate',
      factory: function() { return this.EasyDAO.create({daoType: 'MDAO', of: 'foam.core.Model'}); },
    },
    {
      name: 'promise',
      factory: function() {
        var self = this;
        return self.fillDAO(self.delegate).then(function() {
          return self.delegate;
        });
      },
    },
    {
      name: 'srcDir',
      adapt: function(_, n) {
        return n.replace(/\/$/, '');
      },
      factory: function() {
        return global.FOAM_ROOT;
      },
    },
  ],
  methods: [
    function fillDAO(dao) {
      var self = this;

      var promises = [];

      var context = {
        foam: Object.create(foam)
      };

      context.foam.LIB = function(m) {
        if ( ! m.id ) {
          m.id = 'lib.Lib' + Date.now()
        }
        promises.push(dao.put(self.Lib.create({
          id: m.id,
          json: m,
        })));
      };

      context.foam.SCRIPT = function(m) {
        promises.push(dao.put(self.Script.create(m)));
      };

      context.foam.ENUM = function(m) {
        m.class = m.class || 'foam.core.EnumModel',
        context.foam.CLASS(m);
      };

      context.foam.INTERFACE = function(m) {
        m.class = m.class || 'foam.core.InterfaceModel',
        context.foam.CLASS(m);
      };

      context.foam.CLASS = function(m) {
        promises.push(Promise.all(foam.json.references(self.__context__, m)).then(function(p) {
          var cls = self.lookup(m.class || 'foam.core.Model')
          return dao.put(cls.create(m));
        }))
      };

      context.foam.RELATIONSHIP = function(m) {
        var r = self.Relationship.create(m);
        return dao.put(r);
      };

      var self = this;
      var scriptFilesExp = new RegExp(self.unwrappedScripts.join('|'));
      var blacklistExp = new RegExp(self.blacklist.join('|'));

      var fs = require('fs');
      var sep = require('path').sep;
      self.ftw(self.srcDir, function(path, lstat) {
        if ( ! lstat.isFile() ) return;
        if ( blacklistExp.exec(path) ) return;

        var o = fs.readFileSync(path, 'utf-8');

        if ( scriptFilesExp.exec(path) ) {
          // Remove extension and append 'Script' to name.
          var n = path.split(sep).pop().replace(/\.js$/, '') + 'Script';
          with ( context ) {
            foam.SCRIPT({
              package: 'foam.core',
              name: n,
              code: new Function(o),
            });
          }
        } else if ( path.endsWith('.js') ) {
          try {
            with ( context ) { eval(o) };
          } catch(e) {
            console.log(e);
          }
        } else if ( path.endsWith('.java') ) {
          // TODO: Add a JavaClass model for manually written java classes.
          // Parse the package/name from the .java and then store them in the DAO as
          // well. Also do the same for swift files.
        }
      });

      return Promise.all(promises);
    },
    function ftw(dir, fn) {
      var fs = require('fs');
      var sep = require('path').sep;
      var dirs = [this.srcDir];
      while ( dirs.length ) {
        var dir = dirs.pop();
        fs.readdirSync(dir).forEach(function(f) {
          var path = dir + sep + f;
          var lstat = fs.lstatSync(path);
          if ( lstat.isDirectory() ) dirs.push(path)
          fn(path, lstat)
        })
      }
    },
    function execute() {
      this.select().then(function(a) {
        console.log(a.array.map(function(o) { return o.id }).join('\n'));
      });
    },
  ],
});
<commit_msg>Use UID instead of Date.now for lib IDs.<commit_after>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.build',
  name: 'DirCrawlModelDAO',
  extends: 'foam.dao.PromisedDAO',
  requires: [
    'foam.build.Lib',
    'foam.core.Script',
    'foam.dao.EasyDAO',
    'foam.dao.Relationship',
  ],
  properties: [
    {
      name: 'unwrappedScripts',
      value: [
        'src/foam/core/stdlib\\.js',
        'src/foam/core/events\\.js',
        'src/foam/core/Boot\\.js',
        'src/foam/core/FObject\\.js',
        'src/foam/core/Model\\.js',
        'src/foam/core/Boolean\\.js',
        'src/foam/core/AxiomArray\\.js',
        'src/foam/core/EndBoot\\.js',
      ],
    },
    {
      name: 'blacklist',
      value: [
        // Boot files that cannot be wrapped in foam.SCRIPT
        'src/foam\\.js',
        'src/foam/core/poly\\.js',
        'src/foam/core/lib\\.js',

        // Not models.
        'src/foam/nanos/nanos\\.js',
        'src/files\\.js',

        // Dirs we don't care about.
        'src/com/*',
        'src/apps/*',

        // Test files.
        'src/lib/dao_test\\.js',
        'src/foam/box/node/forkScript\\.js',
      ],
    },
    {
      name: 'delegate',
      factory: function() { return this.EasyDAO.create({daoType: 'MDAO', of: 'foam.core.Model'}); },
    },
    {
      name: 'promise',
      factory: function() {
        var self = this;
        return self.fillDAO(self.delegate).then(function() {
          return self.delegate;
        });
      },
    },
    {
      name: 'srcDir',
      adapt: function(_, n) {
        return n.replace(/\/$/, '');
      },
      factory: function() {
        return global.FOAM_ROOT;
      },
    },
  ],
  methods: [
    function fillDAO(dao) {
      var self = this;

      var promises = [];

      var context = {
        foam: Object.create(foam)
      };

      context.foam.LIB = function(m) {
        if ( ! m.id ) {
          m.id = 'lib.Lib' + foam.next$UID()
        }
        promises.push(dao.put(self.Lib.create({
          id: m.id,
          json: m,
        })));
      };

      context.foam.SCRIPT = function(m) {
        promises.push(dao.put(self.Script.create(m)));
      };

      context.foam.ENUM = function(m) {
        m.class = m.class || 'foam.core.EnumModel',
        context.foam.CLASS(m);
      };

      context.foam.INTERFACE = function(m) {
        m.class = m.class || 'foam.core.InterfaceModel',
        context.foam.CLASS(m);
      };

      context.foam.CLASS = function(m) {
        promises.push(Promise.all(foam.json.references(self.__context__, m)).then(function(p) {
          var cls = self.lookup(m.class || 'foam.core.Model')
          return dao.put(cls.create(m));
        }))
      };

      context.foam.RELATIONSHIP = function(m) {
        var r = self.Relationship.create(m);
        return dao.put(r);
      };

      var self = this;
      var scriptFilesExp = new RegExp(self.unwrappedScripts.join('|'));
      var blacklistExp = new RegExp(self.blacklist.join('|'));

      var fs = require('fs');
      var sep = require('path').sep;
      self.ftw(self.srcDir, function(path, lstat) {
        if ( ! lstat.isFile() ) return;
        if ( blacklistExp.exec(path) ) return;

        var o = fs.readFileSync(path, 'utf-8');

        if ( scriptFilesExp.exec(path) ) {
          // Remove extension and append 'Script' to name.
          var n = path.split(sep).pop().replace(/\.js$/, '') + 'Script';
          with ( context ) {
            foam.SCRIPT({
              package: 'foam.core',
              name: n,
              code: new Function(o),
            });
          }
        } else if ( path.endsWith('.js') ) {
          try {
            with ( context ) { eval(o) };
          } catch(e) {
            console.log(e);
          }
        } else if ( path.endsWith('.java') ) {
          // TODO: Add a JavaClass model for manually written java classes.
          // Parse the package/name from the .java and then store them in the DAO as
          // well. Also do the same for swift files.
        }
      });

      return Promise.all(promises);
    },
    function ftw(dir, fn) {
      var fs = require('fs');
      var sep = require('path').sep;
      var dirs = [this.srcDir];
      while ( dirs.length ) {
        var dir = dirs.pop();
        fs.readdirSync(dir).forEach(function(f) {
          var path = dir + sep + f;
          var lstat = fs.lstatSync(path);
          if ( lstat.isDirectory() ) dirs.push(path)
          fn(path, lstat)
        })
      }
    },
    function execute() {
      this.select().then(function(a) {
        console.log(a.array.map(function(o) { return o.id }).join('\n'));
      });
    },
  ],
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  refines: 'foam.core.Argument',
  flags: ['swift'],
  requires: [
    'foam.swift.Argument',
  ],
  properties: [
    {
      class: 'String',
      name: 'swiftLocalName',
      expression: function(name) { return name; },
    },
    {
      class: 'String',
      name: 'swiftExternalName',
      value: '_',
    },
    {
      class: 'String',
      name: 'swiftDefaultValue',
    },
    {
      class: 'StringArray',
      name: 'swiftAnnotations',
    },
    {
      class: 'Boolean',
      name: 'swiftMutable',
    },
    {
      class: 'String',
      name: 'swiftType',
      expression: function(of, optional) {
        of = foam.String.isInstance(of) ? foam.lookup(of, true) : of;
        return of ? of.model_.swiftName + (optional ? '?' : '') : 'Any?';
      },
    },
  ],
  methods: [
    function toSwiftArg() {
      var arg = this.Argument.create({
        localName: this.swiftLocalName,
        externalName: this.swiftExternalName,
        type: this.swiftType,
        annotations: this.swiftAnnotations,
        mutable: this.swiftMutable,
      });
      if (this.swiftDefaultValue) arg.defaultValue = this.swiftDefaultValue;
      return arg;
    },
  ]
});
<commit_msg>Resolve merge conflict.<commit_after>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  refines: 'foam.core.Argument',
  flags: ['swift'],
  requires: [
    'foam.swift.Argument',
  ],
  properties: [
    {
      class: 'String',
      name: 'swiftLocalName',
      expression: function(name) { return name; },
    },
    {
      class: 'String',
      name: 'swiftExternalName',
      value: '_',
    },
    {
      class: 'String',
      name: 'swiftDefaultValue',
    },
    {
      class: 'StringArray',
      name: 'swiftAnnotations',
      preSet: function(_, n) {
        var i = n.indexOf('inout');
        if ( i != -1 ) {
          console.log('Warning: found "inout" in swiftAnnotations. Set swiftMutable instead.');
          n.splice(i, 1);
          this.swiftMutable = true;
        }
        return n;
      },
    },
    {
      class: 'Boolean',
      name: 'swiftMutable',
      expression: function(swiftAnnotations) {
        return swiftAnnotations.indexOf('inout') != -1;
      },
    },
    {
      class: 'String',
      name: 'swiftType',
      expression: function(of, optional) {
        of = foam.String.isInstance(of) ? foam.lookup(of, true) : of;
        return of ? of.model_.swiftName + (optional ? '?' : '') : 'Any?';
      },
    },
  ],
  methods: [
    function toSwiftArg() {
      var arg = this.Argument.create({
        localName: this.swiftLocalName,
        externalName: this.swiftExternalName,
        type: this.swiftType,
        annotations: this.swiftAnnotations,
        mutable: this.swiftMutable,
      });
      if (this.swiftDefaultValue) arg.defaultValue = this.swiftDefaultValue;
      return arg;
    },
  ]
});
<|endoftext|>


<commit_before>
import { StyleSheet } from 'glamor/lib/sheet'

export const sheet = new StyleSheet()

sheet.inject()

let count = 0
export const cache = {}

export const reset = () => {
  for (let key in cache) {
    delete cache[key]
  }
  sheet.flush()
  count = 0
}

export const css = () => sheet.rules()
  .map(rule => rule.cssText)
  .join('')

export const cxs = (obj) => {
  const classNames = parse(obj)

  return classNames.join(' ')
}

const parse = (obj, media, pseudo = '') => {
  const classNames = []

  for (let key in obj) {
    const value = obj[key]
    const type = typeof value

    if (type === 'string' || type === 'number') {
      classNames.push(createStyle(key, value, media, pseudo))
      continue
    }

    if (Array.isArray(value)) {
      value.forEach(val => {
        classNames.push(createStyle(key, val, media, pseudo))
      })
    }

    if (/^:/.test(key)) {
      parse(value, media, pseudo + key)
        .forEach(s => classNames.push(s))
      continue
    }
    if (/^@media/.test(key)) {
      parse(value, key, pseudo)
        .forEach(s => classNames.push(s))
      continue
    }
  }

  return classNames
}

const createStyle = (key, value, media, pseudo = '') => {
  const id = key + value + (media || '') + pseudo
  const dupe = cache[id]

  if (dupe) return dupe

  const className = hash(count)
  count++
  const selector = '.' + className + pseudo
  const prop = hyphenate(key)
  const val = addPx(key, value)
  const rule = `${selector}{${prop}:${val}}`
  const css = media ? `${media}{${rule}}` : rule

  sheet.insert(css)
  cache[id] = className

  return className
}

export const hyphenate = (str) =>
  str.replace(/[A-Z]/g, '-$&')
  .toLowerCase()

const unitlessProps = [
  'animationIterationCount',
  'boxFlex',
  'boxFlexGroup',
  'boxOrdinalGroup',
  'columnCount',
  'flex',
  'flexGrow',
  'flexPositive',
  'flexShrink',
  'flexNegative',
  'flexOrder',
  'gridRow',
  'gridColumn',
  'fontWeight',
  'lineClamp',
  'lineHeight',
  'opacity',
  'order',
  'orphans',
  'tabSize',
  'widows',
  'zIndex',
  'zoom',
  'fillOpacity',
  'stopOpacity',
  'strokeDashoffset',
  'strokeOpacity',
  'strokeWidth'
]

export const addPx = (prop, value) => {
  if (typeof value !== 'number') return value
  if (unitlessProps.indexOf(prop) > -1) return value
  return value + 'px'
}

const isNum = (str) => {
  const num = parseInt(str)
  return typeof num === 'number' && !isNaN(num)
}

export const hash = (n) => {
  if (alpha[n]) return alpha[n]

  let residual = Math.floor(n)
  let result = ''
  const length = alpha.length

  while (residual !== 0) {
    const i = residual % length
    result = alpha[i] + result
    residual = Math.floor(residual / length)
  }

  return result
}

const alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')

cxs.css = css
cxs.reset = reset

export default cxs

<commit_msg>Test perf<commit_after>
import { StyleSheet } from 'glamor/lib/sheet'

const PSEUDO_REGEX = /^:/
const MEDIA_REGEX = /^@media/

export const sheet = new StyleSheet()

sheet.inject()

let count = 0

export const cache = {}

export const reset = () => {
  for (let key in cache) {
    delete cache[key]
  }
  sheet.flush()
  count = 0
}

export const css = () => sheet.rules()
  .map(rule => rule.cssText)
  .join('')

export const cxs = (obj) => {
  const classNames = parse(obj)

  return classNames.join(' ')
}

const parse = (obj, media, pseudo = '') => {
  const classNames = []

  for (let key in obj) {
    const value = obj[key]
    const type = typeof value

    if (type === 'string' || type === 'number') {
      classNames.push(createStyle(key, value, media, pseudo))
      continue
    }

    if (Array.isArray(value)) {
      value.forEach(val => {
        classNames.push(createStyle(key, val, media, pseudo))
      })
      continue
    }

    const firstChar = key.charAt(0)

    if (firstChar === ':') {
    // if (PSEUDO_REGEX.test(key)) {
      parse(value, media, pseudo + key)
        .forEach(s => classNames.push(s))
      continue
    }

    if (firstChar === '@') {
    // if (MEDIA_REGEX.test(key)) {
      parse(value, key, pseudo)
        .forEach(s => classNames.push(s))
      continue
    }
  }

  return classNames
}

const createStyle = (key, value, media, pseudo = '') => {
  const id = key + value + (media || '') + pseudo
  const dupe = cache[id]

  if (dupe) return dupe

  const className = hash(count)
  count++
  const selector = '.' + className + pseudo
  const prop = hyphenate(key)
  const val = addPx(key, value)
  const rule = `${selector}{${prop}:${val}}`
  const css = media ? `${media}{${rule}}` : rule

  sheet.insert(css)
  cache[id] = className

  return className
}

export const hyphenate = (str) =>
  str.replace(/[A-Z]/g, '-$&')
  .toLowerCase()

const unitlessProps = [
  'animationIterationCount',
  'boxFlex',
  'boxFlexGroup',
  'boxOrdinalGroup',
  'columnCount',
  'flex',
  'flexGrow',
  'flexPositive',
  'flexShrink',
  'flexNegative',
  'flexOrder',
  'gridRow',
  'gridColumn',
  'fontWeight',
  'lineClamp',
  'lineHeight',
  'opacity',
  'order',
  'orphans',
  'tabSize',
  'widows',
  'zIndex',
  'zoom',
  'fillOpacity',
  'stopOpacity',
  'strokeDashoffset',
  'strokeOpacity',
  'strokeWidth'
]

export const addPx = (prop, value) => {
  if (typeof value !== 'number') return value
  if (unitlessProps.indexOf(prop) > -1) return value
  return value + 'px'
}

const isNum = (str) => {
  const num = parseInt(str)
  return typeof num === 'number' && !isNaN(num)
}

export const hash = (n) => {
  if (alpha[n]) return alpha[n]

  let residual = Math.floor(n)
  let result = ''
  const length = alpha.length

  while (residual !== 0) {
    const i = residual % length
    result = alpha[i] + result
    residual = Math.floor(residual / length)
  }

  return result
}

const alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')

cxs.css = css
cxs.reset = reset

export default cxs

<|endoftext|>


<commit_before>
import promise from 'empty-promise'

const handshake = JSON.stringify({
  command: 'hello',
  protocols: [
    'http://livereload.com/protocols/official-7',
    'http://livereload.com/protocols/official-8',
    'http://livereload.com/protocols/2.x-origin-version-negotiation',
    'https://livereload.com/protocols/official-7',
    'https://livereload.com/protocols/official-8',
    'https://livereload.com/protocols/2.x-origin-version-negotiation',
  ],
})

// Instead of throwing. Let's provide some defaults that
// give consumer dev some feedback about missing dependency
// without completely failing.
const unimplemented = {
  onOpen: ()=> console.log('Missing onOpen listener.'),

  onClose: ()=> console.log('Missing onClose listener.'),

  onError: e=> {
    console.log('Error thrown, but no onError to pass it to.')
    console.error(e)
  },

  messenger: {
    message: msg=> console.log(
      'Missing messenger. \nmessage:',
      msg),
  },

  // This dependency is special. Can't even try to do
  // anything without it. So throw instead of warn.
  WebSocket: function (){
    throw 'Missing WebSocket constructor.'
  },

}
/**
 * Factory provider of a LiveReload client implentation
 *
 * @param {Number}    port        port for server
 *
 * @param {String}    host        hostname for server
 *
 * @param {Function}  onOpen      Callback after connection is
 *                                opened.
 *
 * @param {Function}  onClose     Callback after connection is
 *                                closed
 *
 * @param {Function}  onError     Callback after any error.
 *                                This module tries to suppress
 *                                thrown errors, and instead
 *                                passes them to onError.
 *
 * @param {Function} WebSocket   An implentation of WebSocket
 *
 * @param {Object}    messenger   An object with a `message`
 *                                method, which accepts an
 *                                object parameter.
 *
 * @return {Object}               API object with 2 methods:
 *                                `connect` and `disconnect`
 */
function makeLivereloadSocket ({
  // Seems reasonable to use LiveReload's default
  // port on localhost if none provided.
  port = 35729,
  host = 'localhost',
  onOpen = unimplemented.onOpen,
  onClose = unimplemented.onClose,
  onError = unimplemented.onError,
  messenger = unimplemented.messenger,
  WebSocket = unimplemented.WebSocket,
}) {

  let socket // insert active socket here ;)

  // Initiate connection and add listeners
  // Publicly exposed API method.
  //
  function connect() {
    // nothing to do if already connected
    if ( socket && socket.readyState === 1) return

    try{
      socket = new WebSocket(
        `ws://${host}:${port}/livereload`)
    }
    catch(e) { return void onError(e) }

    socket.onmessage = hello
    socket.onerror = onError
    socket.onclose = onSocketClose
    socket.onopen = async ()=> {
      const fullyOpened = await yesItsReallyOpen()
      if(fullyOpened) socket.send(handshake)
      else socket.close()
    }

    return socket
  }

  // Close the socket
  // Publicly exposed API method
  //
  function disconnect() {
    if (!socket || !socket.close)
      return void onError(
        new Error('Invalid Disconnect: Cannot close a '
          + 'socket that has never been opened.')
      )
    socket.close()
    return socket
  }

  // Turns out a websocket can emit `open` before it's
  // really open 😡. Let's wait until it's open-state is
  // fully resolved.
  //
  function yesItsReallyOpen(){
    const isOpen = promise()

    const poll = ()=> {
      if(socket.readyState === 1)
        isOpen.resolve(true)

      else setTimeout(poll, 100)
    }

    // Race against a timeout. 8 seconds is long time to
    // wait for this. But hey, why not?
    setTimeout(() => {isOpen.reject(
      new Error('Timeout: WebSocket half-open too long.'))
    }, 8000),

    poll()
    return isOpen
      .catch(e => { onError(e); return false })
  }

  // Try to parse JSON and pass any error to consumer
  //
  function parseMsg(evt) {
    let msg
    try { msg = JSON.parse(evt.data) }
    catch (e) { onError(
      new Error('Invalid JSON from LiveReload server.')
    )}
    return msg
  }

  // Must receive hello from server before other messages.
  //
  function hello(evt) {
    const msg = parseMsg(evt)
    const helloError =
      new Error('Invalid "hello" from server')

    if (!msg || msg.command !== 'hello') {
      onError(helloError)
      return void socket.close()
    }

    listen()
  }

  // Switch to listening for arbitrary incoming messages.
  //
  function listen() {
    socket.onmessage = evt=> {
      const msg = parseMsg(evt)
      if (!msg)
        return void socket.close()

      if (msg.command === 'reload')
        messenger.message(msg)
    }
    // Finally reached state consumer would view as open
    onOpen() // consumer provided callback
  }

  // Let consumer know we're closed for business.
  // Some light cleanup might help garbage collection.
  //
  function onSocketClose() {
    // some light cleanup
    ['onclose', 'onopen', 'onerror', 'onmessage']
      .forEach(listener=>socket[listener] = void 0)
    socket = void 0

    onClose() // consumer provided callback
  }

  return { connect, disconnect }
}

export default makeLivereloadSocket
<commit_msg>rm onSocketDisconnect()<commit_after>
import promise from 'empty-promise'

const handshake = JSON.stringify({
  command: 'hello',
  protocols: [
    'http://livereload.com/protocols/official-7',
    'http://livereload.com/protocols/official-8',
    'http://livereload.com/protocols/2.x-origin-version-negotiation',
    'https://livereload.com/protocols/official-7',
    'https://livereload.com/protocols/official-8',
    'https://livereload.com/protocols/2.x-origin-version-negotiation',
  ],
})

// Instead of throwing. Let's provide some defaults that
// give consumer dev some feedback about missing dependency
// without completely failing.
const unimplemented = {
  onOpen: ()=> console.log('Missing onOpen listener.'),

  onClose: ()=> console.log('Missing onClose listener.'),

  onError: e=> {
    console.log('Error thrown, but no onError to pass it to.')
    console.error(e)
  },

  messenger: {
    message: msg=> console.log(
      'Missing messenger. \nmessage:',
      msg),
  },

  // This dependency is special. Can't even try to do
  // anything without it. So throw instead of warn.
  WebSocket: function (){
    throw 'Missing WebSocket constructor.'
  },

}
/**
 * Factory provider of a LiveReload client implentation
 *
 * @param {Number}    port        port for server
 *
 * @param {String}    host        hostname for server
 *
 * @param {Function}  onOpen      Callback after connection is
 *                                opened.
 *
 * @param {Function}  onClose     Callback after connection is
 *                                closed
 *
 * @param {Function}  onError     Callback after any error.
 *                                This module tries to suppress
 *                                thrown errors, and instead
 *                                passes them to onError.
 *
 * @param {Function} WebSocket   An implentation of WebSocket
 *
 * @param {Object}    messenger   An object with a `message`
 *                                method, which accepts an
 *                                object parameter.
 *
 * @return {Object}               API object with 2 methods:
 *                                `connect` and `disconnect`
 */
function makeLivereloadSocket ({
  // Seems reasonable to use LiveReload's default
  // port on localhost if none provided.
  port = 35729,
  host = 'localhost',
  onOpen = unimplemented.onOpen,
  onClose = unimplemented.onClose,
  onError = unimplemented.onError,
  messenger = unimplemented.messenger,
  WebSocket = unimplemented.WebSocket,
}) {

  let socket // insert active socket here ;)

  // Initiate connection and add listeners
  // Publicly exposed API method.
  //
  function connect() {
    // nothing to do if already connected
    if ( socket && socket.readyState === 1) return

    try{
      socket = new WebSocket(
        `ws://${host}:${port}/livereload`)
    }
    catch(e) { return void onError(e) }

    socket.onmessage = hello
    socket.onerror = onError // consumer provided callback
    socket.onclose = onClose // consumer provided callback
    socket.onopen = async ()=> {
      const fullyOpened = await yesItsReallyOpen()
      if(fullyOpened) socket.send(handshake)
      else socket.close()
    }

    return socket
  }

  // Close the socket
  // Publicly exposed API method
  //
  function disconnect() {
    if (!socket || !socket.close)
      return void onError(
        new Error('Invalid Disconnect: Cannot close a '
          + 'socket that has never been opened.')
      )
    socket.close()
    return socket
  }

  // Turns out a websocket can emit `open` before it's
  // really open 😡. Let's wait until it's open-state is
  // fully resolved.
  //
  function yesItsReallyOpen(){
    const isOpen = promise()

    const poll = ()=> {
      if(socket.readyState === 1)
        isOpen.resolve(true)

      else setTimeout(poll, 100)
    }

    // Race against a timeout. 8 seconds is long time to
    // wait for this. But hey, why not?
    setTimeout(() => {isOpen.reject(
      new Error('Timeout: WebSocket half-open too long.'))
    }, 8000),

    poll()
    return isOpen
      .catch(e => { onError(e); return false })
  }

  // Try to parse JSON and pass any error to consumer
  //
  function parseMsg(evt) {
    let msg
    try { msg = JSON.parse(evt.data) }
    catch (e) { onError(
      new Error('Invalid JSON from LiveReload server.')
    )}
    return msg
  }

  // Must receive hello from server before other messages.
  //
  function hello(evt) {
    const msg = parseMsg(evt)
    const helloError =
      new Error('Invalid "hello" from server')

    if (!msg || msg.command !== 'hello') {
      onError(helloError)
      return void socket.close()
    }

    listen()
  }

  // Switch to listening for arbitrary incoming messages.
  //
  function listen() {
    socket.onmessage = evt=> {
      const msg = parseMsg(evt)
      if (!msg)
        return void socket.close()

      if (msg.command === 'reload')
        messenger.message(msg)
    }
    // Finally reached state consumer would view as open
    onOpen() // consumer provided callback
  }

  return { connect, disconnect }
}

export default makeLivereloadSocket
<|endoftext|>