<commit_before>//@flow
/* eslint-disable no-param-reassign */

import { htmlHelpers } from 'utils';
import template from '../templates/grid.hbs';
import ListView from './CollectionView';
import RowView from './RowView';
import SelectionPanelView from './SelectionPanelView';
import SelectionCellView from './SelectionCellView';
import GridHeaderView from './GridHeaderView';
import NoColumnsDefaultView from './NoColumnsView';
import LoadingChildView from './LoadingRowView';
import ToolbarView from '../../components/toolbar/ToolbarView';
import MobileService from '../../services/MobileService';
import LoadingBehavior from '../../views/behaviors/LoadingBehavior';
import SearchBarView from '../../views/SearchBarView';

/*
    Public interface:

    This view produce:
        trigger: positionChanged (sender, { oldPosition, position })
        trigger: viewportHeightChanged (sender, { oldViewportHeight, viewportHeight })
    This view react on:
        collection change (via Backbone.Collection events)
        position change (when we scroll with scrollbar for example): updatePosition(newPosition)
 */

/**
 * @name GridView
 * @memberof module:core.list.views
 * @class GridView
 * @constructor
 * @description View-контейнер для заголовка и контента
 * @extends Marionette.View
 * @param {Object} options Constructor options
 * @param {Array} options.collection массив элементов списка
 * @param {Array} options.columns массив колонок
 * @param {Backbone.View} options.emptyView View для отображения пустого списка (нет строк)
 * @param {Number} options.childHeight высота строки списка (childView)
 * @param {Backbone.View} [options.childView] view строки списка
 * @param {Backbone.View} [options.childViewOptions] опции для childView
 * @param {Function} options.childViewSelector ?
 * @param {Object} [options.emptyViewOptions] опции для emptyView
 * @param {Backbone.View} options.gridColumnHeaderView View заголовка списка
 * @param {String} options.height задает как определяется высота строки, значения: fixed, auto
 * @param {Backbone.View} [options.loadingChildView] view-лоадер, показывается при подгрузке строк
 * @param {Backbone.View} options.noColumnsView View для отображения списка без колонок
 * @param {Object} [options.noColumnsViewOptions] опции для noColumnsView
 * @param {Number} options.maxRows максимальное количество отображаемых строк (используется с опцией height: auto)
 * @param {Boolean} options.useDefaultRowView использовать RowView по умолчанию.
 * В случае, если true — обязательно должны быть указаны cellView для каждой колонки
 * @param {Boolean} options.forbidSelection запретить выделять элементы списка при помощи мыши
 * */

export default Marionette.View.extend({
    initialize(options) {
        if (this.collection === undefined) {
            throw new Error('You must provide a collection to display.');
        }

        if (options.columns === undefined) {
            throw new Error('You must provide columns definition ("columns" option)');
        }

        options.onColumnSort && (this.onColumnSort = options.onColumnSort); //jshint ignore:line

        this.uniqueId = _.uniqueId('native-grid');
        this.styleSheet = document.createElement('style');

        const HeaderView = this.options.headerView || GridHeaderView;

        this.columnClasses = [];
        options.columns.forEach((c, i) => {
            const cClass = `${this.uniqueId}-column${i}`;

            this.columnClasses.push(cClass);
            c.columnClass = cClass;
        });

        if (this.options.showHeader !== false) {
            this.options.showHeader = true;
        }

        if (this.options.showHeader) {
            this.headerView = new HeaderView({
                columns: options.columns,
                gridEventAggregator: this,
                checkBoxPadding: options.checkBoxPadding || 0,
                gridColumnHeaderView: options.gridColumnHeaderView,
                uniqueId: this.uniqueId,
                isTree: this.options.isTree,
                expandOnShow: options.expandOnShow
            });

            this.listenTo(this.headerView, 'onColumnSort', this.onColumnSort, this);
            this.listenTo(this.headerView, 'update:width', this.__setColumnWidth);
        }

        if (options.noColumnsView) {
            this.noColumnsView = options.noColumnsView;
        } else {
            this.noColumnsView = NoColumnsDefaultView;
        }
        options.noColumnsViewOptions && (this.noColumnsViewOptions = options.noColumnsViewOptions); // jshint ignore:line

        if (!options.draggable) {
            this.forbidSelection = typeof options.forbidSelection === 'boolean' ? options.forbidSelection : true;
        }
        const childView = options.childView || RowView;

        const showRowIndex = this.getOption('showRowIndex');

        const childViewOptions = Object.assign(options.childViewOptions || {}, {
            columns: options.columns,
            gridEventAggregator: this,
            columnClasses: this.columnClasses,
            isTree: this.options.isTree
        });

        this.isEditable = _.isBoolean(options.editable) ? options.editable : options.columns.some(column => column.editable);
        if (this.isEditable) {
            this.editableCellsIndexes = [];
            this.options.columns.forEach((column, index) => {
                if (column.editable) {
                    this.editableCellsIndexes.push(index);
                }
            });
            this.listenTo(this.collection, 'move:left', () => this.__onCursorMove(-1));
            this.listenTo(this.collection, 'move:right', () => this.__onCursorMove(+1));
            this.listenTo(this.collection, 'select:some select:one', () => this.__onCursorMove(0));
        }

        this.listView = new ListView({
            collection: this.collection,
            gridEventAggregator: this,
            childView,
            childViewSelector: options.childViewSelector,
            emptyView: options.emptyView,
            emptyViewOptions: options.emptyViewOptions,
            noColumnsView: options.noColumnsView,
            noColumnsViewOptions: options.noColumnsViewOptions,
            childHeight: options.childHeight,
            childViewOptions,
            loadingChildView: options.loadingChildView || LoadingChildView,
            maxRows: options.maxRows,
            height: options.height,
            forbidSelection: this.forbidSelection,
            isTree: this.options.isTree,
            isEditable: this.isEditable,
            showRowIndex
        });

        if (this.options.showCheckbox) {
            const draggable = this.getOption('draggable');
            this.selectionPanelView = new SelectionPanelView({
                collection: this.listView.collection,
                gridEventAggregator: this,
                showRowIndex: this.options.showRowIndex,
                childViewOptions: {
                    draggable,
                    showRowIndex,
                    bindSelection: this.getOption('bindSelection')
                }
            });

            this.selectionHeaderView = new SelectionCellView({
                collection: this.collection,
                selectionType: 'all',
                gridEventAggregator: this,
                showRowIndex
            });

            if (draggable) {
                this.listenTo(this.selectionPanelView, 'childview:drag:drop', (...args) => this.trigger('drag:drop', ...args));
                this.listenTo(this.selectionHeaderView, 'drag:drop', (...args) => this.trigger('drag:drop', ...args));
            }
        }

        this.listenTo(this.listView, 'all', (eventName, eventArguments) => {
            if (eventName.startsWith('childview')) {
                this.trigger.apply(this, [eventName].concat(eventArguments));
            }
        });

        if (this.collection.length) {
            //this.__presortCollection(options.columns); TODO WFT
        }
        this.collection = options.collection;

        if (options.showToolbar) {
            this.toolbarView = new ToolbarView({
                allItemsCollection: options.actions || new Backbone.Collection()
            });
            this.listenTo(this.toolbarView, 'command:execute', this.__executeAction);
        }
        if (options.showSearch) {
            this.searchView = new SearchBarView();
            this.listenTo(this.searchView, 'search', this.__onSearch);
        }
    },

    __onCursorMove(delta) {
        const currentSelectedIndex = this.editableCellsIndexes.indexOf(this.pointedCell);
        const newPosition = Math.min(this.editableCellsIndexes.length - 1, Math.max(0, currentSelectedIndex + delta));
        const newSelectedIndex = this.editableCellsIndexes[newPosition];
        this.pointedCell = newSelectedIndex;
        const pointed = this.collection.find(model => model.cid === this.collection.cursorCid);
        if (pointed) {
            pointed.trigger('select:pointed', this.pointedCell);
        }
    },

    onColumnSort(column, comparator) {
        this.collection.comparator = comparator;
        this.collection.sort();
    },

    regions: {
        headerRegion: '.js-grid-header-view',
        contentRegion: '.js-grid-content-view',
        selectionPanelRegion: '.js-grid-selection-panel-view',
        selectionHeaderRegion: '.js-grid-selection-header-view',
        noColumnsViewRegion: '.js-nocolumns-view-region',
        toolbarRegion: '.js-grid-tools-toolbar-region',
        searchRegion: '.js-grid-tools-search-region',
        loadingRegion: '.js-loading-region'
    },

    ui: {
        title: '.js-grid-title',
        tools: '.js-grid-tools',
        header: '.js-grid-header',
        content: '.js-grid-content'
    },

    events: {
        dragleave: '__handleDragLeave'
    },

    className() {
        return `${this.options.class || ''} fr-collection`;
    },

    template: Handlebars.compile(template),

    behaviors: {
        LoadingBehavior: {
            behaviorClass: LoadingBehavior,
            region: 'loadingRegion'
        }
    },

    onRender() {
        if (this.options.columns.length === 0) {
            const noColumnsView = new this.noColumnsView(this.noColumnsViewOptions);
            this.noColumnsViewRegion.show(noColumnsView);
        }

        this.showChildView('contentRegion', this.listView);

        if (this.options.showHeader) {
            this.showChildView('headerRegion', this.headerView);
        } else {
            this.el.classList.add('grid__headless');
        }

        if (this.options.showCheckbox) {
            if (this.options.showHeader) {
                this.showChildView('selectionHeaderRegion', this.selectionHeaderView);
            }
            this.showChildView('selectionPanelRegion', this.selectionPanelView);
            if (this.getOption('showRowIndex')) {
                this.getRegion('selectionHeaderRegion').el.classList.add('cell_selection-index');
            }
        }

        if (this.options.showToolbar) {
            this.showChildView('toolbarRegion', this.toolbarView);
        }
        if (this.options.showSearch) {
            this.showChildView('searchRegion', this.searchView);
        }
        if (!(this.options.showToolbar || this.options.showSearch)) {
            this.ui.tools.hide();
        }

        if (this.getOption('title')) {
            this.ui.title.text(this.getOption('title') || '');
        } else {
            this.ui.title.hide();
        }
        this.updatePosition = this.listView.updatePosition.bind(this.listView.collectionView);
    },

    onAttach() {
        if (this.forbidSelection) {
            htmlHelpers.forbidSelection(this.el);
        }
        this.options.columns.forEach((column, i) => {
            this.__setColumnWidth(i, column.width);
        });
        document.body && document.body.appendChild(this.styleSheet);
        this.__bindListRegionScroll();
        if (this.options.showSearch) {
            this.searchView.focus();
        }
        this.ui.content.css('maxHeight', window.innerHeight);
        // if (this.collection.visibleLength) {
        //     this.collection.select(this.collection.at(0), false, false, false);
        // }
    },

    __executeAction(model) {
        this.trigger('execute:action', model);
    },

    __onSearch(text) {
        this.trigger('search', text);
        if (this.options.isTree) {
            this.trigger('toggle:collapse:all', !text && !this.options.expandOnShow);
        }
    },

    __bindListRegionScroll() {
        const headerRegionEl = this.options.showHeader && this.getRegion('headerRegion').el;
        const selectionPanelRegionEl = this.options.showCheckbox && this.getRegion('selectionPanelRegion').el;

        this.getRegion('contentRegion').el.addEventListener('scroll', event => {
            if (headerRegionEl) {
                headerRegionEl.scrollLeft = event.currentTarget.scrollLeft;
            }
            if (selectionPanelRegionEl) {
                selectionPanelRegionEl.scrollTop = event.currentTarget.scrollTop;
            }
        });
    },

    onDestroy() {
        this.styleSheet && document.body && document.body.removeChild(this.styleSheet);
    },

    sortBy(columnIndex, sorting) {
        const column = this.options.columns[columnIndex];
        if (sorting) {
            this.options.columns.forEach(c => (c.sorting = null));
            column.sorting = sorting;

            switch (sorting) {
                case 'asc':
                    this.collection.comparator = column.sortAsc;
                    break;
                case 'desc':
                    this.collection.comparator = column.sortDesc;
                    break;
                default:
                    break;
            }
        } else {
            sorting = column.sorting;
            this.options.columns.forEach(c => (c.sorting = null));

            switch (sorting) {
                case 'asc':
                    column.sorting = 'desc';
                    this.collection.comparator = column.sortDesc;
                    break;
                case 'desc':
                    column.sorting = 'asc';
                    this.collection.comparator = column.sortAsc;
                    break;
                default:
                    column.sorting = 'asc';
                    this.collection.comparator = column.sortAsc;
                    break;
            }
        }
        this.onColumnSort(column, this.collection.comparator);
        if (this.options.showHeader) {
            this.headerView.updateSorting();
        }
    },

    handleResize() {
        if (this.options.showHeader) {
            this.headerView.handleResize();
        }
    },

    setLoading(state) {
        if (!this.isDestroyed()) {
            this.loading.setLoading(state);
        }
    },

    __presortCollection(columns) {
        const sortingColumn = columns.find(column => column.sorting);
        if (sortingColumn) {
            if (sortingColumn.sorting === 'asc') {
                this.onColumnSort(sortingColumn, sortingColumn.sortAsc);
            } else {
                this.onColumnSort(sortingColumn, sortingColumn.sortDesc);
            }
        }
    },

    __handleDragLeave(e) {
        if (!this.el.contains(e.relatedTarget)) {
            if (this.collection.dragoverModel) {
                this.collection.dragoverModel.trigger('dragleave');
            } else {
                this.collection.trigger('dragleave:head');
            }
            this.collection.dragoverModel = null;
        }
    },

    __setColumnWidth(index, width = 0) {
        const style = this.styleSheet;
        const columnClass = this.columnClasses[index];
        const regexp = new RegExp(`.${columnClass} { flex: [0,1] 0 [+, -]?\\S+\\.?\\S*; } `);
        let basis;
        let widthCell = '';
        if (width > 0) {
            if (width < 1) {
                basis = `${width * 100}%`;
                widthCell = `max-width: ${width * 100}%`;
            } else {
                basis = `${width}px`;
                widthCell = `max-width: ${width}px`;
            }
        } else {
            basis = '0%';
        }

        const grow = width > 0 ? 0 : 1;
        const newValue = `.${columnClass} { flex: ${grow} 0 ${basis}; } `;

        if (MobileService.isIE) {
            if (widthCell) {
                const regexpCells = new RegExp(`.cell.${columnClass} { max-width: [0-9]*\\.?[0-9]*[%,px;]* } `);
                const newCellValue = `.cell.${columnClass} { ${widthCell}; } `;
                if (regexpCells.test(style.innerHTML)) {
                    style.innerHTML = style.innerHTML.replace(regexpCells, newCellValue);
                } else {
                    style.innerHTML += newCellValue;
                }
            }
        }

        if (regexp.test(style.innerHTML)) {
            style.innerHTML = style.innerHTML.replace(regexp, newValue);
        } else {
            style.innerHTML += newValue;
        }
    }
});
<commit_msg>add childHeight property for collection<commit_after>//@flow
/* eslint-disable no-param-reassign */

import { htmlHelpers } from 'utils';
import template from '../templates/grid.hbs';
import ListView from './CollectionView';
import RowView from './RowView';
import SelectionPanelView from './SelectionPanelView';
import SelectionCellView from './SelectionCellView';
import GridHeaderView from './GridHeaderView';
import NoColumnsDefaultView from './NoColumnsView';
import LoadingChildView from './LoadingRowView';
import ToolbarView from '../../components/toolbar/ToolbarView';
import MobileService from '../../services/MobileService';
import LoadingBehavior from '../../views/behaviors/LoadingBehavior';
import SearchBarView from '../../views/SearchBarView';

/*
    Public interface:

    This view produce:
        trigger: positionChanged (sender, { oldPosition, position })
        trigger: viewportHeightChanged (sender, { oldViewportHeight, viewportHeight })
    This view react on:
        collection change (via Backbone.Collection events)
        position change (when we scroll with scrollbar for example): updatePosition(newPosition)
 */

/**
 * @name GridView
 * @memberof module:core.list.views
 * @class GridView
 * @constructor
 * @description View-контейнер для заголовка и контента
 * @extends Marionette.View
 * @param {Object} options Constructor options
 * @param {Array} options.collection массив элементов списка
 * @param {Array} options.columns массив колонок
 * @param {Backbone.View} options.emptyView View для отображения пустого списка (нет строк)
 * @param {Number} options.childHeight высота строки списка (childView)
 * @param {Number} options.maxHeight максимальная высота всего списка
 * @param {Backbone.View} [options.childView] view строки списка
 * @param {Backbone.View} [options.childViewOptions] опции для childView
 * @param {Function} options.childViewSelector ?
 * @param {Object} [options.emptyViewOptions] опции для emptyView
 * @param {Backbone.View} options.gridColumnHeaderView View заголовка списка
 * @param {String} options.height задает как определяется высота строки, значения: fixed, auto
 * @param {Backbone.View} [options.loadingChildView] view-лоадер, показывается при подгрузке строк
 * @param {Backbone.View} options.noColumnsView View для отображения списка без колонок
 * @param {Object} [options.noColumnsViewOptions] опции для noColumnsView
 * @param {Number} options.maxRows максимальное количество отображаемых строк (используется с опцией height: auto)
 * @param {Boolean} options.useDefaultRowView использовать RowView по умолчанию.
 * В случае, если true — обязательно должны быть указаны cellView для каждой колонки
 * @param {Boolean} options.forbidSelection запретить выделять элементы списка при помощи мыши
 * */

export default Marionette.View.extend({
    initialize(options) {
        if (this.collection === undefined) {
            throw new Error('You must provide a collection to display.');
        }

        if (options.columns === undefined) {
            throw new Error('You must provide columns definition ("columns" option)');
        }

        options.onColumnSort && (this.onColumnSort = options.onColumnSort); //jshint ignore:line

        this.uniqueId = _.uniqueId('native-grid');
        this.styleSheet = document.createElement('style');

        const HeaderView = this.options.headerView || GridHeaderView;

        this.columnClasses = [];
        options.columns.forEach((c, i) => {
            const cClass = `${this.uniqueId}-column${i}`;

            this.columnClasses.push(cClass);
            c.columnClass = cClass;
        });

        if (this.options.showHeader !== false) {
            this.options.showHeader = true;
        }

        if (this.options.showHeader) {
            this.headerView = new HeaderView({
                columns: options.columns,
                gridEventAggregator: this,
                checkBoxPadding: options.checkBoxPadding || 0,
                gridColumnHeaderView: options.gridColumnHeaderView,
                uniqueId: this.uniqueId,
                isTree: this.options.isTree,
                expandOnShow: options.expandOnShow
            });

            this.listenTo(this.headerView, 'onColumnSort', this.onColumnSort, this);
            this.listenTo(this.headerView, 'update:width', this.__setColumnWidth);
        }

        if (options.noColumnsView) {
            this.noColumnsView = options.noColumnsView;
        } else {
            this.noColumnsView = NoColumnsDefaultView;
        }
        options.noColumnsViewOptions && (this.noColumnsViewOptions = options.noColumnsViewOptions); // jshint ignore:line

        if (!options.draggable) {
            this.forbidSelection = typeof options.forbidSelection === 'boolean' ? options.forbidSelection : true;
        }
        const childView = options.childView || RowView;

        const showRowIndex = this.getOption('showRowIndex');

        const childViewOptions = Object.assign(options.childViewOptions || {}, {
            columns: options.columns,
            gridEventAggregator: this,
            columnClasses: this.columnClasses,
            isTree: this.options.isTree
        });

        this.isEditable = _.isBoolean(options.editable) ? options.editable : options.columns.some(column => column.editable);
        if (this.isEditable) {
            this.editableCellsIndexes = [];
            this.options.columns.forEach((column, index) => {
                if (column.editable) {
                    this.editableCellsIndexes.push(index);
                }
            });
            this.listenTo(this.collection, 'move:left', () => this.__onCursorMove(-1));
            this.listenTo(this.collection, 'move:right', () => this.__onCursorMove(+1));
            this.listenTo(this.collection, 'select:some select:one', () => this.__onCursorMove(0));
        }

        this.listView = new ListView({
            collection: this.collection,
            gridEventAggregator: this,
            childView,
            childViewSelector: options.childViewSelector,
            emptyView: options.emptyView,
            emptyViewOptions: options.emptyViewOptions,
            noColumnsView: options.noColumnsView,
            noColumnsViewOptions: options.noColumnsViewOptions,
            childHeight: options.childHeight,
            childViewOptions,
            loadingChildView: options.loadingChildView || LoadingChildView,
            maxRows: options.maxRows,
            height: options.height,
            forbidSelection: this.forbidSelection,
            isTree: this.options.isTree,
            isEditable: this.isEditable,
            showRowIndex
        });

        if (this.options.showCheckbox) {
            const draggable = this.getOption('draggable');
            this.selectionPanelView = new SelectionPanelView({
                collection: this.listView.collection,
                gridEventAggregator: this,
                showRowIndex: this.options.showRowIndex,
                childViewOptions: {
                    draggable,
                    showRowIndex,
                    bindSelection: this.getOption('bindSelection')
                }
            });

            this.selectionHeaderView = new SelectionCellView({
                collection: this.collection,
                selectionType: 'all',
                gridEventAggregator: this,
                showRowIndex
            });

            if (draggable) {
                this.listenTo(this.selectionPanelView, 'childview:drag:drop', (...args) => this.trigger('drag:drop', ...args));
                this.listenTo(this.selectionHeaderView, 'drag:drop', (...args) => this.trigger('drag:drop', ...args));
            }
        }

        this.listenTo(this.listView, 'all', (eventName, eventArguments) => {
            if (eventName.startsWith('childview')) {
                this.trigger.apply(this, [eventName].concat(eventArguments));
            }
        });

        if (this.collection.length) {
            //this.__presortCollection(options.columns); TODO WFT
        }
        this.collection = options.collection;

        if (options.showToolbar) {
            this.toolbarView = new ToolbarView({
                allItemsCollection: options.actions || new Backbone.Collection()
            });
            this.listenTo(this.toolbarView, 'command:execute', this.__executeAction);
        }
        if (options.showSearch) {
            this.searchView = new SearchBarView();
            this.listenTo(this.searchView, 'search', this.__onSearch);
        }
    },

    __onCursorMove(delta) {
        const currentSelectedIndex = this.editableCellsIndexes.indexOf(this.pointedCell);
        const newPosition = Math.min(this.editableCellsIndexes.length - 1, Math.max(0, currentSelectedIndex + delta));
        const newSelectedIndex = this.editableCellsIndexes[newPosition];
        this.pointedCell = newSelectedIndex;
        const pointed = this.collection.find(model => model.cid === this.collection.cursorCid);
        if (pointed) {
            pointed.trigger('select:pointed', this.pointedCell);
        }
    },

    onColumnSort(column, comparator) {
        this.collection.comparator = comparator;
        this.collection.sort();
    },

    regions: {
        headerRegion: '.js-grid-header-view',
        contentRegion: '.js-grid-content-view',
        selectionPanelRegion: '.js-grid-selection-panel-view',
        selectionHeaderRegion: '.js-grid-selection-header-view',
        noColumnsViewRegion: '.js-nocolumns-view-region',
        toolbarRegion: '.js-grid-tools-toolbar-region',
        searchRegion: '.js-grid-tools-search-region',
        loadingRegion: '.js-loading-region'
    },

    ui: {
        title: '.js-grid-title',
        tools: '.js-grid-tools',
        header: '.js-grid-header',
        content: '.js-grid-content'
    },

    events: {
        dragleave: '__handleDragLeave'
    },

    className() {
        return `${this.options.class || ''} fr-collection`;
    },

    template: Handlebars.compile(template),

    behaviors: {
        LoadingBehavior: {
            behaviorClass: LoadingBehavior,
            region: 'loadingRegion'
        }
    },

    onRender() {
        if (this.options.columns.length === 0) {
            const noColumnsView = new this.noColumnsView(this.noColumnsViewOptions);
            this.noColumnsViewRegion.show(noColumnsView);
        }

        this.showChildView('contentRegion', this.listView);

        if (this.options.showHeader) {
            this.showChildView('headerRegion', this.headerView);
        } else {
            this.el.classList.add('grid__headless');
        }

        if (this.options.showCheckbox) {
            if (this.options.showHeader) {
                this.showChildView('selectionHeaderRegion', this.selectionHeaderView);
            }
            this.showChildView('selectionPanelRegion', this.selectionPanelView);
            if (this.getOption('showRowIndex')) {
                this.getRegion('selectionHeaderRegion').el.classList.add('cell_selection-index');
            }
        }

        if (this.options.showToolbar) {
            this.showChildView('toolbarRegion', this.toolbarView);
        }
        if (this.options.showSearch) {
            this.showChildView('searchRegion', this.searchView);
        }
        if (!(this.options.showToolbar || this.options.showSearch)) {
            this.ui.tools.hide();
        }

        if (this.getOption('title')) {
            this.ui.title.text(this.getOption('title') || '');
        } else {
            this.ui.title.hide();
        }
        this.updatePosition = this.listView.updatePosition.bind(this.listView.collectionView);
    },

    onAttach() {
        if (this.forbidSelection) {
            htmlHelpers.forbidSelection(this.el);
        }
        this.options.columns.forEach((column, i) => {
            this.__setColumnWidth(i, column.width);
        });
        document.body && document.body.appendChild(this.styleSheet);
        this.__bindListRegionScroll();
        if (this.options.showSearch) {
            this.searchView.focus();
        }
        this.ui.content.css('maxHeight', this.options.maxHeight || window.innerHeight);
        // if (this.collection.visibleLength) {
        //     this.collection.select(this.collection.at(0), false, false, false);
        // }
    },

    __executeAction(model) {
        this.trigger('execute:action', model);
    },

    __onSearch(text) {
        this.trigger('search', text);
        if (this.options.isTree) {
            this.trigger('toggle:collapse:all', !text && !this.options.expandOnShow);
        }
    },

    __bindListRegionScroll() {
        const headerRegionEl = this.options.showHeader && this.getRegion('headerRegion').el;
        const selectionPanelRegionEl = this.options.showCheckbox && this.getRegion('selectionPanelRegion').el;

        this.getRegion('contentRegion').el.addEventListener('scroll', event => {
            if (headerRegionEl) {
                headerRegionEl.scrollLeft = event.currentTarget.scrollLeft;
            }
            if (selectionPanelRegionEl) {
                selectionPanelRegionEl.scrollTop = event.currentTarget.scrollTop;
            }
        });
    },

    onDestroy() {
        this.styleSheet && document.body && document.body.removeChild(this.styleSheet);
    },

    sortBy(columnIndex, sorting) {
        const column = this.options.columns[columnIndex];
        if (sorting) {
            this.options.columns.forEach(c => (c.sorting = null));
            column.sorting = sorting;

            switch (sorting) {
                case 'asc':
                    this.collection.comparator = column.sortAsc;
                    break;
                case 'desc':
                    this.collection.comparator = column.sortDesc;
                    break;
                default:
                    break;
            }
        } else {
            sorting = column.sorting;
            this.options.columns.forEach(c => (c.sorting = null));

            switch (sorting) {
                case 'asc':
                    column.sorting = 'desc';
                    this.collection.comparator = column.sortDesc;
                    break;
                case 'desc':
                    column.sorting = 'asc';
                    this.collection.comparator = column.sortAsc;
                    break;
                default:
                    column.sorting = 'asc';
                    this.collection.comparator = column.sortAsc;
                    break;
            }
        }
        this.onColumnSort(column, this.collection.comparator);
        if (this.options.showHeader) {
            this.headerView.updateSorting();
        }
    },

    handleResize() {
        if (this.options.showHeader) {
            this.headerView.handleResize();
        }
    },

    setLoading(state) {
        if (!this.isDestroyed()) {
            this.loading.setLoading(state);
        }
    },

    __presortCollection(columns) {
        const sortingColumn = columns.find(column => column.sorting);
        if (sortingColumn) {
            if (sortingColumn.sorting === 'asc') {
                this.onColumnSort(sortingColumn, sortingColumn.sortAsc);
            } else {
                this.onColumnSort(sortingColumn, sortingColumn.sortDesc);
            }
        }
    },

    __handleDragLeave(e) {
        if (!this.el.contains(e.relatedTarget)) {
            if (this.collection.dragoverModel) {
                this.collection.dragoverModel.trigger('dragleave');
            } else {
                this.collection.trigger('dragleave:head');
            }
            this.collection.dragoverModel = null;
        }
    },

    __setColumnWidth(index, width = 0) {
        const style = this.styleSheet;
        const columnClass = this.columnClasses[index];
        const regexp = new RegExp(`.${columnClass} { flex: [0,1] 0 [+, -]?\\S+\\.?\\S*; } `);
        let basis;
        let widthCell = '';
        if (width > 0) {
            if (width < 1) {
                basis = `${width * 100}%`;
                widthCell = `max-width: ${width * 100}%`;
            } else {
                basis = `${width}px`;
                widthCell = `max-width: ${width}px`;
            }
        } else {
            basis = '0%';
        }

        const grow = width > 0 ? 0 : 1;
        const newValue = `.${columnClass} { flex: ${grow} 0 ${basis}; } `;

        if (MobileService.isIE) {
            if (widthCell) {
                const regexpCells = new RegExp(`.cell.${columnClass} { max-width: [0-9]*\\.?[0-9]*[%,px;]* } `);
                const newCellValue = `.cell.${columnClass} { ${widthCell}; } `;
                if (regexpCells.test(style.innerHTML)) {
                    style.innerHTML = style.innerHTML.replace(regexpCells, newCellValue);
                } else {
                    style.innerHTML += newCellValue;
                }
            }
        }

        if (regexp.test(style.innerHTML)) {
            style.innerHTML = style.innerHTML.replace(regexp, newValue);
        } else {
            style.innerHTML += newValue;
        }
    }
});
<|endoftext|>


<commit_before>/**
 * Widget directives should be used in layout fines to define the
 * specific type of widget to be used in that space
 */
(function () {
    'use strict';

    angular
        .module(HygieiaConfig.module + '.core')

        // used by widgets to set their current state
        // CONFIGURE will render the common config screen instead of the widget content
        .constant('WidgetState', {
            READY: 1,
            CONFIGURE: 2,
            WAITING: 3,
            NOT_COLLECTING: 4
        })

        // constant to be used by widgets to set their state
        // ERROR: causes the widget's panel to use the 'panel-danger' class
        .constant('DisplayState', {
            DEFAULT: 1,
            ERROR: 2
        })
        .directive('widget', widgetDirective);

    widgetDirective.$inject = ['$controller', '$http', '$templateCache', '$compile', 'widgetManager', '$uibModal', 'WidgetState', 'DisplayState', '$interval', 'dashboardData','userService', 'scoreDataService'];
    function widgetDirective($controller, $http, $templateCache, $compile, widgetManager, $uibModal, WidgetState, DisplayState, $interval, dashboardData, userService, scoreDataService) {
        return {
            templateUrl: 'app/dashboard/views/widget.html',
            require: '^widgetContainer',
            restrict: 'E',
            controller: controller,
            scope: {
                widget: '=',
                title: '@widgetTitle'
            },
            link: link
        };

        function link(scope, element, attrs, containerController) {
            // make it so name is not case sensitive
            attrs.name = attrs.name.toLowerCase();

            scope.$widgetEl = element;
            scope.container = containerController;
            scope.widgetDefinition = widgetManager.getWidget(attrs.name);
            scope.title = attrs.title || scope.widgetDefinition.view.defaults.title;
            scope.header = attrs.header ? attrs.header != 'false' : true;

            // when the widget loads, register it with the container which will then call back to process
            // the widget with the proper config value if it's already been configured on the dashboard
            containerController.registerWidget({
                callback: processWidget,
                element: element,
                attrs: attrs
            });

            // determine what state it's in based on the passed config and load accordingly
            function processWidget(configFromApi, widgetConfig, dashboard) {
                // make sure widget has access to dashboard and config
                scope.dashboard = dashboard;
                scope.widgetConfig = widgetConfig;

                // when the widget registers and sets a 'getState' method use that
                // instead of the default logic to determine whether the widget should be loaded
                if (scope.widgetDefinition.getState) {
                    scope.state = scope.widgetDefinition.getState(widgetConfig);
                }
                else if (!configFromApi) {
                    if (scope.widgetDefinition.config) {
                        scope.state = WidgetState.CONFIGURE;
                    }
                }

                scope.init();
            }
        }

        function controller($scope, $element) {
            $scope.widget_state = WidgetState;
            $scope.display_state = DisplayState;

            // default variables
            $scope.title = '';
            $scope.state = WidgetState.READY;
            $scope.display = DisplayState.DEFAULT;

            // to be set by link
            $scope.widgetConfig = null;
            $scope.widgetDefinition = null;
            $scope.dashboard = null;
            $scope.container = null;
            $scope.owner = null;

            $scope.alerts = [];

            $scope.upsertWidget = upsertWidget;
            $scope.closeAlert = function(index) {
                $scope.alerts.splice(index, 1);
            };

            $scope.lastUpdatedDisplay = '';
            $scope.collectorItems = null;
            $scope.collectionError = false;

            // public methods
            $scope.configModal = configModal;
            $scope.hasPermission = hasPermission;
            $scope.setState = setState;
            $scope.init = init;
            $scope.getWidgetScore = getWidgetScore;

            // method implementations
            function configModal() {
                // load up a modal in the context of the settings defined in
                // the config property when the widget was registered
                var modalConfig = angular.extend({
                	controllerAs: 'ctrl',
                    resolve: {
                        modalData: function () {
                            return {
                                dashboard: $scope.dashboard,
                                widgetConfig: $scope.widgetConfig
                            };
                        }
                    }
                }, $scope.widgetDefinition.config);

                // when the widget closes if an object is passed we'll assume it's an updated
                // widget configuration so try and send it to the api or update the existing one
                $uibModal.open(modalConfig).result.then(upsertWidget);
            }

            function hasPermission() {
            	var dashboard = $scope.dashboard;

            	return userService.hasDashboardConfigPermission(dashboard.owner, dashboard.owners);
            }

            function upsertWidget(newWidgetConfig) {
                if (newWidgetConfig) {
                    // use existing values if they're not defined
                    angular.extend($scope.widgetConfig, newWidgetConfig);

                    // support single value or array values for collectorItemId
                    if ($scope.widgetConfig.collectorItemId) {
                        $scope.widgetConfig.collectorItemIds = [$scope.widgetConfig.collectorItemId];
                        delete $scope.widgetConfig.collectorItemId;
                    }

                    dashboardData
                        .upsertWidget($scope.dashboard.id, $scope.widgetConfig)
                        .then(function (response) {
                            // response comes back with two properties, a widget and a component
                            // we need to update the component on the dashboard so that when the
                            // widget loads it will be able to get to the collector data. we
                            // then need to update the widget configuration stored on the container

                            // add or update the widget from the response.
                            // required when a new widget id is created
                            if(response.widget !== null && typeof response.widget === 'object') {
                                angular.extend($scope.widgetConfig, response.widget);
                            }

                            // save the widget locally
                            $scope.container.upsertWidget($scope.widgetConfig);
                            $scope.container.upsertComponent(response.component);

                            // TODO: should probably call back to the widget's getState method
                            $scope.state = WidgetState.READY;

                            init();
                        });
                }
            }

            function getWidgetScore() {
                return scoreDataService.getScoreByDashboardWidget($scope.dashboard.id, $scope.widgetConfig.id);
            }

            // redraws the widget which forces it to go through the entire flow
            // TODO: this method causes the screen to flash and should probably just render and replace content
            function init() {
                stopInterval();

                // don't request if widget is not in the read state
                if ($scope.state !== WidgetState.READY) {
                    return;
                }

                // grab values from the registered configuration
                var templateUrl = $scope.widgetDefinition.view.templateUrl;
                var controllerName = $scope.widgetDefinition.view.controller;
                var controllerAs = $scope.widgetDefinition.view.controllerAs || 'ctrl';

                // create the widget's controller based on config values
                $scope.widgetViewController = $controller(controllerName + ' as ' + controllerAs, {
                    $scope: $scope
                });

                if(!$scope.widgetViewController.load) {
                    throw new Error(controllerName + ' must define a load method');
                }

                // load the widget with content from the given template url
                $http.get(templateUrl, {cache: $templateCache})
                    .then(function (response) {
                        //TODO: widget implementation should actually start this up after all the data is loaded
                        startInterval();

                        // request the content and add it to the placeholder
                        var $contentEl = angular.element($scope.$widgetEl[0].querySelector('.widget-body-main'));
                        $contentEl.html(response.data);
                        $contentEl.children().data('$ngControllerController', $scope.widgetViewCtrl);
                        $compile($contentEl.contents())($scope);

                        // Ask the widget to update itself
                        refresh();
                    });
            }

            function setState(state) {
                $scope.state = state;
                stopInterval();
            }

            var refreshInterval;

            function startInterval() {
                stopInterval();

                // TODO: make timeout a setting in the widget configuration
                if($scope.widgetViewController && $scope.widgetViewController.load) {
                    refreshInterval = $interval(refresh, HygieiaConfig.refresh * 1000);
                }
            }

            function stopInterval() {
                $interval.cancel(refreshInterval);
            }

            function refresh() {
                var load = $scope.widgetViewController.load();
                if (load && load.then) {
                    load.then(function(result) {
                        var lastUpdated = angular.isArray(result) ? _.max(result) : result;
                        var collectorItems = result.collectorItem;
                        if(typeof lastUpdated === 'object'){
                            lastUpdated = angular.isDefined(lastUpdated.collectorItem[0])?lastUpdated.collectorItem[0].lastUpdated :lastUpdated.lastUpdated;

                        }
                        $scope.lastUpdatedActual = lastUpdated;
                        $scope.lastUpdatedDisplay = moment(lastUpdated).dash('ago');
                        $scope.collectorItems = collectorItems;
                        if (collectorItems) {
                            for (var i = 0; (i < collectorItems.length) && !$scope.collectionError ; i++ ) {
                                $scope.collectionError = collectorItems[i].errors.length > 0;
                            }
                        }
                    });
                }
            }

            // prevent intervals from continuing to be called when changing pages
            $scope.$on('$routeChangeStart', stopInterval);
        }
    }
})();
<commit_msg>ensure widgets stop collecting on destroy rather than just on route change (#3160)<commit_after>/**
 * Widget directives should be used in layout fines to define the
 * specific type of widget to be used in that space
 */
(function () {
    'use strict';

    angular
        .module(HygieiaConfig.module + '.core')

        // used by widgets to set their current state
        // CONFIGURE will render the common config screen instead of the widget content
        .constant('WidgetState', {
            READY: 1,
            CONFIGURE: 2,
            WAITING: 3,
            NOT_COLLECTING: 4
        })

        // constant to be used by widgets to set their state
        // ERROR: causes the widget's panel to use the 'panel-danger' class
        .constant('DisplayState', {
            DEFAULT: 1,
            ERROR: 2
        })
        .directive('widget', widgetDirective);

    widgetDirective.$inject = ['$controller', '$http', '$templateCache', '$compile', 'widgetManager', '$uibModal', 'WidgetState', 'DisplayState', '$interval', 'dashboardData','userService', 'scoreDataService'];
    function widgetDirective($controller, $http, $templateCache, $compile, widgetManager, $uibModal, WidgetState, DisplayState, $interval, dashboardData, userService, scoreDataService) {
        return {
            templateUrl: 'app/dashboard/views/widget.html',
            require: '^widgetContainer',
            restrict: 'E',
            controller: controller,
            scope: {
                widget: '=',
                title: '@widgetTitle'
            },
            link: link
        };

        function link(scope, element, attrs, containerController) {
            // make it so name is not case sensitive
            attrs.name = attrs.name.toLowerCase();

            scope.$widgetEl = element;
            scope.container = containerController;
            scope.widgetDefinition = widgetManager.getWidget(attrs.name);
            scope.title = attrs.title || scope.widgetDefinition.view.defaults.title;
            scope.header = attrs.header ? attrs.header != 'false' : true;

            // when the widget loads, register it with the container which will then call back to process
            // the widget with the proper config value if it's already been configured on the dashboard
            containerController.registerWidget({
                callback: processWidget,
                element: element,
                attrs: attrs
            });

            // determine what state it's in based on the passed config and load accordingly
            function processWidget(configFromApi, widgetConfig, dashboard) {
                // make sure widget has access to dashboard and config
                scope.dashboard = dashboard;
                scope.widgetConfig = widgetConfig;

                // when the widget registers and sets a 'getState' method use that
                // instead of the default logic to determine whether the widget should be loaded
                if (scope.widgetDefinition.getState) {
                    scope.state = scope.widgetDefinition.getState(widgetConfig);
                }
                else if (!configFromApi) {
                    if (scope.widgetDefinition.config) {
                        scope.state = WidgetState.CONFIGURE;
                    }
                }

                scope.init();
            }
        }

        function controller($scope, $element) {
            $scope.widget_state = WidgetState;
            $scope.display_state = DisplayState;

            // default variables
            $scope.title = '';
            $scope.state = WidgetState.READY;
            $scope.display = DisplayState.DEFAULT;

            // to be set by link
            $scope.widgetConfig = null;
            $scope.widgetDefinition = null;
            $scope.dashboard = null;
            $scope.container = null;
            $scope.owner = null;

            $scope.alerts = [];

            $scope.upsertWidget = upsertWidget;
            $scope.closeAlert = function(index) {
                $scope.alerts.splice(index, 1);
            };

            $scope.lastUpdatedDisplay = '';
            $scope.collectorItems = null;
            $scope.collectionError = false;

            // public methods
            $scope.configModal = configModal;
            $scope.hasPermission = hasPermission;
            $scope.setState = setState;
            $scope.init = init;
            $scope.getWidgetScore = getWidgetScore;

            // method implementations
            function configModal() {
                // load up a modal in the context of the settings defined in
                // the config property when the widget was registered
                var modalConfig = angular.extend({
                	controllerAs: 'ctrl',
                    resolve: {
                        modalData: function () {
                            return {
                                dashboard: $scope.dashboard,
                                widgetConfig: $scope.widgetConfig
                            };
                        }
                    }
                }, $scope.widgetDefinition.config);

                // when the widget closes if an object is passed we'll assume it's an updated
                // widget configuration so try and send it to the api or update the existing one
                $uibModal.open(modalConfig).result.then(upsertWidget);
            }

            function hasPermission() {
            	var dashboard = $scope.dashboard;

            	return userService.hasDashboardConfigPermission(dashboard.owner, dashboard.owners);
            }

            function upsertWidget(newWidgetConfig) {
                if (newWidgetConfig) {
                    // use existing values if they're not defined
                    angular.extend($scope.widgetConfig, newWidgetConfig);

                    // support single value or array values for collectorItemId
                    if ($scope.widgetConfig.collectorItemId) {
                        $scope.widgetConfig.collectorItemIds = [$scope.widgetConfig.collectorItemId];
                        delete $scope.widgetConfig.collectorItemId;
                    }

                    dashboardData
                        .upsertWidget($scope.dashboard.id, $scope.widgetConfig)
                        .then(function (response) {
                            // response comes back with two properties, a widget and a component
                            // we need to update the component on the dashboard so that when the
                            // widget loads it will be able to get to the collector data. we
                            // then need to update the widget configuration stored on the container

                            // add or update the widget from the response.
                            // required when a new widget id is created
                            if(response.widget !== null && typeof response.widget === 'object') {
                                angular.extend($scope.widgetConfig, response.widget);
                            }

                            // save the widget locally
                            $scope.container.upsertWidget($scope.widgetConfig);
                            $scope.container.upsertComponent(response.component);

                            // TODO: should probably call back to the widget's getState method
                            $scope.state = WidgetState.READY;

                            init();
                        });
                }
            }

            function getWidgetScore() {
                return scoreDataService.getScoreByDashboardWidget($scope.dashboard.id, $scope.widgetConfig.id);
            }

            // redraws the widget which forces it to go through the entire flow
            // TODO: this method causes the screen to flash and should probably just render and replace content
            function init() {
                stopInterval();

                // don't request if widget is not in the read state
                if ($scope.state !== WidgetState.READY) {
                    return;
                }

                // grab values from the registered configuration
                var templateUrl = $scope.widgetDefinition.view.templateUrl;
                var controllerName = $scope.widgetDefinition.view.controller;
                var controllerAs = $scope.widgetDefinition.view.controllerAs || 'ctrl';

                // create the widget's controller based on config values
                $scope.widgetViewController = $controller(controllerName + ' as ' + controllerAs, {
                    $scope: $scope
                });

                if(!$scope.widgetViewController.load) {
                    throw new Error(controllerName + ' must define a load method');
                }

                // load the widget with content from the given template url
                $http.get(templateUrl, {cache: $templateCache})
                    .then(function (response) {
                        //TODO: widget implementation should actually start this up after all the data is loaded
                        startInterval();

                        // request the content and add it to the placeholder
                        var $contentEl = angular.element($scope.$widgetEl[0].querySelector('.widget-body-main'));
                        $contentEl.html(response.data);
                        $contentEl.children().data('$ngControllerController', $scope.widgetViewCtrl);
                        $compile($contentEl.contents())($scope);

                        // Ask the widget to update itself
                        refresh();
                    });
            }

            function setState(state) {
                $scope.state = state;
                stopInterval();
            }

            var refreshInterval;

            function startInterval() {
                stopInterval();

                // TODO: make timeout a setting in the widget configuration
                if($scope.widgetViewController && $scope.widgetViewController.load) {
                    refreshInterval = $interval(refresh, HygieiaConfig.refresh * 1000);
                }
            }

            function stopInterval() {
                $interval.cancel(refreshInterval);
            }

            function refresh() {
                var load = $scope.widgetViewController.load();
                if (load && load.then) {
                    load.then(function(result) {
                        var lastUpdated = angular.isArray(result) ? _.max(result) : result;
                        var collectorItems = result.collectorItem;
                        if(typeof lastUpdated === 'object'){
                            lastUpdated = angular.isDefined(lastUpdated.collectorItem[0])?lastUpdated.collectorItem[0].lastUpdated :lastUpdated.lastUpdated;

                        }
                        $scope.lastUpdatedActual = lastUpdated;
                        $scope.lastUpdatedDisplay = moment(lastUpdated).dash('ago');
                        $scope.collectorItems = collectorItems;
                        if (collectorItems) {
                            for (var i = 0; (i < collectorItems.length) && !$scope.collectionError ; i++ ) {
                                $scope.collectionError = collectorItems[i].errors.length > 0;
                            }
                        }
                    });
                }
            }

            // prevent intervals from continuing to be called when changing pages
            $scope.$on('$routeChangeStart', stopInterval);

            $scope.$on("$destroy", stopInterval);
        }
    }
})();
<|endoftext|>


<commit_before>erpnext.updates = [
	["28th December 2012", [
		"Workflow: Added System for Multi-level approval before Submission. \

See video at https://www.youtube.com/watch?v=zuGv59_wJKw.",
		"Stock Level Report: New report to see available and estimated qty of stock",
	]],
	["27th December 2012", [
		"Website: Added auto-generated Contact Us and About Us Pages",
		"Website: Added option to create slideshows and add them to Pages, Products and Product Groups",
	]],
	["25th December 2012", [
		"Stock Balance Report: Inflow, outflow and balance of stock within a defined period",
		"Stock Reports: Added Brand filter on some of the reports"
	]],
	["21st December 2012", [
		"Manufacturing: For Material Transfer against Production Order, \
			fetch quantity pending to be transferred for each item.",
		"Desktop: New Icons and now sortable by dragging."
	]],
	["20th December 2012", [
		"Website: Create Product Search Page, Product Group Page on the website.",
		"Manufacturing: \
			Refactored code, improved usability."
	]],
	["14th December 2012", [
		"Website Module: Major Refactor - removed framework code from website."
	]],
	["12th December 2012", [
		"Attachments: Attachments can be set as URLs or File Uploads. This will help if people want to share documents from Google Docs, Dropbox and other such services (esp for the Product listings on websites).",
		"Global Defaults: Session Expiry can now be set in Global Defaults.",
	]],
	["6th December 2012", [
		"Rename: Cost Center, Item Group, Customer Group, Supplier Type, Territory, Sales Person can now be renamed.",
		"Newsletter: Send newsletter to a list of email addresses.",
	]],
	["5th December 2012", [
		"Leave Application: Now can set approver.",
		"New Roles Added: Leave Approver and Expense Approver.",
		"Production Order: Now linked with Sales Order.",
		"Production Planning Tool: The field 'Allow SA items as raw material' has been renamed to 'Use multi-level BOM', 'Include in plan' column from SO table has been deleted",
		"Batch Numbers: Batch nos are now filtered with Item and available qty at time of selection in transactions.",
		"BOM: 'Update Costing' button has been deleted, once submitted cost are fixed.",
		"[For indian customer only] Deprecated TDS related documents and fields. Old TDS amount added into tax table in Purchase Invoice and entries table in case of JV",
	]],
	["4th December 2012", [
		"POS / Mode of Payment: Select default bank / cash account in Mode of Payment and it will be automatically selected in POS Invoice.",
		"Email: Add contact name as 'Dear so-and-so' in Email.",
		"Report Builder: Remember last column setup for users",
		"Report Builder: Autoset column width (remember)",
	]],
	["3rd December 2012", [
		"Linked With: Added new Linked with in all Forms.",
		"Rename Tool: Documents that can be renamed will have a 'Rename' option in the sidebar (wherever applicable).",
		"Chart of Accounts: Ability to rename / delete from Chart of Accounts.",
		"Delivery and Billing status now updated in sales order, if POS made against that sales order"
	]],
	["30th November 2012", [
		"Auto Notifications: System will prompt user with pre-set message for auto-notification.",
		"Employee: Users with role Employee will only be able to see their Employee Records.",
		"Leave Application: Users with role Employee can now apply for leaves. HR User will be able to set Approval or Rejection.",
	]],
	["29th November 2012", [
		"EMail: Form Emails are now via Communication (with Rich Text Etc.).",
	]],
	["28th November 2012", [
		"Profile: Profile Settings (My Settings...) is now the Profile Form.",
		"Financial Analytics: Show Net Profit/Loss",
	]],
	["27th November 2012", [
		"Communication: Made common communication thread and added it in Lead, Contact.",
	]],
	["26th November 2012", [
		"Email: Added User Signature",
		"Support Ticket: Added link to Lead / Contact. If incoming ticket is not from an existing Lead / Contact, create a new Lead",
	]],
	["24ht November 2012", [
		"Support Ticket: Support Ticket Response is now Communication",
	]],
	["23rd November 2012", [
		"General Ledger: Auto-suggest Accounts for filtering",
		"Calendar: User Interface Fixes, small text for events",
		"Email Settings: Setup outgoing email without a login id \
			(applicable for a local email server)",
		"Delivered Items To Be Billed: New report in 'Accounts'",
	]],
	["22nd November 2012", [
		"Support Ticket: Compose a reply using Markdown",
		"Supplier Link Field: Search by Supplier Name instead of ID",
		"Supplier Link Field: Show only ID in auto-suggest \
			if ID created using Supplier Name (as defined in Global Defaults)",
	]],
	["21st November 2012", [
		"Tree Report: Added missing expand / collapse buttons.",
		"List View: Do not show restricted records, as defined in Permission Manager.",
		"Customer Link Field: Search by Customer Name instead of ID",
		"Customer Link Field: Show only ID in auto-suggest \
			if ID created using Customer Name (as defined in Global Defaults)",
		"Letter Head: Fixed bug causing cursor position to reset in Content",
	]],
	["20th November 2012", [
		"Auto-suggest: Show main label in bold",
		"Data Import Tool: Fixed #Name error faced by MS Excel users in import template",
	]],
	["19th November 2012", [
		"Sales Order: Bugfix - Shipping Address should be a Link field.",
		"Link Fields: Search Profile, Employee and Lead using Full Names instead of ID.",
		"Knowledge Base: Always open links, embedded in an answer, in a new tab."
	]],
	["16th November 2012", [
		"Appraisal: Cleaned up form and logic. Removed complex and unnecessary approval logic, \
			the appraiser can select the template and role and make an appraisal. \
			Normal user can see self created Appraisals. HR Manager can see all Appraisals.",
		"Project: Bugfix in Gantt Chart (caused due to jquery conflict)",
		"Serial No: Ability to rename.",
		"Rename Tool: Added Serial No to rename tool.",
	]],
	["15th November 2012", [
		"Customer Issue: Moved all allocations to 'Assigned' so that there is avoid duplication fo features.",
		"Letter Head: Show preview, make upload button more visible.",
		"Price List: Removed import, now import from Data Import Tool.",
		"Data Import Tool: More help in template.",
	]],
	["14th November 2012", [
		"Employee: If User ID is set, Employee Name will be updated in defaults and will appear automatically in all relevant forms.",
		"Backups: Link to download both database and files.",
	]],
	["13th November 2012", [
		"Customize Form View: Validate correct 'Options' for Link and Table fields.",
		"Report Builder (new): Added formatters for Date, Currency, Links etc.",
		"Trial Balance (new): Feature to export Ledgers or Groups selectively. Indent Groups with spaces.",
		"General Ledger (new): Will show entries with 'Is Opening' as Opening.",
		"General Ledger (new): Show against account entries if filtered by account.",
	]],
	["12th November 2012", [
		"Document Lists: Automatically Refresh lists when opened (again).",
		"Messages: Popups will not be shown (annoying).",
		"Email Digest: New option to get ten latest Open Support Tickets.",
		"Journal Voucher: 'Against JV' will now be filtered by the Account selected.",
		"Query Report: Allow user to rename and save reports.",
		"Employee Leave Balance Report: Bugfix"
	]]
]

wn.pages['latest-updates'].onload = function(wrapper) {
	wn.ui.make_app_page({
		parent: wrapper,
		title: 'Latest Updates',
		single_column: true
	});

	var parent = $(wrapper).find(".layout-main");

	$("
Report issues by sending a mail to support@erpnext.com or \
		via GitHub Issues

").appendTo(parent);

	$.each(erpnext.updates, function(i, day) {
		$("
" + day[0] + "
").appendTo(parent);
		$.each(day[1], function(j, item) {
			$("
").html(item).appendTo(parent);
		})
		$("
").appendTo(parent);
	});
}<commit_msg>updated latest updates page<commit_after>erpnext.updates = [
	["1st January 2013", [
		"Trial Balance: Added an option to see the report without period closing entry\
			You will find this checkbox at the bottom of the report.",
		"General Ledger: Now, on filter of any account group, you will have an option to see \
			the report grouped by ledgers under that group and with opening \
			and closing balance for each of them."
	]]
	["28th December 2012", [
		"Workflow: Added System for Multi-level approval before Submission. \

See video at https://www.youtube.com/watch?v=zuGv59_wJKw.",
		"Stock Level Report: New report to see available and estimated qty of stock",
	]],
	["27th December 2012", [
		"Website: Added auto-generated Contact Us and About Us Pages",
		"Website: Added option to create slideshows and add them to Pages, Products and Product Groups",
	]],
	["25th December 2012", [
		"Stock Balance Report: Inflow, outflow and balance of stock within a defined period",
		"Stock Reports: Added Brand filter on some of the reports"
	]],
	["21st December 2012", [
		"Manufacturing: For Material Transfer against Production Order, \
			fetch quantity pending to be transferred for each item.",
		"Desktop: New Icons and now sortable by dragging."
	]],
	["20th December 2012", [
		"Website: Create Product Search Page, Product Group Page on the website.",
		"Manufacturing: \
			Refactored code, improved usability."
	]],
	["14th December 2012", [
		"Website Module: Major Refactor - removed framework code from website."
	]],
	["12th December 2012", [
		"Attachments: Attachments can be set as URLs or File Uploads. This will help if people want to share documents from Google Docs, Dropbox and other such services (esp for the Product listings on websites).",
		"Global Defaults: Session Expiry can now be set in Global Defaults.",
	]],
	["6th December 2012", [
		"Rename: Cost Center, Item Group, Customer Group, Supplier Type, Territory, Sales Person can now be renamed.",
		"Newsletter: Send newsletter to a list of email addresses.",
	]],
	["5th December 2012", [
		"Leave Application: Now can set approver.",
		"New Roles Added: Leave Approver and Expense Approver.",
		"Production Order: Now linked with Sales Order.",
		"Production Planning Tool: The field 'Allow SA items as raw material' has been renamed to 'Use multi-level BOM', 'Include in plan' column from SO table has been deleted",
		"Batch Numbers: Batch nos are now filtered with Item and available qty at time of selection in transactions.",
		"BOM: 'Update Costing' button has been deleted, once submitted cost are fixed.",
		"[For indian customer only] Deprecated TDS related documents and fields. Old TDS amount added into tax table in Purchase Invoice and entries table in case of JV",
	]],
	["4th December 2012", [
		"POS / Mode of Payment: Select default bank / cash account in Mode of Payment and it will be automatically selected in POS Invoice.",
		"Email: Add contact name as 'Dear so-and-so' in Email.",
		"Report Builder: Remember last column setup for users",
		"Report Builder: Autoset column width (remember)",
	]],
	["3rd December 2012", [
		"Linked With: Added new Linked with in all Forms.",
		"Rename Tool: Documents that can be renamed will have a 'Rename' option in the sidebar (wherever applicable).",
		"Chart of Accounts: Ability to rename / delete from Chart of Accounts.",
		"Delivery and Billing status now updated in sales order, if POS made against that sales order"
	]],
	["30th November 2012", [
		"Auto Notifications: System will prompt user with pre-set message for auto-notification.",
		"Employee: Users with role Employee will only be able to see their Employee Records.",
		"Leave Application: Users with role Employee can now apply for leaves. HR User will be able to set Approval or Rejection.",
	]],
	["29th November 2012", [
		"EMail: Form Emails are now via Communication (with Rich Text Etc.).",
	]],
	["28th November 2012", [
		"Profile: Profile Settings (My Settings...) is now the Profile Form.",
		"Financial Analytics: Show Net Profit/Loss",
	]],
	["27th November 2012", [
		"Communication: Made common communication thread and added it in Lead, Contact.",
	]],
	["26th November 2012", [
		"Email: Added User Signature",
		"Support Ticket: Added link to Lead / Contact. If incoming ticket is not from an existing Lead / Contact, create a new Lead",
	]],
	["24ht November 2012", [
		"Support Ticket: Support Ticket Response is now Communication",
	]],
	["23rd November 2012", [
		"General Ledger: Auto-suggest Accounts for filtering",
		"Calendar: User Interface Fixes, small text for events",
		"Email Settings: Setup outgoing email without a login id \
			(applicable for a local email server)",
		"Delivered Items To Be Billed: New report in 'Accounts'",
	]],
	["22nd November 2012", [
		"Support Ticket: Compose a reply using Markdown",
		"Supplier Link Field: Search by Supplier Name instead of ID",
		"Supplier Link Field: Show only ID in auto-suggest \
			if ID created using Supplier Name (as defined in Global Defaults)",
	]],
	["21st November 2012", [
		"Tree Report: Added missing expand / collapse buttons.",
		"List View: Do not show restricted records, as defined in Permission Manager.",
		"Customer Link Field: Search by Customer Name instead of ID",
		"Customer Link Field: Show only ID in auto-suggest \
			if ID created using Customer Name (as defined in Global Defaults)",
		"Letter Head: Fixed bug causing cursor position to reset in Content",
	]],
	["20th November 2012", [
		"Auto-suggest: Show main label in bold",
		"Data Import Tool: Fixed #Name error faced by MS Excel users in import template",
	]],
	["19th November 2012", [
		"Sales Order: Bugfix - Shipping Address should be a Link field.",
		"Link Fields: Search Profile, Employee and Lead using Full Names instead of ID.",
		"Knowledge Base: Always open links, embedded in an answer, in a new tab."
	]],
	["16th November 2012", [
		"Appraisal: Cleaned up form and logic. Removed complex and unnecessary approval logic, \
			the appraiser can select the template and role and make an appraisal. \
			Normal user can see self created Appraisals. HR Manager can see all Appraisals.",
		"Project: Bugfix in Gantt Chart (caused due to jquery conflict)",
		"Serial No: Ability to rename.",
		"Rename Tool: Added Serial No to rename tool.",
	]],
	["15th November 2012", [
		"Customer Issue: Moved all allocations to 'Assigned' so that there is avoid duplication fo features.",
		"Letter Head: Show preview, make upload button more visible.",
		"Price List: Removed import, now import from Data Import Tool.",
		"Data Import Tool: More help in template.",
	]],
	["14th November 2012", [
		"Employee: If User ID is set, Employee Name will be updated in defaults and will appear automatically in all relevant forms.",
		"Backups: Link to download both database and files.",
	]],
	["13th November 2012", [
		"Customize Form View: Validate correct 'Options' for Link and Table fields.",
		"Report Builder (new): Added formatters for Date, Currency, Links etc.",
		"Trial Balance (new): Feature to export Ledgers or Groups selectively. Indent Groups with spaces.",
		"General Ledger (new): Will show entries with 'Is Opening' as Opening.",
		"General Ledger (new): Show against account entries if filtered by account.",
	]],
	["12th November 2012", [
		"Document Lists: Automatically Refresh lists when opened (again).",
		"Messages: Popups will not be shown (annoying).",
		"Email Digest: New option to get ten latest Open Support Tickets.",
		"Journal Voucher: 'Against JV' will now be filtered by the Account selected.",
		"Query Report: Allow user to rename and save reports.",
		"Employee Leave Balance Report: Bugfix"
	]]
]

wn.pages['latest-updates'].onload = function(wrapper) {
	wn.ui.make_app_page({
		parent: wrapper,
		title: 'Latest Updates',
		single_column: true
	});

	var parent = $(wrapper).find(".layout-main");

	$("
Report issues by sending a mail to support@erpnext.com or \
		via GitHub Issues

").appendTo(parent);

	$.each(erpnext.updates, function(i, day) {
		$("
" + day[0] + "
").appendTo(parent);
		$.each(day[1], function(j, item) {
			$("
").html(item).appendTo(parent);
		})
		$("
").appendTo(parent);
	});
}<|endoftext|>


<commit_before>(function()
{
	Jobber = {

		jobber_url: "",

		FixPng: function()
		{
			var arVersion = navigator.appVersion.split("MSIE");
			var version = parseFloat(arVersion[1]);

			if ((version >= 5.5) && (document.body.filters))
			{
			   for(var i=0; i<document.images.length; i++)
			   {
			      var img = document.images[i];
			      var imgName = img.src.toUpperCase();

			      if (imgName == this.jobber_url.toUpperCase() + "IMG/BT-RSS.PNG")
			      {
			         var imgID = (img.id) ? "id='" + img.id + "' " : "";
			         var imgClass = (img.className) ? "class='" + img.className + "' " : "";
			         var imgTitle = (img.title) ? "title='" + img.title + "' " : "title='" + img.alt + "' ";
			         var imgStyle = "display:inline-block;" + img.style.cssText;
			         if (img.align == "left") imgStyle = "float:left;" + imgStyle;
			         if (img.align == "right") imgStyle = "float:right;" + imgStyle;
			         if (img.parentElement.href) imgStyle = "cursor:hand;" + imgStyle;
			         var strNewHTML = "";
			         img.outerHTML = strNewHTML;
			         i = i - 1;
			      }
			   }
			}

		},

		PerformSearch: function(url, keywords)
		{
			clearTimeout(window.search_timer);
			$('#job-listings').load(url + encodeURIComponent(keywords) + '/');
			$("#indicator").hide();
		},

		HandleLocationOutsideRo: function()
		{
			if (document.getElementById("location_outside_ro").style.display == "none")
			{
				document.getElementById("city_id").setAttribute("disabled", "disabled");
				$("div#location_outside_ro").show();
				document.getElementById("location_outside_ro_where").focus();
				$("a#other_location_label").html(Jobber.I18n.js.location_pick_from_list);
			}
			else
			{
				document.getElementById("city_id").removeAttribute("disabled");
				$("div#location_outside_ro").hide();
				$("a#other_location_label").html(Jobber.I18n.js.location_other);
			}
		},

		SendToFriend: {
			showHide: function()
			{
				$("#send-to-friend").toggle();
			},

			sendMsg: function()
			{
				$("#frm-send-to-friend").ajaxForm(function(responseText) {
					if (responseText == "0")
					{
						var msg = Jobber.I18n.js.send_to_friend_unsuccessful;
						$("#send-to-friend-response").css({ color: "red" });
					}
					else
					{
						var msg = Jobber.I18n.js.send_to_friend_successful;
						$("#frm-send-to-friend").clearForm();
						$("#send-to-friend-response").css({ color: "green" });
					}
					$("#send-to-friend-response").html(msg);
				});
			}
		},

		ReportSpam: function(url, job_id)
		{
			$.ajax({
			  type: "POST",
			  url: url,
			  data: "job_id=" + job_id,
			  success: function(msg) {
			   	if (msg == "0")
					{
						var status = Jobber.I18n.js.report_spam_unsuccessful;
						$("#report-spam-response").css({ color: "red" });
					}
					else
					{
						var status = Jobber.I18n.js.report_spam_successful;
						$("#frm-send-to-friend").clearForm();
						$("#report-spam-response").css({ color: "green" });
					}
					$("#report-spam-response").html(status);
			  }
			});
		}
	}
})();<commit_msg>refactored: rewritten the HandleLocationOutsideRo method to use only jQuery<commit_after>(function()
{
	Jobber = {

		jobber_url: "",

		FixPng: function()
		{
			var arVersion = navigator.appVersion.split("MSIE");
			var version = parseFloat(arVersion[1]);

			if ((version >= 5.5) && (document.body.filters))
			{
			   for(var i=0; i<document.images.length; i++)
			   {
			      var img = document.images[i];
			      var imgName = img.src.toUpperCase();

			      if (imgName == this.jobber_url.toUpperCase() + "IMG/BT-RSS.PNG")
			      {
			         var imgID = (img.id) ? "id='" + img.id + "' " : "";
			         var imgClass = (img.className) ? "class='" + img.className + "' " : "";
			         var imgTitle = (img.title) ? "title='" + img.title + "' " : "title='" + img.alt + "' ";
			         var imgStyle = "display:inline-block;" + img.style.cssText;
			         if (img.align == "left") imgStyle = "float:left;" + imgStyle;
			         if (img.align == "right") imgStyle = "float:right;" + imgStyle;
			         if (img.parentElement.href) imgStyle = "cursor:hand;" + imgStyle;
			         var strNewHTML = "";
			         img.outerHTML = strNewHTML;
			         i = i - 1;
			      }
			   }
			}

		},

		PerformSearch: function(url, keywords)
		{
			clearTimeout(window.search_timer);
			$('#job-listings').load(url + encodeURIComponent(keywords) + '/');
			$("#indicator").hide();
		},

		HandleLocationOutsideRo: function()
		{
			if ($("#location_outside_ro").is(":visible"))
			{
				$("#city_id").removeAttr("disabled");
				$("#location_outside_ro").hide();
				$("#other_location_label").html(Jobber.I18n.js.location_other);
			}
			else
			{
				$("#city_id").attr("disabled", "disabled");
				$("#location_outside_ro").show();
				$("#location_outside_ro_where").focus();
				$("#other_location_label").html(Jobber.I18n.js.location_pick_from_list);
			}
		},

		SendToFriend: {
			showHide: function()
			{
				$("#send-to-friend").toggle();
			},

			sendMsg: function()
			{
				$("#frm-send-to-friend").ajaxForm(function(responseText) {
					if (responseText == "0")
					{
						var msg = Jobber.I18n.js.send_to_friend_unsuccessful;
						$("#send-to-friend-response").css({ color: "red" });
					}
					else
					{
						var msg = Jobber.I18n.js.send_to_friend_successful;
						$("#frm-send-to-friend").clearForm();
						$("#send-to-friend-response").css({ color: "green" });
					}
					$("#send-to-friend-response").html(msg);
				});
			}
		},

		ReportSpam: function(url, job_id)
		{
			$.ajax({
			  type: "POST",
			  url: url,
			  data: "job_id=" + job_id,
			  success: function(msg) {
			   	if (msg == "0")
					{
						var status = Jobber.I18n.js.report_spam_unsuccessful;
						$("#report-spam-response").css({ color: "red" });
					}
					else
					{
						var status = Jobber.I18n.js.report_spam_successful;
						$("#frm-send-to-friend").clearForm();
						$("#report-spam-response").css({ color: "green" });
					}
					$("#report-spam-response").html(status);
			  }
			});
		}
	}
})();<|endoftext|>


<commit_before>function Ant(a_board) {
    this.row(5);
    this.column(5);
    this.direction('up');
    this.board(a_board);
}

accessor(Ant, "row");
accessor(Ant, "column");
accessor(Ant, "direction");
accessor(Ant, "board");

Ant.prototype.step= function() {
    this.board().cell_color(this.row(), this.column(), 'black');
    this.column(6);
    this.direction('right');
};
<commit_msg>Specs pass.<commit_after>function Ant(a_board) {
    this.row(5);
    this.column(5);
    this.direction('up');
    this.board(a_board);
}

accessor(Ant, "row");
accessor(Ant, "column");
accessor(Ant, "direction");
accessor(Ant, "board");

Ant.prototype.step= function() {
    this.board().cell_color(this.row(), this.column(), 'black');
    switch(this.direction()) {
    case 'up':
	this.column(6);
	this.direction('right');
	break;
    case 'left':
	this.row(4);
	this.direction('up');
	break;
    case 'down':
	this.column(4);
	this.direction('left');
	break;
    case 'right':
	this.row(6);
	this.direction('down');
	break;
    }
};
<|endoftext|>