Our blazing fast Grid component built with pure JavaScript


Post by Animal »

Grid's Location.js need to be this:

import DomHelper from '../../Core/helper/DomHelper.js';
import Widget from '../../Core/widget/Widget.js';

/**
 * @module Grid/util/Location
 */

/**
 * This class encapsulates a reference to a specific navigable grid location.
 *
 * This encapsulates a grid cell based upon the record and column, but in addition, it could represent
 * an actionable location *within a cell** if the {@link #property-target} is not the grid cell in
 * question.
 *
 * A Location is immutable. That is, once instantiated, the record and column which it references
 * cannot be changed. The {@link #function-move} method returns a new instance.
 *
 * A `Location` that encapsulates a cell within the body of a grid will have the following
 * read-only properties:
 *
 *  - grid        : `Grid` The Grid that owns the Location.
 *  - record      : `Model` The record of the row that owns the Location. (`null` if the header).
 *  - rowIndex    : `Number` The zero-based index of the row that owns the Location. (-1 means the header).
 *  - column      : `Column` The Column that owns the Location.
 *  - columnIndex : `Number` The zero-based index of the column that owns the Location.
 *  - cell        : `HTMLElement` The referenced cell element.
 *  - target      : `HTMLElement` The focusable element. This may be the cell, or a child of the cell.
 *
 * If the location is a column *header*, the `record` will be `null` and the `rowIndex` will be `-1`.
 *
 */
export default class Location {
    /**
     * The grid which this Location references.
     * @config {Grid.view.Grid} grid
     */
    /**
     * The record which this Location references. (unless {@link #config-rowIndex} is used to configure)
     * @config {Core.data.Model} record
     */
    /**
     *
     * The row index which this Location references. (unless {@link #config-record} is used to configure).
     *
     * `-1` means the header row, in which case the {@link #config-record} will be `null`.
     * @config {Number} rowIndex
     */
    /**
     * The Column which this location references. (unless {@link #config-columnIndex} or {@link #config-columnId} is used to configure)
     * @config {Grid.column.Column} column
     */
    /**
     * The column id which this location references. (unless {@link #config-column} or {@link #config-columnIndex} is used to configure)
     * @config {String|Number} columnId
     */
    /**
     * The column index which this location references. (unless {@link #config-column} or {@link #config-columnId} is used to configure)
     * @config {Number} columnIndex
     */
    /**
     * The field of the column index which this location references. (unless another column identifier is used to configure)
     * @config {String} field
     */

    /**
     * Initializes a new Location.
     * @param {Object|HTMLElement} location A grid location specifier. This may be:
     *  * An element inside a grid cell or a grid cell.
     *  * An object identifying a cell location using the following properties:
     *    * grid
     *    * record
     *    * rowIndex
     *    * column
     *    * columnIndex
     * @function constructor
     */
    constructor(location) {
        // Private usage of init means that we can create an un attached Location
        // The move method does this.
        if (location) {
            // They passed us a Location, so they already know where to go.
            if (location.isLocation) {
                return location;
            }

            // Passed a DOM node.
            if (location.nodeType === Node.ELEMENT_NODE) {
                const
                    grid = Widget.fromElement(location, 'gridbase'),
                    cell = grid && location.closest(grid.focusableSelector);

                // We are targeted on, or within a cell.
                if (cell) {
                    const { dataset } = cell.parentNode;

                    this.init({
                        grid,

                        // A .b-grid-row will have a data-index
                        // If it' a column header, we use rowIndex -1
                        rowIndex : grid.store.includes(dataset.id) ? grid.store.indexOf(dataset.id) : (dataset.index || -1),
                        columnId : cell.dataset.columnId
                    });
                    this.initialTarget = location;
                }
            }
            else {
                this.init(location);
            }
        }
    }

    init(config) {
        const me = this;

        //<debug>
        if (!config.grid) {
            throw new Error('Grid Location must include grid property');
        }
        //</debug>
        const
            grid               = me.grid = config.grid,
            { store, columns } = grid,
            { visibleColumns } = columns;

        // If we have a target. This is usually only for actionable locations.
        if (config.target) {
            me.actionTargets = [me._target = config.target];
        }

        // Determine our record and rowIndex
        if (config.record) {
            me._id = config.record.id;
        }
        else if ('id' in config) {
            me._id = config.id;

            // Null means that the Location is in the grid header, so rowIndex -1
            if (config.id == null) {
                me._rowIndex = -1;
            }
        }
        else {
            const rowIndex = !isNaN(config.row) ? config.row : !isNaN(config.rowIndex) ? config.rowIndex : NaN;
            //<debug>
            if (isNaN(rowIndex)) {
                throw new Error('Grid Location must include either record, or id or rowIndex property');
            }
            //</debug>
            me._rowIndex = Math.max(Math.min(Number(rowIndex), store.count - 1), grid.hideHeaders ? 0 : -1);
            me._id = store.getAt(me._rowIndex)?.id;
        }
        if (!('_rowIndex' in me)) {
            me._rowIndex = store.indexOf(me.id);
        }

        // Cache value that we use now. We do not hold a reference to a record
        me.isSpecialRow = me.record?.isSpecialRow;

        // Determine our column and columnIndex
        if ('columnId' in config) {
            me._column = columns.getById(config.columnId);
        }
        else if ('field' in config) {
            me._column = columns.get(config.field);
        }
        else {
            const columnIndex = !isNaN(config.column) ? config.column : !isNaN(config.columnIndex) ? config.columnIndex : NaN;

            if (!isNaN(columnIndex)) {
                me._columnIndex = Math.min(Number(columnIndex), visibleColumns.length - 1);
                me._column = visibleColumns[me._columnIndex];
            }
            // Fall back to using 'column' property either as index or the Column.
            // If no column property, use column zero.
            else {
                me._column = ('column' in config) ? isNaN(config.column) ? config.column : visibleColumns[config.column] : visibleColumns[0];
            }
        }
        if (!('_columnIndex' in me)) {
            me._columnIndex = visibleColumns.indexOf(me._column);
        }
    }

    // Class identity indicator. Usually added by extending Base, but we don't do that for perf.
    get isLocation() {
        return true;
    }

    equals(other) {
        return other?.isLocation &&
            other.grid   === this.grid &&
            other.record === this.record &&
            other.column === this.column &&
            other.target === this.target;
    }

    /**
     * Yields the row index of this location.
     * @property {Number}
     * @readonly
     */
    get rowIndex() {
        const
            { _id }   = this,
            { store } = this.grid;

        // Return the up to date row index for our record
        return store.includes(_id) ? store.indexOf(_id) : Math.min(this._rowIndex, store.count - 1);
    }

    /**
     * Used by GridNavigation.
     * @private
     */
    get visibleRowIndex() {
        const
            { rowManager } = this.grid,
            { rowIndex }   = this;

        return rowIndex === -1 ? rowIndex : Math.max(Math.min(rowIndex, rowManager.lastFullyVisibleTow.dataIndex), rowManager.firstFullyVisibleTow.dataIndex);
    }

    /**
     * Yields `true` if the cell and row are selectable.
     *
     * That is if the record is present in the grid's store and it's not a group summary or group header record.
     * @property {Boolean}
     * @readonly
     */
    get isSelectable() {
        return this.grid.store.includes(this._id) && !this.isSpecialRow;
    }

    get record() {
        // -1 means the header row
        if (this._rowIndex > -1) {
            const { store } = this.grid;

            // Location's record no longer in store; fall back to record at same index.
            if (!store.includes(this._id)) {
                return store.getAt(this._rowIndex);
            }

            return store.getById(this._id);
        }
    }

    get id() {
        return this._id;
    }

    get column() {
        const { visibleColumns } = this.grid.columns;

        // Location's column no longer visible; fall back to column at same index.
        if (!visibleColumns?.includes(this._column)) {
            return visibleColumns?.[this.columnIndex];
        }

        return this._column;
    }

    get columnId() {
        return this.column?.id;
    }

    get columnIndex() {
        return Math.min(this._columnIndex, this.grid.columns.visibleColumns?.length - 1);
    }

    /**
     * Returns a __*new *__ `Location` instance having moved from the current location in the
     * mode specified.
     * @param {Number} where Where to move from this Location. May be:
     *
     *  - `Location.UP`
     *  - `Location.NEXT_CELL`
     *  - `Location.DOWN`
     *  - `Location.PREV_CELL`
     *  - `Location.FIRST_COLUMN`
     *  - `Location.LAST_COLUMN`
     *  - `Location.FIRST_CELL`
     *  - `Location.LAST_CELL`
     *  - `Location.PREV_PAGE`
     *  - `Location.NEXT_PAGE`
     * @returns {Grid.util.Location} A Location object encapsulating the target location.
     */
    move(where) {
        const
            me        = this,
            {
                record,
                column,
                grid
            }         = me,
            { store } = grid,
            columns   = grid.columns.visibleColumns,
            result    = new Location();

        let rowIndex    = store.includes(record)   ? store.indexOf(record)   : me.rowIndex,
            columnIndex = columns.includes(column) ? columns.indexOf(column) : me.columnIndex;

        const
            rowMin        = grid.hideHeaders ? 0 : -1,
            rowMax        = store.count - 1,
            colMax        = columns.length - 1,
            atFirstRow    = rowIndex === rowMin,
            atLastRow     = rowIndex === rowMax,
            atFirstColumn = columnIndex === 0,
            atLastColumn  = columnIndex === colMax;

        switch (where) {
            case Location.PREV_CELL:
                if (atFirstColumn) {
                    if (!atFirstRow) {
                        columnIndex = colMax;
                        rowIndex--;
                    }
                }
                else {
                    columnIndex--;
                }
                break;
            case Location.NEXT_CELL:
                if (atLastColumn) {
                    if (!atLastRow) {
                        columnIndex = 0;
                        rowIndex++;
                    }
                }
                else {
                    columnIndex++;
                }
                break;
            case Location.UP:
                if (!atFirstRow) {
                    rowIndex--;
                }
                break;
            case Location.DOWN:
                if (!atLastRow) {
                    // From the col header, we drop to the topmost fully visible row.
                    if (rowIndex === -1) {
                        rowIndex = grid.rowManager.firstFullyVisibleRow.dataIndex;
                    }
                    else {
                        rowIndex++;
                    }
                }
                break;
            case Location.FIRST_COLUMN:
                columnIndex = 0;
                break;
            case Location.LAST_COLUMN:
                columnIndex = colMax;
                break;
            case Location.FIRST_CELL:
                rowIndex = rowMin;
                columnIndex = 0;
                break;
            case Location.LAST_CELL:
                rowIndex = rowMax;
                columnIndex = colMax;
                break;
            case Location.PREV_PAGE:
                rowIndex = Math.max(rowMin, rowIndex - Math.floor(grid.scrollable.clientHeight / grid.rowHeight));
                break;
            case Location.NEXT_PAGE:
                rowIndex = Math.min(rowMax, rowIndex + Math.floor(grid.scrollable.clientHeight / grid.rowHeight));
                break;
        }

        // Set the calculated coordinates in the result.
        result.init({
            grid,
            rowIndex,
            columnIndex
        });

        return result;
    }

    /**
     * The cell DOM element which this Location references.
     * @property {HTMLElement}
     * @readonly
     */
    get cell() {
        const
            me = this,
            {
                grid,
                id,
                _cell
            }  = me;

        // Property value set
        if (_cell) {
            return _cell;
        }

        // On a header cell
        if (id == null) {
            return grid.columns.getById(me.columnId)?.element;
        }
        else {
            const { row } = me;

            if (row) {
                return row.getCell(me.columnId) || row.getCell(grid.columns.getAt(me.columnIndex)?.id);
            }
        }
    }

    get row() {
        // Use our record ID by preference, but fall back to our row index if not present
        return this.grid.getRowById(this.id) || this.grid.getRow(this.rowIndex);
    }

    /**
     * The DOM element which encapsulates the focusable target of this Location.
     *
     * This is usually the {@link #property-cell}, but if this is an actionable location, this
     * may be another DOM element within the cell.
     * @property {HTMLElement}
     * @readonly
     */
    get target() {
        const
            { cell, _target }   = this,
            { focusableFinder } = this.grid;

        // We might be asked for our focusElement before we're fully rendered and painted.
        if (cell) {
            // Location was created in disableActionable mode with the target
            // explicitly directed to the cell.
            if (_target) {
                return _target;
            }
            focusableFinder.currentNode = this.grid.focusableFinderCell = cell;

            return focusableFinder.nextNode() || cell;
        }
    }

    /**
     * This property is `true` if the focus target is not the cell itself.
     * @property {Boolean}
     * @readonly
     */
    get isActionable() {
        const
            { cell, _target } = this,
            containsFocus     = cell.compareDocumentPosition(DomHelper.getActiveElement(cell)) & Node.DOCUMENT_POSITION_CONTAINED_BY;

        // The actual target may be inside the cell, or just positioned to *appear* inside the cell
        // such as event/task rendering.
        return Boolean(containsFocus || (_target && _target !== this.cell));
    }

    /**
     * This property is `true` if this location represents a column header.
     * @property {Boolean}
     * @readonly
     */
    get isColumnHeader() {
        return this.cell && this.rowIndex === -1;
    }

    /**
     * This property is `true` if this location represents a cell in the grid body.
     * @property {Boolean}
     * @readonly
     */
    get isCell() {
        return this.cell && this.record;
    }
}

Location.UP           = 1;
Location.NEXT_CELL    = 2;
Location.DOWN         = 3;
Location.PREV_CELL    = 4;
Location.FIRST_COLUMN = 5;
Location.LAST_COLUMN  = 6;
Location.FIRST_CELL   = 7;
Location.LAST_CELL    = 8;
Location.PREV_PAGE    = 9;
Location.NEXT_PAGE    = 10;

Grid's GridNavigation.js needs to be this:

import Base from '../../../Core/Base.js';
import Rectangle from '../../../Core/helper/util/Rectangle.js';
import Location from '../../util/Location.js';
import DomHelper from '../../../Core/helper/DomHelper.js';

/**
 * @module Grid/view/mixin/GridNavigation
 */

const
    defaultFocusOptions = Object.freeze({}),
    disableScrolling = Object.freeze({
        x : false,
        y : false
    }),
    containedFocusable = function(e) {
        // When we step outside of the target cell, throw.
        // The TreeWalker silences the exception and terminates the traverse.
        if (!this.focusableFinderCell.contains(e)) {
            return DomHelper.NodeFilter.FILTER_REJECT;
        }
        if (DomHelper.isFocusable(e) && !e.disabled) {
            return DomHelper.NodeFilter.FILTER_ACCEPT;
        }
        return DomHelper.NodeFilter.FILTER_SKIP;
    };

/**
 * Mixin for Grid that handles cell to cell navigation.
 *
 * See {@link Grid.view.Grid} for more information on grid cell keyboard navigation.
 *
 * @mixin
 */
export default Target => class GridNavigation extends (Target || Base) {
    static get $name() {
        return 'GridNavigation';
    }

    static get configurable() {
        return {
            focusable : false,

            focusableSelector : '.b-grid-cell,.b-grid-header.b-depth-0',

            // Documented on Grid
            keyMap : {
                ArrowUp    : { handler : 'navigateUpByKey', weight : 10 },
                ArrowRight : { handler : 'navigateRightByKey', weight : 10 },
                ArrowDown  : { handler : 'navigateDownByKey', weight : 10 },
                ArrowLeft  : { handler : 'navigateLeftByKey', weight : 10 },

                /* This configuration is needed at the moment because of Shift-key checks further up the road (in
                 * GridSelection). This makes these keyMaps strongly dependent of each other until this is refactored.
                 */
                'Shift+ArrowUp'   : 'navigateUpByKey',
                'Shift+ArrowDown' : 'navigateDownByKey',

                'Ctrl+Home' : 'navigateFirstCell',
                Home        : 'navigateFirstColumn',
                'Ctrl+End'  : 'navigateLastCell',
                End         : 'navigateLastColumn',
                PageUp      : 'navigatePrevPage',
                PageDown    : 'navigateNextPage',
                Enter       : 'activateHeader',

                // Private
                Escape      : 'onEscape',
                'Shift+Tab' : { handler : 'onShiftTab', preventDefault : false },
                Tab         : { handler : 'onTab', preventDefault : false },
                ' '         : { handler : 'onSpace', preventDefault : false }
            }
        };
    }

    onStoreRecordIdChange(event) {
        super.onStoreRecordIdChange?.(event);

        const
            { focusedCell }     = this,
            { oldValue, value } = event;

        // https://github.com/bryntum/support/issues/4935
        if (focusedCell && focusedCell.id === oldValue) {
            focusedCell._id = value;
        }
    }

    onElementMouseDown(event) {
        // Cache this so that focusin handling can tell whether its a mousedown focus
        // in which case it must be obeyed. If not, it's taken to be a TAB in and that
        // must redirect to the last focused cell.
        this.lastMousedownEvent = event;
        super.onElementMouseDown(event);
    }

    /**
     * Called by the RowManager when the row which contains the focus location is derendered.
     *
     * This keeps focus in a consistent place.
     * @protected
     */
    onFocusedRowDerender() {
        const
            me              = this,
            { focusedCell } = me;

        if (focusedCell?.id != null && focusedCell.cell) {
            const isActive = focusedCell.cell.contains(DomHelper.getActiveElement(me));

            if (me.hideHeaders) {
                if (isActive) {
                    me.revertFocus();
                }
            }
            else {
                const headerContext = me.normalizeCellContext({
                    rowIndex    : -1,
                    columnIndex : isActive ? focusedCell.columnIndex : 0
                });

                // The row contained focus, focus the corresponding header
                if (isActive) {
                    me.focusCell(headerContext);
                }
                else {
                    headerContext.cell.tabIndex = 0;
                }
            }
            focusedCell.cell.tabIndex = -1;
        }
    }

    navigateUpByKey(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.UP);
    }

    navigateDownByKey(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.DOWN);
    }

    navigateLeftByKey(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(this.rtl ? Location.NEXT_CELL : Location.PREV_CELL);
    }

    navigateRightByKey(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(this.rtl ? Location.PREV_CELL : Location.NEXT_CELL);
    }

    navigateFirstCell(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.FIRST_CELL);
    }

    navigateFirstColumn(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.FIRST_COLUMN);
    }

    navigateLastCell(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.LAST_CELL);
    }

    navigateLastColumn(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.LAST_COLUMN);
    }

    navigatePrevPage(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.PREV_PAGE);
    }

    navigateNextPage(keyEvent) {
        this.navigationEvent = keyEvent;
        this.focusCell(Location.NEXT_PAGE);
    }

    activateHeader(keyEvent) {
        this.navigationEvent = keyEvent;
        if (keyEvent.target.classList.contains('b-grid-header') && this.focusedCell.isColumnHeader) {
            const { column } = this.focusedCell;

            column.onKeyDown?.(keyEvent);

            this.getHeaderElement(column.id).click();
        }
        return false;
    }

    onEscape(keyEvent) {
        const { focusedCell } = this;

        this.navigationEvent = keyEvent;
        if (!keyEvent.target.closest('.b-dragging') && focusedCell?.isActionable) {
            // The escape must not be processed by handlers for the cell we are about to focus.
            // We need to just push focus upwards to the cell, and stop there.
            keyEvent.stopImmediatePropagation();

            // To prevent the focusCell from being rejected as a no-op
            this._focusedCell = null;

            // Focus the cell with an explicit request to not jump in
            this.focusCell({
                rowIndex : focusedCell.rowIndex,
                column   : focusedCell.column
            }, {
                disableActionable : true
            });
        }
    }

    onTab(keyEvent) {
        const
            { target } = keyEvent,
            {
                focusedCell,
                bodyElement
            }          = this,
            {
                isActionable,
                actionTargets
            }          = focusedCell,
            isEditable = isActionable && DomHelper.isEditable(target) && !target.readOnly;

        this.navigationEvent = keyEvent;

        // If we're on the last editable in a cell, TAB navigates right
        if (isEditable && target === actionTargets[actionTargets.length - 1]) {
            keyEvent.preventDefault();
            this.navigateRightByKey(keyEvent);
        }
        // If we're *on* a cell, or on last subtarget, TAB moves off the grid.
        // Temporarily hide the grid body, and let TAB take effect from there
        else if (!isActionable || target === actionTargets[actionTargets.length - 1]) {
            bodyElement.style.display = 'none';
            this.requestAnimationFrame(() => bodyElement.style.display = '');

            // So that Navigator#onKeyDown does not continue to preventDefault;
            return false;
        }
    }

    onShiftTab(keyEvent) {
        const
            me = this,
            { target } = keyEvent,
            {
                focusedCell,
                bodyElement
            }   = me,
            {
                cell,
                isActionable,
                actionTargets
            } = focusedCell,
            isEditable  = isActionable && DomHelper.isEditable(target) && !target.readOnly,
            onFirstCell = focusedCell.columnIndex === 0 && focusedCell.rowIndex === (me.hideHeaders ? 0 : -1);

        me.navigationEvent = keyEvent;

        // If we're on the first editable in a cell that is not the first cell, SHIFT+TAB navigates left
        if (!onFirstCell && isEditable && target === actionTargets[0]) {
            keyEvent.preventDefault();
            me.navigateLeftByKey(keyEvent);
        }
        
        // If we're *on* a cell, or on first subtarget, SHIFT+TAB moves off the grid.
        else if (!isActionable || target === actionTargets[0]) {
            // Focus the first header cell and then let the key's default action take its course
            const f = !onFirstCell && !me.hideHeaders && me.focusCell({
                rowIndex : -1,
                column   : 0
            }, {
                disableActionable : true
            });

            // If that was successful then reset the tabIndex
            if (f) {
                f.cell.tabIndex = -1;
                cell.tabIndex = 0;
                me._focusedCell = focusedCell;
            }
            // Otherwise, temporarily hide the grid body, and let TAB take effect from there
            else {
                bodyElement.style.display = 'none';
                me.requestAnimationFrame(() => bodyElement.style.display = '');
            }

            // So that Navigator#onKeyDown does not continue to preventDefault;
            return false;
        }
    }

    onSpace(keyEvent) {
        // SPACE scrolls, so disable that
        if (!this.focusedCell.isActionable) {
            keyEvent.preventDefault();
        }
        // Return false to tell keyMap that any other actions should be called
        return false;
    }

    //region Cell

    /**
     * Triggered when a user navigates to a grid cell
     * @event navigate
     * @param {Grid.view.Grid} grid The grid instance
     * @param {Grid.util.Location} last The previously focused location
     * @param {Grid.util.Location} location The new focused location
     * @param {Event} [event] The UI event which caused navigation.
     */

    /**
     * Grid Location which encapsulates the currently focused cell.
     * Set to focus a cell or use {@link #function-focusCell}.
     * @property {Grid.util.Location}
     */
    get focusedCell() {
        return this._focusedCell;
    }

    /**
     * This property is `true` if an element _within_ a cell is focused.
     * @property {Boolean}
     * @readonly
     */
    get isActionableLocation() {
        return this._focusedCell?.isActionable;
    }

    set focusedCell(cellSelector) {
        this.focusCell(cellSelector);
    }

    get focusedRecord() {
        return this._focusedCell?.record;
    }

    /**
     * CSS selector for currently focused cell. Format is "[data-index=index] [data-column-id=columnId]".
     * @property {String}
     * @readonly
     */
    get cellCSSSelector() {
        const cell = this._focusedCell;

        return cell ? `[data-index=${cell.rowIndex}] [data-column-id=${cell.columnId}]` : '';
    }

    afterHide() {
        super.afterHide(...arguments);

        // Do not scroll back to the last focused cell/last moused over cell upon reshow
        this.lastFocusedCell = this.mouseMoveEvent = null;
    }

    /**
     * Checks whether or not a cell is focused.
     * @param {Object|String|Number} cellSelector Cell selector { id: x, columnId: xx } or row id
     * @returns {Boolean} true if cell or row is focused, otherwise false
     */
    isFocused(cellSelector) {
        return Boolean(this._focusedCell?.equals(this.normalizeCellContext(cellSelector)));
    }

    get focusElement() {
        if (!this.isDestroying) {
            let focusCell;

            // If the store is not empty, focusedCell can return the closest cell
            if (this.store.count && this._focusedCell) {
                focusCell = this._focusedCell.target;
            }
            // If the store is empty, or we have had no focusedCell set, focus a column header.
            else {
                focusCell = this.normalizeCellContext({
                    rowIndex    : -1,
                    columnIndex : this._focusedCell?.columnIndex || 0
                }).target;
            }

            const superFocusEl = super.focusElement;

            // If there's no cell, or the Container's focus element is before the cell
            // use the Container's focus element.
            // For example, we may have a top toolbar.
            if (superFocusEl && (!focusCell || focusCell.compareDocumentPosition(superFocusEl) === Node.DOCUMENT_POSITION_PRECEDING)) {
                return superFocusEl;
            }

            return focusCell;
        }
    }

    onPaint({ firstPaint }) {
        const me = this;

        super.onPaint?.(...arguments);

        // Make the grid initally tabbable into.
        // The first cell has to have the initial roving tabIndex set into it.
        const defaultFocus = this.normalizeCellContext({
            rowIndex : me.hideHeaders ? 0 : -1,
            column   : me.hideHeaders ? 0 : me.columns.find(col => col.isFocusable)
        });

        if (defaultFocus.cell) {
            me._focusedCell = defaultFocus;

            const { target } = defaultFocus;

            // If cell doesn't contain a focusable target, it needs tabIndex 0.
            if (target === defaultFocus.cell) {
                defaultFocus.cell.tabIndex = 0;
            }
        }
    }

    /**
     * This function handles focus moving into, or within the grid.
     * @param {Event} focusEvent
     * @private
     */
    onGridBodyFocusIn(focusEvent) {
        const
            me              = this,
            {
                bodyElement,
                lastMousedownEvent,
                navigationEvent
            }               = me,
            event           = navigationEvent || focusEvent,
            lastFocusedCell = me.focusedCell,
            lastTarget      = lastFocusedCell?.initialTarget || lastFocusedCell?.target,
            {
                target,
                relatedTarget
            }               = focusEvent,
            targetCell      = target.closest(me.focusableSelector);

        me.navigationEvent = me.lastMousedownEvent = null;

        // If focus moved into a valid cell...
        if (targetCell) {
            const
                cellSelector  = new Location(target),
                { cell }      = cellSelector,
                lastCell      = lastFocusedCell?.cell,
                actionTargets = cellSelector.actionTargets = me.findFocusables(targetCell),
                // Don't select on focus on a contained actionable location
                doSelect      = ((Boolean(navigationEvent) || me.selectOnFocus) && target === cell);

            // https://github.com/bryntum/support/issues/4039
            // Only try focusing cell is current target cell is getting removed
            if (!me.store.getById(targetCell.parentNode.dataset.id) && cell !== targetCell) {
                cell.focus({ preventScroll : true });
                return;
            }

            if (target.matches(me.focusableSelector)) {
                if (me.disableActionable) {
                    cellSelector._target = cell;
                }
                // Focus first focusable target if we are configured to.
                else if (actionTargets.length) {
                    me.navigationEvent = event;
                    actionTargets[0].focus();
                    return;
                }
            }
            else {
                // If we have tabbed in and *NOT* mousedowned in, and hit a tabbable element which was not our
                // last focused cell, go back to last focused cell.
                if (lastFocusedCell?.target && relatedTarget && (!lastMousedownEvent || !bodyElement.contains(lastMousedownEvent.target)) && !bodyElement.contains(relatedTarget) && !cellSelector.equals(lastFocusedCell)) {
                    lastTarget.focus();
                    return;
                }
                cellSelector._target = target;
            }

            if (lastCell) {
                lastCell.classList.remove('b-focused');
                lastCell.tabIndex = -1;
            }
            if (cell) {
                cell.classList.add('b-focused');

                // Column may update DOM on cell focus for A11Y purposes.
                cellSelector.column.onCellFocus(cellSelector);

                // Only switch the cell to be tabbable if focus was not directed to an inner focusable.
                if (cell === target) {
                    cell.tabIndex = 0;
                }

                // Moving back to a cell from a cell-contained Editor
                if (cell.contains(focusEvent.relatedTarget)) {
                    if (lastTarget === target) {
                        return;
                    }
                }
            }

            //Remember
            me._focusedCell = cellSelector;

            me.onCellNavigate?.(me, lastFocusedCell, cellSelector, event, doSelect);

            me.trigger('navigate', { lastFocusedCell, focusedCell : cellSelector, event });
            //TODO: should be able to cancel selectcell from listeners
        }
        // Focus not moved into a valid cell, refocus last cell's target
        // if there was a previously focused cell.
        else {
            lastTarget?.focus();
        }
    }

    findFocusables(cell) {
        const
            { focusableFinder } = this,
            result              = [];

        focusableFinder.currentNode = this.focusableFinderCell = cell;

        for (let focusable = focusableFinder.nextNode(); focusable; focusable = focusableFinder.nextNode()) {
            result.push(focusable);
        }
        return result;
    }

    get focusableFinder() {
        const me = this;

        if (!me._focusableFinder) {
            me._focusableFinder = me.setupTreeWalker(me.bodyElement, DomHelper.NodeFilter.SHOW_ELEMENT, {
                acceptNode : containedFocusable.bind(me)
            });
        }

        return me._focusableFinder;
    }

    /**
     * Sets the passed record as the current focused record for keyboard navigation and selection purposes.
     * This API is used by Combo to activate items in its picker.
     * @param {Core.data.Model|Number|String} activeItem The record, or record index, or record id to highlight as the active ("focused") item.
     * @internal
     */
    restoreActiveItem(item = this._focusedCell) {
        if (this.rowManager.count) {
            // They sent a row number.
            if (!isNaN(item)) {
                item = this.store.getAt(item);
            }
            // Still not a record, treat it as a record ID.
            else if (!item.isModel) {
                item = this.store.getById(item);
            }
            return this.focusCell(item);
        }
    }

    /**
     * Navigates to a cell and/or its row (depending on selectionMode)
     * @param {Object} cellSelector { id: rowId, columnId: 'columnId' }
     * @param {Object} options Modifier options for how to deal with focusing the cell. These
     * are used as the {@link Core.helper.util.Scroller#function-scrollTo} options.
     * @param {Object|Boolean} [options.scroll=true] Pass `false` to not scroll the cell into view, or a
     * scroll options object to affect the scroll.
     * @returns {Grid.util.Location} A Location object representing the focused location.
     * @fires navigate
     */
    focusCell(cellSelector, options = defaultFocusOptions) {
        const
            me               = this,
            { _focusedCell } = me,
            {
                scroll,
                doSelect,
                disableActionable
            }                = options;

        // If we're being asked to go to a nonexistent header row, revert focus outwards
        if (cellSelector?.rowIndex === -1 && me.hideHeaders) {
            me.revertFocus();
            return;
        }

        // Get a Grid Location.
        // If the cellSelector is a number, it is taken to be a "relative" location as defined
        // in the Location class eg Location.UP, and we move the current focus accordingly.
        cellSelector = typeof cellSelector === 'number' && _focusedCell?.isLocation ? _focusedCell.move(cellSelector) : me.normalizeCellContext(cellSelector);

        // Request is a no-op, but it's still a navigate request which selection processing needs to know about
        if (cellSelector.equals(_focusedCell)) {
            // doSelect only set if the cell is not actionable.
            // If it can be interacted with, that interaction should not affect selection.
            me.onCellNavigate?.(me, _focusedCell, cellSelector, me.navigationEvent, ('doSelect' in options) ? doSelect : !cellSelector.isActionable || cellSelector.initialTarget === cellSelector.cell);
            return _focusedCell;
        }

        const
            subGrid     = me.getSubGridFromColumn(cellSelector.columnId),
            { cell }    = cellSelector,
            testCell    = cell || me.getCell({
                rowIndex : me.rowManager.topIndex,
                columnId : cellSelector.columnId
            }),
            subGridRect = Rectangle.from(subGrid.element),
            bodyRect    = Rectangle.from(me.bodyElement),
            cellRect    = Rectangle.from(testCell).moveTo(null, subGridRect.y);

        // No scrolling possible if we're movoing to a column header
        if (scroll === false || cellSelector.rowIndex === -1) {
            options = Object.assign({}, options, disableScrolling);
        }
        else {
            options = Object.assign({}, options, scroll);

            // If the test cell is larger than the subGrid, in any dimension, disable scrolling
            if (cellRect.width > subGridRect.width || cellRect.height > bodyRect.height) {
                options.x = options.y = false;
            }
            // Else ask for the column to be scrolled into view
            else {
                options.column = cellSelector.columnId;
            }

            me.scrollRowIntoView(cellSelector.id, options);
        }

        // Disable auto stepping into the focused cell.
        me.disableActionable = disableActionable;

        // Go through select pathway upon focus
        me.selectOnFocus = doSelect;

        // Focus the location's target, be it a cell, or an interior element.
        // The onFocusIn element in this module responds to this.
        cellSelector[disableActionable ? 'cell' : 'target']?.focus();

        me.disableActionable = me.selectOnFocus = false;

        return cellSelector;
    }

    blurCell(cellSelector) {
        const me   = this,
            cell = me.getCell(cellSelector);

        if (cell) {
            cell.classList.remove('b-focused');
        }
    }

    clearFocus(fullClear) {
        const me = this;

        if (me._focusedCell) {
            // set last to have focus return to previous cell when alt tabbing
            me.lastFocusedCell = fullClear ? null : me._focusedCell;

            me.blurCell(me._focusedCell);
            me._focusedCell = null;
        }
    }

    /**
     * Selects the cell before or after currently focused cell.
     * @private
     * @param next Specify true to select the next cell, false to select the previous
     * @param {Event} [event] Optionally, the UI event which caused navigation.
     * @returns {Object} Used cell selector
     */
    internalNextPrevCell(next = true, event) {
        const
            me           = this,
            cellSelector = me._focusedCell;

        if (cellSelector) {
            me.navigationEvent = event;

            return me.focusCell({
                id       : cellSelector.id,
                columnId : me.columns.getAdjacentVisibleLeafColumn(cellSelector.columnId, next, true).id
            }, {
                doSelect : true,
                event
            });
        }
        return null;
    }

    /**
     * Select the cell after the currently focused one.
     * @param {Event} [event] Optionally, the UI event which caused navigation.
     * @returns {Object} Cell selector
     */
    navigateRight(event) {
        return this.internalNextPrevCell(!this.rtl, event);
    }

    /**
     * Select the cell before the currently focused one.
     * @param {Event} [event] Optionally, the UI event which caused navigation.
     * @returns {Object} Cell selector
     */
    navigateLeft(event) {
        return this.internalNextPrevCell(Boolean(this.rtl), event);
    }

    //endregion

    //region Row

    /**
     * Selects the next or previous record in relation to the current selection. Scrolls into view if outside.
     * @private
     * @param next Next record (true) or previous (false)
     * @param {Boolean} skipSpecialRows True to not return specialRows like headers
     * @param {Event} [event] Optionally, the UI event which caused navigation.
     * @returns {Object|Boolean} Selection context for the focused row (& cell) or false if no selection was made
     */
    internalNextPrevRow(next, skipSpecialRows = true, event, moveToHeader = true) {
        const
            me   = this,
            cell = me._focusedCell;

        if (!cell) return false;

        const record = me.store[`get${next ? 'Next' : 'Prev'}`](cell.id, false, skipSpecialRows);

        if (record) {
            return me.focusCell({
                id       : record.id,
                columnId : cell.columnId,
                scroll   : {
                    x : false
                }
            }, {
                doSelect : true,
                event
            });
        }
        else if (!next && moveToHeader) {
            this.clearFocus();
            return this.getHeaderElement(cell.columnId).focus();
        }
    }

    /**
     * Navigates to the cell below the currently focused cell
     * @param {Event} [event] Optionally, the UI event which caused navigation.
     * @returns {Object} Selector for focused row (& cell)
     */
    navigateDown(event) {
        return this.internalNextPrevRow(true, false, event);
    }

    /**
     * Navigates to the cell above the currently focused cell
     * @param {Event} [event] Optionally, the UI event which caused navigation.
     * @returns {Object} Selector for focused row (& cell)
     */
    navigateUp(event) {
        return this.internalNextPrevRow(false, true, event);
    }

    //endregion

    // This does not need a className on Widgets.
    // Each *Class* which doesn't need 'b-' + constructor.name.toLowerCase() automatically adding
    // to the Widget it's mixed in to should implement thus.
    get widgetClass() {}
};

Post by thejas.pateel »

Hi,
thanks for the reply can i know when will be release?


Post by mats »

Should be out later this week!


Post by thejas.pateel »

Hey Is this checkbox issue is fixed in 5.2.9.Even i upgrading brytum i am getting same issue .Kindly give update


Post by thejas.pateel »

Even when i replace your code to location.js and GridNavigation.js.its not working correctly .rows got deselected when i click to checkbox column .kindly give proper solution or code we are waiting from past month .This will impact on your product and customer satisfaction


Post by mats »

It works ok for us, can you please share a video showing our online sample where you can reproduce this? Please clear cache first.


Post by thejas.pateel »

See ,
I have uploaded video of your example . its not working fine .when we click on checkbox column rows are deselecting. for ther columns it is working fine.i have cleared the cache as well

screen-capture (2).webm
(2.53 MiB) Downloaded 20 times

Post by alex.l »

Sorry for inconveniences.
Could you please try to reproduce this on 5.3.0-beta version? It seems to be fixed there.

All the best,
Alex


Post by thejas.pateel »

Hi ,
In version 5.2.9 you have told that it is fixed

Release2.jpg
Release2.jpg (83.86 KiB) Viewed 223 times

I don't know what happening here .in which thing i need to believe. kindly provide proper fix please .Please understand our business doesn't accept BETA versions


Post by alex.l »

Yes, I know we marked the issue as resolved. I guess there is some misunderstanding in ticket description which I did, so a developer fixed it not fully.
I asked you to check in beta just to make sure it works as you expected. The fix will be released with official 5.3.0, but I provided you files which you could use to manage it before the release.
Please check attachments.
After you upgraded to 5.3.0, please use original 5.3.0 files, they're already contain that fix and something else.

Please let us know if it works.

Attachments
GridNavigation.js.zip
(6.89 KiB) Downloaded 13 times
GridSelection.js.zip
(15.47 KiB) Downloaded 18 times

All the best,
Alex


Post Reply